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)]
488#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
489pub enum MatrixVersion {
490 V1_0,
494
495 V1_1,
499
500 V1_2,
504
505 V1_3,
509
510 V1_4,
514
515 V1_5,
519
520 V1_6,
524
525 V1_7,
529
530 V1_8,
534
535 V1_9,
539
540 V1_10,
544
545 V1_11,
549
550 V1_12,
554
555 V1_13,
559}
560
561impl TryFrom<&str> for MatrixVersion {
562 type Error = UnknownVersionError;
563
564 fn try_from(value: &str) -> Result<MatrixVersion, Self::Error> {
565 use MatrixVersion::*;
566
567 Ok(match value {
568 "v1.0" |
570 "r0.5.0" | "r0.6.0" | "r0.6.1" => V1_0,
572 "v1.1" => V1_1,
573 "v1.2" => V1_2,
574 "v1.3" => V1_3,
575 "v1.4" => V1_4,
576 "v1.5" => V1_5,
577 "v1.6" => V1_6,
578 "v1.7" => V1_7,
579 "v1.8" => V1_8,
580 "v1.9" => V1_9,
581 "v1.10" => V1_10,
582 "v1.11" => V1_11,
583 "v1.12" => V1_12,
584 "v1.13" => V1_13,
585 _ => return Err(UnknownVersionError),
586 })
587 }
588}
589
590impl FromStr for MatrixVersion {
591 type Err = UnknownVersionError;
592
593 fn from_str(s: &str) -> Result<Self, Self::Err> {
594 Self::try_from(s)
595 }
596}
597
598impl MatrixVersion {
599 pub fn is_superset_of(self, other: Self) -> bool {
615 let (major_l, minor_l) = self.into_parts();
616 let (major_r, minor_r) = other.into_parts();
617 major_l == major_r && minor_l >= minor_r
618 }
619
620 pub const fn into_parts(self) -> (u8, u8) {
622 match self {
623 MatrixVersion::V1_0 => (1, 0),
624 MatrixVersion::V1_1 => (1, 1),
625 MatrixVersion::V1_2 => (1, 2),
626 MatrixVersion::V1_3 => (1, 3),
627 MatrixVersion::V1_4 => (1, 4),
628 MatrixVersion::V1_5 => (1, 5),
629 MatrixVersion::V1_6 => (1, 6),
630 MatrixVersion::V1_7 => (1, 7),
631 MatrixVersion::V1_8 => (1, 8),
632 MatrixVersion::V1_9 => (1, 9),
633 MatrixVersion::V1_10 => (1, 10),
634 MatrixVersion::V1_11 => (1, 11),
635 MatrixVersion::V1_12 => (1, 12),
636 MatrixVersion::V1_13 => (1, 13),
637 }
638 }
639
640 pub const fn from_parts(major: u8, minor: u8) -> Result<Self, UnknownVersionError> {
642 match (major, minor) {
643 (1, 0) => Ok(MatrixVersion::V1_0),
644 (1, 1) => Ok(MatrixVersion::V1_1),
645 (1, 2) => Ok(MatrixVersion::V1_2),
646 (1, 3) => Ok(MatrixVersion::V1_3),
647 (1, 4) => Ok(MatrixVersion::V1_4),
648 (1, 5) => Ok(MatrixVersion::V1_5),
649 (1, 6) => Ok(MatrixVersion::V1_6),
650 (1, 7) => Ok(MatrixVersion::V1_7),
651 (1, 8) => Ok(MatrixVersion::V1_8),
652 (1, 9) => Ok(MatrixVersion::V1_9),
653 (1, 10) => Ok(MatrixVersion::V1_10),
654 (1, 11) => Ok(MatrixVersion::V1_11),
655 (1, 12) => Ok(MatrixVersion::V1_12),
656 (1, 13) => Ok(MatrixVersion::V1_13),
657 _ => Err(UnknownVersionError),
658 }
659 }
660
661 #[doc(hidden)]
665 pub const fn from_lit(lit: &'static str) -> Self {
666 use konst::{option, primitive::parse_u8, result, string};
667
668 let major: u8;
669 let minor: u8;
670
671 let mut lit_iter = string::split(lit, ".").next();
672
673 {
674 let (checked_first, checked_split) = option::unwrap!(lit_iter); major = result::unwrap_or_else!(parse_u8(checked_first), |_| panic!(
677 "major version is not a valid number"
678 ));
679
680 lit_iter = checked_split.next();
681 }
682
683 match lit_iter {
684 Some((checked_second, checked_split)) => {
685 minor = result::unwrap_or_else!(parse_u8(checked_second), |_| panic!(
686 "minor version is not a valid number"
687 ));
688
689 lit_iter = checked_split.next();
690 }
691 None => panic!("could not find dot to denote second number"),
692 }
693
694 if lit_iter.is_some() {
695 panic!("version literal contains more than one dot")
696 }
697
698 result::unwrap_or_else!(Self::from_parts(major, minor), |_| panic!(
699 "not a valid version literal"
700 ))
701 }
702
703 const fn const_ord(&self, other: &Self) -> Ordering {
705 let self_parts = self.into_parts();
706 let other_parts = other.into_parts();
707
708 use konst::primitive::cmp::cmp_u8;
709
710 let major_ord = cmp_u8(self_parts.0, other_parts.0);
711 if major_ord.is_ne() {
712 major_ord
713 } else {
714 cmp_u8(self_parts.1, other_parts.1)
715 }
716 }
717
718 const fn is_legacy(&self) -> bool {
720 let self_parts = self.into_parts();
721
722 use konst::primitive::cmp::cmp_u8;
723
724 cmp_u8(self_parts.0, 1).is_eq() && cmp_u8(self_parts.1, 0).is_eq()
725 }
726
727 pub fn default_room_version(&self) -> RoomVersionId {
729 match self {
730 MatrixVersion::V1_0
732 | MatrixVersion::V1_1
734 | MatrixVersion::V1_2 => RoomVersionId::V6,
736 MatrixVersion::V1_3
738 | MatrixVersion::V1_4
740 | MatrixVersion::V1_5 => RoomVersionId::V9,
742 MatrixVersion::V1_6
744 | MatrixVersion::V1_7
746 | MatrixVersion::V1_8
748 | MatrixVersion::V1_9
750 | MatrixVersion::V1_10
752 | MatrixVersion::V1_11
754 | MatrixVersion::V1_12
756 | MatrixVersion::V1_13 => RoomVersionId::V10,
758 }
759 }
760}
761
762impl Display for MatrixVersion {
763 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
764 let (major, minor) = self.into_parts();
765 f.write_str(&format!("v{major}.{minor}"))
766 }
767}
768
769#[cfg(test)]
770mod tests {
771 use assert_matches2::assert_matches;
772 use http::Method;
773
774 use super::{
775 AuthScheme,
776 MatrixVersion::{self, V1_0, V1_1, V1_2, V1_3},
777 Metadata, VersionHistory,
778 };
779 use crate::api::error::IntoHttpError;
780
781 fn stable_only_metadata(stable_paths: &'static [(MatrixVersion, &'static str)]) -> Metadata {
782 Metadata {
783 method: Method::GET,
784 rate_limited: false,
785 authentication: AuthScheme::None,
786 history: VersionHistory {
787 unstable_paths: &[],
788 stable_paths,
789 deprecated: None,
790 removed: None,
791 },
792 }
793 }
794
795 #[test]
798 fn make_simple_endpoint_url() {
799 let meta = stable_only_metadata(&[(V1_0, "/s")]);
800 let url = meta.make_endpoint_url(&[V1_0], "https://example.org", &[], "").unwrap();
801 assert_eq!(url, "https://example.org/s");
802 }
803
804 #[test]
805 fn make_endpoint_url_with_path_args() {
806 let meta = stable_only_metadata(&[(V1_0, "/s/:x")]);
807 let url = meta.make_endpoint_url(&[V1_0], "https://example.org", &[&"123"], "").unwrap();
808 assert_eq!(url, "https://example.org/s/123");
809 }
810
811 #[test]
812 fn make_endpoint_url_with_path_args_with_dash() {
813 let meta = stable_only_metadata(&[(V1_0, "/s/:x")]);
814 let url =
815 meta.make_endpoint_url(&[V1_0], "https://example.org", &[&"my-path"], "").unwrap();
816 assert_eq!(url, "https://example.org/s/my-path");
817 }
818
819 #[test]
820 fn make_endpoint_url_with_path_args_with_reserved_char() {
821 let meta = stable_only_metadata(&[(V1_0, "/s/:x")]);
822 let url = meta.make_endpoint_url(&[V1_0], "https://example.org", &[&"#path"], "").unwrap();
823 assert_eq!(url, "https://example.org/s/%23path");
824 }
825
826 #[test]
827 fn make_endpoint_url_with_query() {
828 let meta = stable_only_metadata(&[(V1_0, "/s/")]);
829 let url = meta.make_endpoint_url(&[V1_0], "https://example.org", &[], "foo=bar").unwrap();
830 assert_eq!(url, "https://example.org/s/?foo=bar");
831 }
832
833 #[test]
834 #[should_panic]
835 fn make_endpoint_url_wrong_num_path_args() {
836 let meta = stable_only_metadata(&[(V1_0, "/s/:x")]);
837 _ = meta.make_endpoint_url(&[V1_0], "https://example.org", &[], "");
838 }
839
840 const EMPTY: VersionHistory =
841 VersionHistory { unstable_paths: &[], stable_paths: &[], deprecated: None, removed: None };
842
843 #[test]
844 fn select_latest_stable() {
845 let hist = VersionHistory { stable_paths: &[(V1_1, "/s")], ..EMPTY };
846 assert_matches!(hist.select_path(&[V1_0, V1_1]), Ok("/s"));
847 }
848
849 #[test]
850 fn select_unstable() {
851 let hist = VersionHistory { unstable_paths: &["/u"], ..EMPTY };
852 assert_matches!(hist.select_path(&[V1_0]), Ok("/u"));
853 }
854
855 #[test]
856 fn select_r0() {
857 let hist = VersionHistory { stable_paths: &[(V1_0, "/r")], ..EMPTY };
858 assert_matches!(hist.select_path(&[V1_0]), Ok("/r"));
859 }
860
861 #[test]
862 fn select_removed_err() {
863 let hist = VersionHistory {
864 stable_paths: &[(V1_0, "/r"), (V1_1, "/s")],
865 unstable_paths: &["/u"],
866 deprecated: Some(V1_2),
867 removed: Some(V1_3),
868 };
869 assert_matches!(hist.select_path(&[V1_3]), Err(IntoHttpError::EndpointRemoved(V1_3)));
870 }
871
872 #[test]
873 fn partially_removed_but_stable() {
874 let hist = VersionHistory {
875 stable_paths: &[(V1_0, "/r"), (V1_1, "/s")],
876 unstable_paths: &[],
877 deprecated: Some(V1_2),
878 removed: Some(V1_3),
879 };
880 assert_matches!(hist.select_path(&[V1_2]), Ok("/s"));
881 }
882
883 #[test]
884 fn no_unstable() {
885 let hist = VersionHistory { stable_paths: &[(V1_1, "/s")], ..EMPTY };
886 assert_matches!(hist.select_path(&[V1_0]), Err(IntoHttpError::NoUnstablePath));
887 }
888
889 #[test]
890 fn version_literal() {
891 const LIT: MatrixVersion = MatrixVersion::from_lit("1.0");
892
893 assert_eq!(LIT, V1_0);
894 }
895}