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}