ruma-identifiers-validation-0.9.0/.cargo_vcs_info.json0000644000000002000000000000100164310ustar { "git": { "sha1": "5d516ca5446bc25c21da6bbe2e0b95ec4a8b73d0" }, "path_in_vcs": "crates/ruma-identifiers-validation" }ruma-identifiers-validation-0.9.0/CHANGELOG.md000064400000000000000000000025041046102023000170430ustar 00000000000000# [unreleased] # 0.9.0 Breaking changes: * Remove `room_name` module * Room name size limits were never enforced, so they are now just regular `String`s in Ruma ([Spec change removing the size limit][spec]) [spec]: https://github.com/matrix-org/matrix-spec-proposals/pull/3669 # 0.8.1 Improvements: * Remove unused dependency on `url` # 0.8.0 Breaking changes: * Rework the `Error` type (merge / rename variants) # 0.7.0 Improvements: * Add more `Error` variants # 0.6.0 Breaking changes: * Most validation functions no longer return the colon position on success Improvements: * Add `mxc_uri` validation # 0.5.0 Breaking changes: * Make `Error` type non-exhaustive # 0.4.0 Breaking changes: * Fix a typo in a public function name: `user_id::localpart_is_fully_conforming` # 0.3.0 Breaking changes: * Remove the `serde` feature # 0.2.4 Improvements: * Restore the `serde` feature which was accidentally removed in a patch release # 0.2.3 Improvements: * Add a `compat` feature * Under this feature, more user IDs are accepted that exist in the while but are not spec-compliant # 0.2.2 Improvements: * Add verification of `mxc://` URIs # 0.2.1 Improvements: * Drop unused dependencies # 0.2.0 Breaking changes: * Remove `key_algorithms` module (moved to ruma-identifiers as `crypto_algorithms`) ruma-identifiers-validation-0.9.0/Cargo.toml0000644000000015700000000000100144420ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.60" name = "ruma-identifiers-validation" version = "0.9.0" description = "Validation logic for ruma-common and ruma-macros" homepage = "https://www.ruma.io/" license = "MIT" repository = "https://github.com/ruma/ruma" [package.metadata.docs.rs] all-features = true [dependencies.js_int] version = "0.2.0" [dependencies.thiserror] version = "1.0.26" [features] compat = [] ruma-identifiers-validation-0.9.0/Cargo.toml.orig000064400000000000000000000006051046102023000201210ustar 00000000000000[package] name = "ruma-identifiers-validation" description = "Validation logic for ruma-common and ruma-macros" homepage = "https://www.ruma.io/" repository = "https://github.com/ruma/ruma" license = "MIT" version = "0.9.0" edition = "2021" rust-version = "1.60" [package.metadata.docs.rs] all-features = true [features] compat = [] [dependencies] js_int = "0.2.0" thiserror = "1.0.26" ruma-identifiers-validation-0.9.0/src/client_secret.rs000064400000000000000000000005331046102023000212120ustar 00000000000000use crate::Error; pub fn validate(s: &str) -> Result<(), Error> { if s.len() > 255 { return Err(Error::MaximumLengthExceeded); } else if !s.chars().all(|c| c.is_alphanumeric() || ".=_-".contains(c)) { return Err(Error::InvalidCharacters); } else if s.is_empty() { return Err(Error::Empty); } Ok(()) } ruma-identifiers-validation-0.9.0/src/device_key_id.rs000064400000000000000000000004611046102023000211520ustar 00000000000000use crate::Error; pub fn validate(s: &str) -> Result<(), Error> { let colon_idx = s.find(':').ok_or(Error::MissingColon)?; if colon_idx == 0 { Err(Error::Empty) } else { // Any non-empty string is accepted as a key algorithm for forwards compatibility Ok(()) } } ruma-identifiers-validation-0.9.0/src/error.rs000064400000000000000000000122671046102023000175270ustar 00000000000000//! Error conditions. use std::str::Utf8Error; /// An error encountered when trying to parse an invalid ID string. #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, thiserror::Error)] #[non_exhaustive] pub enum Error { /// The identifier or a required part of it is empty. #[error("identifier or required part of it is empty")] Empty, /// The identifier contains invalid characters. #[error("identifier contains invalid characters")] InvalidCharacters, /// The string isn't a valid Matrix ID. #[error("invalid matrix ID: {0}")] InvalidMatrixId(#[from] MatrixIdError), /// The string isn't a valid Matrix.to URI. #[error("invalid matrix.to URI: {0}")] InvalidMatrixToUri(#[from] MatrixToError), /// The string isn't a valid Matrix URI. #[error("invalid matrix URI: {0}")] InvalidMatrixUri(#[from] MatrixUriError), /// The mxc:// isn't a valid Matrix Content URI. #[error("invalid Matrix Content URI: {0}")] InvalidMxcUri(#[from] MxcUriError), /// The value isn't a valid VoIP version Id. #[error("invalid VoIP version ID: {0}")] InvalidVoipVersionId(#[from] VoipVersionIdError), /// The server name part of the the ID string is not a valid server name. #[error("server name is not a valid IP address or domain name")] InvalidServerName, /// The string isn't valid UTF-8. #[error("invalid UTF-8")] InvalidUtf8, /// The ID exceeds 255 bytes (or 32 codepoints for a room version ID). #[error("ID exceeds 255 bytes")] MaximumLengthExceeded, /// The ID is missing the colon delimiter between localpart and server name, or between key /// algorithm and key name / version. #[error("required colon is missing")] MissingColon, /// The ID is missing the correct leading sigil. #[error("leading sigil is incorrect or missing")] MissingLeadingSigil, } impl From for Error { fn from(_: Utf8Error) -> Self { Self::InvalidUtf8 } } /// An error occurred while validating an MXC URI. #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, thiserror::Error)] #[non_exhaustive] pub enum MxcUriError { /// MXC URI did not start with `mxc://`. #[error("MXC URI schema was not mxc://")] WrongSchema, /// MXC URI did not have first slash, required for `server.name/media_id`. #[error("MXC URI does not have first slash")] MissingSlash, /// Media identifier malformed due to invalid characters detected. /// /// Valid characters are (in regex notation) `[A-Za-z0-9_-]+`. /// See [here](https://spec.matrix.org/v1.2/client-server-api/#security-considerations-5) for more details. #[error("Media Identifier malformed, invalid characters")] MediaIdMalformed, /// Server identifier malformed: invalid IP or domain name. #[error("invalid Server Name")] ServerNameMalformed, } /// An error occurred while validating a `MatrixId`. #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, thiserror::Error)] #[non_exhaustive] pub enum MatrixIdError { /// The string contains an invalid number of parts. #[error("invalid number of parts")] InvalidPartsNumber, /// The string is missing a room ID or alias. #[error("missing room ID or alias")] MissingRoom, /// The string contains no identifier. #[error("no identifier")] NoIdentifier, /// The string contains too many identifiers. #[error("too many identifiers")] TooManyIdentifiers, /// The string contains an unknown identifier. #[error("unknown identifier")] UnknownIdentifier, /// The string contains two identifiers that cannot be paired. #[error("unknown identifier pair")] UnknownIdentifierPair, /// The string contains an unknown identifier type. #[error("unknown identifier type")] UnknownType, } /// An error occurred while validating a `matrix.to` URI. #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, thiserror::Error)] #[non_exhaustive] pub enum MatrixToError { /// String is not a valid URI. #[error("given string is not a valid URL")] InvalidUrl, /// String did not start with `https://matrix.to/#/`. #[error("base URL is not https://matrix.to/#/")] WrongBaseUrl, /// String has an unknown additional argument. #[error("unknown additional argument")] UnknownArgument, } /// An error occurred while validating a `MatrixURI`. #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, thiserror::Error)] #[non_exhaustive] pub enum MatrixUriError { /// The string does not start with `matrix:`. #[error("scheme is not 'matrix:'")] WrongScheme, /// The string contains too many actions. #[error("too many actions")] TooManyActions, /// The string contains an unknown query item. #[error("unknown query item")] UnknownQueryItem, } /// An error occurred while validating a `VoipVersionId`. #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, thiserror::Error)] #[non_exhaustive] pub enum VoipVersionIdError { /// The value of the `UInt` is not 0. #[error("UInt value is not 0")] WrongUintValue, } #[cfg(test)] mod tests { use std::mem::size_of; use super::Error; #[test] fn small_error_type() { assert!(size_of::() <= 8); } } ruma-identifiers-validation-0.9.0/src/event_id.rs000064400000000000000000000004101046102023000201560ustar 00000000000000use crate::{validate_delimited_id, Error}; pub fn validate(s: &str) -> Result<(), Error> { if s.contains(':') { validate_delimited_id(s, &['$'])?; } else if !s.starts_with('$') { return Err(Error::MissingLeadingSigil); } Ok(()) } ruma-identifiers-validation-0.9.0/src/key_id.rs000064400000000000000000000011651046102023000176350ustar 00000000000000use std::num::NonZeroU8; use crate::Error; pub fn validate(s: &str) -> Result { let colon_idx = NonZeroU8::new(s.find(':').ok_or(Error::MissingColon)? as u8).ok_or(Error::MissingColon)?; #[cfg(not(feature = "compat"))] validate_version(&s[colon_idx.get() as usize + 1..])?; Ok(colon_idx) } #[cfg(not(feature = "compat"))] fn validate_version(version: &str) -> Result<(), Error> { if version.is_empty() { return Err(Error::Empty); } else if !version.chars().all(|c| c.is_alphanumeric() || c == '_') { return Err(Error::InvalidCharacters); } Ok(()) } ruma-identifiers-validation-0.9.0/src/lib.rs000064400000000000000000000024541046102023000171410ustar 00000000000000#![doc(html_favicon_url = "https://www.ruma.io/favicon.ico")] #![doc(html_logo_url = "https://www.ruma.io/images/logo.png")] pub mod client_secret; pub mod device_key_id; pub mod error; pub mod event_id; pub mod key_id; pub mod mxc_uri; pub mod room_alias_id; pub mod room_id; pub mod room_id_or_alias_id; pub mod room_version_id; pub mod server_name; pub mod user_id; pub mod voip_version_id; pub use error::Error; /// All identifiers must be 255 bytes or less. const MAX_BYTES: usize = 255; /// 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.starts_with(valid_sigils) { return Err(Error::MissingLeadingSigil); } Ok(()) } /// Checks an identifier that contains a localpart and hostname for validity. fn parse_id(id: &str, valid_sigils: &[char]) -> Result { validate_id(id, valid_sigils)?; let colon_idx = id.find(':').ok_or(Error::MissingColon)?; server_name::validate(&id[colon_idx + 1..])?; Ok(colon_idx) } /// Checks an identifier that contains a localpart and hostname for validity. fn validate_delimited_id(id: &str, valid_sigils: &[char]) -> Result<(), Error> { parse_id(id, valid_sigils)?; Ok(()) } ruma-identifiers-validation-0.9.0/src/mxc_uri.rs000064400000000000000000000017211046102023000200350ustar 00000000000000use std::num::NonZeroU8; use crate::{error::MxcUriError, server_name}; const PROTOCOL: &str = "mxc://"; pub fn validate(uri: &str) -> Result { let uri = match uri.strip_prefix(PROTOCOL) { Some(uri) => uri, None => return Err(MxcUriError::WrongSchema), }; let index = match uri.find('/') { Some(index) => index, None => return Err(MxcUriError::MissingSlash), }; let server_name = &uri[..index]; let media_id = &uri[index + 1..]; // See: https://spec.matrix.org/v1.2/client-server-api/#security-considerations-5 let media_id_is_valid = media_id.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z' | b'-' )); if !media_id_is_valid { Err(MxcUriError::MediaIdMalformed) } else if server_name::validate(server_name).is_err() { Err(MxcUriError::ServerNameMalformed) } else { Ok(NonZeroU8::new((index + 6) as u8).unwrap()) } } ruma-identifiers-validation-0.9.0/src/room_alias_id.rs000064400000000000000000000002031046102023000211620ustar 00000000000000use crate::{validate_delimited_id, Error}; pub fn validate(s: &str) -> Result<(), Error> { validate_delimited_id(s, &['#']) } ruma-identifiers-validation-0.9.0/src/room_id.rs000064400000000000000000000002031046102023000200110ustar 00000000000000use crate::{validate_delimited_id, Error}; pub fn validate(s: &str) -> Result<(), Error> { validate_delimited_id(s, &['!']) } ruma-identifiers-validation-0.9.0/src/room_id_or_alias_id.rs000064400000000000000000000002101046102023000223340ustar 00000000000000use crate::{validate_delimited_id, Error}; pub fn validate(s: &str) -> Result<(), Error> { validate_delimited_id(s, &['#', '!']) } ruma-identifiers-validation-0.9.0/src/room_version_id.rs000064400000000000000000000005321046102023000215630ustar 00000000000000use 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::Empty) } else if s.chars().count() > MAX_CODE_POINTS { Err(Error::MaximumLengthExceeded) } else { Ok(()) } } ruma-identifiers-validation-0.9.0/src/server_name.rs000064400000000000000000000025371046102023000207030ustar 00000000000000use 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 { #[allow(clippy::unnecessary_lazy_evaluations)] 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(()) } } ruma-identifiers-validation-0.9.0/src/user_id.rs000064400000000000000000000030711046102023000200210ustar 00000000000000use crate::{parse_id, Error}; pub fn validate(s: &str) -> Result<(), Error> { let colon_idx = parse_id(s, &['@'])?; let localpart = &s[1..colon_idx]; let _ = localpart_is_fully_conforming(localpart)?; Ok(()) } /// 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. /// /// With the `compat` feature enabled, this will also return `Ok(false)` for invalid user ID /// localparts. User IDs that don't even meet the historical user ID restrictions exist in the wild /// due to Synapse allowing them over federation. This will likely be fixed in an upcoming room /// version; see [MSC2828](https://github.com/matrix-org/matrix-spec-proposals/pull/2828). pub fn localpart_is_fully_conforming(localpart: &str) -> Result { // See https://spec.matrix.org/v1.2/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://spec.matrix.org/v1.2/appendices/#historical-user-ids #[cfg(not(feature = "compat"))] if !is_fully_conforming && localpart.bytes().any(|b| b < 0x21 || b == b':' || b > 0x7E) { return Err(Error::InvalidCharacters); } Ok(is_fully_conforming) } ruma-identifiers-validation-0.9.0/src/voip_version_id.rs000064400000000000000000000003451046102023000215660ustar 00000000000000use js_int::{uint, UInt}; use crate::{error::VoipVersionIdError, Error}; pub fn validate(u: UInt) -> Result<(), Error> { if u != uint!(0) { return Err(VoipVersionIdError::WrongUintValue.into()); } Ok(()) }