diff --git a/ruma-identifiers-validation/Cargo.toml b/ruma-identifiers-validation/Cargo.toml new file mode 100644 index 00000000..81cf8bf1 --- /dev/null +++ b/ruma-identifiers-validation/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "ruma-identifiers-validation" +description = "Validation logic for ruma-identifiers and ruma-identifiers-macros" +documentation = "https://docs.rs/ruma-identifiers" +homepage = "https://www.ruma.io/" +repository = "https://github.com/ruma/ruma" +authors = [ + "Jimmy Cuadra ", + "Jonas Platte ", +] +license = "MIT" +version = "0.1.0" +edition = "2018" + +[dependencies] +strum = { version = "0.18.0", features = ["derive"] } diff --git a/ruma-identifiers-validation/LICENSE b/ruma-identifiers-validation/LICENSE new file mode 100644 index 00000000..de62627d --- /dev/null +++ b/ruma-identifiers-validation/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2016 Jimmy Cuadra + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/ruma-identifiers-validation/src/device_key_id.rs b/ruma-identifiers-validation/src/device_key_id.rs new file mode 100644 index 00000000..596651c4 --- /dev/null +++ b/ruma-identifiers-validation/src/device_key_id.rs @@ -0,0 +1,13 @@ +use std::{num::NonZeroU8, str::FromStr}; + +use crate::{key_algorithms::DeviceKeyAlgorithm, Error}; + +pub fn validate(s: &str) -> Result { + let colon_idx = NonZeroU8::new(s.find(':').ok_or(Error::MissingDeviceKeyDelimiter)? as u8) + .ok_or(Error::UnknownKeyAlgorithm)?; + + DeviceKeyAlgorithm::from_str(&s[0..colon_idx.get() as usize]) + .map_err(|_| Error::UnknownKeyAlgorithm)?; + + Ok(colon_idx) +} diff --git a/ruma-identifiers/src/error.rs b/ruma-identifiers-validation/src/error.rs similarity index 100% rename from ruma-identifiers/src/error.rs rename to ruma-identifiers-validation/src/error.rs diff --git a/ruma-identifiers-validation/src/event_id.rs b/ruma-identifiers-validation/src/event_id.rs new file mode 100644 index 00000000..a6481399 --- /dev/null +++ b/ruma-identifiers-validation/src/event_id.rs @@ -0,0 +1,13 @@ +use std::num::NonZeroU8; + +use crate::{parse_id, validate_id, Error}; + +pub fn validate(s: &str) -> Result, Error> { + Ok(match s.contains(':') { + true => Some(parse_id(s, &['$'])?), + false => { + validate_id(s, &['$'])?; + None + } + }) +} diff --git a/ruma-identifiers/src/key_algorithms.rs b/ruma-identifiers-validation/src/key_algorithms.rs similarity index 100% rename from ruma-identifiers/src/key_algorithms.rs rename to ruma-identifiers-validation/src/key_algorithms.rs diff --git a/ruma-identifiers-validation/src/lib.rs b/ruma-identifiers-validation/src/lib.rs new file mode 100644 index 00000000..85785373 --- /dev/null +++ b/ruma-identifiers-validation/src/lib.rs @@ -0,0 +1,56 @@ +pub mod device_key_id; +pub mod error; +pub mod event_id; +pub mod key_algorithms; +pub mod room_alias_id; +pub mod room_id; +pub mod room_id_or_alias_id; +pub mod room_version_id; +pub mod server_key_id; +pub mod server_name; +pub mod user_id; + +use std::num::NonZeroU8; + +pub use error::Error; + +/// All identifiers must be 255 bytes or less. +const MAX_BYTES: usize = 255; + +/// The minimum number of characters an ID can be. +/// +/// This is an optimization and not required by the spec. The shortest possible valid ID is a sigil +/// + a single character local ID + a colon + a single character hostname. +const MIN_CHARS: usize = 4; + +/// Checks if an identifier is valid. +fn validate_id(id: &str, valid_sigils: &[char]) -> Result<(), Error> { + if id.len() > MAX_BYTES { + return Err(Error::MaximumLengthExceeded); + } + + if id.len() < MIN_CHARS { + return Err(Error::MinimumLengthNotSatisfied); + } + + if !valid_sigils.contains(&id.chars().next().unwrap()) { + return Err(Error::MissingSigil); + } + + Ok(()) +} + +/// Checks an identifier that contains a localpart and hostname for validity, +/// and returns the index of the colon that separates the two. +fn parse_id(id: &str, valid_sigils: &[char]) -> Result { + validate_id(id, valid_sigils)?; + + let colon_idx = id.find(':').ok_or(Error::MissingDelimiter)?; + if colon_idx < 2 { + return Err(Error::InvalidLocalPart); + } + + server_name::validate(&id[colon_idx + 1..])?; + + Ok(NonZeroU8::new(colon_idx as u8).unwrap()) +} diff --git a/ruma-identifiers-validation/src/room_alias_id.rs b/ruma-identifiers-validation/src/room_alias_id.rs new file mode 100644 index 00000000..48ab1220 --- /dev/null +++ b/ruma-identifiers-validation/src/room_alias_id.rs @@ -0,0 +1,7 @@ +use std::num::NonZeroU8; + +use crate::{parse_id, Error}; + +pub fn validate(s: &str) -> Result { + parse_id(s, &['#']) +} diff --git a/ruma-identifiers-validation/src/room_id.rs b/ruma-identifiers-validation/src/room_id.rs new file mode 100644 index 00000000..cb2fa3f0 --- /dev/null +++ b/ruma-identifiers-validation/src/room_id.rs @@ -0,0 +1,7 @@ +use std::num::NonZeroU8; + +use crate::{parse_id, Error}; + +pub fn validate(s: &str) -> Result { + parse_id(s, &['!']) +} diff --git a/ruma-identifiers-validation/src/room_id_or_alias_id.rs b/ruma-identifiers-validation/src/room_id_or_alias_id.rs new file mode 100644 index 00000000..e477d164 --- /dev/null +++ b/ruma-identifiers-validation/src/room_id_or_alias_id.rs @@ -0,0 +1,7 @@ +use std::num::NonZeroU8; + +use crate::{parse_id, Error}; + +pub fn validate(s: &str) -> Result { + parse_id(s, &['#', '!']) +} diff --git a/ruma-identifiers-validation/src/room_version_id.rs b/ruma-identifiers-validation/src/room_version_id.rs new file mode 100644 index 00000000..894a9bdd --- /dev/null +++ b/ruma-identifiers-validation/src/room_version_id.rs @@ -0,0 +1,14 @@ +use crate::Error; + +/// Room version identifiers cannot be more than 32 code points. +const MAX_CODE_POINTS: usize = 32; + +pub fn validate(s: &str) -> Result<(), Error> { + if s.is_empty() { + Err(Error::MinimumLengthNotSatisfied) + } else if s.chars().count() > MAX_CODE_POINTS { + Err(Error::MaximumLengthExceeded) + } else { + Ok(()) + } +} diff --git a/ruma-identifiers-validation/src/server_key_id.rs b/ruma-identifiers-validation/src/server_key_id.rs new file mode 100644 index 00000000..b3982acd --- /dev/null +++ b/ruma-identifiers-validation/src/server_key_id.rs @@ -0,0 +1,30 @@ +use std::{num::NonZeroU8, str::FromStr}; + +use crate::{key_algorithms::ServerKeyAlgorithm, Error}; + +pub fn validate(s: &str) -> Result { + let colon_idx = NonZeroU8::new(s.find(':').ok_or(Error::MissingServerKeyDelimiter)? as u8) + .ok_or(Error::UnknownKeyAlgorithm)?; + + validate_server_key_algorithm(&s[..colon_idx.get() as usize])?; + validate_version(&s[colon_idx.get() as usize + 1..])?; + + Ok(colon_idx) +} + +fn validate_version(version: &str) -> Result<(), Error> { + if version.is_empty() { + return Err(Error::MinimumLengthNotSatisfied); + } else if !version.chars().all(|c| c.is_alphanumeric() || c == '_') { + return Err(Error::InvalidCharacters); + } + + Ok(()) +} + +fn validate_server_key_algorithm(algorithm: &str) -> Result<(), Error> { + match ServerKeyAlgorithm::from_str(algorithm) { + Ok(_) => Ok(()), + Err(_) => Err(Error::UnknownKeyAlgorithm), + } +} diff --git a/ruma-identifiers-validation/src/server_name.rs b/ruma-identifiers-validation/src/server_name.rs new file mode 100644 index 00000000..a9888d62 --- /dev/null +++ b/ruma-identifiers-validation/src/server_name.rs @@ -0,0 +1,46 @@ +use crate::error::Error; + +pub fn validate(server_name: &str) -> Result<(), Error> { + use std::net::Ipv6Addr; + + if server_name.is_empty() { + return Err(Error::InvalidServerName); + } + + let end_of_host = if server_name.starts_with('[') { + let end_of_ipv6 = match server_name.find(']') { + Some(idx) => idx, + None => return Err(Error::InvalidServerName), + }; + + if server_name[1..end_of_ipv6].parse::().is_err() { + return Err(Error::InvalidServerName); + } + + end_of_ipv6 + 1 + } else { + let end_of_host = server_name.find(':').unwrap_or_else(|| server_name.len()); + + if server_name[..end_of_host] + .bytes() + .any(|byte| !(byte.is_ascii_alphanumeric() || byte == b'-' || byte == b'.')) + { + return Err(Error::InvalidServerName); + } + + end_of_host + }; + + if server_name.len() != end_of_host + && ( + // hostname is followed by something other than ":port" + server_name.as_bytes()[end_of_host] != b':' + // the remaining characters after ':' are not a valid port + || server_name[end_of_host + 1..].parse::().is_err() + ) + { + Err(Error::InvalidServerName) + } else { + Ok(()) + } +} diff --git a/ruma-identifiers-validation/src/user_id.rs b/ruma-identifiers-validation/src/user_id.rs new file mode 100644 index 00000000..6362099f --- /dev/null +++ b/ruma-identifiers-validation/src/user_id.rs @@ -0,0 +1,35 @@ +use std::num::NonZeroU8; + +use crate::{parse_id, Error}; + +pub fn validate(s: &str) -> Result<(NonZeroU8, bool), Error> { + let colon_idx = parse_id(s, &['@'])?; + let localpart = &s[1..colon_idx.get() as usize]; + let is_historical = localpart_is_fully_comforming(localpart)?; + + Ok((colon_idx, is_historical)) +} + +/// Check whether the given user id localpart is valid and fully conforming +/// +/// Returns an `Err` for invalid user ID localparts, `Ok(false)` for historical user ID localparts +/// and `Ok(true)` for fully conforming user ID localparts. +pub fn localpart_is_fully_comforming(localpart: &str) -> Result { + if localpart.is_empty() { + return Err(Error::InvalidLocalPart); + } + + // See https://matrix.org/docs/spec/appendices#user-identifiers + let is_fully_conforming = localpart + .bytes() + .all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'z' | b'-' | b'.' | b'=' | b'_' | b'/')); + + // If it's not fully conforming, check if it contains characters that are also disallowed + // for historical user IDs. If there are, return an error. + // See https://matrix.org/docs/spec/appendices#historical-user-ids + if !is_fully_conforming && localpart.bytes().any(|b| b < 0x21 || b == b':' || b > 0x7E) { + Err(Error::InvalidCharacters) + } else { + Ok(is_fully_conforming) + } +} diff --git a/ruma-identifiers/Cargo.toml b/ruma-identifiers/Cargo.toml index cf1ee9a9..925a55d7 100644 --- a/ruma-identifiers/Cargo.toml +++ b/ruma-identifiers/Cargo.toml @@ -1,5 +1,8 @@ [package] -authors = ["Jimmy Cuadra "] +authors = [ + "Jimmy Cuadra ", + "Jonas Platte ", +] categories = ["api-bindings"] description = "Resource identifiers for Matrix." documentation = "https://docs.rs/ruma-identifiers" @@ -22,9 +25,10 @@ default = ["serde"] [dependencies] either = { version = "1.5.3", optional = true } rand = { version = "0.7.3", optional = true } +ruma-identifiers-validation = { version = "0.1.0", path = "../ruma-identifiers-validation" } serde = { version = "1.0.114", optional = true, features = ["derive"] } strum = { version = "0.18.0", features = ["derive"] } [dev-dependencies] matches = "0.1.8" -serde_json = "1.0.56" +serde_json = "1.0.57" diff --git a/ruma-identifiers/src/device_key_id.rs b/ruma-identifiers/src/device_key_id.rs index 284bb4e5..491dbd42 100644 --- a/ruma-identifiers/src/device_key_id.rs +++ b/ruma-identifiers/src/device_key_id.rs @@ -1,8 +1,11 @@ //! Identifiers for device keys for end-to-end encryption. -use crate::{error::Error, key_algorithms::DeviceKeyAlgorithm, DeviceId}; use std::{num::NonZeroU8, str::FromStr}; +use ruma_identifiers_validation::{key_algorithms::DeviceKeyAlgorithm, Error}; + +use crate::DeviceId; + /// A key algorithm and a device id, combined with a ':' #[derive(Clone, Debug)] pub struct DeviceKeyId { @@ -26,14 +29,7 @@ fn try_from(key_id: S) -> Result where S: AsRef + Into>, { - let key_str = key_id.as_ref(); - let colon_idx = - NonZeroU8::new(key_str.find(':').ok_or(Error::MissingDeviceKeyDelimiter)? as u8) - .ok_or(Error::UnknownKeyAlgorithm)?; - - DeviceKeyAlgorithm::from_str(&key_str[0..colon_idx.get() as usize]) - .map_err(|_| Error::UnknownKeyAlgorithm)?; - + let colon_idx = ruma_identifiers_validation::device_key_id::validate(key_id.as_ref())?; Ok(DeviceKeyId { full_id: key_id.into(), colon_idx }) } @@ -43,11 +39,11 @@ common_impls!(DeviceKeyId, try_from, "Device key ID with algorithm and device ID mod test { use std::convert::TryFrom; + use ruma_identifiers_validation::{key_algorithms::DeviceKeyAlgorithm, Error}; #[cfg(feature = "serde")] use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; use super::DeviceKeyId; - use crate::{error::Error, key_algorithms::DeviceKeyAlgorithm}; #[test] fn convert_device_key_id() { diff --git a/ruma-identifiers/src/event_id.rs b/ruma-identifiers/src/event_id.rs index c030f6ae..46856568 100644 --- a/ruma-identifiers/src/event_id.rs +++ b/ruma-identifiers/src/event_id.rs @@ -2,7 +2,7 @@ use std::{convert::TryFrom, num::NonZeroU8}; -use crate::{error::Error, parse_id, validate_id, ServerName}; +use crate::{Error, ServerName}; /// A Matrix event ID. /// @@ -91,15 +91,8 @@ fn try_from(event_id: S) -> Result where S: AsRef + Into>, { - if event_id.as_ref().contains(':') { - let colon_idx = parse_id(event_id.as_ref(), &['$'])?; - - Ok(EventId { full_id: event_id.into(), colon_idx: Some(colon_idx) }) - } else { - validate_id(event_id.as_ref(), &['$'])?; - - Ok(EventId { full_id: event_id.into(), colon_idx: None }) - } + let colon_idx = ruma_identifiers_validation::event_id::validate(event_id.as_ref())?; + Ok(EventId { full_id: event_id.into(), colon_idx }) } common_impls!(EventId, try_from, "a Matrix event ID"); @@ -112,7 +105,7 @@ mod tests { use serde_json::{from_str, to_string}; use super::EventId; - use crate::error::Error; + use crate::Error; #[test] fn valid_original_event_id() { diff --git a/ruma-identifiers/src/lib.rs b/ruma-identifiers/src/lib.rs index 9ec58a63..f797beb8 100644 --- a/ruma-identifiers/src/lib.rs +++ b/ruma-identifiers/src/lib.rs @@ -9,25 +9,21 @@ )] #![cfg_attr(docsrs, feature(doc_cfg))] -use std::{convert::TryFrom, num::NonZeroU8}; +use std::convert::TryFrom; #[cfg(feature = "serde")] use serde::de::{self, Deserialize as _, Deserializer, Unexpected}; #[doc(inline)] pub use crate::{ - device_id::DeviceId, - device_key_id::DeviceKeyId, + device_id::DeviceId, device_key_id::DeviceKeyId, event_id::EventId, room_alias_id::RoomAliasId, + room_id::RoomId, room_id_or_room_alias_id::RoomIdOrAliasId, room_version_id::RoomVersionId, + server_key_id::ServerKeyId, server_name::ServerName, user_id::UserId, +}; +#[doc(inline)] +pub use ruma_identifiers_validation::{ error::Error, - event_id::EventId, key_algorithms::{DeviceKeyAlgorithm, ServerKeyAlgorithm}, - room_alias_id::RoomAliasId, - room_id::RoomId, - room_id_or_room_alias_id::RoomIdOrAliasId, - room_version_id::RoomVersionId, - server_key_id::ServerKeyId, - server_name::ServerName, - user_id::UserId, }; #[macro_use] @@ -37,9 +33,7 @@ pub mod device_id; pub mod user_id; mod device_key_id; -mod error; mod event_id; -mod key_algorithms; mod room_alias_id; mod room_id; mod room_id_or_room_alias_id; @@ -55,14 +49,6 @@ pub fn is_valid_server_name(name: &str) -> bool { <&ServerName>::try_from(name).is_ok() } -/// All identifiers must be 255 bytes or less. -const MAX_BYTES: usize = 255; -/// The minimum number of characters an ID can be. -/// -/// This is an optimization and not required by the spec. The shortest possible valid ID is a sigil -/// + a single character local ID + a colon + a single character hostname. -const MIN_CHARS: usize = 4; - /// Generates a random identifier localpart. #[cfg(feature = "rand")] fn generate_localpart(length: usize) -> Box { @@ -74,38 +60,6 @@ fn generate_localpart(length: usize) -> Box { .into_boxed_str() } -/// Checks if an identifier is valid. -fn validate_id(id: &str, valid_sigils: &[char]) -> Result<(), Error> { - if id.len() > MAX_BYTES { - return Err(Error::MaximumLengthExceeded); - } - - if id.len() < MIN_CHARS { - return Err(Error::MinimumLengthNotSatisfied); - } - - if !valid_sigils.contains(&id.chars().next().unwrap()) { - return Err(Error::MissingSigil); - } - - Ok(()) -} - -/// Checks an identifier that contains a localpart and hostname for validity, -/// and returns the index of the colon that separates the two. -fn parse_id(id: &str, valid_sigils: &[char]) -> Result { - validate_id(id, valid_sigils)?; - - let colon_idx = id.find(':').ok_or(Error::MissingDelimiter)?; - if colon_idx < 2 { - return Err(Error::InvalidLocalPart); - } - - server_name::validate(&id[colon_idx + 1..])?; - - Ok(NonZeroU8::new(colon_idx as u8).unwrap()) -} - /// Deserializes any type of id using the provided TryFrom implementation. /// /// This is a helper function to reduce the boilerplate of the Deserialize implementations. diff --git a/ruma-identifiers/src/macros.rs b/ruma-identifiers/src/macros.rs index 600949a9..01c69c8c 100644 --- a/ruma-identifiers/src/macros.rs +++ b/ruma-identifiers/src/macros.rs @@ -55,7 +55,7 @@ macro_rules! common_impls { } impl ::std::convert::TryFrom<&str> for $id { - type Error = crate::error::Error; + type Error = crate::Error; fn try_from(s: &str) -> Result { $try_from(s) @@ -63,7 +63,7 @@ macro_rules! common_impls { } impl ::std::convert::TryFrom for $id { - type Error = crate::error::Error; + type Error = crate::Error; fn try_from(s: String) -> Result { $try_from(s) diff --git a/ruma-identifiers/src/room_alias_id.rs b/ruma-identifiers/src/room_alias_id.rs index 1242134f..7478a6ac 100644 --- a/ruma-identifiers/src/room_alias_id.rs +++ b/ruma-identifiers/src/room_alias_id.rs @@ -2,7 +2,7 @@ use std::{convert::TryFrom, num::NonZeroU8}; -use crate::{error::Error, parse_id, server_name::ServerName}; +use crate::{server_name::ServerName, Error}; /// A Matrix room alias ID. /// @@ -38,13 +38,12 @@ impl RoomAliasId { /// Attempts to create a new Matrix room alias ID from a string representation. /// /// The string must include the leading # sigil, the alias, a literal colon, and a server name. -fn try_from(room_id: S) -> Result +fn try_from(room_alias_id: S) -> Result where S: AsRef + Into>, { - let colon_idx = parse_id(room_id.as_ref(), &['#'])?; - - Ok(RoomAliasId { full_id: room_id.into(), colon_idx }) + let colon_idx = ruma_identifiers_validation::room_alias_id::validate(room_alias_id.as_ref())?; + Ok(RoomAliasId { full_id: room_alias_id.into(), colon_idx }) } common_impls!(RoomAliasId, try_from, "a Matrix room alias ID"); @@ -57,7 +56,7 @@ mod tests { use serde_json::{from_str, to_string}; use super::RoomAliasId; - use crate::error::Error; + use crate::Error; #[test] fn valid_room_alias_id() { diff --git a/ruma-identifiers/src/room_id.rs b/ruma-identifiers/src/room_id.rs index 5743fb90..e2c69516 100644 --- a/ruma-identifiers/src/room_id.rs +++ b/ruma-identifiers/src/room_id.rs @@ -2,7 +2,7 @@ use std::{convert::TryFrom, num::NonZeroU8}; -use crate::{error::Error, parse_id, ServerName}; +use crate::{Error, ServerName}; /// A Matrix room ID. /// @@ -58,8 +58,7 @@ fn try_from(room_id: S) -> Result where S: AsRef + Into>, { - let colon_idx = parse_id(room_id.as_ref(), &['!'])?; - + let colon_idx = ruma_identifiers_validation::room_id::validate(room_id.as_ref())?; Ok(RoomId { full_id: room_id.into(), colon_idx }) } @@ -73,7 +72,7 @@ mod tests { use serde_json::{from_str, to_string}; use super::RoomId; - use crate::error::Error; + use crate::Error; #[test] fn valid_room_id() { diff --git a/ruma-identifiers/src/room_id_or_room_alias_id.rs b/ruma-identifiers/src/room_id_or_room_alias_id.rs index 60e89bd7..05f13ce6 100644 --- a/ruma-identifiers/src/room_id_or_room_alias_id.rs +++ b/ruma-identifiers/src/room_id_or_room_alias_id.rs @@ -2,7 +2,7 @@ use std::{convert::TryFrom, hint::unreachable_unchecked, num::NonZeroU8}; -use crate::{error::Error, parse_id, server_name::ServerName, RoomAliasId, RoomId}; +use crate::{server_name::ServerName, Error, RoomAliasId, RoomId}; /// A Matrix room ID or a Matrix room alias ID. /// @@ -89,7 +89,8 @@ fn try_from(room_id_or_alias_id: S) -> Result where S: AsRef + Into>, { - let colon_idx = parse_id(room_id_or_alias_id.as_ref(), &['#', '!'])?; + let colon_idx = + ruma_identifiers_validation::room_id_or_alias_id::validate(room_id_or_alias_id.as_ref())?; Ok(RoomIdOrAliasId { full_id: room_id_or_alias_id.into(), colon_idx }) } @@ -141,7 +142,7 @@ mod tests { use serde_json::{from_str, to_string}; use super::RoomIdOrAliasId; - use crate::error::Error; + use crate::Error; #[test] fn valid_room_id_or_alias_id_with_a_room_alias_id() { diff --git a/ruma-identifiers/src/room_version_id.rs b/ruma-identifiers/src/room_version_id.rs index 94dce568..85cb0fe9 100644 --- a/ruma-identifiers/src/room_version_id.rs +++ b/ruma-identifiers/src/room_version_id.rs @@ -9,10 +9,7 @@ use std::{ #[cfg(feature = "serde")] use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use crate::error::Error; - -/// Room version identifiers cannot be more than 32 code points. -const MAX_CODE_POINTS: usize = 32; +use crate::Error; /// A Matrix room version ID. /// @@ -231,13 +228,8 @@ where "4" => RoomVersionId::Version4, "5" => RoomVersionId::Version5, custom => { - if custom.is_empty() { - return Err(Error::MinimumLengthNotSatisfied); - } else if custom.chars().count() > MAX_CODE_POINTS { - return Err(Error::MaximumLengthExceeded); - } else { - RoomVersionId::Custom(CustomRoomVersion(room_version_id.into())) - } + ruma_identifiers_validation::room_version_id::validate(custom)?; + RoomVersionId::Custom(CustomRoomVersion(room_version_id.into())) } }; @@ -245,7 +237,7 @@ where } impl TryFrom<&str> for RoomVersionId { - type Error = crate::error::Error; + type Error = crate::Error; fn try_from(s: &str) -> Result { try_from(s) @@ -253,7 +245,7 @@ impl TryFrom<&str> for RoomVersionId { } impl TryFrom for RoomVersionId { - type Error = crate::error::Error; + type Error = crate::Error; fn try_from(s: String) -> Result { try_from(s) @@ -314,7 +306,7 @@ mod tests { use serde_json::{from_str, to_string}; use super::RoomVersionId; - use crate::error::Error; + use crate::Error; #[test] fn valid_version_1_room_version_id() { diff --git a/ruma-identifiers/src/server_key_id.rs b/ruma-identifiers/src/server_key_id.rs index 448c0bfa..efb6d168 100644 --- a/ruma-identifiers/src/server_key_id.rs +++ b/ruma-identifiers/src/server_key_id.rs @@ -2,7 +2,7 @@ use std::{num::NonZeroU8, str::FromStr}; -use crate::{error::Error, key_algorithms::ServerKeyAlgorithm}; +use ruma_identifiers_validation::{key_algorithms::ServerKeyAlgorithm, Error}; /// Key identifiers used for homeserver signing keys. #[derive(Clone, Debug)] @@ -27,37 +27,12 @@ fn try_from(key_id: S) -> Result where S: AsRef + Into>, { - let key_str = key_id.as_ref(); - let colon_idx = - NonZeroU8::new(key_str.find(':').ok_or(Error::MissingServerKeyDelimiter)? as u8) - .ok_or(Error::UnknownKeyAlgorithm)?; - - validate_server_key_algorithm(&key_str[..colon_idx.get() as usize])?; - - validate_version(&key_str[colon_idx.get() as usize + 1..])?; - + let colon_idx = ruma_identifiers_validation::server_key_id::validate(key_id.as_ref())?; Ok(ServerKeyId { full_id: key_id.into(), colon_idx }) } common_impls!(ServerKeyId, try_from, "Key ID with algorithm and version"); -fn validate_version(version: &str) -> Result<(), Error> { - if version.is_empty() { - return Err(Error::MinimumLengthNotSatisfied); - } else if !version.chars().all(|c| c.is_alphanumeric() || c == '_') { - return Err(Error::InvalidCharacters); - } - - Ok(()) -} - -fn validate_server_key_algorithm(algorithm: &str) -> Result<(), Error> { - match ServerKeyAlgorithm::from_str(algorithm) { - Ok(_) => Ok(()), - Err(_) => Err(Error::UnknownKeyAlgorithm), - } -} - #[cfg(test)] mod tests { use std::convert::TryFrom; @@ -65,10 +40,10 @@ mod tests { #[cfg(feature = "serde")] use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; - use crate::{error::Error, ServerKeyId}; + use crate::{Error, ServerKeyId}; #[cfg(feature = "serde")] - use crate::key_algorithms::ServerKeyAlgorithm; + use ruma_identifiers_validation::key_algorithms::ServerKeyAlgorithm; #[cfg(feature = "serde")] #[test] diff --git a/ruma-identifiers/src/server_name.rs b/ruma-identifiers/src/server_name.rs index 58808ba3..fc4e1764 100644 --- a/ruma-identifiers/src/server_name.rs +++ b/ruma-identifiers/src/server_name.rs @@ -6,7 +6,9 @@ use std::{ mem, }; -use crate::error::Error; +use ruma_identifiers_validation::server_name::validate; + +use crate::Error; /// A Matrix-spec compliant server name. #[repr(transparent)] @@ -59,51 +61,6 @@ impl From<&ServerName> for Box { } } -pub(crate) fn validate(server_name: &str) -> Result<(), Error> { - use std::net::Ipv6Addr; - - if server_name.is_empty() { - return Err(Error::InvalidServerName); - } - - let end_of_host = if server_name.starts_with('[') { - let end_of_ipv6 = match server_name.find(']') { - Some(idx) => idx, - None => return Err(Error::InvalidServerName), - }; - - if server_name[1..end_of_ipv6].parse::().is_err() { - return Err(Error::InvalidServerName); - } - - end_of_ipv6 + 1 - } else { - let end_of_host = server_name.find(':').unwrap_or_else(|| server_name.len()); - - if server_name[..end_of_host] - .bytes() - .any(|byte| !(byte.is_ascii_alphanumeric() || byte == b'-' || byte == b'.')) - { - return Err(Error::InvalidServerName); - } - - end_of_host - }; - - if server_name.len() != end_of_host - && ( - // hostname is followed by something other than ":port" - server_name.as_bytes()[end_of_host] != b':' - // the remaining characters after ':' are not a valid port - || server_name[end_of_host + 1..].parse::().is_err() - ) - { - Err(Error::InvalidServerName) - } else { - Ok(()) - } -} - fn try_from(server_name: S) -> Result, Error> where S: AsRef + Into>, @@ -140,7 +97,7 @@ impl<'a> TryFrom<&'a str> for &'a ServerName { } impl TryFrom<&str> for Box { - type Error = crate::error::Error; + type Error = crate::Error; fn try_from(s: &str) -> Result { try_from(s) @@ -148,7 +105,7 @@ impl TryFrom<&str> for Box { } impl TryFrom for Box { - type Error = crate::error::Error; + type Error = crate::Error; fn try_from(s: String) -> Result { try_from(s) diff --git a/ruma-identifiers/src/user_id.rs b/ruma-identifiers/src/user_id.rs index 9e8847e1..50bb4878 100644 --- a/ruma-identifiers/src/user_id.rs +++ b/ruma-identifiers/src/user_id.rs @@ -2,7 +2,7 @@ use std::{convert::TryFrom, num::NonZeroU8}; -use crate::{error::Error, parse_id, ServerName}; +use crate::{Error, ServerName}; /// A Matrix user ID. /// @@ -95,41 +95,14 @@ fn try_from(user_id: S) -> Result where S: AsRef + Into>, { - let user_id_str = user_id.as_ref(); - - let colon_idx = parse_id(user_id_str, &['@'])?; - let localpart = &user_id_str[1..colon_idx.get() as usize]; - - let is_historical = localpart_is_fully_comforming(localpart)?; - + let (colon_idx, is_historical) = + ruma_identifiers_validation::user_id::validate(user_id.as_ref())?; Ok(UserId { full_id: user_id.into(), colon_idx, is_historical: !is_historical }) } common_impls!(UserId, try_from, "a Matrix user ID"); -/// Check whether the given user id localpart is valid and fully conforming -/// -/// Returns an `Err` for invalid user ID localparts, `Ok(false)` for historical user ID localparts -/// and `Ok(true)` for fully conforming user ID localparts. -pub fn localpart_is_fully_comforming(localpart: &str) -> Result { - if localpart.is_empty() { - return Err(Error::InvalidLocalPart); - } - - // See https://matrix.org/docs/spec/appendices#user-identifiers - let is_fully_conforming = localpart - .bytes() - .all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'z' | b'-' | b'.' | b'=' | b'_' | b'/')); - - // If it's not fully conforming, check if it contains characters that are also disallowed - // for historical user IDs. If there are, return an error. - // See https://matrix.org/docs/spec/appendices#historical-user-ids - if !is_fully_conforming && localpart.bytes().any(|b| b < 0x21 || b == b':' || b > 0x7E) { - Err(Error::InvalidCharacters) - } else { - Ok(is_fully_conforming) - } -} +pub use ruma_identifiers_validation::user_id::localpart_is_fully_comforming; #[cfg(test)] mod tests { @@ -139,7 +112,7 @@ mod tests { use serde_json::{from_str, to_string}; use super::UserId; - use crate::{error::Error, ServerName}; + use crate::{Error, ServerName}; #[test] fn valid_user_id_from_str() {