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
575impl TryFrom<&str> for MatrixVersion {
576 type Error = UnknownVersionError;
577
578 fn try_from(value: &str) -> Result<MatrixVersion, Self::Error> {
579 use MatrixVersion::*;
580
581 Ok(match value {
582 "r0.2.0" | "r0.2.1" | "r0.3.0" |
585 "r0.5.0" | "r0.6.0" | "r0.6.1" => V1_0,
587 "v1.1" => V1_1,
588 "v1.2" => V1_2,
589 "v1.3" => V1_3,
590 "v1.4" => V1_4,
591 "v1.5" => V1_5,
592 "v1.6" => V1_6,
593 "v1.7" => V1_7,
594 "v1.8" => V1_8,
595 "v1.9" => V1_9,
596 "v1.10" => V1_10,
597 "v1.11" => V1_11,
598 "v1.12" => V1_12,
599 "v1.13" => V1_13,
600 _ => return Err(UnknownVersionError),
601 })
602 }
603}
604
605impl FromStr for MatrixVersion {
606 type Err = UnknownVersionError;
607
608 fn from_str(s: &str) -> Result<Self, Self::Err> {
609 Self::try_from(s)
610 }
611}
612
613impl MatrixVersion {
614 pub fn is_superset_of(self, other: Self) -> bool {
624 self >= other
625 }
626
627 pub const fn into_parts(self) -> (u8, u8) {
629 match self {
630 MatrixVersion::V1_0 => (1, 0),
631 MatrixVersion::V1_1 => (1, 1),
632 MatrixVersion::V1_2 => (1, 2),
633 MatrixVersion::V1_3 => (1, 3),
634 MatrixVersion::V1_4 => (1, 4),
635 MatrixVersion::V1_5 => (1, 5),
636 MatrixVersion::V1_6 => (1, 6),
637 MatrixVersion::V1_7 => (1, 7),
638 MatrixVersion::V1_8 => (1, 8),
639 MatrixVersion::V1_9 => (1, 9),
640 MatrixVersion::V1_10 => (1, 10),
641 MatrixVersion::V1_11 => (1, 11),
642 MatrixVersion::V1_12 => (1, 12),
643 MatrixVersion::V1_13 => (1, 13),
644 }
645 }
646
647 pub const fn from_parts(major: u8, minor: u8) -> Result<Self, UnknownVersionError> {
649 match (major, minor) {
650 (1, 0) => Ok(MatrixVersion::V1_0),
651 (1, 1) => Ok(MatrixVersion::V1_1),
652 (1, 2) => Ok(MatrixVersion::V1_2),
653 (1, 3) => Ok(MatrixVersion::V1_3),
654 (1, 4) => Ok(MatrixVersion::V1_4),
655 (1, 5) => Ok(MatrixVersion::V1_5),
656 (1, 6) => Ok(MatrixVersion::V1_6),
657 (1, 7) => Ok(MatrixVersion::V1_7),
658 (1, 8) => Ok(MatrixVersion::V1_8),
659 (1, 9) => Ok(MatrixVersion::V1_9),
660 (1, 10) => Ok(MatrixVersion::V1_10),
661 (1, 11) => Ok(MatrixVersion::V1_11),
662 (1, 12) => Ok(MatrixVersion::V1_12),
663 (1, 13) => Ok(MatrixVersion::V1_13),
664 _ => Err(UnknownVersionError),
665 }
666 }
667
668 #[doc(hidden)]
672 pub const fn from_lit(lit: &'static str) -> Self {
673 use konst::{option, primitive::parse_u8, result, string};
674
675 let major: u8;
676 let minor: u8;
677
678 let mut lit_iter = string::split(lit, ".").next();
679
680 {
681 let (checked_first, checked_split) = option::unwrap!(lit_iter); major = result::unwrap_or_else!(parse_u8(checked_first), |_| panic!(
684 "major version is not a valid number"
685 ));
686
687 lit_iter = checked_split.next();
688 }
689
690 match lit_iter {
691 Some((checked_second, checked_split)) => {
692 minor = result::unwrap_or_else!(parse_u8(checked_second), |_| panic!(
693 "minor version is not a valid number"
694 ));
695
696 lit_iter = checked_split.next();
697 }
698 None => panic!("could not find dot to denote second number"),
699 }
700
701 if lit_iter.is_some() {
702 panic!("version literal contains more than one dot")
703 }
704
705 result::unwrap_or_else!(Self::from_parts(major, minor), |_| panic!(
706 "not a valid version literal"
707 ))
708 }
709
710 const fn const_ord(&self, other: &Self) -> Ordering {
712 let self_parts = self.into_parts();
713 let other_parts = other.into_parts();
714
715 use konst::primitive::cmp::cmp_u8;
716
717 let major_ord = cmp_u8(self_parts.0, other_parts.0);
718 if major_ord.is_ne() {
719 major_ord
720 } else {
721 cmp_u8(self_parts.1, other_parts.1)
722 }
723 }
724
725 const fn is_legacy(&self) -> bool {
727 let self_parts = self.into_parts();
728
729 use konst::primitive::cmp::cmp_u8;
730
731 cmp_u8(self_parts.0, 1).is_eq() && cmp_u8(self_parts.1, 0).is_eq()
732 }
733
734 pub fn default_room_version(&self) -> RoomVersionId {
736 match self {
737 MatrixVersion::V1_0
739 | MatrixVersion::V1_1
741 | MatrixVersion::V1_2 => RoomVersionId::V6,
743 MatrixVersion::V1_3
745 | MatrixVersion::V1_4
747 | MatrixVersion::V1_5 => RoomVersionId::V9,
749 MatrixVersion::V1_6
751 | MatrixVersion::V1_7
753 | MatrixVersion::V1_8
755 | MatrixVersion::V1_9
757 | MatrixVersion::V1_10
759 | MatrixVersion::V1_11
761 | MatrixVersion::V1_12
763 | MatrixVersion::V1_13 => RoomVersionId::V10,
765 }
766 }
767}
768
769impl Display for MatrixVersion {
770 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
771 let (major, minor) = self.into_parts();
772 f.write_str(&format!("v{major}.{minor}"))
773 }
774}
775
776#[cfg(test)]
777mod tests {
778 use assert_matches2::assert_matches;
779 use http::Method;
780
781 use super::{
782 AuthScheme,
783 MatrixVersion::{self, V1_0, V1_1, V1_2, V1_3},
784 Metadata, VersionHistory,
785 };
786 use crate::api::error::IntoHttpError;
787
788 fn stable_only_metadata(stable_paths: &'static [(MatrixVersion, &'static str)]) -> Metadata {
789 Metadata {
790 method: Method::GET,
791 rate_limited: false,
792 authentication: AuthScheme::None,
793 history: VersionHistory {
794 unstable_paths: &[],
795 stable_paths,
796 deprecated: None,
797 removed: None,
798 },
799 }
800 }
801
802 #[test]
805 fn make_simple_endpoint_url() {
806 let meta = stable_only_metadata(&[(V1_0, "/s")]);
807 let url = meta.make_endpoint_url(&[V1_0], "https://example.org", &[], "").unwrap();
808 assert_eq!(url, "https://example.org/s");
809 }
810
811 #[test]
812 fn make_endpoint_url_with_path_args() {
813 let meta = stable_only_metadata(&[(V1_0, "/s/:x")]);
814 let url = meta.make_endpoint_url(&[V1_0], "https://example.org", &[&"123"], "").unwrap();
815 assert_eq!(url, "https://example.org/s/123");
816 }
817
818 #[test]
819 fn make_endpoint_url_with_path_args_with_dash() {
820 let meta = stable_only_metadata(&[(V1_0, "/s/:x")]);
821 let url =
822 meta.make_endpoint_url(&[V1_0], "https://example.org", &[&"my-path"], "").unwrap();
823 assert_eq!(url, "https://example.org/s/my-path");
824 }
825
826 #[test]
827 fn make_endpoint_url_with_path_args_with_reserved_char() {
828 let meta = stable_only_metadata(&[(V1_0, "/s/:x")]);
829 let url = meta.make_endpoint_url(&[V1_0], "https://example.org", &[&"#path"], "").unwrap();
830 assert_eq!(url, "https://example.org/s/%23path");
831 }
832
833 #[test]
834 fn make_endpoint_url_with_query() {
835 let meta = stable_only_metadata(&[(V1_0, "/s/")]);
836 let url = meta.make_endpoint_url(&[V1_0], "https://example.org", &[], "foo=bar").unwrap();
837 assert_eq!(url, "https://example.org/s/?foo=bar");
838 }
839
840 #[test]
841 #[should_panic]
842 fn make_endpoint_url_wrong_num_path_args() {
843 let meta = stable_only_metadata(&[(V1_0, "/s/:x")]);
844 _ = meta.make_endpoint_url(&[V1_0], "https://example.org", &[], "");
845 }
846
847 const EMPTY: VersionHistory =
848 VersionHistory { unstable_paths: &[], stable_paths: &[], deprecated: None, removed: None };
849
850 #[test]
851 fn select_latest_stable() {
852 let hist = VersionHistory { stable_paths: &[(V1_1, "/s")], ..EMPTY };
853 assert_matches!(hist.select_path(&[V1_0, V1_1]), Ok("/s"));
854 }
855
856 #[test]
857 fn select_unstable() {
858 let hist = VersionHistory { unstable_paths: &["/u"], ..EMPTY };
859 assert_matches!(hist.select_path(&[V1_0]), Ok("/u"));
860 }
861
862 #[test]
863 fn select_r0() {
864 let hist = VersionHistory { stable_paths: &[(V1_0, "/r")], ..EMPTY };
865 assert_matches!(hist.select_path(&[V1_0]), Ok("/r"));
866 }
867
868 #[test]
869 fn select_removed_err() {
870 let hist = VersionHistory {
871 stable_paths: &[(V1_0, "/r"), (V1_1, "/s")],
872 unstable_paths: &["/u"],
873 deprecated: Some(V1_2),
874 removed: Some(V1_3),
875 };
876 assert_matches!(hist.select_path(&[V1_3]), Err(IntoHttpError::EndpointRemoved(V1_3)));
877 }
878
879 #[test]
880 fn partially_removed_but_stable() {
881 let hist = VersionHistory {
882 stable_paths: &[(V1_0, "/r"), (V1_1, "/s")],
883 unstable_paths: &[],
884 deprecated: Some(V1_2),
885 removed: Some(V1_3),
886 };
887 assert_matches!(hist.select_path(&[V1_2]), Ok("/s"));
888 }
889
890 #[test]
891 fn no_unstable() {
892 let hist = VersionHistory { stable_paths: &[(V1_1, "/s")], ..EMPTY };
893 assert_matches!(hist.select_path(&[V1_0]), Err(IntoHttpError::NoUnstablePath));
894 }
895
896 #[test]
897 fn version_literal() {
898 const LIT: MatrixVersion = MatrixVersion::from_lit("1.0");
899
900 assert_eq!(LIT, V1_0);
901 }
902}