1use std::{
2 cmp::Ordering,
3 fmt::{self, Display, Write},
4 str::FromStr,
5};
6
7use bytes::BufMut;
8use http::{
9 header::{self, HeaderName, HeaderValue},
10 Method,
11};
12use percent_encoding::utf8_percent_encode;
13use tracing::warn;
14
15use super::{
16 error::{IntoHttpError, UnknownVersionError},
17 AuthScheme, SendAccessToken,
18};
19use crate::{percent_encode::PATH_PERCENT_ENCODE_SET, serde::slice_to_buf, RoomVersionId};
20
21#[derive(Clone, Debug, PartialEq, Eq)]
23#[allow(clippy::exhaustive_structs)]
24pub struct Metadata {
25 pub method: Method,
27
28 pub rate_limited: bool,
30
31 pub authentication: AuthScheme,
33
34 pub history: VersionHistory,
36}
37
38impl Metadata {
39 pub fn empty_request_body<B>(&self) -> B
44 where
45 B: Default + BufMut,
46 {
47 if self.method == Method::GET {
48 Default::default()
49 } else {
50 slice_to_buf(b"{}")
51 }
52 }
53
54 pub fn authorization_header(
60 &self,
61 access_token: SendAccessToken<'_>,
62 ) -> Result<Option<(HeaderName, HeaderValue)>, IntoHttpError> {
63 Ok(match self.authentication {
64 AuthScheme::None => match access_token.get_not_required_for_endpoint() {
65 Some(token) => Some((header::AUTHORIZATION, format!("Bearer {token}").try_into()?)),
66 None => None,
67 },
68
69 AuthScheme::AccessToken => {
70 let token = access_token
71 .get_required_for_endpoint()
72 .ok_or(IntoHttpError::NeedsAuthentication)?;
73
74 Some((header::AUTHORIZATION, format!("Bearer {token}").try_into()?))
75 }
76
77 AuthScheme::AccessTokenOptional => match access_token.get_required_for_endpoint() {
78 Some(token) => Some((header::AUTHORIZATION, format!("Bearer {token}").try_into()?)),
79 None => None,
80 },
81
82 AuthScheme::AppserviceToken => match access_token.get_required_for_appservice() {
83 Some(token) => Some((header::AUTHORIZATION, format!("Bearer {token}").try_into()?)),
84 None => None,
85 },
86
87 AuthScheme::ServerSignatures => None,
88 })
89 }
90
91 pub fn make_endpoint_url(
93 &self,
94 versions: &[MatrixVersion],
95 base_url: &str,
96 path_args: &[&dyn Display],
97 query_string: &str,
98 ) -> Result<String, IntoHttpError> {
99 let path_with_placeholders = self.history.select_path(versions)?;
100
101 let mut res = base_url.strip_suffix('/').unwrap_or(base_url).to_owned();
102 let mut segments = path_with_placeholders.split('/');
103 let mut path_args = path_args.iter();
104
105 let first_segment = segments.next().expect("split iterator is never empty");
106 assert!(first_segment.is_empty(), "endpoint paths must start with '/'");
107
108 for segment in segments {
109 if segment.starts_with(':') {
110 let arg = path_args
111 .next()
112 .expect("number of placeholders must match number of arguments")
113 .to_string();
114 let arg = utf8_percent_encode(&arg, PATH_PERCENT_ENCODE_SET);
115
116 write!(res, "/{arg}").expect("writing to a String using fmt::Write can't fail");
117 } else {
118 res.reserve(segment.len() + 1);
119 res.push('/');
120 res.push_str(segment);
121 }
122 }
123
124 if !query_string.is_empty() {
125 res.push('?');
126 res.push_str(query_string);
127 }
128
129 Ok(res)
130 }
131
132 #[doc(hidden)]
134 pub fn _path_parameters(&self) -> Vec<&'static str> {
135 let path = self.history.all_paths().next().unwrap();
136 path.split('/').filter_map(|segment| segment.strip_prefix(':')).collect()
137 }
138}
139
140#[derive(Clone, Debug, PartialEq, Eq)]
145#[allow(clippy::exhaustive_structs)]
146pub struct VersionHistory {
147 unstable_paths: &'static [&'static str],
151
152 stable_paths: &'static [(MatrixVersion, &'static str)],
156
157 deprecated: Option<MatrixVersion>,
164
165 removed: Option<MatrixVersion>,
170}
171
172impl VersionHistory {
173 pub const fn new(
186 unstable_paths: &'static [&'static str],
187 stable_paths: &'static [(MatrixVersion, &'static str)],
188 deprecated: Option<MatrixVersion>,
189 removed: Option<MatrixVersion>,
190 ) -> Self {
191 use konst::{iter, slice, string};
192
193 const fn check_path_is_valid(path: &'static str) {
194 iter::for_each!(path_b in slice::iter(path.as_bytes()) => {
195 match *path_b {
196 0x21..=0x7E => {},
197 _ => panic!("path contains invalid (non-ascii or whitespace) characters")
198 }
199 });
200 }
201
202 const fn check_path_args_equal(first: &'static str, second: &'static str) {
203 let mut second_iter = string::split(second, "/").next();
204
205 iter::for_each!(first_s in string::split(first, "/") => {
206 if let Some(first_arg) = string::strip_prefix(first_s, ":") {
207 let second_next_arg: Option<&'static str> = loop {
208 let (second_s, second_n_iter) = match second_iter {
209 Some(tuple) => tuple,
210 None => break None,
211 };
212
213 let maybe_second_arg = string::strip_prefix(second_s, ":");
214
215 second_iter = second_n_iter.next();
216
217 if let Some(second_arg) = maybe_second_arg {
218 break Some(second_arg);
219 }
220 };
221
222 if let Some(second_next_arg) = second_next_arg {
223 if !string::eq_str(second_next_arg, first_arg) {
224 panic!("Path Arguments do not match");
225 }
226 } else {
227 panic!("Amount of Path Arguments do not match");
228 }
229 }
230 });
231
232 while let Some((second_s, second_n_iter)) = second_iter {
234 if string::starts_with(second_s, ":") {
235 panic!("Amount of Path Arguments do not match");
236 }
237 second_iter = second_n_iter.next();
238 }
239 }
240
241 let ref_path: &str = if let Some(s) = unstable_paths.first() {
243 s
244 } else if let Some((_, s)) = stable_paths.first() {
245 s
246 } else {
247 panic!("No paths supplied")
248 };
249
250 iter::for_each!(unstable_path in slice::iter(unstable_paths) => {
251 check_path_is_valid(unstable_path);
252 check_path_args_equal(ref_path, unstable_path);
253 });
254
255 let mut prev_seen_version: Option<MatrixVersion> = None;
256
257 iter::for_each!(stable_path in slice::iter(stable_paths) => {
258 check_path_is_valid(stable_path.1);
259 check_path_args_equal(ref_path, stable_path.1);
260
261 let current_version = stable_path.0;
262
263 if let Some(prev_seen_version) = prev_seen_version {
264 let cmp_result = current_version.const_ord(&prev_seen_version);
265
266 if cmp_result.is_eq() {
267 panic!("Duplicate matrix version in stable_paths")
269 } else if cmp_result.is_lt() {
270 panic!("No ascending order in stable_paths")
272 }
273 }
274
275 prev_seen_version = Some(current_version);
276 });
277
278 if let Some(deprecated) = deprecated {
279 if let Some(prev_seen_version) = prev_seen_version {
280 let ord_result = prev_seen_version.const_ord(&deprecated);
281 if !deprecated.is_legacy() && ord_result.is_eq() {
282 panic!("deprecated version is equal to latest stable path version")
286 } else if ord_result.is_gt() {
287 panic!("deprecated version is older than latest stable path version")
289 }
290 } else {
291 panic!("Defined deprecated version while no stable path exists")
292 }
293 }
294
295 if let Some(removed) = removed {
296 if let Some(deprecated) = deprecated {
297 let ord_result = deprecated.const_ord(&removed);
298 if ord_result.is_eq() {
299 panic!("removed version is equal to deprecated version")
301 } else if ord_result.is_gt() {
302 panic!("removed version is older than deprecated version")
304 }
305 } else {
306 panic!("Defined removed version while no deprecated version exists")
307 }
308 }
309
310 VersionHistory { unstable_paths, stable_paths, deprecated, removed }
311 }
312
313 fn select_path(&self, versions: &[MatrixVersion]) -> Result<&'static str, IntoHttpError> {
315 match self.versioning_decision_for(versions) {
316 VersioningDecision::Removed => Err(IntoHttpError::EndpointRemoved(
317 self.removed.expect("VersioningDecision::Removed implies metadata.removed"),
318 )),
319 VersioningDecision::Stable { any_deprecated, all_deprecated, any_removed } => {
320 if any_removed {
321 if all_deprecated {
322 warn!(
323 "endpoint is removed in some (and deprecated in ALL) \
324 of the following versions: {versions:?}",
325 );
326 } else if any_deprecated {
327 warn!(
328 "endpoint is removed (and deprecated) in some of the \
329 following versions: {versions:?}",
330 );
331 } else {
332 unreachable!("any_removed implies *_deprecated");
333 }
334 } else if all_deprecated {
335 warn!(
336 "endpoint is deprecated in ALL of the following versions: \
337 {versions:?}",
338 );
339 } else if any_deprecated {
340 warn!(
341 "endpoint is deprecated in some of the following versions: \
342 {versions:?}",
343 );
344 }
345
346 Ok(self
347 .stable_endpoint_for(versions)
348 .expect("VersioningDecision::Stable implies that a stable path exists"))
349 }
350 VersioningDecision::Unstable => self.unstable().ok_or(IntoHttpError::NoUnstablePath),
351 }
352 }
353
354 pub fn versioning_decision_for(&self, versions: &[MatrixVersion]) -> VersioningDecision {
365 let greater_or_equal_any =
366 |version: MatrixVersion| versions.iter().any(|v| v.is_superset_of(version));
367 let greater_or_equal_all =
368 |version: MatrixVersion| versions.iter().all(|v| v.is_superset_of(version));
369
370 if self.removed.is_some_and(greater_or_equal_all) {
372 return VersioningDecision::Removed;
373 }
374
375 if self.added_in().is_some_and(greater_or_equal_any) {
377 let all_deprecated = self.deprecated.is_some_and(greater_or_equal_all);
378
379 return VersioningDecision::Stable {
380 any_deprecated: all_deprecated || self.deprecated.is_some_and(greater_or_equal_any),
381 all_deprecated,
382 any_removed: self.removed.is_some_and(greater_or_equal_any),
383 };
384 }
385
386 VersioningDecision::Unstable
387 }
388
389 pub fn added_in(&self) -> Option<MatrixVersion> {
393 self.stable_paths.first().map(|(v, _)| *v)
394 }
395
396 pub fn deprecated_in(&self) -> Option<MatrixVersion> {
398 self.deprecated
399 }
400
401 pub fn removed_in(&self) -> Option<MatrixVersion> {
403 self.removed
404 }
405
406 pub fn unstable(&self) -> Option<&'static str> {
408 self.unstable_paths.last().copied()
409 }
410
411 pub fn all_paths(&self) -> impl Iterator<Item = &'static str> {
413 self.unstable_paths().chain(self.stable_paths().map(|(_, path)| path))
414 }
415
416 pub fn unstable_paths(&self) -> impl Iterator<Item = &'static str> {
418 self.unstable_paths.iter().copied()
419 }
420
421 pub fn stable_paths(&self) -> impl Iterator<Item = (MatrixVersion, &'static str)> {
423 self.stable_paths.iter().map(|(version, data)| (*version, *data))
424 }
425
426 pub fn stable_endpoint_for(&self, versions: &[MatrixVersion]) -> Option<&'static str> {
438 for (ver, path) in self.stable_paths.iter().rev() {
440 if versions.iter().any(|v| v.is_superset_of(*ver)) {
442 return Some(path);
443 }
444 }
445
446 None
447 }
448}
449
450#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
452#[allow(clippy::exhaustive_enums)]
453pub enum VersioningDecision {
454 Unstable,
456
457 Stable {
459 any_deprecated: bool,
461
462 all_deprecated: bool,
464
465 any_removed: bool,
467 },
468
469 Removed,
471}
472
473#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
494#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
495pub enum MatrixVersion {
496 V1_0,
508
509 V1_1,
513
514 V1_2,
518
519 V1_3,
523
524 V1_4,
528
529 V1_5,
533
534 V1_6,
538
539 V1_7,
543
544 V1_8,
548
549 V1_9,
553
554 V1_10,
558
559 V1_11,
563
564 V1_12,
568
569 V1_13,
573
574 V1_14,
578}
579
580impl TryFrom<&str> for MatrixVersion {
581 type Error = UnknownVersionError;
582
583 fn try_from(value: &str) -> Result<MatrixVersion, Self::Error> {
584 use MatrixVersion::*;
585
586 Ok(match value {
587 "r0.2.0" | "r0.2.1" | "r0.3.0" |
590 "r0.5.0" | "r0.6.0" | "r0.6.1" => V1_0,
592 "v1.1" => V1_1,
593 "v1.2" => V1_2,
594 "v1.3" => V1_3,
595 "v1.4" => V1_4,
596 "v1.5" => V1_5,
597 "v1.6" => V1_6,
598 "v1.7" => V1_7,
599 "v1.8" => V1_8,
600 "v1.9" => V1_9,
601 "v1.10" => V1_10,
602 "v1.11" => V1_11,
603 "v1.12" => V1_12,
604 "v1.13" => V1_13,
605 "v1.14" => V1_14,
606 _ => return Err(UnknownVersionError),
607 })
608 }
609}
610
611impl FromStr for MatrixVersion {
612 type Err = UnknownVersionError;
613
614 fn from_str(s: &str) -> Result<Self, Self::Err> {
615 Self::try_from(s)
616 }
617}
618
619impl MatrixVersion {
620 pub fn is_superset_of(self, other: Self) -> bool {
630 self >= other
631 }
632
633 pub const fn into_parts(self) -> (u8, u8) {
635 match self {
636 MatrixVersion::V1_0 => (1, 0),
637 MatrixVersion::V1_1 => (1, 1),
638 MatrixVersion::V1_2 => (1, 2),
639 MatrixVersion::V1_3 => (1, 3),
640 MatrixVersion::V1_4 => (1, 4),
641 MatrixVersion::V1_5 => (1, 5),
642 MatrixVersion::V1_6 => (1, 6),
643 MatrixVersion::V1_7 => (1, 7),
644 MatrixVersion::V1_8 => (1, 8),
645 MatrixVersion::V1_9 => (1, 9),
646 MatrixVersion::V1_10 => (1, 10),
647 MatrixVersion::V1_11 => (1, 11),
648 MatrixVersion::V1_12 => (1, 12),
649 MatrixVersion::V1_13 => (1, 13),
650 MatrixVersion::V1_14 => (1, 14),
651 }
652 }
653
654 pub const fn from_parts(major: u8, minor: u8) -> Result<Self, UnknownVersionError> {
656 match (major, minor) {
657 (1, 0) => Ok(MatrixVersion::V1_0),
658 (1, 1) => Ok(MatrixVersion::V1_1),
659 (1, 2) => Ok(MatrixVersion::V1_2),
660 (1, 3) => Ok(MatrixVersion::V1_3),
661 (1, 4) => Ok(MatrixVersion::V1_4),
662 (1, 5) => Ok(MatrixVersion::V1_5),
663 (1, 6) => Ok(MatrixVersion::V1_6),
664 (1, 7) => Ok(MatrixVersion::V1_7),
665 (1, 8) => Ok(MatrixVersion::V1_8),
666 (1, 9) => Ok(MatrixVersion::V1_9),
667 (1, 10) => Ok(MatrixVersion::V1_10),
668 (1, 11) => Ok(MatrixVersion::V1_11),
669 (1, 12) => Ok(MatrixVersion::V1_12),
670 (1, 13) => Ok(MatrixVersion::V1_13),
671 (1, 14) => Ok(MatrixVersion::V1_14),
672 _ => Err(UnknownVersionError),
673 }
674 }
675
676 #[doc(hidden)]
680 pub const fn from_lit(lit: &'static str) -> Self {
681 use konst::{option, primitive::parse_u8, result, string};
682
683 let major: u8;
684 let minor: u8;
685
686 let mut lit_iter = string::split(lit, ".").next();
687
688 {
689 let (checked_first, checked_split) = option::unwrap!(lit_iter); major = result::unwrap_or_else!(parse_u8(checked_first), |_| panic!(
692 "major version is not a valid number"
693 ));
694
695 lit_iter = checked_split.next();
696 }
697
698 match lit_iter {
699 Some((checked_second, checked_split)) => {
700 minor = result::unwrap_or_else!(parse_u8(checked_second), |_| panic!(
701 "minor version is not a valid number"
702 ));
703
704 lit_iter = checked_split.next();
705 }
706 None => panic!("could not find dot to denote second number"),
707 }
708
709 if lit_iter.is_some() {
710 panic!("version literal contains more than one dot")
711 }
712
713 result::unwrap_or_else!(Self::from_parts(major, minor), |_| panic!(
714 "not a valid version literal"
715 ))
716 }
717
718 const fn const_ord(&self, other: &Self) -> Ordering {
720 let self_parts = self.into_parts();
721 let other_parts = other.into_parts();
722
723 use konst::primitive::cmp::cmp_u8;
724
725 let major_ord = cmp_u8(self_parts.0, other_parts.0);
726 if major_ord.is_ne() {
727 major_ord
728 } else {
729 cmp_u8(self_parts.1, other_parts.1)
730 }
731 }
732
733 const fn is_legacy(&self) -> bool {
735 let self_parts = self.into_parts();
736
737 use konst::primitive::cmp::cmp_u8;
738
739 cmp_u8(self_parts.0, 1).is_eq() && cmp_u8(self_parts.1, 0).is_eq()
740 }
741
742 pub fn default_room_version(&self) -> RoomVersionId {
744 match self {
745 MatrixVersion::V1_0
747 | MatrixVersion::V1_1
749 | MatrixVersion::V1_2 => RoomVersionId::V6,
751 MatrixVersion::V1_3
753 | MatrixVersion::V1_4
755 | MatrixVersion::V1_5 => RoomVersionId::V9,
757 MatrixVersion::V1_6
759 | MatrixVersion::V1_7
761 | MatrixVersion::V1_8
763 | MatrixVersion::V1_9
765 | MatrixVersion::V1_10
767 | MatrixVersion::V1_11
769 | MatrixVersion::V1_12
771 | MatrixVersion::V1_13 => RoomVersionId::V10,
773 | MatrixVersion::V1_14 => RoomVersionId::V11,
775 }
776 }
777}
778
779impl Display for MatrixVersion {
780 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
781 let (major, minor) = self.into_parts();
782 f.write_str(&format!("v{major}.{minor}"))
783 }
784}
785
786#[cfg(test)]
787mod tests {
788 use assert_matches2::assert_matches;
789 use http::Method;
790
791 use super::{
792 AuthScheme,
793 MatrixVersion::{self, V1_0, V1_1, V1_2, V1_3},
794 Metadata, VersionHistory,
795 };
796 use crate::api::error::IntoHttpError;
797
798 fn stable_only_metadata(stable_paths: &'static [(MatrixVersion, &'static str)]) -> Metadata {
799 Metadata {
800 method: Method::GET,
801 rate_limited: false,
802 authentication: AuthScheme::None,
803 history: VersionHistory {
804 unstable_paths: &[],
805 stable_paths,
806 deprecated: None,
807 removed: None,
808 },
809 }
810 }
811
812 #[test]
815 fn make_simple_endpoint_url() {
816 let meta = stable_only_metadata(&[(V1_0, "/s")]);
817 let url = meta.make_endpoint_url(&[V1_0], "https://example.org", &[], "").unwrap();
818 assert_eq!(url, "https://example.org/s");
819 }
820
821 #[test]
822 fn make_endpoint_url_with_path_args() {
823 let meta = stable_only_metadata(&[(V1_0, "/s/:x")]);
824 let url = meta.make_endpoint_url(&[V1_0], "https://example.org", &[&"123"], "").unwrap();
825 assert_eq!(url, "https://example.org/s/123");
826 }
827
828 #[test]
829 fn make_endpoint_url_with_path_args_with_dash() {
830 let meta = stable_only_metadata(&[(V1_0, "/s/:x")]);
831 let url =
832 meta.make_endpoint_url(&[V1_0], "https://example.org", &[&"my-path"], "").unwrap();
833 assert_eq!(url, "https://example.org/s/my-path");
834 }
835
836 #[test]
837 fn make_endpoint_url_with_path_args_with_reserved_char() {
838 let meta = stable_only_metadata(&[(V1_0, "/s/:x")]);
839 let url = meta.make_endpoint_url(&[V1_0], "https://example.org", &[&"#path"], "").unwrap();
840 assert_eq!(url, "https://example.org/s/%23path");
841 }
842
843 #[test]
844 fn make_endpoint_url_with_query() {
845 let meta = stable_only_metadata(&[(V1_0, "/s/")]);
846 let url = meta.make_endpoint_url(&[V1_0], "https://example.org", &[], "foo=bar").unwrap();
847 assert_eq!(url, "https://example.org/s/?foo=bar");
848 }
849
850 #[test]
851 #[should_panic]
852 fn make_endpoint_url_wrong_num_path_args() {
853 let meta = stable_only_metadata(&[(V1_0, "/s/:x")]);
854 _ = meta.make_endpoint_url(&[V1_0], "https://example.org", &[], "");
855 }
856
857 const EMPTY: VersionHistory =
858 VersionHistory { unstable_paths: &[], stable_paths: &[], deprecated: None, removed: None };
859
860 #[test]
861 fn select_latest_stable() {
862 let hist = VersionHistory { stable_paths: &[(V1_1, "/s")], ..EMPTY };
863 assert_matches!(hist.select_path(&[V1_0, V1_1]), Ok("/s"));
864 }
865
866 #[test]
867 fn select_unstable() {
868 let hist = VersionHistory { unstable_paths: &["/u"], ..EMPTY };
869 assert_matches!(hist.select_path(&[V1_0]), Ok("/u"));
870 }
871
872 #[test]
873 fn select_r0() {
874 let hist = VersionHistory { stable_paths: &[(V1_0, "/r")], ..EMPTY };
875 assert_matches!(hist.select_path(&[V1_0]), Ok("/r"));
876 }
877
878 #[test]
879 fn select_removed_err() {
880 let hist = VersionHistory {
881 stable_paths: &[(V1_0, "/r"), (V1_1, "/s")],
882 unstable_paths: &["/u"],
883 deprecated: Some(V1_2),
884 removed: Some(V1_3),
885 };
886 assert_matches!(hist.select_path(&[V1_3]), Err(IntoHttpError::EndpointRemoved(V1_3)));
887 }
888
889 #[test]
890 fn partially_removed_but_stable() {
891 let hist = VersionHistory {
892 stable_paths: &[(V1_0, "/r"), (V1_1, "/s")],
893 unstable_paths: &[],
894 deprecated: Some(V1_2),
895 removed: Some(V1_3),
896 };
897 assert_matches!(hist.select_path(&[V1_2]), Ok("/s"));
898 }
899
900 #[test]
901 fn no_unstable() {
902 let hist = VersionHistory { stable_paths: &[(V1_1, "/s")], ..EMPTY };
903 assert_matches!(hist.select_path(&[V1_0]), Err(IntoHttpError::NoUnstablePath));
904 }
905
906 #[test]
907 fn version_literal() {
908 const LIT: MatrixVersion = MatrixVersion::from_lit("1.0");
909
910 assert_eq!(LIT, V1_0);
911 }
912}