diff --git a/crates/ruma-common/Cargo.toml b/crates/ruma-common/Cargo.toml index 5a78a377..fe575b3a 100644 --- a/crates/ruma-common/Cargo.toml +++ b/crates/ruma-common/Cargo.toml @@ -57,6 +57,7 @@ indexmap = { version = "1.9.1", features = ["serde"] } itoa = "1.0.1" js_int = { version = "0.2.0", features = ["serde"] } js_option = "0.1.0" +konst = { version = "0.2.19", features = ["rust_1_64", "alloc"] } percent-encoding = "2.1.0" phf = { version = "0.10.1", features = ["macros"], optional = true } pulldown-cmark = { version = "0.9.1", default-features = false, optional = true } @@ -83,7 +84,7 @@ assert_matches = "1.5.0" assign = "1.1.1" http = "0.2.2" maplit = "1.0.2" -trybuild = "1.0.42" +trybuild = "1.0.71" [[bench]] name = "event_deserialize" diff --git a/crates/ruma-common/src/api/metadata.rs b/crates/ruma-common/src/api/metadata.rs index bc46a162..70f35a9d 100644 --- a/crates/ruma-common/src/api/metadata.rs +++ b/crates/ruma-common/src/api/metadata.rs @@ -1,4 +1,5 @@ use std::{ + cmp::Ordering, fmt::{self, Display, Write}, str::FromStr, }; @@ -158,6 +159,144 @@ pub struct VersionHistory { } impl VersionHistory { + /// Constructs an instance of [`VersionHistory`], erroring on compilation if it does not pass + /// invariants. + /// + /// Specifically, this checks the following invariants: + /// - Path Arguments are equal (in order, amount, and argument name) in all path strings + /// - In stable_paths: + /// - matrix versions are in ascending order + /// - no matrix version is referenced twice + /// - deprecated's version comes after the latest version mentioned in stable_paths, and only if + /// any stable path is defined + /// - removed comes after deprecated, or after the latest referenced stable_paths, like + /// deprecated + pub const fn new( + unstable_paths: &'static [&'static str], + stable_paths: &'static [(MatrixVersion, &'static str)], + deprecated: Option, + removed: Option, + ) -> Self { + use konst::{iter, slice, string}; + + const fn check_path_is_valid(path: &'static str) { + iter::for_each!(path_b in slice::iter(path.as_bytes()) => { + match *path_b { + 0x21..=0x7E => {}, + _ => panic!("path contains invalid (non-ascii or whitespace) characters") + } + }); + } + + const fn check_path_args_equal(first: &'static str, second: &'static str) { + let mut second_iter = string::split(second, "/").next(); + + iter::for_each!(first_s in string::split(first, "/") => { + if let Some(first_arg) = string::strip_prefix(first_s, ":") { + let second_next_arg: Option<&'static str> = loop { + let (second_s, second_n_iter) = match second_iter { + Some(tuple) => tuple, + None => break None, + }; + + let maybe_second_arg = string::strip_prefix(second_s, ":"); + + second_iter = second_n_iter.next(); + + if let Some(second_arg) = maybe_second_arg { + break Some(second_arg); + } + }; + + if let Some(second_next_arg) = second_next_arg { + if !string::eq_str(second_next_arg, first_arg) { + panic!("Path Arguments do not match"); + } + } else { + panic!("Amount of Path Arguments do not match"); + } + } + }); + + // If second iterator still has some values, empty first. + while let Some((second_s, second_n_iter)) = second_iter { + if string::starts_with(second_s, ":") { + panic!("Amount of Path Arguments do not match"); + } + second_iter = second_n_iter.next(); + } + } + + // The path we're going to use to compare all other paths with + let ref_path: &str = if let Some(s) = unstable_paths.first() { + s + } else if let Some((_, s)) = stable_paths.first() { + s + } else { + panic!("No paths supplied") + }; + + iter::for_each!(unstable_path in slice::iter(unstable_paths) => { + check_path_is_valid(unstable_path); + check_path_args_equal(ref_path, unstable_path); + }); + + let mut prev_seen_version: Option = None; + + iter::for_each!(stable_path in slice::iter(stable_paths) => { + check_path_is_valid(stable_path.1); + check_path_args_equal(ref_path, stable_path.1); + + let current_version = stable_path.0; + + if let Some(prev_seen_version) = prev_seen_version { + let cmp_result = current_version.const_ord(&prev_seen_version); + + if cmp_result.is_eq() { + // Found a duplicate, current == previous + panic!("Duplicate matrix version in stable_paths") + } else if cmp_result.is_lt() { + // Found an older version, current < previous + panic!("No ascending order in stable_paths") + } + } + + prev_seen_version = Some(current_version); + }); + + if let Some(deprecated) = deprecated { + if let Some(prev_seen_version) = prev_seen_version { + let ord_result = prev_seen_version.const_ord(&deprecated); + if ord_result.is_eq() { + // prev_seen_version == deprecated + panic!("deprecated version is equal to latest stable path version") + } else if ord_result.is_gt() { + // prev_seen_version > deprecated + panic!("deprecated version is older than latest stable path version") + } + } else { + panic!("Defined deprecated version while no stable path exists") + } + } + + if let Some(removed) = removed { + if let Some(deprecated) = deprecated { + let ord_result = deprecated.const_ord(&removed); + if ord_result.is_eq() { + // deprecated == removed + panic!("removed version is equal to deprecated version") + } else if ord_result.is_gt() { + // deprecated > removed + panic!("removed version is older than deprecated version") + } + } else { + panic!("Defined removed version while no deprecated version exists") + } + } + + VersionHistory { unstable_paths, stable_paths, deprecated, removed } + } + // This function helps picks the right path (or an error) from a set of Matrix versions. fn select_path( &self, @@ -409,7 +548,7 @@ impl MatrixVersion { } /// Decompose the Matrix version into its major and minor number. - pub fn into_parts(self) -> (u8, u8) { + pub const fn into_parts(self) -> (u8, u8) { match self { MatrixVersion::V1_0 => (1, 0), MatrixVersion::V1_1 => (1, 1), @@ -431,6 +570,21 @@ impl MatrixVersion { } } + // Internal function to do ordering in const-fn contexts + const fn const_ord(&self, other: &Self) -> Ordering { + let self_parts = self.into_parts(); + let other_parts = other.into_parts(); + + use konst::primitive::cmp::cmp_u8; + + let major_ord = cmp_u8(self_parts.0, other_parts.0); + if major_ord.is_ne() { + major_ord + } else { + cmp_u8(self_parts.1, other_parts.1) + } + } + /// Get the default [`RoomVersionId`] for this `MatrixVersion`. pub fn default_room_version(&self) -> RoomVersionId { match self { diff --git a/crates/ruma-common/tests/api/manual_endpoint_impl.rs b/crates/ruma-common/tests/api/manual_endpoint_impl.rs index 227f32dd..34b4cbd9 100644 --- a/crates/ruma-common/tests/api/manual_endpoint_impl.rs +++ b/crates/ruma-common/tests/api/manual_endpoint_impl.rs @@ -30,15 +30,15 @@ const METADATA: Metadata = Metadata { rate_limited: false, authentication: AuthScheme::None, - history: VersionHistory { - unstable_paths: &["/_matrix/client/unstable/directory/room/:room_alias"], - stable_paths: &[ + history: VersionHistory::new( + &["/_matrix/client/unstable/directory/room/:room_alias"], + &[ (MatrixVersion::V1_0, "/_matrix/client/r0/directory/room/:room_alias"), (MatrixVersion::V1_1, "/_matrix/client/v3/directory/room/:room_alias"), ], - deprecated: Some(MatrixVersion::V1_2), - removed: Some(MatrixVersion::V1_3), - }, + Some(MatrixVersion::V1_2), + Some(MatrixVersion::V1_3), + ), }; impl OutgoingRequest for Request {