From 9ad1e0fc69492be7657eb5ea2da61c2fe550cd9c Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Sun, 17 Jul 2016 22:10:17 -0700 Subject: [PATCH 001/140] ruma-identifiers --- .gitignore | 1 + Cargo.lock | 130 +++++++++++++++++++++++++++++++++++++++ Cargo.toml | 16 +++++ LICENSE | 20 ++++++ README.md | 7 +++ src/lib.rs | 174 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 348 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 src/lib.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..ea8c4bf7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000..55006674 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,130 @@ +[root] +name = "ruma-identifiers" +version = "0.1.0" +dependencies = [ + "lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 0.1.73 (registry+https://github.com/rust-lang/crates.io-index)", + "url 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "aho-corasick" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "idna" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "matches 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-bidi 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-normalization 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "lazy_static" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "libc" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "matches" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "memchr" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "regex" +version = "0.1.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "aho-corasick 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", + "regex-syntax 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "thread_local 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", + "utf8-ranges 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "regex-syntax" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "thread-id" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "thread_local" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "thread-id 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unicode-bidi" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "matches 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unicode-normalization" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "url" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "idna 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "matches 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "utf8-ranges" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..3cdae31a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,16 @@ +[package] +authors = ["Jimmy Cuadra "] +description = "Opaque identifiers for Matrix." +documentation = "http://ruma.github.io/ruma-identifiers/ruma_identifiers" +homepage = "https://github.com/ruma/ruma-identifiers" +keywords = ["matrix", "matrix.org", "chat", "messaging", "ruma"] +license = "MIT" +name = "ruma-identifiers" +readme = "README.md" +repository = "https://github.com/ruma/ruma-identifiers" +version = "0.1.0" + +[dependencies] +lazy_static = "0.2.1" +regex = "0.1.73" +url = "1.1.1" diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..de62627d --- /dev/null +++ b/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/README.md b/README.md new file mode 100644 index 00000000..7e125d2f --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# ruma-identifiers + +**ruma-identifiers** contains types for [Matrix](https://matrix.org/) opaque identifiers, such as user IDs, room IDs, and room aliases. + +## License + +[MIT](http://opensource.org/licenses/MIT) diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 00000000..917b2500 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,174 @@ +//! Crate **ruma_identifiers** contains types for [Matrix](https://matrix.org/) opaque identifiers, +//! such as user IDs, room IDs, and room aliases. + +#![deny(missing_docs)] + +#[macro_use] +extern crate lazy_static; +extern crate regex; +extern crate url; + +use std::fmt::{Display, Formatter, Result as FmtResult}; + +use regex::Regex; +use url::{Host, ParseError, Url}; + +lazy_static! { + static ref USER_ID_PATTERN: Regex = + Regex::new(r"\A@(?P[a-z0-9._=-]+):(?P.+)\z") + .expect("Failed to compile user ID regex."); +} + +/// An error encountered when trying to parse an invalid user ID string. +#[derive(Debug)] +pub enum Error { + /// The user ID string did not match the "@:" format, or used invalid + /// characters in its localpart. + InvalidFormat, + /// The domain part of the user ID string was not a valid IP address or DNS name. + InvalidHost(ParseError), +} + +/// A Matrix user ID. +/// +/// A `UserId` is created from a string slice, and can be converted back into a string as needed: +/// +/// ``` +/// # use ruma_identifiers::UserId; +/// assert_eq!(UserId::new("@carl:example.com").unwrap().to_string(), "@carl:example.com"); +/// ``` +#[derive(Debug)] +pub struct UserId { + hostname: Host, + localpart: String, + port: u16, +} + +impl UserId { + /// Create a new Matrix user ID from a string representation. + /// + /// The string must include the leading @ sigil, the localpart, a literal colon, and a valid + /// server name. + pub fn new(user_id: &str) -> Result { + let captures = match USER_ID_PATTERN.captures(user_id) { + Some(captures) => captures, + None => return Err(Error::InvalidFormat), + }; + + let raw_host = captures.name("host").expect("Failed to extract hostname from regex."); + + let url_string = format!("https://{}", raw_host); + + let url = try!(Url::parse(&url_string)); + + let host = match url.host() { + Some(host) => host, + None => return Err(Error::InvalidFormat), + }; + + let port = url.port().unwrap_or(443); + + Ok(UserId { + hostname: host.to_owned(), + port: port, + localpart: captures + .name("localpart") + .expect("Failed to extract localpart from regex.") + .to_string(), + }) + } + + /// Returns a `url::Host` for the user ID, containing the server name (minus the port) of the + /// user's homeserver. + /// + /// This host can be either a domain name, an IPv4 address, or an IPv6 address. + pub fn hostname(&self) -> &Host { + &self.hostname + } + + /// Returns the user's localpart. + pub fn localpart(&self) -> &str { + &self.localpart + } + + /// Returns the port the user's homeserver can be accessed on. + pub fn port(&self) -> u16 { + self.port + } +} + +impl From for Error { + fn from(error: ParseError) -> Error { + Error::InvalidHost(error) + } +} + +impl Display for UserId { + fn fmt(&self, f: &mut Formatter) -> FmtResult { + if self.port == 443 { + write!(f, "@{}:{}", self.localpart, self.hostname) + } else { + write!(f, "@{}:{}:{}", self.localpart, self.hostname, self.port) + } + } +} + +#[cfg(test)] +mod tests { + use super::UserId; + + #[test] + fn valid_user_id() { + assert_eq!( + UserId::new("@carl:example.com") + .expect("Failed to create UserId.") + .to_string(), + "@carl:example.com" + ); + } + + #[test] + fn valid_user_id_with_explicit_standard_port() { + assert_eq!( + UserId::new("@carl:example.com:443") + .expect("Failed to create UserId.") + .to_string(), + "@carl:example.com" + ); + } + + #[test] + fn valid_user_id_with_non_standard_port() { + assert_eq!( + UserId::new("@carl:example.com:5000") + .expect("Failed to create UserId.") + .to_string(), + "@carl:example.com:5000" + ); + } + + #[test] + fn invalid_characters_in_localpart() { + assert!(UserId::new("@CARL:example.com").is_err()); + } + + #[test] + fn missing_sigil() { + assert!(UserId::new("carl:example.com").is_err()); + } + + #[test] + fn missing_domain() { + assert!(UserId::new("carl").is_err()); + } + + #[test] + fn invalid_host() { + assert!(UserId::new("@carl:-").is_err()); + } + + #[test] + fn invalid_port() { + assert!(UserId::new("@carl:example.com:notaport").is_err()); + } +} From 2a03702976a269fee3264a4d585e1ec95ff6ee86 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Fri, 22 Jul 2016 21:55:28 -0700 Subject: [PATCH 002/140] Only use regex for checking valid characters in user localparts. --- src/lib.rs | 129 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 91 insertions(+), 38 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 917b2500..d8e86fee 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,20 +13,36 @@ use std::fmt::{Display, Formatter, Result as FmtResult}; use regex::Regex; use url::{Host, ParseError, Url}; +/// All events 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; +/// The number of bytes in a valid sigil. +const SIGIL_BYTES: usize = 1; + lazy_static! { - static ref USER_ID_PATTERN: Regex = - Regex::new(r"\A@(?P[a-z0-9._=-]+):(?P.+)\z") - .expect("Failed to compile user ID regex."); + static ref USER_LOCALPART_PATTERN: Regex = + Regex::new(r"\A[a-z0-9._=-]+\z").expect("Failed to create user localpart regex."); } /// An error encountered when trying to parse an invalid user ID string. -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub enum Error { - /// The user ID string did not match the "@:" format, or used invalid - /// characters in its localpart. - InvalidFormat, + /// The ID's localpart contains invalid characters. + InvalidCharacters, /// The domain part of the user ID string was not a valid IP address or DNS name. - InvalidHost(ParseError), + InvalidHost, + /// The ID exceeds 255 bytes. + MaximumLengthExceeded, + /// The ID is less than 4 characters. + MinimumLengthNotSatisfied, + /// The ID is missing the colon delimiter between localpart and server name. + MissingDelimiter, + /// The ID is missing the leading sigil. + MissingSigil, } /// A Matrix user ID. @@ -44,37 +60,59 @@ pub struct UserId { port: u16, } +fn parse_id<'a>(required_sigil: char, id: &'a str) -> Result<(&'a str, Host, u16), Error> { + if id.len() > MAX_BYTES { + return Err(Error::MaximumLengthExceeded); + } + + let mut chars = id.chars(); + + if id.len() < MIN_CHARS { + return Err(Error::MinimumLengthNotSatisfied); + } + + let sigil = chars.nth(0).expect("ID missing first character."); + + if sigil != required_sigil { + return Err(Error::MissingSigil); + } + + let delimiter_index = match chars.position(|c| c == ':') { + Some(index) => index + 1, + None => return Err(Error::MissingDelimiter), + }; + + let localpart = &id[1..delimiter_index]; + let raw_host = &id[delimiter_index + SIGIL_BYTES..]; + let url_string = format!("https://{}", raw_host); + let url = try!(Url::parse(&url_string)); + + let host = match url.host() { + Some(host) => host.to_owned(), + None => return Err(Error::InvalidHost), + }; + + let port = url.port().unwrap_or(443); + + Ok((localpart, host, port)) +} + impl UserId { /// Create a new Matrix user ID from a string representation. /// /// The string must include the leading @ sigil, the localpart, a literal colon, and a valid /// server name. pub fn new(user_id: &str) -> Result { - let captures = match USER_ID_PATTERN.captures(user_id) { - Some(captures) => captures, - None => return Err(Error::InvalidFormat), - }; + let (localpart, host, port) = try!(parse_id('@', user_id)); - let raw_host = captures.name("host").expect("Failed to extract hostname from regex."); - - let url_string = format!("https://{}", raw_host); - - let url = try!(Url::parse(&url_string)); - - let host = match url.host() { - Some(host) => host, - None => return Err(Error::InvalidFormat), - }; - - let port = url.port().unwrap_or(443); + if !USER_LOCALPART_PATTERN.is_match(localpart) { + return Err(Error::InvalidCharacters); + } Ok(UserId { - hostname: host.to_owned(), + hostname: host, port: port, - localpart: captures - .name("localpart") - .expect("Failed to extract localpart from regex.") - .to_string(), + localpart: localpart.to_owned(), }) } @@ -98,8 +136,8 @@ impl UserId { } impl From for Error { - fn from(error: ParseError) -> Error { - Error::InvalidHost(error) + fn from(_: ParseError) -> Error { + Error::InvalidHost } } @@ -115,7 +153,7 @@ impl Display for UserId { #[cfg(test)] mod tests { - use super::UserId; + use super::{Error, UserId}; #[test] fn valid_user_id() { @@ -149,26 +187,41 @@ mod tests { #[test] fn invalid_characters_in_localpart() { - assert!(UserId::new("@CARL:example.com").is_err()); + assert_eq!( + UserId::new("@CARL:example.com").err().unwrap(), + Error::InvalidCharacters + ); } #[test] fn missing_sigil() { - assert!(UserId::new("carl:example.com").is_err()); + assert_eq!( + UserId::new("carl:example.com").err().unwrap(), + Error::MissingSigil + ); } #[test] - fn missing_domain() { - assert!(UserId::new("carl").is_err()); + fn missing_delimiter() { + assert_eq!( + UserId::new("@carl").err().unwrap(), + Error::MissingDelimiter + ); } #[test] fn invalid_host() { - assert!(UserId::new("@carl:-").is_err()); + assert_eq!( + UserId::new("@carl:-").err().unwrap(), + Error::InvalidHost + ); } #[test] fn invalid_port() { - assert!(UserId::new("@carl:example.com:notaport").is_err()); + assert_eq!( + UserId::new("@carl:example.com:notaport").err().unwrap(), + Error::InvalidHost + ); } } From 9055983642229afc7b64ef204f02dcf1a6f3408a Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Fri, 22 Jul 2016 22:41:40 -0700 Subject: [PATCH 003/140] Add event and room IDs. --- src/lib.rs | 269 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 254 insertions(+), 15 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d8e86fee..39777ec0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,6 +45,36 @@ pub enum Error { MissingSigil, } +/// A Matrix event ID. +/// +/// An `EventId` is created from a string slice, and can be converted back into a string as needed: +/// +/// ``` +/// # use ruma_identifiers::EventId; +/// assert_eq!(EventId::new("$h29iv0s8:example.com").unwrap().to_string(), "$h29iv0s8:example.com"); +/// ``` +#[derive(Debug)] +pub struct EventId { + hostname: Host, + opaque_id: String, + port: u16, +} + +/// A Matrix room ID. +/// +/// An `RoomId` is created from a string slice, and can be converted back into a string as needed: +/// +/// ``` +/// # use ruma_identifiers::RoomId; +/// assert_eq!(RoomId::new("!n8f893n9:example.com").unwrap().to_string(), "!n8f893n9:example.com"); +/// ``` +#[derive(Debug)] +pub struct RoomId { + hostname: Host, + opaque_id: String, + port: u16, +} + /// A Matrix user ID. /// /// A `UserId` is created from a string slice, and can be converted back into a string as needed: @@ -60,6 +90,15 @@ pub struct UserId { port: u16, } +fn display(f: &mut Formatter, sigil: char, localpart: &str, hostname: &Host, port: u16) +-> FmtResult { + if port == 443 { + write!(f, "{}{}:{}", sigil, localpart, hostname) + } else { + write!(f, "{}{}:{}:{}", sigil, localpart, hostname, port) + } +} + fn parse_id<'a>(required_sigil: char, id: &'a str) -> Result<(&'a str, Host, u16), Error> { if id.len() > MAX_BYTES { return Err(Error::MaximumLengthExceeded); @@ -97,8 +136,76 @@ fn parse_id<'a>(required_sigil: char, id: &'a str) -> Result<(&'a str, Host, u16 Ok((localpart, host, port)) } +impl EventId { + /// Creates a new Matrix event ID from a string representation. + /// + /// The string must include the leading $ sigil, the opaque ID, a literal colon, and a valid + /// server name. + pub fn new(event_id: &str) -> Result { + let (opaque_id, host, port) = try!(parse_id('$', event_id)); + + Ok(EventId { + hostname: host, + opaque_id: opaque_id.to_owned(), + port: port, + }) + } + + /// Returns a `url::Host` for the event ID, containing the server name (minus the port) of the + /// originating homeserver. + /// + /// The host can be either a domain name, an IPv4 address, or an IPv6 address. + pub fn hostname(&self) -> &Host { + &self.hostname + } + + /// Returns the event's opaque ID. + pub fn opaque_id(&self) -> &str { + &self.opaque_id + } + + /// Returns the port the originating homeserver can be accessed on. + pub fn port(&self) -> u16 { + self.port + } +} + +impl RoomId { + /// Creates a new Matrix room ID from a string representation. + /// + /// The string must include the leading ! sigil, the opaque ID, a literal colon, and a valid + /// server name. + pub fn new(room_id: &str) -> Result { + let (opaque_id, host, port) = try!(parse_id('!', room_id)); + + Ok(RoomId { + hostname: host, + opaque_id: opaque_id.to_owned(), + port: port, + }) + } + + /// Returns a `url::Host` for the room ID, containing the server name (minus the port) of the + /// originating homeserver. + /// + /// The host can be either a domain name, an IPv4 address, or an IPv6 address. + pub fn hostname(&self) -> &Host { + &self.hostname + } + + /// Returns the event's opaque ID. + pub fn opaque_id(&self) -> &str { + &self.opaque_id + } + + /// Returns the port the originating homeserver can be accessed on. + pub fn port(&self) -> u16 { + self.port + } +} + impl UserId { - /// Create a new Matrix user ID from a string representation. + /// Creates a new Matrix user ID from a string representation. /// /// The string must include the leading @ sigil, the localpart, a literal colon, and a valid /// server name. @@ -117,9 +224,9 @@ impl UserId { } /// Returns a `url::Host` for the user ID, containing the server name (minus the port) of the - /// user's homeserver. + /// originating homeserver. /// - /// This host can be either a domain name, an IPv4 address, or an IPv6 address. + /// The host can be either a domain name, an IPv4 address, or an IPv6 address. pub fn hostname(&self) -> &Host { &self.hostname } @@ -129,7 +236,7 @@ impl UserId { &self.localpart } - /// Returns the port the user's homeserver can be accessed on. + /// Returns the port the originating homeserver can be accessed on. pub fn port(&self) -> u16 { self.port } @@ -141,19 +248,151 @@ impl From for Error { } } +impl Display for EventId { + fn fmt(&self, f: &mut Formatter) -> FmtResult { + display(f, '$', &self.opaque_id, &self.hostname, self.port) + } +} + +impl Display for RoomId { + fn fmt(&self, f: &mut Formatter) -> FmtResult { + display(f, '!', &self.opaque_id, &self.hostname, self.port) + } +} + impl Display for UserId { fn fmt(&self, f: &mut Formatter) -> FmtResult { - if self.port == 443 { - write!(f, "@{}:{}", self.localpart, self.hostname) - } else { - write!(f, "@{}:{}:{}", self.localpart, self.hostname, self.port) - } + display(f, '@', &self.localpart, &self.hostname, self.port) } } #[cfg(test)] mod tests { - use super::{Error, UserId}; + use super::{Error, EventId, RoomId, UserId}; + + #[test] + fn valid_event_id() { + assert_eq!( + EventId::new("$39hvsi03hlne:example.com") + .expect("Failed to create EventId.") + .to_string(), + "$39hvsi03hlne:example.com" + ); + } + + #[test] + fn valid_event_id_with_explicit_standard_port() { + assert_eq!( + EventId::new("$39hvsi03hlne:example.com:443") + .expect("Failed to create EventId.") + .to_string(), + "$39hvsi03hlne:example.com" + ); + } + + #[test] + fn valid_event_id_with_non_standard_port() { + assert_eq!( + EventId::new("$39hvsi03hlne:example.com:5000") + .expect("Failed to create EventId.") + .to_string(), + "$39hvsi03hlne:example.com:5000" + ); + } + + #[test] + fn missing_event_id_sigil() { + assert_eq!( + EventId::new("39hvsi03hlne:example.com").err().unwrap(), + Error::MissingSigil + ); + } + + #[test] + fn missing_event_id_delimiter() { + assert_eq!( + EventId::new("$39hvsi03hlne").err().unwrap(), + Error::MissingDelimiter + ); + } + + #[test] + fn invalid_event_id_host() { + assert_eq!( + EventId::new("$39hvsi03hlne:-").err().unwrap(), + Error::InvalidHost + ); + } + + #[test] + fn invalid_event_id_port() { + assert_eq!( + EventId::new("$39hvsi03hlne:example.com:notaport").err().unwrap(), + Error::InvalidHost + ); + } + + #[test] + fn valid_room_id() { + assert_eq!( + RoomId::new("!29fhd83h92h0:example.com") + .expect("Failed to create RoomId.") + .to_string(), + "!29fhd83h92h0:example.com" + ); + } + + #[test] + fn valid_room_id_with_explicit_standard_port() { + assert_eq!( + RoomId::new("!29fhd83h92h0:example.com:443") + .expect("Failed to create RoomId.") + .to_string(), + "!29fhd83h92h0:example.com" + ); + } + + #[test] + fn valid_room_id_with_non_standard_port() { + assert_eq!( + RoomId::new("!29fhd83h92h0:example.com:5000") + .expect("Failed to create RoomId.") + .to_string(), + "!29fhd83h92h0:example.com:5000" + ); + } + + #[test] + fn missing_room_id_sigil() { + assert_eq!( + RoomId::new("carl:example.com").err().unwrap(), + Error::MissingSigil + ); + } + + #[test] + fn missing_room_id_delimiter() { + assert_eq!( + RoomId::new("!29fhd83h92h0").err().unwrap(), + Error::MissingDelimiter + ); + } + + #[test] + fn invalid_room_id_host() { + assert_eq!( + RoomId::new("!29fhd83h92h0:-").err().unwrap(), + Error::InvalidHost + ); + } + + #[test] + fn invalid_room_id_port() { + assert_eq!( + RoomId::new("!29fhd83h92h0:example.com:notaport").err().unwrap(), + Error::InvalidHost + ); + } #[test] fn valid_user_id() { @@ -186,7 +425,7 @@ mod tests { } #[test] - fn invalid_characters_in_localpart() { + fn invalid_characters_in_user_id_localpart() { assert_eq!( UserId::new("@CARL:example.com").err().unwrap(), Error::InvalidCharacters @@ -194,7 +433,7 @@ mod tests { } #[test] - fn missing_sigil() { + fn missing_user_id_sigil() { assert_eq!( UserId::new("carl:example.com").err().unwrap(), Error::MissingSigil @@ -202,7 +441,7 @@ mod tests { } #[test] - fn missing_delimiter() { + fn missing_user_id_delimiter() { assert_eq!( UserId::new("@carl").err().unwrap(), Error::MissingDelimiter @@ -210,7 +449,7 @@ mod tests { } #[test] - fn invalid_host() { + fn invalid_user_id_host() { assert_eq!( UserId::new("@carl:-").err().unwrap(), Error::InvalidHost @@ -218,7 +457,7 @@ mod tests { } #[test] - fn invalid_port() { + fn invalid_user_id_port() { assert_eq!( UserId::new("@carl:example.com:notaport").err().unwrap(), Error::InvalidHost From c357991bbddc976974f0450d1cf75fd0a2aaf8ae Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Fri, 22 Jul 2016 22:50:00 -0700 Subject: [PATCH 004/140] Fix some docstrings. --- Cargo.toml | 2 +- README.md | 2 +- src/lib.rs | 10 ++++++---- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3cdae31a..2cdbdf42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] authors = ["Jimmy Cuadra "] -description = "Opaque identifiers for Matrix." +description = "Resource identifiers for Matrix." documentation = "http://ruma.github.io/ruma-identifiers/ruma_identifiers" homepage = "https://github.com/ruma/ruma-identifiers" keywords = ["matrix", "matrix.org", "chat", "messaging", "ruma"] diff --git a/README.md b/README.md index 7e125d2f..91cbb4d5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ruma-identifiers -**ruma-identifiers** contains types for [Matrix](https://matrix.org/) opaque identifiers, such as user IDs, room IDs, and room aliases. +**ruma-identifiers** contains types for [Matrix](https://matrix.org/) identifiers for events, rooms, room aliases, and users. ## License diff --git a/src/lib.rs b/src/lib.rs index 39777ec0..6a3d2809 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,5 @@ -//! Crate **ruma_identifiers** contains types for [Matrix](https://matrix.org/) opaque identifiers, -//! such as user IDs, room IDs, and room aliases. +//! Crate **ruma_identifiers** contains types for [Matrix](https://matrix.org/) identifiers +//! for events, rooms, room aliases, and users. #![deny(missing_docs)] @@ -28,12 +28,14 @@ lazy_static! { Regex::new(r"\A[a-z0-9._=-]+\z").expect("Failed to create user localpart regex."); } -/// An error encountered when trying to parse an invalid user ID string. +/// An error encountered when trying to parse an invalid ID string. #[derive(Debug, PartialEq)] pub enum Error { /// The ID's localpart contains invalid characters. + /// + /// Only relevant for user IDs. InvalidCharacters, - /// The domain part of the user ID string was not a valid IP address or DNS name. + /// The domain part of the the ID string is not a valid IP address or DNS name. InvalidHost, /// The ID exceeds 255 bytes. MaximumLengthExceeded, From 4f3db4fff3c938f6411a49887c15a60b4aaa03f7 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Fri, 22 Jul 2016 22:59:53 -0700 Subject: [PATCH 005/140] Add RoomAliasId. --- src/lib.rs | 121 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 119 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 6a3d2809..6de22d69 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -62,9 +62,25 @@ pub struct EventId { port: u16, } +/// A Matrix room alias ID. +/// +/// A `RoomAliasId` is created from a string slice, and can be converted back into a string as +/// needed: +/// +/// ``` +/// # use ruma_identifiers::RoomAliasId; +/// assert_eq!(RoomAliasId::new("#ruma:example.com").unwrap().to_string(), "#ruma:example.com"); +/// ``` +#[derive(Debug)] +pub struct RoomAliasId { + alias: String, + hostname: Host, + port: u16, +} + /// A Matrix room ID. /// -/// An `RoomId` is created from a string slice, and can be converted back into a string as needed: +/// A `RoomId` is created from a string slice, and can be converted back into a string as needed: /// /// ``` /// # use ruma_identifiers::RoomId; @@ -206,6 +222,40 @@ impl RoomId { } } +impl RoomAliasId { + /// Creates a new Matrix room alias ID from a string representation. + /// + /// The string must include the leading # sigil, the alias, a literal colon, and a valid + /// server name. + pub fn new(room_id: &str) -> Result { + let (alias, host, port) = try!(parse_id('#', room_id)); + + Ok(RoomAliasId { + alias: alias.to_owned(), + hostname: host, + port: port, + }) + } + + /// Returns a `url::Host` for the room alias ID, containing the server name (minus the port) of + /// the originating homeserver. + /// + /// The host can be either a domain name, an IPv4 address, or an IPv6 address. + pub fn hostname(&self) -> &Host { + &self.hostname + } + + /// Returns the room's alias. + pub fn alias(&self) -> &str { + &self.alias + } + + /// Returns the port the originating homeserver can be accessed on. + pub fn port(&self) -> u16 { + self.port + } +} + impl UserId { /// Creates a new Matrix user ID from a string representation. /// @@ -256,6 +306,12 @@ impl Display for EventId { } } +impl Display for RoomAliasId { + fn fmt(&self, f: &mut Formatter) -> FmtResult { + display(f, '#', &self.alias, &self.hostname, self.port) + } +} + impl Display for RoomId { fn fmt(&self, f: &mut Formatter) -> FmtResult { display(f, '!', &self.opaque_id, &self.hostname, self.port) @@ -270,7 +326,7 @@ impl Display for UserId { #[cfg(test)] mod tests { - use super::{Error, EventId, RoomId, UserId}; + use super::{Error, EventId, RoomAliasId, RoomId, UserId}; #[test] fn valid_event_id() { @@ -334,6 +390,67 @@ mod tests { ); } + #[test] + fn valid_room_alias_id() { + assert_eq!( + RoomAliasId::new("#ruma:example.com") + .expect("Failed to create RoomAliasId.") + .to_string(), + "#ruma:example.com" + ); + } + + #[test] + fn valid_room_alias_id_with_explicit_standard_port() { + assert_eq!( + RoomAliasId::new("#ruma:example.com:443") + .expect("Failed to create RoomAliasId.") + .to_string(), + "#ruma:example.com" + ); + } + + #[test] + fn valid_room_alias_id_with_non_standard_port() { + assert_eq!( + RoomAliasId::new("#ruma:example.com:5000") + .expect("Failed to create RoomAliasId.") + .to_string(), + "#ruma:example.com:5000" + ); + } + + #[test] + fn missing_room_alias_id_sigil() { + assert_eq!( + RoomAliasId::new("39hvsi03hlne:example.com").err().unwrap(), + Error::MissingSigil + ); + } + + #[test] + fn missing_room_alias_id_delimiter() { + assert_eq!( + RoomAliasId::new("#ruma").err().unwrap(), + Error::MissingDelimiter + ); + } + + #[test] + fn invalid_room_alias_id_host() { + assert_eq!( + RoomAliasId::new("#ruma:-").err().unwrap(), + Error::InvalidHost + ); + } + + #[test] + fn invalid_room_alias_id_port() { + assert_eq!( + RoomAliasId::new("#ruma:example.com:notaport").err().unwrap(), + Error::InvalidHost + ); + } #[test] fn valid_room_id() { assert_eq!( From 920767c0204b50d4f0388ad82231b33d52c1179e Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Fri, 22 Jul 2016 23:01:28 -0700 Subject: [PATCH 006/140] Reexport url::Host. --- src/lib.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 6de22d69..949a2dcc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,7 +11,9 @@ extern crate url; use std::fmt::{Display, Formatter, Result as FmtResult}; use regex::Regex; -use url::{Host, ParseError, Url}; +use url::{ParseError, Url}; + +pub use url::Host; /// All events must be 255 bytes or less. const MAX_BYTES: usize = 255; @@ -169,7 +171,7 @@ impl EventId { }) } - /// Returns a `url::Host` for the event ID, containing the server name (minus the port) of the + /// Returns a `Host` for the event ID, containing the server name (minus the port) of the /// originating homeserver. /// /// The host can be either a domain name, an IPv4 address, or an IPv6 address. @@ -203,7 +205,7 @@ impl RoomId { }) } - /// Returns a `url::Host` for the room ID, containing the server name (minus the port) of the + /// Returns a `Host` for the room ID, containing the server name (minus the port) of the /// originating homeserver. /// /// The host can be either a domain name, an IPv4 address, or an IPv6 address. @@ -237,7 +239,7 @@ impl RoomAliasId { }) } - /// Returns a `url::Host` for the room alias ID, containing the server name (minus the port) of + /// Returns a `Host` for the room alias ID, containing the server name (minus the port) of /// the originating homeserver. /// /// The host can be either a domain name, an IPv4 address, or an IPv6 address. @@ -275,7 +277,7 @@ impl UserId { }) } - /// Returns a `url::Host` for the user ID, containing the server name (minus the port) of the + /// Returns a `Host` for the user ID, containing the server name (minus the port) of the /// originating homeserver. /// /// The host can be either a domain name, an IPv4 address, or an IPv6 address. From d05822199a763fe7b2c8ad55fc490490be8278cb Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Sat, 23 Jul 2016 01:19:12 -0700 Subject: [PATCH 007/140] Use question mark feature instead of try macro. --- src/lib.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 949a2dcc..07475bcf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ //! Crate **ruma_identifiers** contains types for [Matrix](https://matrix.org/) identifiers //! for events, rooms, room aliases, and users. +#![feature(question_mark)] #![deny(missing_docs)] #[macro_use] @@ -144,7 +145,7 @@ fn parse_id<'a>(required_sigil: char, id: &'a str) -> Result<(&'a str, Host, u16 let localpart = &id[1..delimiter_index]; let raw_host = &id[delimiter_index + SIGIL_BYTES..]; let url_string = format!("https://{}", raw_host); - let url = try!(Url::parse(&url_string)); + let url = Url::parse(&url_string)?; let host = match url.host() { Some(host) => host.to_owned(), @@ -162,7 +163,7 @@ impl EventId { /// The string must include the leading $ sigil, the opaque ID, a literal colon, and a valid /// server name. pub fn new(event_id: &str) -> Result { - let (opaque_id, host, port) = try!(parse_id('$', event_id)); + let (opaque_id, host, port) = parse_id('$', event_id)?; Ok(EventId { hostname: host, @@ -196,7 +197,7 @@ impl RoomId { /// The string must include the leading ! sigil, the opaque ID, a literal colon, and a valid /// server name. pub fn new(room_id: &str) -> Result { - let (opaque_id, host, port) = try!(parse_id('!', room_id)); + let (opaque_id, host, port) = parse_id('!', room_id)?; Ok(RoomId { hostname: host, @@ -230,7 +231,7 @@ impl RoomAliasId { /// The string must include the leading # sigil, the alias, a literal colon, and a valid /// server name. pub fn new(room_id: &str) -> Result { - let (alias, host, port) = try!(parse_id('#', room_id)); + let (alias, host, port) = parse_id('#', room_id)?; Ok(RoomAliasId { alias: alias.to_owned(), @@ -264,7 +265,7 @@ impl UserId { /// The string must include the leading @ sigil, the localpart, a literal colon, and a valid /// server name. pub fn new(user_id: &str) -> Result { - let (localpart, host, port) = try!(parse_id('@', user_id)); + let (localpart, host, port) = parse_id('@', user_id)?; if !USER_LOCALPART_PATTERN.is_match(localpart) { return Err(Error::InvalidCharacters); From d560ccebeb70d9304a5d00bf0c5bdefd745ecff2 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Tue, 26 Jul 2016 00:19:12 -0700 Subject: [PATCH 008/140] Implement serialization for ID types. --- Cargo.lock | 33 +++++++++++++++++++++++++ Cargo.toml | 4 ++++ src/lib.rs | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 55006674..18dc34ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,6 +4,8 @@ version = "0.1.0" dependencies = [ "lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "regex 0.1.73 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 0.8.0-rc3 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 0.8.0-rc1 (registry+https://github.com/rust-lang/crates.io-index)", "url 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -15,6 +17,11 @@ dependencies = [ "memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "dtoa" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "idna" version = "0.1.0" @@ -25,6 +32,11 @@ dependencies = [ "unicode-normalization 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "itoa" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "kernel32-sys" version = "0.2.2" @@ -57,6 +69,11 @@ dependencies = [ "libc 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "num-traits" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "regex" version = "0.1.73" @@ -74,6 +91,22 @@ name = "regex-syntax" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "serde" +version = "0.8.0-rc3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "serde_json" +version = "0.8.0-rc1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "dtoa 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "itoa 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.1.34 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 0.8.0-rc3 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "thread-id" version = "2.0.0" diff --git a/Cargo.toml b/Cargo.toml index 2cdbdf42..a05b8577 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,4 +13,8 @@ version = "0.1.0" [dependencies] lazy_static = "0.2.1" regex = "0.1.73" +serde = "0.8.0-rc3" url = "1.1.1" + +[dev-dependencies] +serde_json = "0.8.0-rc1" diff --git a/src/lib.rs b/src/lib.rs index 07475bcf..c7861589 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,11 +7,16 @@ #[macro_use] extern crate lazy_static; extern crate regex; +extern crate serde; extern crate url; +#[cfg(test)] +extern crate serde_json; + use std::fmt::{Display, Formatter, Result as FmtResult}; use regex::Regex; +use serde::{Serialize, Serializer}; use url::{ParseError, Url}; pub use url::Host; @@ -327,8 +332,33 @@ impl Display for UserId { } } +impl Serialize for EventId { + fn serialize(&self, serializer: &mut S) -> Result<(), S::Error> where S: Serializer { + serializer.serialize_str(&self.to_string()) + } +} + +impl Serialize for RoomAliasId { + fn serialize(&self, serializer: &mut S) -> Result<(), S::Error> where S: Serializer { + serializer.serialize_str(&self.to_string()) + } +} + +impl Serialize for RoomId { + fn serialize(&self, serializer: &mut S) -> Result<(), S::Error> where S: Serializer { + serializer.serialize_str(&self.to_string()) + } +} + +impl Serialize for UserId { + fn serialize(&self, serializer: &mut S) -> Result<(), S::Error> where S: Serializer { + serializer.serialize_str(&self.to_string()) + } +} + #[cfg(test)] mod tests { + use serde_json::to_string; use super::{Error, EventId, RoomAliasId, RoomId, UserId}; #[test] @@ -341,6 +371,16 @@ mod tests { ); } + #[test] + fn serialize_valid_event_id() { + assert_eq!( + to_string( + &EventId::new("$39hvsi03hlne:example.com").expect("Failed to create EventId.") + ).expect("Failed to convert EventId to JSON."), + r#""$39hvsi03hlne:example.com""# + ); + } + #[test] fn valid_event_id_with_explicit_standard_port() { assert_eq!( @@ -403,6 +443,16 @@ mod tests { ); } + #[test] + fn serialize_valid_room_alias_id() { + assert_eq!( + to_string( + &RoomAliasId::new("#ruma:example.com").expect("Failed to create RoomAliasId.") + ).expect("Failed to convert RoomAliasId to JSON."), + r##""#ruma:example.com""## + ); + } + #[test] fn valid_room_alias_id_with_explicit_standard_port() { assert_eq!( @@ -464,6 +514,16 @@ mod tests { ); } + #[test] + fn serialize_valid_room_id() { + assert_eq!( + to_string( + &RoomId::new("!29fhd83h92h0:example.com").expect("Failed to create RoomId.") + ).expect("Failed to convert RoomId to JSON."), + r#""!29fhd83h92h0:example.com""# + ); + } + #[test] fn valid_room_id_with_explicit_standard_port() { assert_eq!( @@ -526,6 +586,16 @@ mod tests { ); } + #[test] + fn serialize_valid_user_id() { + assert_eq!( + to_string( + &UserId::new("@carl:example.com").expect("Failed to create UserId.") + ).expect("Failed to convert UserId to JSON."), + r#""@carl:example.com""# + ); + } + #[test] fn valid_user_id_with_explicit_standard_port() { assert_eq!( From e8abd71cd0dcdac4037e709e245ef8ba398b2900 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Tue, 26 Jul 2016 01:45:15 -0700 Subject: [PATCH 009/140] Implement deserialization for ID types. --- src/lib.rs | 126 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 120 insertions(+), 6 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index c7861589..878230cf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,7 +16,8 @@ extern crate serde_json; use std::fmt::{Display, Formatter, Result as FmtResult}; use regex::Regex; -use serde::{Serialize, Serializer}; +use serde::{Deserialize, Deserializer, Error as SerdeError, Serialize, Serializer}; +use serde::de::Visitor; use url::{ParseError, Url}; pub use url::Host; @@ -63,7 +64,7 @@ pub enum Error { /// # use ruma_identifiers::EventId; /// assert_eq!(EventId::new("$h29iv0s8:example.com").unwrap().to_string(), "$h29iv0s8:example.com"); /// ``` -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub struct EventId { hostname: Host, opaque_id: String, @@ -79,7 +80,7 @@ pub struct EventId { /// # use ruma_identifiers::RoomAliasId; /// assert_eq!(RoomAliasId::new("#ruma:example.com").unwrap().to_string(), "#ruma:example.com"); /// ``` -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub struct RoomAliasId { alias: String, hostname: Host, @@ -94,7 +95,7 @@ pub struct RoomAliasId { /// # use ruma_identifiers::RoomId; /// assert_eq!(RoomId::new("!n8f893n9:example.com").unwrap().to_string(), "!n8f893n9:example.com"); /// ``` -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub struct RoomId { hostname: Host, opaque_id: String, @@ -109,13 +110,18 @@ pub struct RoomId { /// # use ruma_identifiers::UserId; /// assert_eq!(UserId::new("@carl:example.com").unwrap().to_string(), "@carl:example.com"); /// ``` -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub struct UserId { hostname: Host, localpart: String, port: u16, } +struct EventIdVisitor; +struct RoomAliasIdVisitor; +struct RoomIdVisitor; +struct UserIdVisitor; + fn display(f: &mut Formatter, sigil: char, localpart: &str, hostname: &Host, port: u16) -> FmtResult { if port == 443 { @@ -356,9 +362,77 @@ impl Serialize for UserId { } } +impl Deserialize for EventId { + fn deserialize(deserializer: &mut D) -> Result where D: Deserializer { + deserializer.deserialize(EventIdVisitor) + } +} + +impl Deserialize for RoomAliasId { + fn deserialize(deserializer: &mut D) -> Result where D: Deserializer { + deserializer.deserialize(RoomAliasIdVisitor) + } +} + +impl Deserialize for RoomId { + fn deserialize(deserializer: &mut D) -> Result where D: Deserializer { + deserializer.deserialize(RoomIdVisitor) + } +} + +impl Deserialize for UserId { + fn deserialize(deserializer: &mut D) -> Result where D: Deserializer { + deserializer.deserialize(UserIdVisitor) + } +} + +impl Visitor for EventIdVisitor { + type Value = EventId; + + fn visit_str(&mut self, v: &str) -> Result where E: SerdeError { + match EventId::new(v) { + Ok(event_id) => Ok(event_id), + Err(_) => Err(SerdeError::custom("invalid ID")), + } + } +} + +impl Visitor for RoomAliasIdVisitor { + type Value = RoomAliasId; + + fn visit_str(&mut self, v: &str) -> Result where E: SerdeError { + match RoomAliasId::new(v) { + Ok(room_alias_id) => Ok(room_alias_id), + Err(_) => Err(SerdeError::custom("invalid ID")), + } + } +} + +impl Visitor for RoomIdVisitor { + type Value = RoomId; + + fn visit_str(&mut self, v: &str) -> Result where E: SerdeError { + match RoomId::new(v) { + Ok(room_id) => Ok(room_id), + Err(_) => Err(SerdeError::custom("invalid ID")), + } + } +} + +impl Visitor for UserIdVisitor { + type Value = UserId; + + fn visit_str(&mut self, v: &str) -> Result where E: SerdeError { + match UserId::new(v) { + Ok(user_id) => Ok(user_id), + Err(_) => Err(SerdeError::custom("invalid ID")), + } + } +} + #[cfg(test)] mod tests { - use serde_json::to_string; + use serde_json::{from_str, to_string}; use super::{Error, EventId, RoomAliasId, RoomId, UserId}; #[test] @@ -381,6 +455,16 @@ mod tests { ); } + #[test] + fn deserialize_valid_event_id() { + assert_eq!( + from_str::( + r#""$39hvsi03hlne:example.com""# + ).expect("Failed to convert JSON to EventId"), + EventId::new("$39hvsi03hlne:example.com").expect("Failed to create EventId.") + ); + } + #[test] fn valid_event_id_with_explicit_standard_port() { assert_eq!( @@ -453,6 +537,16 @@ mod tests { ); } + #[test] + fn deserialize_valid_room_alias_id() { + assert_eq!( + from_str::( + r##""#ruma:example.com""## + ).expect("Failed to convert JSON to RoomAliasId"), + RoomAliasId::new("#ruma:example.com").expect("Failed to create RoomAliasId.") + ); + } + #[test] fn valid_room_alias_id_with_explicit_standard_port() { assert_eq!( @@ -524,6 +618,16 @@ mod tests { ); } + #[test] + fn deserialize_valid_room_id() { + assert_eq!( + from_str::( + r#""!29fhd83h92h0:example.com""# + ).expect("Failed to convert JSON to RoomId"), + RoomId::new("!29fhd83h92h0:example.com").expect("Failed to create RoomId.") + ); + } + #[test] fn valid_room_id_with_explicit_standard_port() { assert_eq!( @@ -596,6 +700,16 @@ mod tests { ); } + #[test] + fn deserialize_valid_user_id() { + assert_eq!( + from_str::( + r#""@carl:example.com""# + ).expect("Failed to convert JSON to UserId"), + UserId::new("@carl:example.com").expect("Failed to create UserId.") + ); + } + #[test] fn valid_user_id_with_explicit_standard_port() { assert_eq!( From 91711c6544ada95734f2449a80a8860b8a9385f6 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Tue, 26 Jul 2016 05:36:51 -0700 Subject: [PATCH 010/140] new --> try_from --- src/lib.rs | 265 +++++++++++++++++++++++++++++++---------------------- 1 file changed, 154 insertions(+), 111 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 878230cf..5bbf6af5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,7 @@ //! Crate **ruma_identifiers** contains types for [Matrix](https://matrix.org/) identifiers //! for events, rooms, room aliases, and users. -#![feature(question_mark)] +#![feature(question_mark, try_from)] #![deny(missing_docs)] #[macro_use] @@ -13,6 +13,7 @@ extern crate url; #[cfg(test)] extern crate serde_json; +use std::convert::TryFrom; use std::fmt::{Display, Formatter, Result as FmtResult}; use regex::Regex; @@ -58,11 +59,17 @@ pub enum Error { /// A Matrix event ID. /// -/// An `EventId` is created from a string slice, and can be converted back into a string as needed: +/// An `EventId` is generated randomly or converted from a string slice, and can be converted back +/// into a string as needed. /// /// ``` +/// # #![feature(try_from)] +/// # use std::convert::TryFrom; /// # use ruma_identifiers::EventId; -/// assert_eq!(EventId::new("$h29iv0s8:example.com").unwrap().to_string(), "$h29iv0s8:example.com"); +/// assert_eq!( +/// EventId::try_from("$h29iv0s8:example.com").unwrap().to_string(), +/// "$h29iv0s8:example.com" +/// ); /// ``` #[derive(Debug, PartialEq)] pub struct EventId { @@ -73,12 +80,17 @@ pub struct EventId { /// A Matrix room alias ID. /// -/// A `RoomAliasId` is created from a string slice, and can be converted back into a string as -/// needed: +/// A `RoomAliasId` is generated randomly or converted from a string slice, and can be converted +/// back into a string as needed. /// /// ``` +/// # #![feature(try_from)] +/// # use std::convert::TryFrom; /// # use ruma_identifiers::RoomAliasId; -/// assert_eq!(RoomAliasId::new("#ruma:example.com").unwrap().to_string(), "#ruma:example.com"); +/// assert_eq!( +/// RoomAliasId::try_from("#ruma:example.com").unwrap().to_string(), +/// "#ruma:example.com" +/// ); /// ``` #[derive(Debug, PartialEq)] pub struct RoomAliasId { @@ -89,11 +101,17 @@ pub struct RoomAliasId { /// A Matrix room ID. /// -/// A `RoomId` is created from a string slice, and can be converted back into a string as needed: +/// A `RoomId` is generated randomly or converted from a string slice, and can be converted back +/// into a string as needed. /// /// ``` +/// # #![feature(try_from)] +/// # use std::convert::TryFrom; /// # use ruma_identifiers::RoomId; -/// assert_eq!(RoomId::new("!n8f893n9:example.com").unwrap().to_string(), "!n8f893n9:example.com"); +/// assert_eq!( +/// RoomId::try_from("!n8f893n9:example.com").unwrap().to_string(), +/// "!n8f893n9:example.com" +/// ); /// ``` #[derive(Debug, PartialEq)] pub struct RoomId { @@ -104,11 +122,17 @@ pub struct RoomId { /// A Matrix user ID. /// -/// A `UserId` is created from a string slice, and can be converted back into a string as needed: +/// A `UserId` is generated randomly or converted from a string slice, and can be converted back +/// into a string as needed. /// /// ``` +/// # #![feature(try_from)] +/// # use std::convert::TryFrom; /// # use ruma_identifiers::UserId; -/// assert_eq!(UserId::new("@carl:example.com").unwrap().to_string(), "@carl:example.com"); +/// assert_eq!( +/// UserId::try_from("@carl:example.com").unwrap().to_string(), +/// "@carl:example.com" +/// ); /// ``` #[derive(Debug, PartialEq)] pub struct UserId { @@ -169,20 +193,6 @@ fn parse_id<'a>(required_sigil: char, id: &'a str) -> Result<(&'a str, Host, u16 } impl EventId { - /// Creates a new Matrix event ID from a string representation. - /// - /// The string must include the leading $ sigil, the opaque ID, a literal colon, and a valid - /// server name. - pub fn new(event_id: &str) -> Result { - let (opaque_id, host, port) = parse_id('$', event_id)?; - - Ok(EventId { - hostname: host, - opaque_id: opaque_id.to_owned(), - port: port, - }) - } - /// Returns a `Host` for the event ID, containing the server name (minus the port) of the /// originating homeserver. /// @@ -203,20 +213,6 @@ impl EventId { } impl RoomId { - /// Creates a new Matrix room ID from a string representation. - /// - /// The string must include the leading ! sigil, the opaque ID, a literal colon, and a valid - /// server name. - pub fn new(room_id: &str) -> Result { - let (opaque_id, host, port) = parse_id('!', room_id)?; - - Ok(RoomId { - hostname: host, - opaque_id: opaque_id.to_owned(), - port: port, - }) - } - /// Returns a `Host` for the room ID, containing the server name (minus the port) of the /// originating homeserver. /// @@ -237,20 +233,6 @@ impl RoomId { } impl RoomAliasId { - /// Creates a new Matrix room alias ID from a string representation. - /// - /// The string must include the leading # sigil, the alias, a literal colon, and a valid - /// server name. - pub fn new(room_id: &str) -> Result { - let (alias, host, port) = parse_id('#', room_id)?; - - Ok(RoomAliasId { - alias: alias.to_owned(), - hostname: host, - port: port, - }) - } - /// Returns a `Host` for the room alias ID, containing the server name (minus the port) of /// the originating homeserver. /// @@ -271,24 +253,6 @@ impl RoomAliasId { } impl UserId { - /// Creates a new Matrix user ID from a string representation. - /// - /// The string must include the leading @ sigil, the localpart, a literal colon, and a valid - /// server name. - pub fn new(user_id: &str) -> Result { - let (localpart, host, port) = parse_id('@', user_id)?; - - if !USER_LOCALPART_PATTERN.is_match(localpart) { - return Err(Error::InvalidCharacters); - } - - Ok(UserId { - hostname: host, - port: port, - localpart: localpart.to_owned(), - }) - } - /// Returns a `Host` for the user ID, containing the server name (minus the port) of the /// originating homeserver. /// @@ -386,11 +350,87 @@ impl Deserialize for UserId { } } +impl<'a> TryFrom<&'a str> for EventId { + type Err = Error; + + /// Attempts to create a new Matrix event ID from a string representation. + /// + /// The string must include the leading $ sigil, the opaque ID, a literal colon, and a valid + /// server name. + fn try_from(event_id: &'a str) -> Result { + let (opaque_id, host, port) = parse_id('$', event_id)?; + + Ok(EventId { + hostname: host, + opaque_id: opaque_id.to_owned(), + port: port, + }) + } +} + +impl<'a> TryFrom<&'a str> for RoomAliasId { + type Err = Error; + + /// 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 valid + /// server name. + fn try_from(room_id: &'a str) -> Result { + let (alias, host, port) = parse_id('#', room_id)?; + + Ok(RoomAliasId { + alias: alias.to_owned(), + hostname: host, + port: port, + }) + } +} + +impl<'a> TryFrom<&'a str> for RoomId { + type Err = Error; + + /// Attempts to create a new Matrix room ID from a string representation. + /// + /// The string must include the leading ! sigil, the opaque ID, a literal colon, and a valid + /// server name. + fn try_from(room_id: &'a str) -> Result { + let (opaque_id, host, port) = parse_id('!', room_id)?; + + Ok(RoomId { + hostname: host, + opaque_id: opaque_id.to_owned(), + port: port, + }) + } +} + +impl<'a> TryFrom<&'a str> for UserId { + type Err = Error; + + /// Attempts to create a new Matrix user ID from a string representation. + /// + /// The string must include the leading @ sigil, the localpart, a literal colon, and a valid + /// server name. + fn try_from(user_id: &'a str) -> Result { + let (localpart, host, port) = parse_id('@', user_id)?; + + if !USER_LOCALPART_PATTERN.is_match(localpart) { + return Err(Error::InvalidCharacters); + } + + Ok(UserId { + hostname: host, + port: port, + localpart: localpart.to_owned(), + }) + } +} + impl Visitor for EventIdVisitor { type Value = EventId; fn visit_str(&mut self, v: &str) -> Result where E: SerdeError { - match EventId::new(v) { + match EventId::try_from(v) { Ok(event_id) => Ok(event_id), Err(_) => Err(SerdeError::custom("invalid ID")), } @@ -401,7 +441,7 @@ impl Visitor for RoomAliasIdVisitor { type Value = RoomAliasId; fn visit_str(&mut self, v: &str) -> Result where E: SerdeError { - match RoomAliasId::new(v) { + match RoomAliasId::try_from(v) { Ok(room_alias_id) => Ok(room_alias_id), Err(_) => Err(SerdeError::custom("invalid ID")), } @@ -412,7 +452,7 @@ impl Visitor for RoomIdVisitor { type Value = RoomId; fn visit_str(&mut self, v: &str) -> Result where E: SerdeError { - match RoomId::new(v) { + match RoomId::try_from(v) { Ok(room_id) => Ok(room_id), Err(_) => Err(SerdeError::custom("invalid ID")), } @@ -423,7 +463,7 @@ impl Visitor for UserIdVisitor { type Value = UserId; fn visit_str(&mut self, v: &str) -> Result where E: SerdeError { - match UserId::new(v) { + match UserId::try_from(v) { Ok(user_id) => Ok(user_id), Err(_) => Err(SerdeError::custom("invalid ID")), } @@ -432,13 +472,16 @@ impl Visitor for UserIdVisitor { #[cfg(test)] mod tests { + use std::convert::TryFrom; + use serde_json::{from_str, to_string}; + use super::{Error, EventId, RoomAliasId, RoomId, UserId}; #[test] fn valid_event_id() { assert_eq!( - EventId::new("$39hvsi03hlne:example.com") + EventId::try_from("$39hvsi03hlne:example.com") .expect("Failed to create EventId.") .to_string(), "$39hvsi03hlne:example.com" @@ -449,7 +492,7 @@ mod tests { fn serialize_valid_event_id() { assert_eq!( to_string( - &EventId::new("$39hvsi03hlne:example.com").expect("Failed to create EventId.") + &EventId::try_from("$39hvsi03hlne:example.com").expect("Failed to create EventId.") ).expect("Failed to convert EventId to JSON."), r#""$39hvsi03hlne:example.com""# ); @@ -461,14 +504,14 @@ mod tests { from_str::( r#""$39hvsi03hlne:example.com""# ).expect("Failed to convert JSON to EventId"), - EventId::new("$39hvsi03hlne:example.com").expect("Failed to create EventId.") + EventId::try_from("$39hvsi03hlne:example.com").expect("Failed to create EventId.") ); } #[test] fn valid_event_id_with_explicit_standard_port() { assert_eq!( - EventId::new("$39hvsi03hlne:example.com:443") + EventId::try_from("$39hvsi03hlne:example.com:443") .expect("Failed to create EventId.") .to_string(), "$39hvsi03hlne:example.com" @@ -478,7 +521,7 @@ mod tests { #[test] fn valid_event_id_with_non_standard_port() { assert_eq!( - EventId::new("$39hvsi03hlne:example.com:5000") + EventId::try_from("$39hvsi03hlne:example.com:5000") .expect("Failed to create EventId.") .to_string(), "$39hvsi03hlne:example.com:5000" @@ -488,7 +531,7 @@ mod tests { #[test] fn missing_event_id_sigil() { assert_eq!( - EventId::new("39hvsi03hlne:example.com").err().unwrap(), + EventId::try_from("39hvsi03hlne:example.com").err().unwrap(), Error::MissingSigil ); } @@ -496,7 +539,7 @@ mod tests { #[test] fn missing_event_id_delimiter() { assert_eq!( - EventId::new("$39hvsi03hlne").err().unwrap(), + EventId::try_from("$39hvsi03hlne").err().unwrap(), Error::MissingDelimiter ); } @@ -504,7 +547,7 @@ mod tests { #[test] fn invalid_event_id_host() { assert_eq!( - EventId::new("$39hvsi03hlne:-").err().unwrap(), + EventId::try_from("$39hvsi03hlne:-").err().unwrap(), Error::InvalidHost ); } @@ -512,7 +555,7 @@ mod tests { #[test] fn invalid_event_id_port() { assert_eq!( - EventId::new("$39hvsi03hlne:example.com:notaport").err().unwrap(), + EventId::try_from("$39hvsi03hlne:example.com:notaport").err().unwrap(), Error::InvalidHost ); } @@ -520,7 +563,7 @@ mod tests { #[test] fn valid_room_alias_id() { assert_eq!( - RoomAliasId::new("#ruma:example.com") + RoomAliasId::try_from("#ruma:example.com") .expect("Failed to create RoomAliasId.") .to_string(), "#ruma:example.com" @@ -531,7 +574,7 @@ mod tests { fn serialize_valid_room_alias_id() { assert_eq!( to_string( - &RoomAliasId::new("#ruma:example.com").expect("Failed to create RoomAliasId.") + &RoomAliasId::try_from("#ruma:example.com").expect("Failed to create RoomAliasId.") ).expect("Failed to convert RoomAliasId to JSON."), r##""#ruma:example.com""## ); @@ -543,14 +586,14 @@ mod tests { from_str::( r##""#ruma:example.com""## ).expect("Failed to convert JSON to RoomAliasId"), - RoomAliasId::new("#ruma:example.com").expect("Failed to create RoomAliasId.") + RoomAliasId::try_from("#ruma:example.com").expect("Failed to create RoomAliasId.") ); } #[test] fn valid_room_alias_id_with_explicit_standard_port() { assert_eq!( - RoomAliasId::new("#ruma:example.com:443") + RoomAliasId::try_from("#ruma:example.com:443") .expect("Failed to create RoomAliasId.") .to_string(), "#ruma:example.com" @@ -560,7 +603,7 @@ mod tests { #[test] fn valid_room_alias_id_with_non_standard_port() { assert_eq!( - RoomAliasId::new("#ruma:example.com:5000") + RoomAliasId::try_from("#ruma:example.com:5000") .expect("Failed to create RoomAliasId.") .to_string(), "#ruma:example.com:5000" @@ -570,7 +613,7 @@ mod tests { #[test] fn missing_room_alias_id_sigil() { assert_eq!( - RoomAliasId::new("39hvsi03hlne:example.com").err().unwrap(), + RoomAliasId::try_from("39hvsi03hlne:example.com").err().unwrap(), Error::MissingSigil ); } @@ -578,7 +621,7 @@ mod tests { #[test] fn missing_room_alias_id_delimiter() { assert_eq!( - RoomAliasId::new("#ruma").err().unwrap(), + RoomAliasId::try_from("#ruma").err().unwrap(), Error::MissingDelimiter ); } @@ -586,7 +629,7 @@ mod tests { #[test] fn invalid_room_alias_id_host() { assert_eq!( - RoomAliasId::new("#ruma:-").err().unwrap(), + RoomAliasId::try_from("#ruma:-").err().unwrap(), Error::InvalidHost ); } @@ -594,14 +637,14 @@ mod tests { #[test] fn invalid_room_alias_id_port() { assert_eq!( - RoomAliasId::new("#ruma:example.com:notaport").err().unwrap(), + RoomAliasId::try_from("#ruma:example.com:notaport").err().unwrap(), Error::InvalidHost ); } #[test] fn valid_room_id() { assert_eq!( - RoomId::new("!29fhd83h92h0:example.com") + RoomId::try_from("!29fhd83h92h0:example.com") .expect("Failed to create RoomId.") .to_string(), "!29fhd83h92h0:example.com" @@ -612,7 +655,7 @@ mod tests { fn serialize_valid_room_id() { assert_eq!( to_string( - &RoomId::new("!29fhd83h92h0:example.com").expect("Failed to create RoomId.") + &RoomId::try_from("!29fhd83h92h0:example.com").expect("Failed to create RoomId.") ).expect("Failed to convert RoomId to JSON."), r#""!29fhd83h92h0:example.com""# ); @@ -624,14 +667,14 @@ mod tests { from_str::( r#""!29fhd83h92h0:example.com""# ).expect("Failed to convert JSON to RoomId"), - RoomId::new("!29fhd83h92h0:example.com").expect("Failed to create RoomId.") + RoomId::try_from("!29fhd83h92h0:example.com").expect("Failed to create RoomId.") ); } #[test] fn valid_room_id_with_explicit_standard_port() { assert_eq!( - RoomId::new("!29fhd83h92h0:example.com:443") + RoomId::try_from("!29fhd83h92h0:example.com:443") .expect("Failed to create RoomId.") .to_string(), "!29fhd83h92h0:example.com" @@ -641,7 +684,7 @@ mod tests { #[test] fn valid_room_id_with_non_standard_port() { assert_eq!( - RoomId::new("!29fhd83h92h0:example.com:5000") + RoomId::try_from("!29fhd83h92h0:example.com:5000") .expect("Failed to create RoomId.") .to_string(), "!29fhd83h92h0:example.com:5000" @@ -651,7 +694,7 @@ mod tests { #[test] fn missing_room_id_sigil() { assert_eq!( - RoomId::new("carl:example.com").err().unwrap(), + RoomId::try_from("carl:example.com").err().unwrap(), Error::MissingSigil ); } @@ -659,7 +702,7 @@ mod tests { #[test] fn missing_room_id_delimiter() { assert_eq!( - RoomId::new("!29fhd83h92h0").err().unwrap(), + RoomId::try_from("!29fhd83h92h0").err().unwrap(), Error::MissingDelimiter ); } @@ -667,7 +710,7 @@ mod tests { #[test] fn invalid_room_id_host() { assert_eq!( - RoomId::new("!29fhd83h92h0:-").err().unwrap(), + RoomId::try_from("!29fhd83h92h0:-").err().unwrap(), Error::InvalidHost ); } @@ -675,7 +718,7 @@ mod tests { #[test] fn invalid_room_id_port() { assert_eq!( - RoomId::new("!29fhd83h92h0:example.com:notaport").err().unwrap(), + RoomId::try_from("!29fhd83h92h0:example.com:notaport").err().unwrap(), Error::InvalidHost ); } @@ -683,7 +726,7 @@ mod tests { #[test] fn valid_user_id() { assert_eq!( - UserId::new("@carl:example.com") + UserId::try_from("@carl:example.com") .expect("Failed to create UserId.") .to_string(), "@carl:example.com" @@ -694,7 +737,7 @@ mod tests { fn serialize_valid_user_id() { assert_eq!( to_string( - &UserId::new("@carl:example.com").expect("Failed to create UserId.") + &UserId::try_from("@carl:example.com").expect("Failed to create UserId.") ).expect("Failed to convert UserId to JSON."), r#""@carl:example.com""# ); @@ -706,14 +749,14 @@ mod tests { from_str::( r#""@carl:example.com""# ).expect("Failed to convert JSON to UserId"), - UserId::new("@carl:example.com").expect("Failed to create UserId.") + UserId::try_from("@carl:example.com").expect("Failed to create UserId.") ); } #[test] fn valid_user_id_with_explicit_standard_port() { assert_eq!( - UserId::new("@carl:example.com:443") + UserId::try_from("@carl:example.com:443") .expect("Failed to create UserId.") .to_string(), "@carl:example.com" @@ -723,7 +766,7 @@ mod tests { #[test] fn valid_user_id_with_non_standard_port() { assert_eq!( - UserId::new("@carl:example.com:5000") + UserId::try_from("@carl:example.com:5000") .expect("Failed to create UserId.") .to_string(), "@carl:example.com:5000" @@ -733,7 +776,7 @@ mod tests { #[test] fn invalid_characters_in_user_id_localpart() { assert_eq!( - UserId::new("@CARL:example.com").err().unwrap(), + UserId::try_from("@CARL:example.com").err().unwrap(), Error::InvalidCharacters ); } @@ -741,7 +784,7 @@ mod tests { #[test] fn missing_user_id_sigil() { assert_eq!( - UserId::new("carl:example.com").err().unwrap(), + UserId::try_from("carl:example.com").err().unwrap(), Error::MissingSigil ); } @@ -749,7 +792,7 @@ mod tests { #[test] fn missing_user_id_delimiter() { assert_eq!( - UserId::new("@carl").err().unwrap(), + UserId::try_from("@carl").err().unwrap(), Error::MissingDelimiter ); } @@ -757,7 +800,7 @@ mod tests { #[test] fn invalid_user_id_host() { assert_eq!( - UserId::new("@carl:-").err().unwrap(), + UserId::try_from("@carl:-").err().unwrap(), Error::InvalidHost ); } @@ -765,7 +808,7 @@ mod tests { #[test] fn invalid_user_id_port() { assert_eq!( - UserId::new("@carl:example.com:notaport").err().unwrap(), + UserId::try_from("@carl:example.com:notaport").err().unwrap(), Error::InvalidHost ); } From 20cfd5200e2d1aaeb73ce27aa3ba58f3fc8fae1d Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Tue, 26 Jul 2016 06:00:30 -0700 Subject: [PATCH 011/140] Add random generation constructors. --- Cargo.lock | 9 +++++ Cargo.toml | 1 + src/lib.rs | 100 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 108 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 18dc34ca..2a9fa604 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,6 +3,7 @@ name = "ruma-identifiers" version = "0.1.0" dependencies = [ "lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", "regex 0.1.73 (registry+https://github.com/rust-lang/crates.io-index)", "serde 0.8.0-rc3 (registry+https://github.com/rust-lang/crates.io-index)", "serde_json 0.8.0-rc1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -74,6 +75,14 @@ name = "num-traits" version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "rand" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "regex" version = "0.1.73" diff --git a/Cargo.toml b/Cargo.toml index a05b8577..2cf20488 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ version = "0.1.0" [dependencies] lazy_static = "0.2.1" +rand = "0.3.14" regex = "0.1.73" serde = "0.8.0-rc3" url = "1.1.1" diff --git a/src/lib.rs b/src/lib.rs index 5bbf6af5..c173c638 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,7 @@ #[macro_use] extern crate lazy_static; +extern crate rand; extern crate regex; extern crate serde; extern crate url; @@ -16,6 +17,7 @@ extern crate serde_json; use std::convert::TryFrom; use std::fmt::{Display, Formatter, Result as FmtResult}; +use rand::{Rng, thread_rng}; use regex::Regex; use serde::{Deserialize, Deserializer, Error as SerdeError, Serialize, Serializer}; use serde::de::Visitor; @@ -80,8 +82,8 @@ pub struct EventId { /// A Matrix room alias ID. /// -/// A `RoomAliasId` is generated randomly or converted from a string slice, and can be converted -/// back into a string as needed. +/// A `RoomAliasId` is converted from a string slice, and can be converted back into a string as +/// needed. /// /// ``` /// # #![feature(try_from)] @@ -155,6 +157,10 @@ fn display(f: &mut Formatter, sigil: char, localpart: &str, hostname: &Host, por } } +fn generate_localpart(length: usize) -> String { + thread_rng().gen_ascii_chars().take(length).collect() +} + fn parse_id<'a>(required_sigil: char, id: &'a str) -> Result<(&'a str, Host, u16), Error> { if id.len() > MAX_BYTES { return Err(Error::MaximumLengthExceeded); @@ -193,6 +199,21 @@ fn parse_id<'a>(required_sigil: char, id: &'a str) -> Result<(&'a str, Host, u16 } impl EventId { + /// Attempts to generate an `EventId` for the given origin server with a localpart consisting + /// of 18 random ASCII characters. + /// + /// Fails if the given origin server name cannot be parsed as a valid host. + pub fn new(server_name: &str) -> Result { + let event_id = format!("${}:{}", generate_localpart(18), server_name); + let (opaque_id, host, port) = parse_id('$', &event_id)?; + + Ok(EventId { + hostname: host, + opaque_id: opaque_id.to_string(), + port: port, + }) + } + /// Returns a `Host` for the event ID, containing the server name (minus the port) of the /// originating homeserver. /// @@ -213,6 +234,21 @@ impl EventId { } impl RoomId { + /// Attempts to generate a `RoomId` for the given origin server with a localpart consisting of + /// 18 random ASCII characters. + /// + /// Fails if the given origin server name cannot be parsed as a valid host. + pub fn new(server_name: &str) -> Result { + let room_id = format!("!{}:{}", generate_localpart(18), server_name); + let (opaque_id, host, port) = parse_id('!', &room_id)?; + + Ok(RoomId { + hostname: host, + opaque_id: opaque_id.to_string(), + port: port, + }) + } + /// Returns a `Host` for the room ID, containing the server name (minus the port) of the /// originating homeserver. /// @@ -253,6 +289,21 @@ impl RoomAliasId { } impl UserId { + /// Attempts to generate a `UserId` for the given origin server with a localpart consisting of + /// 12 random ASCII characters. + /// + /// Fails if the given origin server name cannot be parsed as a valid host. + pub fn new(server_name: &str) -> Result { + let user_id = format!("@{}:{}", generate_localpart(12), server_name); + let (localpart, host, port) = parse_id('@', &user_id)?; + + Ok(UserId { + hostname: host, + localpart: localpart.to_string(), + port: port, + }) + } + /// Returns a `Host` for the user ID, containing the server name (minus the port) of the /// originating homeserver. /// @@ -488,6 +539,21 @@ mod tests { ); } + #[test] + fn generate_random_valid_event_id() { + let event_id = EventId::new("example.com") + .expect("Failed to generate EventId.") + .to_string(); + + assert!(event_id.to_string().starts_with('$')); + assert_eq!(event_id.len(), 31); + } + + #[test] + fn generate_random_invalid_event_id() { + assert!(EventId::new("").is_err()); + } + #[test] fn serialize_valid_event_id() { assert_eq!( @@ -651,6 +717,21 @@ mod tests { ); } + #[test] + fn generate_random_valid_room_id() { + let room_id = RoomId::new("example.com") + .expect("Failed to generate RoomId.") + .to_string(); + + assert!(room_id.to_string().starts_with('!')); + assert_eq!(room_id.len(), 31); + } + + #[test] + fn generate_random_invalid_room_id() { + assert!(RoomId::new("").is_err()); + } + #[test] fn serialize_valid_room_id() { assert_eq!( @@ -733,6 +814,21 @@ mod tests { ); } + #[test] + fn generate_random_valid_user_id() { + let user_id = UserId::new("example.com") + .expect("Failed to generate UserId.") + .to_string(); + + assert!(user_id.to_string().starts_with('@')); + assert_eq!(user_id.len(), 25); + } + + #[test] + fn generate_random_invalid_user_id() { + assert!(UserId::new("").is_err()); + } + #[test] fn serialize_valid_user_id() { assert_eq!( From 43d668e9efdc795669c16a9a09a7e7606e58a98e Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Tue, 26 Jul 2016 06:06:51 -0700 Subject: [PATCH 012/140] Implement std::error::Error for Error. --- src/lib.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index c173c638..0b69cdad 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,6 +14,7 @@ extern crate url; #[cfg(test)] extern crate serde_json; +use std::error::Error as StdError; use std::convert::TryFrom; use std::fmt::{Display, Formatter, Result as FmtResult}; @@ -198,6 +199,25 @@ fn parse_id<'a>(required_sigil: char, id: &'a str) -> Result<(&'a str, Host, u16 Ok((localpart, host, port)) } +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> FmtResult { + write!(f, "{}", self.description()) + } +} + +impl StdError for Error { + fn description(&self) -> &str { + match *self { + Error::InvalidCharacters => "localpart contains invalid characters", + Error::InvalidHost => "server name is not a valid IP address or domain name", + Error::MaximumLengthExceeded => "ID exceeds 255 bytes", + Error::MinimumLengthNotSatisfied => "ID must be at least 4 characters", + Error::MissingDelimiter => "colon is required between localpart and server name", + Error::MissingSigil => "leading sigil is missing", + } + } +} + impl EventId { /// Attempts to generate an `EventId` for the given origin server with a localpart consisting /// of 18 random ASCII characters. From 7b93b18d14457265e12b213398781ed08f924aa7 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Thu, 28 Jul 2016 01:57:54 -0700 Subject: [PATCH 013/140] Bump Serde to 0.8.0. --- Cargo.lock | 10 +++++----- Cargo.toml | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2a9fa604..34e17e27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5,8 +5,8 @@ dependencies = [ "lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", "regex 0.1.73 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 0.8.0-rc3 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 0.8.0-rc1 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "url 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -102,18 +102,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "serde" -version = "0.8.0-rc3" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "serde_json" -version = "0.8.0-rc1" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "dtoa 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "itoa 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", "num-traits 0.1.34 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 0.8.0-rc3 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 2cf20488..cba9effb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,8 +14,8 @@ version = "0.1.0" lazy_static = "0.2.1" rand = "0.3.14" regex = "0.1.73" -serde = "0.8.0-rc3" +serde = "0.8.0" url = "1.1.1" [dev-dependencies] -serde_json = "0.8.0-rc1" +serde_json = "0.8.0" From e78f817f3e09528aabf1a08d0685c65795291ba9 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Thu, 28 Jul 2016 01:59:39 -0700 Subject: [PATCH 014/140] Add Travis badge to README. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 91cbb4d5..73c1257c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # ruma-identifiers +[![Build Status](https://travis-ci.org/ruma/ruma-identifiers.svg?branch=master)](https://travis-ci.org/ruma/ruma-identifiers) + **ruma-identifiers** contains types for [Matrix](https://matrix.org/) identifiers for events, rooms, room aliases, and users. ## License From 2c7d937dfc2567154fdedd85c6130f691bcb3722 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Thu, 28 Jul 2016 02:01:49 -0700 Subject: [PATCH 015/140] Add .travis.yml. --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..c2099e6d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,3 @@ +language: "rust" +rust: + - "nightly" From 8aa5339b6420be2eee277fd5d20b35ba4ecdd598 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Thu, 28 Jul 2016 02:04:45 -0700 Subject: [PATCH 016/140] Add notification settings to .travis.yml. --- .travis.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.travis.yml b/.travis.yml index c2099e6d..4e2e913d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,9 @@ language: "rust" +notifications: + email: false + irc: + channels: + - "chat.freenode.net#ruma" + use_notice: true rust: - "nightly" From 6032f730f889f06b4a8110c04769a71342363468 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Thu, 28 Jul 2016 02:17:48 -0700 Subject: [PATCH 017/140] Remove bad keyword from Cargo.toml and add docs link to the README. [ci skip] --- Cargo.toml | 4 ++-- README.md | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index cba9effb..b791e604 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,9 @@ [package] authors = ["Jimmy Cuadra "] description = "Resource identifiers for Matrix." -documentation = "http://ruma.github.io/ruma-identifiers/ruma_identifiers" +documentation = "http://ruma.github.io/ruma-identifiers/ruma_identifiers/" homepage = "https://github.com/ruma/ruma-identifiers" -keywords = ["matrix", "matrix.org", "chat", "messaging", "ruma"] +keywords = ["matrix", "chat", "messaging", "ruma"] license = "MIT" name = "ruma-identifiers" readme = "README.md" diff --git a/README.md b/README.md index 73c1257c..a6a7589c 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![Build Status](https://travis-ci.org/ruma/ruma-identifiers.svg?branch=master)](https://travis-ci.org/ruma/ruma-identifiers) **ruma-identifiers** contains types for [Matrix](https://matrix.org/) identifiers for events, rooms, room aliases, and users. +[Documentation](http://ruma.github.io/ruma-identifiers/ruma_identifiers/) is available for the latest release. ## License From 28fdff9a6564b75a83f557a2caabe04fb901e65f Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Thu, 28 Jul 2016 02:57:36 -0700 Subject: [PATCH 018/140] Derive Eq and Hash for ID types. --- src/lib.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 0b69cdad..17840f00 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -74,7 +74,7 @@ pub enum Error { /// "$h29iv0s8:example.com" /// ); /// ``` -#[derive(Debug, PartialEq)] +#[derive(Debug, Eq, Hash, PartialEq)] pub struct EventId { hostname: Host, opaque_id: String, @@ -95,7 +95,7 @@ pub struct EventId { /// "#ruma:example.com" /// ); /// ``` -#[derive(Debug, PartialEq)] +#[derive(Debug, Eq, Hash, PartialEq)] pub struct RoomAliasId { alias: String, hostname: Host, @@ -116,7 +116,7 @@ pub struct RoomAliasId { /// "!n8f893n9:example.com" /// ); /// ``` -#[derive(Debug, PartialEq)] +#[derive(Debug, Eq, Hash, PartialEq)] pub struct RoomId { hostname: Host, opaque_id: String, @@ -137,7 +137,7 @@ pub struct RoomId { /// "@carl:example.com" /// ); /// ``` -#[derive(Debug, PartialEq)] +#[derive(Debug, Eq, Hash, PartialEq)] pub struct UserId { hostname: Host, localpart: String, From a9388b93453b83d797147468382280f10f982ed7 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Thu, 28 Jul 2016 02:58:06 -0700 Subject: [PATCH 019/140] Bump version to 0.2.0. --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b791e604..884101b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,14 +1,14 @@ [package] authors = ["Jimmy Cuadra "] description = "Resource identifiers for Matrix." -documentation = "http://ruma.github.io/ruma-identifiers/ruma_identifiers/" +documentation = "https://ruma.github.io/ruma-identifiers/ruma_identifiers/" homepage = "https://github.com/ruma/ruma-identifiers" keywords = ["matrix", "chat", "messaging", "ruma"] license = "MIT" name = "ruma-identifiers" readme = "README.md" repository = "https://github.com/ruma/ruma-identifiers" -version = "0.1.0" +version = "0.2.0" [dependencies] lazy_static = "0.2.1" From f7a2df85a244c4fd23c363c09181293426254c4d Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Thu, 28 Jul 2016 03:00:05 -0700 Subject: [PATCH 020/140] Ignore Cargo.lock. [ci skip] --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ea8c4bf7..06aba01b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ +Cargo.lock /target From 3c66f7268a2afc1397fa9732459448413983c8f8 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Sat, 13 Aug 2016 00:50:36 -0700 Subject: [PATCH 021/140] Derive Clone for ID types and Copy for Error. --- Cargo.lock | 172 ----------------------------------------------------- src/lib.rs | 10 ++-- 2 files changed, 5 insertions(+), 177 deletions(-) delete mode 100644 Cargo.lock diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index 34e17e27..00000000 --- a/Cargo.lock +++ /dev/null @@ -1,172 +0,0 @@ -[root] -name = "ruma-identifiers" -version = "0.1.0" -dependencies = [ - "lazy_static 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", - "regex 0.1.73 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", - "url 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "aho-corasick" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "dtoa" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "idna" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "matches 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-bidi 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-normalization 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "itoa" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "kernel32-sys" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "lazy_static" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "libc" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "matches" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "memchr" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "libc 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "num-traits" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "rand" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "libc 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "regex" -version = "0.1.73" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "aho-corasick 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", - "memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", - "regex-syntax 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "thread_local 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", - "utf8-ranges 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "regex-syntax" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "serde" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "serde_json" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "dtoa 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", - "itoa 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", - "num-traits 0.1.34 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "thread-id" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "thread_local" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "thread-id 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "unicode-bidi" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "matches 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "unicode-normalization" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "url" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "idna 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "matches 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "utf8-ranges" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "winapi" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "winapi-build" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" - diff --git a/src/lib.rs b/src/lib.rs index 17840f00..a85b6d9b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,7 +42,7 @@ lazy_static! { } /// An error encountered when trying to parse an invalid ID string. -#[derive(Debug, PartialEq)] +#[derive(Copy, Clone, Debug, PartialEq)] pub enum Error { /// The ID's localpart contains invalid characters. /// @@ -74,7 +74,7 @@ pub enum Error { /// "$h29iv0s8:example.com" /// ); /// ``` -#[derive(Debug, Eq, Hash, PartialEq)] +#[derive(Clone, Debug, Eq, Hash, PartialEq)] pub struct EventId { hostname: Host, opaque_id: String, @@ -95,7 +95,7 @@ pub struct EventId { /// "#ruma:example.com" /// ); /// ``` -#[derive(Debug, Eq, Hash, PartialEq)] +#[derive(Clone, Debug, Eq, Hash, PartialEq)] pub struct RoomAliasId { alias: String, hostname: Host, @@ -116,7 +116,7 @@ pub struct RoomAliasId { /// "!n8f893n9:example.com" /// ); /// ``` -#[derive(Debug, Eq, Hash, PartialEq)] +#[derive(Clone, Debug, Eq, Hash, PartialEq)] pub struct RoomId { hostname: Host, opaque_id: String, @@ -137,7 +137,7 @@ pub struct RoomId { /// "@carl:example.com" /// ); /// ``` -#[derive(Debug, Eq, Hash, PartialEq)] +#[derive(Clone, Debug, Eq, Hash, PartialEq)] pub struct UserId { hostname: Host, localpart: String, From 7443da026973f467d820a73fd3ae2c5744543d97 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Sat, 13 Aug 2016 00:52:11 -0700 Subject: [PATCH 022/140] Update deps. --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 884101b9..8fe53086 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,8 +14,8 @@ version = "0.2.0" lazy_static = "0.2.1" rand = "0.3.14" regex = "0.1.73" -serde = "0.8.0" -url = "1.1.1" +serde = "0.8.1" +url = "1.2.0" [dev-dependencies] -serde_json = "0.8.0" +serde_json = "0.8.1" From 44026081bc594517ffee1db08c1f5c7ea5818ccc Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Sat, 13 Aug 2016 00:52:48 -0700 Subject: [PATCH 023/140] Bump version to 0.3.0. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 8fe53086..2783d2de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT" name = "ruma-identifiers" readme = "README.md" repository = "https://github.com/ruma/ruma-identifiers" -version = "0.2.0" +version = "0.3.0" [dependencies] lazy_static = "0.2.1" From 971948d3866205544390651d3323b74b5af4cac0 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Sat, 27 Aug 2016 03:50:12 -0700 Subject: [PATCH 024/140] Add Diesel integration. --- Cargo.toml | 4 +++ src/lib.rs | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 2783d2de..fe904aff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,5 +17,9 @@ regex = "0.1.73" serde = "0.8.1" url = "1.2.0" +[dependencies.diesel] +optional = true +version = "0.7.1" + [dev-dependencies] serde_json = "0.8.1" diff --git a/src/lib.rs b/src/lib.rs index a85b6d9b..c9a0c57b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,6 +11,9 @@ extern crate regex; extern crate serde; extern crate url; +#[cfg(feature = "diesel")] +extern crate diesel; + #[cfg(test)] extern crate serde_json; @@ -541,6 +544,83 @@ impl Visitor for UserIdVisitor { } } +#[cfg(feature = "diesel")] +mod diesel_integration { + use std::convert::TryFrom; + use std::error::Error; + use std::io::Write; + + use diesel::Queryable; + use diesel::backend::Backend; + use diesel::expression::AsExpression; + use diesel::expression::bound::Bound; + use diesel::row::Row; + use diesel::types::{FromSql, FromSqlRow, HasSqlType, IsNull, Text, ToSql}; + + macro_rules! diesel_impl { + ($name:ident) => { + impl FromSql for $crate::$name + where DB: Backend + HasSqlType, String: FromSql { + fn from_sql(bytes: Option<&DB::RawValue>) + -> Result> { + let string = >::from_sql(bytes)?; + + $crate::$name::try_from(&string) + .map_err(|error| Box::new(error) as Box) + } + } + + impl FromSqlRow for $crate::$name + where DB: Backend + HasSqlType, String: FromSql { + fn build_from_row>(row: &mut T) + -> Result> { + FromSql::::from_sql(row.take()) + } + } + + impl ToSql for $crate::$name + where DB: Backend + HasSqlType, String: ToSql { + fn to_sql(&self, out: &mut W) + -> Result> { + self.to_string().to_sql(out) + } + } + + impl Queryable for $crate::$name where + $crate::$name: FromSqlRow, + DB: Backend + HasSqlType, + { + type Row = Self; + + fn build(row: Self::Row) -> Self { + row + } + } + + impl AsExpression for $crate::$name { + type Expression = Bound; + + fn as_expression(self) -> Self::Expression { + Bound::new(self) + } + } + + impl<'a> AsExpression for &'a $crate::$name { + type Expression = Bound; + + fn as_expression(self) -> Self::Expression { + Bound::new(self) + } + } + } + } + + diesel_impl!(EventId); + diesel_impl!(RoomAliasId); + diesel_impl!(RoomId); + diesel_impl!(UserId); +} + #[cfg(test)] mod tests { use std::convert::TryFrom; From 0caed9be182cd8cd82b5db66d97b3f760681a753 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Sat, 27 Aug 2016 03:51:16 -0700 Subject: [PATCH 025/140] Bump version to 0.4.0. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index fe904aff..a6c54437 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT" name = "ruma-identifiers" readme = "README.md" repository = "https://github.com/ruma/ruma-identifiers" -version = "0.3.0" +version = "0.4.0" [dependencies] lazy_static = "0.2.1" From 9da1442ffc48c8a7b7b006acff0c4498cd471074 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Sun, 28 Aug 2016 04:27:32 -0700 Subject: [PATCH 026/140] Automatically downcase UserId localparts. --- src/lib.rs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index c9a0c57b..c1654513 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -317,7 +317,7 @@ impl UserId { /// /// Fails if the given origin server name cannot be parsed as a valid host. pub fn new(server_name: &str) -> Result { - let user_id = format!("@{}:{}", generate_localpart(12), server_name); + let user_id = format!("@{}:{}", generate_localpart(12).to_lowercase(), server_name); let (localpart, host, port) = parse_id('@', &user_id)?; Ok(UserId { @@ -487,15 +487,16 @@ impl<'a> TryFrom<&'a str> for UserId { /// server name. fn try_from(user_id: &'a str) -> Result { let (localpart, host, port) = parse_id('@', user_id)?; + let downcased_localpart = localpart.to_lowercase(); - if !USER_LOCALPART_PATTERN.is_match(localpart) { + if !USER_LOCALPART_PATTERN.is_match(&downcased_localpart) { return Err(Error::InvalidCharacters); } Ok(UserId { hostname: host, port: port, - localpart: localpart.to_owned(), + localpart: downcased_localpart.to_owned(), }) } } @@ -914,6 +915,16 @@ mod tests { ); } + #[test] + fn downcase_user_id() { + assert_eq!( + UserId::try_from("@CARL:example.com") + .expect("Failed to create UserId.") + .to_string(), + "@carl:example.com" + ); + } + #[test] fn generate_random_valid_user_id() { let user_id = UserId::new("example.com") @@ -972,7 +983,7 @@ mod tests { #[test] fn invalid_characters_in_user_id_localpart() { assert_eq!( - UserId::try_from("@CARL:example.com").err().unwrap(), + UserId::try_from("@%%%:example.com").err().unwrap(), Error::InvalidCharacters ); } From e4e1ea1440beb010a0d481bf67827d88f6fdfefd Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Sun, 28 Aug 2016 04:28:13 -0700 Subject: [PATCH 027/140] Bump version to 0.4.1. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index a6c54437..9e79b1f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT" name = "ruma-identifiers" readme = "README.md" repository = "https://github.com/ruma/ruma-identifiers" -version = "0.4.0" +version = "0.4.1" [dependencies] lazy_static = "0.2.1" From ff55576d0fd05bd8c8d81227d1aa47b59bf5b774 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Sun, 16 Oct 2016 15:52:06 -0700 Subject: [PATCH 028/140] Update deps, remove question_mark feature, bump version to 0.4.2. --- Cargo.toml | 12 ++++++------ src/lib.rs | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9e79b1f0..7599527c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,18 +8,18 @@ license = "MIT" name = "ruma-identifiers" readme = "README.md" repository = "https://github.com/ruma/ruma-identifiers" -version = "0.4.1" +version = "0.4.2" [dependencies] lazy_static = "0.2.1" rand = "0.3.14" -regex = "0.1.73" -serde = "0.8.1" -url = "1.2.0" +regex = "0.1.77" +serde = "0.8.13" +url = "1.2.1" [dependencies.diesel] optional = true -version = "0.7.1" +version = "0.8.0" [dev-dependencies] -serde_json = "0.8.1" +serde_json = "0.8.3" diff --git a/src/lib.rs b/src/lib.rs index c1654513..9e3930ff 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,7 @@ //! Crate **ruma_identifiers** contains types for [Matrix](https://matrix.org/) identifiers //! for events, rooms, room aliases, and users. -#![feature(question_mark, try_from)] +#![feature(try_from)] #![deny(missing_docs)] #[macro_use] From 9b956152d0b06eecbc3c42a95350250ff5c8c8dd Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Tue, 29 Nov 2016 03:58:59 -0800 Subject: [PATCH 029/140] Update dependencies and bump version to 0.4.3. --- Cargo.toml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7599527c..a12c98e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,18 +8,18 @@ license = "MIT" name = "ruma-identifiers" readme = "README.md" repository = "https://github.com/ruma/ruma-identifiers" -version = "0.4.2" +version = "0.4.3" [dependencies] -lazy_static = "0.2.1" -rand = "0.3.14" -regex = "0.1.77" -serde = "0.8.13" -url = "1.2.1" +lazy_static = "0.2.2" +rand = "0.3.15" +regex = "0.1.80" +serde = "0.8.19" +url = "1.2.3" [dependencies.diesel] optional = true -version = "0.8.0" +version = "0.8.2" [dev-dependencies] serde_json = "0.8.3" From 1893c6939f029658894fe0078be6580602b1e9b5 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Fri, 9 Dec 2016 17:27:48 -0800 Subject: [PATCH 030/140] Bump Diesel to 0.9.0. --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a12c98e9..b5c27899 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ url = "1.2.3" [dependencies.diesel] optional = true -version = "0.8.2" +version = "0.9.0" [dev-dependencies] -serde_json = "0.8.3" +serde_json = "0.8.4" From ead08e2ec56eb9656f318f90c63a8842ae48bfbe Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Fri, 9 Dec 2016 17:28:29 -0800 Subject: [PATCH 031/140] Bump version to 0.5.0. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index b5c27899..86fceac7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT" name = "ruma-identifiers" readme = "README.md" repository = "https://github.com/ruma/ruma-identifiers" -version = "0.4.3" +version = "0.5.0" [dependencies] lazy_static = "0.2.2" From 9d42cd54139dd7bf36ad631565c080a801419773 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Sun, 11 Dec 2016 23:25:26 -0800 Subject: [PATCH 032/140] Encrypt IRC channel name for Travis notifications. See https://github.com/travis-ci/travis-ci/issues/1094 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 4e2e913d..a9217dc2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ notifications: email: false irc: channels: - - "chat.freenode.net#ruma" + - secure: "eyhVIoItrOoGxIRYXWQp+uqeTlbqn0MW2/+1Gl4raWzHkeZBME8GFezNxBR1bQrse3qlYj+/UXA43vHCDeU2+xBmvYujpBfjWD7f7huj1zIag+dNHP7k9sIQ09fmKSY+D3Vwr6Y9/wj+xrPNzL7xsxXCduL3w37QTZNqFWTcVuFetqqEFasSnNlkSCsM7Gv6QsaLNNBWsLT9q3va7PQdFwYhitisBAtI79tTcGpBoPpH4xegnai02fYu7WNCPetdQF7tyZb+ioRt9b159neqBb/BoftOvLEzEHtpirTMXYizlURdR63am/pYaEk0S7//WSatHLs2UB1JD5qDWjCCldZD7q+Xn2JEdNVwWv0vTtKd4jYfbGNbIYXgEIvOpoBrQoIAkfa4mVd8pOaexydFStdW/Q7P+h8ZSJoituq0Z6tCbeb+zVKi9n5Qx9F+OQcXREV6FKo5YE+L0VQ91QNnoJKPNxj1OMk+R2/xwbu2+qcDsrwzhwC6woaboxw1jLUTD/T3VeMIKB3NvDt4UivkKvWugMN0QR72xD3eU3+3yXaGVbmSZhC+rbaXE8x3ZViukbPlBMKvKtCI6nzuHt9lw7cl5qub/hz3Pt379fj92Z5Q7R1RvlwFwm7dJ7zypAsg2ZCWyKP0q4gvnd8SNxJ972YTK+tHZuq56JzVA7mi2IU=" use_notice: true rust: - "nightly" From 5c729ca988cf47c5d8703202eaddb1b239244221 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Br=C3=B6nnimann?= Date: Sat, 17 Dec 2016 12:08:16 +0100 Subject: [PATCH 033/140] Add RoomIdOrAliasId --- src/lib.rs | 181 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 177 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 9e3930ff..61bc8f8a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -126,6 +126,31 @@ pub struct RoomId { port: u16, } +/// A Matrix room ID or a Matrix room alias ID. +/// +/// A `RoomIdOrAliasId` is converted from a string slice, and can be converted back into a +/// string as needed. +/// +/// ``` +/// # #![feature(try_from)] +/// # use std::convert::TryFrom; +/// # use ruma_identifiers::RoomIdOrAliasId; +/// assert_eq!( +/// RoomIdOrAliasId::try_from("#ruma:example.com").unwrap().to_string(), +/// "#ruma:example.com" +/// ); +/// assert_eq!( +/// RoomIdOrAliasId::try_from("!n8f893n9:example.com").unwrap().to_string(), +/// "!n8f893n9:example.com" +/// ); +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub enum RoomIdOrAliasId { + /// A Matrix room alias ID. + RoomAliasId(RoomAliasId), + /// A Matrix room ID. + RoomId(RoomId), +} + /// A Matrix user ID. /// /// A `UserId` is generated randomly or converted from a string slice, and can be converted back @@ -150,6 +175,7 @@ pub struct UserId { struct EventIdVisitor; struct RoomAliasIdVisitor; struct RoomIdVisitor; +struct RoomIdOrAliasIdVisitor; struct UserIdVisitor; fn display(f: &mut Formatter, sigil: char, localpart: &str, hostname: &Host, port: u16) @@ -165,17 +191,23 @@ fn generate_localpart(length: usize) -> String { thread_rng().gen_ascii_chars().take(length).collect() } -fn parse_id<'a>(required_sigil: char, id: &'a str) -> Result<(&'a str, Host, u16), Error> { +fn validate_id<'a>(id: &'a str) -> Result<(), Error> { if id.len() > MAX_BYTES { return Err(Error::MaximumLengthExceeded); } - let mut chars = id.chars(); - if id.len() < MIN_CHARS { return Err(Error::MinimumLengthNotSatisfied); } + Ok(()) +} + +fn parse_id<'a>(required_sigil: char, id: &'a str) -> Result<(&'a str, Host, u16), Error> { + validate_id(id)?; + + let mut chars = id.chars(); + let sigil = chars.nth(0).expect("ID missing first character."); if sigil != required_sigil { @@ -370,6 +402,19 @@ impl Display for RoomId { } } +impl Display for RoomIdOrAliasId { + fn fmt(&self, f: &mut Formatter) -> FmtResult { + match *self { + RoomIdOrAliasId::RoomAliasId(ref room_alias_id) => { + display(f, '#', &room_alias_id.alias, &room_alias_id.hostname, room_alias_id.port) + } + RoomIdOrAliasId::RoomId(ref room_id) => { + display(f, '!', &room_id.opaque_id, &room_id.hostname, room_id.port) + } + } + } +} + impl Display for UserId { fn fmt(&self, f: &mut Formatter) -> FmtResult { display(f, '@', &self.localpart, &self.hostname, self.port) @@ -394,6 +439,19 @@ impl Serialize for RoomId { } } +impl Serialize for RoomIdOrAliasId { + fn serialize(&self, serializer: &mut S) -> Result<(), S::Error> where S: Serializer { + match *self { + RoomIdOrAliasId::RoomAliasId(ref room_alias_id) => { + serializer.serialize_str(&room_alias_id.to_string()) + } + RoomIdOrAliasId::RoomId(ref room_id) => { + serializer.serialize_str(&room_id.to_string()) + } + } + } +} + impl Serialize for UserId { fn serialize(&self, serializer: &mut S) -> Result<(), S::Error> where S: Serializer { serializer.serialize_str(&self.to_string()) @@ -418,6 +476,12 @@ impl Deserialize for RoomId { } } +impl Deserialize for RoomIdOrAliasId { + fn deserialize(deserializer: &mut D) -> Result where D: Deserializer { + deserializer.deserialize(RoomIdOrAliasIdVisitor) + } +} + impl Deserialize for UserId { fn deserialize(deserializer: &mut D) -> Result where D: Deserializer { deserializer.deserialize(UserIdVisitor) @@ -478,6 +542,35 @@ impl<'a> TryFrom<&'a str> for RoomId { } } +impl<'a> TryFrom<&'a str> for RoomIdOrAliasId { + type Err = Error; + + /// Attempts to create a new Matrix room ID or a room alias ID from a string representation. + /// + /// The string must either + /// include the leading ! sigil, the opaque ID, a literal colon, and a valid server name or + /// include the leading # sigil, the alias, a literal colon, and a valid server name. + fn try_from(room_id_or_alias_id: &'a str) -> Result { + validate_id(room_id_or_alias_id)?; + + let mut chars = room_id_or_alias_id.chars(); + + let sigil = chars.nth(0).expect("ID missing first character."); + + match sigil { + '#' => { + let room_alias_id = RoomAliasId::try_from(room_id_or_alias_id)?; + Ok(RoomIdOrAliasId::RoomAliasId(room_alias_id)) + } + '!' => { + let room_id = RoomId::try_from(room_id_or_alias_id)?; + Ok(RoomIdOrAliasId::RoomId(room_id)) + } + _ => Err(Error::MissingSigil) + } + } +} + impl<'a> TryFrom<&'a str> for UserId { type Err = Error; @@ -534,6 +627,17 @@ impl Visitor for RoomIdVisitor { } } +impl Visitor for RoomIdOrAliasIdVisitor { + type Value = RoomIdOrAliasId; + + fn visit_str(&mut self, v: &str) -> Result where E: SerdeError { + match RoomIdOrAliasId::try_from(v) { + Ok(room_id_or_alias_id) => Ok(room_id_or_alias_id), + Err(_) => Err(SerdeError::custom("invalid ID")), + } + } +} + impl Visitor for UserIdVisitor { type Value = UserId; @@ -619,6 +723,7 @@ mod diesel_integration { diesel_impl!(EventId); diesel_impl!(RoomAliasId); diesel_impl!(RoomId); + diesel_impl!(RoomIdOrAliasId); diesel_impl!(UserId); } @@ -628,7 +733,7 @@ mod tests { use serde_json::{from_str, to_string}; - use super::{Error, EventId, RoomAliasId, RoomId, UserId}; + use super::{Error, EventId, RoomAliasId, RoomId, RoomIdOrAliasId, UserId}; #[test] fn valid_event_id() { @@ -905,6 +1010,74 @@ mod tests { ); } + #[test] + fn valid_room_id_or_alias_id_with_a_room_alias_id() { + assert_eq!( + RoomIdOrAliasId::try_from("#ruma:example.com") + .expect("Failed to create RoomAliasId.") + .to_string(), + "#ruma:example.com" + ); + } + + #[test] + fn valid_room_id_or_alias_id_with_a_room_id() { + assert_eq!( + RoomIdOrAliasId::try_from("!29fhd83h92h0:example.com") + .expect("Failed to create RoomId.") + .to_string(), + "!29fhd83h92h0:example.com" + ); + } + + #[test] + fn missing_sigil_for_room_id_or_alias_id() { + assert_eq!( + RoomIdOrAliasId::try_from("ruma:example.com").err().unwrap(), + Error::MissingSigil + ); + } + + #[test] + fn serialize_valid_room_id_or_alias_id_with_a_room_alias_id() { + assert_eq!( + to_string( + &RoomIdOrAliasId::try_from("#ruma:example.com").expect("Failed to create RoomAliasId.") + ).expect("Failed to convert RoomAliasId to JSON."), + r##""#ruma:example.com""## + ); + } + + #[test] + fn serialize_valid_room_id_or_alias_id_with_a_room_id() { + assert_eq!( + to_string( + &RoomIdOrAliasId::try_from("!29fhd83h92h0:example.com").expect("Failed to create RoomId.") + ).expect("Failed to convert RoomId to JSON."), + r#""!29fhd83h92h0:example.com""# + ); + } + + #[test] + fn deserialize_valid_room_id_or_alias_id_with_a_room_alias_id() { + assert_eq!( + from_str::( + r##""#ruma:example.com""## + ).expect("Failed to convert JSON to RoomAliasId"), + RoomIdOrAliasId::try_from("#ruma:example.com").expect("Failed to create RoomAliasId.") + ); + } + + #[test] + fn deserialize_valid_room_id_or_alias_id_with_a_room_id() { + assert_eq!( + from_str::( + r##""!29fhd83h92h0:example.com""## + ).expect("Failed to convert JSON to RoomId"), + RoomIdOrAliasId::try_from("!29fhd83h92h0:example.com").expect("Failed to create RoomAliasId.") + ); + } + #[test] fn valid_user_id() { assert_eq!( From 059b747cbb0cd2badcb5b4c74bc944da2e2e2495 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Tue, 20 Dec 2016 22:58:05 -0800 Subject: [PATCH 034/140] Add a little more detail to the RoomIdOrAliasId docstring. --- src/lib.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 61bc8f8a..cc0140ff 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -128,8 +128,9 @@ pub struct RoomId { /// A Matrix room ID or a Matrix room alias ID. /// -/// A `RoomIdOrAliasId` is converted from a string slice, and can be converted back into a -/// string as needed. +/// `RoomIdOrAliasId` is useful for APIs that accept either kind of room identifier. It is converted +/// from a string slice, and can be converted back into a string as needed. When converted from a +/// string slice, the variant is determined by the leading sigil character. /// /// ``` /// # #![feature(try_from)] @@ -139,6 +140,7 @@ pub struct RoomId { /// RoomIdOrAliasId::try_from("#ruma:example.com").unwrap().to_string(), /// "#ruma:example.com" /// ); +/// /// assert_eq!( /// RoomIdOrAliasId::try_from("!n8f893n9:example.com").unwrap().to_string(), /// "!n8f893n9:example.com" From bf3b408cce5fa88a144bc970f0ba3090a5aaa368 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Tue, 20 Dec 2016 22:58:12 -0800 Subject: [PATCH 035/140] Bump version to 0.6.0. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 86fceac7..b96a0966 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT" name = "ruma-identifiers" readme = "README.md" repository = "https://github.com/ruma/ruma-identifiers" -version = "0.5.0" +version = "0.6.0" [dependencies] lazy_static = "0.2.2" From 67ee31b3aa0ef31c65d7b519d71827762d1527d9 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Thu, 26 Jan 2017 00:08:17 -0800 Subject: [PATCH 036/140] Update to serde 0.9, regex 0.2, and url 1.4. --- Cargo.toml | 8 +++---- src/lib.rs | 64 +++++++++++++++++++++++++++++++++++------------------- 2 files changed, 46 insertions(+), 26 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b96a0966..422a55c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,13 +13,13 @@ version = "0.6.0" [dependencies] lazy_static = "0.2.2" rand = "0.3.15" -regex = "0.1.80" -serde = "0.8.19" -url = "1.2.3" +regex = "0.2.1" +serde = "0.9.1" +url = "1.4.0" [dependencies.diesel] optional = true version = "0.9.0" [dev-dependencies] -serde_json = "0.8.4" +serde_json = "0.9.1" diff --git a/src/lib.rs b/src/lib.rs index cc0140ff..48360e3c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,8 +23,8 @@ use std::fmt::{Display, Formatter, Result as FmtResult}; use rand::{Rng, thread_rng}; use regex::Regex; -use serde::{Deserialize, Deserializer, Error as SerdeError, Serialize, Serializer}; -use serde::de::Visitor; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use serde::de::{Error as SerdeError, Unexpected, Visitor}; use url::{ParseError, Url}; pub use url::Host; @@ -424,25 +424,25 @@ impl Display for UserId { } impl Serialize for EventId { - fn serialize(&self, serializer: &mut S) -> Result<(), S::Error> where S: Serializer { + fn serialize(&self, serializer: S) -> Result where S: Serializer { serializer.serialize_str(&self.to_string()) } } impl Serialize for RoomAliasId { - fn serialize(&self, serializer: &mut S) -> Result<(), S::Error> where S: Serializer { + fn serialize(&self, serializer: S) -> Result where S: Serializer { serializer.serialize_str(&self.to_string()) } } impl Serialize for RoomId { - fn serialize(&self, serializer: &mut S) -> Result<(), S::Error> where S: Serializer { + fn serialize(&self, serializer: S) -> Result where S: Serializer { serializer.serialize_str(&self.to_string()) } } impl Serialize for RoomIdOrAliasId { - fn serialize(&self, serializer: &mut S) -> Result<(), S::Error> where S: Serializer { + fn serialize(&self, serializer: S) -> Result where S: Serializer { match *self { RoomIdOrAliasId::RoomAliasId(ref room_alias_id) => { serializer.serialize_str(&room_alias_id.to_string()) @@ -455,37 +455,37 @@ impl Serialize for RoomIdOrAliasId { } impl Serialize for UserId { - fn serialize(&self, serializer: &mut S) -> Result<(), S::Error> where S: Serializer { + fn serialize(&self, serializer: S) -> Result where S: Serializer { serializer.serialize_str(&self.to_string()) } } impl Deserialize for EventId { - fn deserialize(deserializer: &mut D) -> Result where D: Deserializer { + fn deserialize(deserializer: D) -> Result where D: Deserializer { deserializer.deserialize(EventIdVisitor) } } impl Deserialize for RoomAliasId { - fn deserialize(deserializer: &mut D) -> Result where D: Deserializer { + fn deserialize(deserializer: D) -> Result where D: Deserializer { deserializer.deserialize(RoomAliasIdVisitor) } } impl Deserialize for RoomId { - fn deserialize(deserializer: &mut D) -> Result where D: Deserializer { + fn deserialize(deserializer: D) -> Result where D: Deserializer { deserializer.deserialize(RoomIdVisitor) } } impl Deserialize for RoomIdOrAliasId { - fn deserialize(deserializer: &mut D) -> Result where D: Deserializer { + fn deserialize(deserializer: D) -> Result where D: Deserializer { deserializer.deserialize(RoomIdOrAliasIdVisitor) } } impl Deserialize for UserId { - fn deserialize(deserializer: &mut D) -> Result where D: Deserializer { + fn deserialize(deserializer: D) -> Result where D: Deserializer { deserializer.deserialize(UserIdVisitor) } } @@ -599,10 +599,14 @@ impl<'a> TryFrom<&'a str> for UserId { impl Visitor for EventIdVisitor { type Value = EventId; - fn visit_str(&mut self, v: &str) -> Result where E: SerdeError { + fn expecting(&self, formatter: &mut Formatter) -> FmtResult { + write!(formatter, "a Matrix event ID as a string") + } + + fn visit_str(self, v: &str) -> Result where E: SerdeError { match EventId::try_from(v) { Ok(event_id) => Ok(event_id), - Err(_) => Err(SerdeError::custom("invalid ID")), + Err(_) => Err(SerdeError::invalid_value(Unexpected::Str(v), &self)), } } } @@ -610,10 +614,14 @@ impl Visitor for EventIdVisitor { impl Visitor for RoomAliasIdVisitor { type Value = RoomAliasId; - fn visit_str(&mut self, v: &str) -> Result where E: SerdeError { + fn expecting(&self, formatter: &mut Formatter) -> FmtResult { + write!(formatter, "a Matrix room alias ID as a string") + } + + fn visit_str(self, v: &str) -> Result where E: SerdeError { match RoomAliasId::try_from(v) { Ok(room_alias_id) => Ok(room_alias_id), - Err(_) => Err(SerdeError::custom("invalid ID")), + Err(_) => Err(SerdeError::invalid_value(Unexpected::Str(v), &self)), } } } @@ -621,10 +629,14 @@ impl Visitor for RoomAliasIdVisitor { impl Visitor for RoomIdVisitor { type Value = RoomId; - fn visit_str(&mut self, v: &str) -> Result where E: SerdeError { + fn expecting(&self, formatter: &mut Formatter) -> FmtResult { + write!(formatter, "a Matrix room ID as a string") + } + + fn visit_str(self, v: &str) -> Result where E: SerdeError { match RoomId::try_from(v) { Ok(room_id) => Ok(room_id), - Err(_) => Err(SerdeError::custom("invalid ID")), + Err(_) => Err(SerdeError::invalid_value(Unexpected::Str(v), &self)), } } } @@ -632,10 +644,14 @@ impl Visitor for RoomIdVisitor { impl Visitor for RoomIdOrAliasIdVisitor { type Value = RoomIdOrAliasId; - fn visit_str(&mut self, v: &str) -> Result where E: SerdeError { + fn expecting(&self, formatter: &mut Formatter) -> FmtResult { + write!(formatter, "a Matrix room ID or room alias ID as a string") + } + + fn visit_str(self, v: &str) -> Result where E: SerdeError { match RoomIdOrAliasId::try_from(v) { Ok(room_id_or_alias_id) => Ok(room_id_or_alias_id), - Err(_) => Err(SerdeError::custom("invalid ID")), + Err(_) => Err(SerdeError::invalid_value(Unexpected::Str(v), &self)), } } } @@ -643,10 +659,14 @@ impl Visitor for RoomIdOrAliasIdVisitor { impl Visitor for UserIdVisitor { type Value = UserId; - fn visit_str(&mut self, v: &str) -> Result where E: SerdeError { + fn expecting(&self, formatter: &mut Formatter) -> FmtResult { + write!(formatter, "a Matrix user ID as a string") + } + + fn visit_str(self, v: &str) -> Result where E: SerdeError { match UserId::try_from(v) { Ok(user_id) => Ok(user_id), - Err(_) => Err(SerdeError::custom("invalid ID")), + Err(_) => Err(SerdeError::invalid_value(Unexpected::Str(v), &self)), } } } From 0d426bbc9ee68360a587eb60c6ca02a963096900 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Thu, 26 Jan 2017 00:09:32 -0800 Subject: [PATCH 037/140] Bump version to 0.7.0. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 422a55c8..56bde883 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT" name = "ruma-identifiers" readme = "README.md" repository = "https://github.com/ruma/ruma-identifiers" -version = "0.6.0" +version = "0.7.0" [dependencies] lazy_static = "0.2.2" From c82edfb3169591bb5efa2c744f789de019c64231 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Thu, 26 Jan 2017 00:12:45 -0800 Subject: [PATCH 038/140] Update documentation URL. --- Cargo.toml | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 56bde883..f7c58b62 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] authors = ["Jimmy Cuadra "] description = "Resource identifiers for Matrix." -documentation = "https://ruma.github.io/ruma-identifiers/ruma_identifiers/" +documentation = "https://docs.rs/ruma-identifiers" homepage = "https://github.com/ruma/ruma-identifiers" keywords = ["matrix", "chat", "messaging", "ruma"] license = "MIT" diff --git a/README.md b/README.md index a6a7589c..df7790ef 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Build Status](https://travis-ci.org/ruma/ruma-identifiers.svg?branch=master)](https://travis-ci.org/ruma/ruma-identifiers) **ruma-identifiers** contains types for [Matrix](https://matrix.org/) identifiers for events, rooms, room aliases, and users. -[Documentation](http://ruma.github.io/ruma-identifiers/ruma_identifiers/) is available for the latest release. +[Documentation](https://docs.rs/ruma-identifiers) is available for the latest release. ## License From c6b959c7a1e85b4532c679f251d258db3af9c171 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Fri, 3 Feb 2017 03:11:35 -0800 Subject: [PATCH 039/140] Update to Diesel 0.10. --- Cargo.toml | 14 +++++++------- src/lib.rs | 2 ++ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f7c58b62..383ea704 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,15 +11,15 @@ repository = "https://github.com/ruma/ruma-identifiers" version = "0.7.0" [dependencies] -lazy_static = "0.2.2" -rand = "0.3.15" -regex = "0.2.1" -serde = "0.9.1" -url = "1.4.0" +lazy_static = "0.2" +rand = "0.3" +regex = "0.2" +serde = "0.9" +url = "1.4" [dependencies.diesel] optional = true -version = "0.9.0" +version = "0.10" [dev-dependencies] -serde_json = "0.9.1" +serde_json = "0.9" diff --git a/src/lib.rs b/src/lib.rs index 48360e3c..5b75256c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,9 @@ //! for events, rooms, room aliases, and users. #![feature(try_from)] +#![deny(missing_debug_implementations)] #![deny(missing_docs)] +#![deny(warnings)] #[macro_use] extern crate lazy_static; From 29f76ca95d295bca36ec53cd8da72217c5aa0f75 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Fri, 3 Feb 2017 03:12:06 -0800 Subject: [PATCH 040/140] Bump version to 0.8.0. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 383ea704..e3e6dbc8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT" name = "ruma-identifiers" readme = "README.md" repository = "https://github.com/ruma/ruma-identifiers" -version = "0.7.0" +version = "0.8.0" [dependencies] lazy_static = "0.2" From e4c3cae7fd9a2a8940f9cab098e674e3480c2a32 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Wed, 8 Feb 2017 22:25:01 -0800 Subject: [PATCH 041/140] Add missing Diesel implementations. --- src/lib.rs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 5b75256c..05e1a1c3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -684,7 +684,7 @@ mod diesel_integration { use diesel::expression::AsExpression; use diesel::expression::bound::Bound; use diesel::row::Row; - use diesel::types::{FromSql, FromSqlRow, HasSqlType, IsNull, Text, ToSql}; + use diesel::types::{FromSql, FromSqlRow, HasSqlType, IsNull, Nullable, Text, ToSql}; macro_rules! diesel_impl { ($name:ident) => { @@ -741,6 +741,22 @@ mod diesel_integration { Bound::new(self) } } + + impl AsExpression> for $crate::$name { + type Expression = Bound, Self>; + + fn as_expression(self) -> Self::Expression { + Bound::new(self) + } + } + + impl<'a> AsExpression> for &'a $crate::$name { + type Expression = Bound, Self>; + + fn as_expression(self) -> Self::Expression { + Bound::new(self) + } + } } } From dcfc581d44e98acf0a228be1e2ce881a0ef9f69a Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Wed, 8 Feb 2017 22:26:37 -0800 Subject: [PATCH 042/140] Give docs their own section in the README. --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index df7790ef..29c8d640 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,10 @@ [![Build Status](https://travis-ci.org/ruma/ruma-identifiers.svg?branch=master)](https://travis-ci.org/ruma/ruma-identifiers) **ruma-identifiers** contains types for [Matrix](https://matrix.org/) identifiers for events, rooms, room aliases, and users. -[Documentation](https://docs.rs/ruma-identifiers) is available for the latest release. + +## Documentation + +ruma-identifiers has [comprehensive documentation](https://docs.rs/ruma-identifiers) available on docs.rs. ## License From bd6dd985d78037a62c833ca6b969814d86188657 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Wed, 8 Feb 2017 22:25:46 -0800 Subject: [PATCH 043/140] Bump version to 0.8.1. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index e3e6dbc8..5df8bcbf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT" name = "ruma-identifiers" readme = "README.md" repository = "https://github.com/ruma/ruma-identifiers" -version = "0.8.0" +version = "0.8.1" [dependencies] lazy_static = "0.2" From 1ae596d2512791f69f01e6780152daa0cb2a6f1c Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Thu, 16 Feb 2017 19:38:56 -0800 Subject: [PATCH 044/140] Update diesel to 0.11 and bump version to 0.9.0. --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5df8bcbf..d72e62c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT" name = "ruma-identifiers" readme = "README.md" repository = "https://github.com/ruma/ruma-identifiers" -version = "0.8.1" +version = "0.9.0" [dependencies] lazy_static = "0.2" @@ -19,7 +19,7 @@ url = "1.4" [dependencies.diesel] optional = true -version = "0.10" +version = "0.11" [dev-dependencies] serde_json = "0.9" From 70e6b604070fae20b4bb4a995ee8e256eac18bce Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Fri, 24 Mar 2017 01:31:10 -0700 Subject: [PATCH 045/140] Update Diesel to 0.12. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index d72e62c4..a68428eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ url = "1.4" [dependencies.diesel] optional = true -version = "0.11" +version = "0.12" [dev-dependencies] serde_json = "0.9" From 918122091700e109631d94f5e436375b0b51282e Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Fri, 24 Mar 2017 01:31:33 -0700 Subject: [PATCH 046/140] Update TryFrom::Err to TryFrom::Error. --- src/lib.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 05e1a1c3..1d04ed57 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -493,13 +493,13 @@ impl Deserialize for UserId { } impl<'a> TryFrom<&'a str> for EventId { - type Err = Error; + type Error = Error; /// Attempts to create a new Matrix event ID from a string representation. /// /// The string must include the leading $ sigil, the opaque ID, a literal colon, and a valid /// server name. - fn try_from(event_id: &'a str) -> Result { + fn try_from(event_id: &'a str) -> Result { let (opaque_id, host, port) = parse_id('$', event_id)?; Ok(EventId { @@ -511,7 +511,7 @@ impl<'a> TryFrom<&'a str> for EventId { } impl<'a> TryFrom<&'a str> for RoomAliasId { - type Err = Error; + type Error = Error; /// Attempts to create a new Matrix room alias ID from a string representation. /// @@ -529,7 +529,7 @@ impl<'a> TryFrom<&'a str> for RoomAliasId { } impl<'a> TryFrom<&'a str> for RoomId { - type Err = Error; + type Error = Error; /// Attempts to create a new Matrix room ID from a string representation. /// @@ -547,7 +547,7 @@ impl<'a> TryFrom<&'a str> for RoomId { } impl<'a> TryFrom<&'a str> for RoomIdOrAliasId { - type Err = Error; + type Error = Error; /// Attempts to create a new Matrix room ID or a room alias ID from a string representation. /// @@ -576,7 +576,7 @@ impl<'a> TryFrom<&'a str> for RoomIdOrAliasId { } impl<'a> TryFrom<&'a str> for UserId { - type Err = Error; + type Error = Error; /// Attempts to create a new Matrix user ID from a string representation. /// From a38c7e048c28df1d41cf9c2b9b9bf2b8d686c969 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Fri, 24 Mar 2017 01:32:00 -0700 Subject: [PATCH 047/140] Bump version to 0.10.0. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index a68428eb..a8957ca5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT" name = "ruma-identifiers" readme = "README.md" repository = "https://github.com/ruma/ruma-identifiers" -version = "0.9.0" +version = "0.10.0" [dependencies] lazy_static = "0.2" From 418c91d5ff1129581d24f62ee911843b04d7f638 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Thu, 20 Apr 2017 21:57:36 -0700 Subject: [PATCH 048/140] Update serde and serde_json to 1.0. --- Cargo.toml | 4 ++-- src/lib.rs | 40 ++++++++++++++++++++-------------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a8957ca5..4c095a7a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ version = "0.10.0" lazy_static = "0.2" rand = "0.3" regex = "0.2" -serde = "0.9" +serde = "1.0" url = "1.4" [dependencies.diesel] @@ -22,4 +22,4 @@ optional = true version = "0.12" [dev-dependencies] -serde_json = "0.9" +serde_json = "1.0" diff --git a/src/lib.rs b/src/lib.rs index 1d04ed57..8091cc69 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -462,33 +462,33 @@ impl Serialize for UserId { } } -impl Deserialize for EventId { - fn deserialize(deserializer: D) -> Result where D: Deserializer { - deserializer.deserialize(EventIdVisitor) +impl<'de> Deserialize<'de> for EventId { + fn deserialize(deserializer: D) -> Result where D: Deserializer<'de> { + deserializer.deserialize_any(EventIdVisitor) } } -impl Deserialize for RoomAliasId { - fn deserialize(deserializer: D) -> Result where D: Deserializer { - deserializer.deserialize(RoomAliasIdVisitor) +impl<'de> Deserialize<'de> for RoomAliasId { + fn deserialize(deserializer: D) -> Result where D: Deserializer<'de> { + deserializer.deserialize_any(RoomAliasIdVisitor) } } -impl Deserialize for RoomId { - fn deserialize(deserializer: D) -> Result where D: Deserializer { - deserializer.deserialize(RoomIdVisitor) +impl<'de> Deserialize<'de> for RoomId { + fn deserialize(deserializer: D) -> Result where D: Deserializer<'de> { + deserializer.deserialize_any(RoomIdVisitor) } } -impl Deserialize for RoomIdOrAliasId { - fn deserialize(deserializer: D) -> Result where D: Deserializer { - deserializer.deserialize(RoomIdOrAliasIdVisitor) +impl<'de> Deserialize<'de> for RoomIdOrAliasId { + fn deserialize(deserializer: D) -> Result where D: Deserializer<'de> { + deserializer.deserialize_any(RoomIdOrAliasIdVisitor) } } -impl Deserialize for UserId { - fn deserialize(deserializer: D) -> Result where D: Deserializer { - deserializer.deserialize(UserIdVisitor) +impl<'de> Deserialize<'de> for UserId { + fn deserialize(deserializer: D) -> Result where D: Deserializer<'de> { + deserializer.deserialize_any(UserIdVisitor) } } @@ -598,7 +598,7 @@ impl<'a> TryFrom<&'a str> for UserId { } } -impl Visitor for EventIdVisitor { +impl<'de> Visitor<'de> for EventIdVisitor { type Value = EventId; fn expecting(&self, formatter: &mut Formatter) -> FmtResult { @@ -613,7 +613,7 @@ impl Visitor for EventIdVisitor { } } -impl Visitor for RoomAliasIdVisitor { +impl<'de> Visitor<'de> for RoomAliasIdVisitor { type Value = RoomAliasId; fn expecting(&self, formatter: &mut Formatter) -> FmtResult { @@ -628,7 +628,7 @@ impl Visitor for RoomAliasIdVisitor { } } -impl Visitor for RoomIdVisitor { +impl<'de> Visitor<'de> for RoomIdVisitor { type Value = RoomId; fn expecting(&self, formatter: &mut Formatter) -> FmtResult { @@ -643,7 +643,7 @@ impl Visitor for RoomIdVisitor { } } -impl Visitor for RoomIdOrAliasIdVisitor { +impl<'de> Visitor<'de> for RoomIdOrAliasIdVisitor { type Value = RoomIdOrAliasId; fn expecting(&self, formatter: &mut Formatter) -> FmtResult { @@ -658,7 +658,7 @@ impl Visitor for RoomIdOrAliasIdVisitor { } } -impl Visitor for UserIdVisitor { +impl<'de> Visitor<'de> for UserIdVisitor { type Value = UserId; fn expecting(&self, formatter: &mut Formatter) -> FmtResult { From 2de6d5dc0a0d1bf88c4c4783ccc4b06587025f3e Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Thu, 20 Apr 2017 21:58:18 -0700 Subject: [PATCH 049/140] Bump version to 0.11.0. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 4c095a7a..451c6746 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT" name = "ruma-identifiers" readme = "README.md" repository = "https://github.com/ruma/ruma-identifiers" -version = "0.10.0" +version = "0.11.0" [dependencies] lazy_static = "0.2" From 3a4041982ca5f1c41d02b22c544d63d9d7eb0deb Mon Sep 17 00:00:00 2001 From: Digital Date: Wed, 4 Apr 2018 14:31:16 +0200 Subject: [PATCH 050/140] Add unicode compatibility for identifiers Fix issue with unicode character boundaries in parse_id. Add Unit test for unicode room aliases. --- src/lib.rs | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 8091cc69..3fcdb14b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -210,16 +210,12 @@ fn validate_id<'a>(id: &'a str) -> Result<(), Error> { fn parse_id<'a>(required_sigil: char, id: &'a str) -> Result<(&'a str, Host, u16), Error> { validate_id(id)?; - let mut chars = id.chars(); - - let sigil = chars.nth(0).expect("ID missing first character."); - - if sigil != required_sigil { + if !id.starts_with(required_sigil) { return Err(Error::MissingSigil); } - let delimiter_index = match chars.position(|c| c == ':') { - Some(index) => index + 1, + let delimiter_index = match id.find(':') { + Some(index) => index, None => return Err(Error::MissingDelimiter), }; @@ -922,6 +918,16 @@ mod tests { ); } + #[test] + fn valid_room_alias_id_unicode() { + assert_eq!( + RoomAliasId::try_from("#老虎£я:example.com") + .expect("Failed to create RoomAliasId.") + .to_string(), + "#老虎£я:example.com" + ); + } + #[test] fn missing_room_alias_id_sigil() { assert_eq!( @@ -953,6 +959,7 @@ mod tests { Error::InvalidHost ); } + #[test] fn valid_room_id() { assert_eq!( From b994d1620377761db5bfacaddf3763c6f9cbadaa Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Thu, 24 May 2018 03:43:15 -0700 Subject: [PATCH 051/140] Bump dependencies. --- Cargo.toml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 451c6746..5e6ba4fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,15 +11,15 @@ repository = "https://github.com/ruma/ruma-identifiers" version = "0.11.0" [dependencies] -lazy_static = "0.2" -rand = "0.3" -regex = "0.2" +lazy_static = "1.0" +rand = "0.5" +regex = "1.0" serde = "1.0" -url = "1.4" +url = "1.7" [dependencies.diesel] optional = true -version = "0.12" +version = "1.3" [dev-dependencies] serde_json = "1.0" From c31794f581cb066e4276697cf13e0f006ff83cc4 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Thu, 24 May 2018 04:04:08 -0700 Subject: [PATCH 052/140] Use new rand 0.5 API. --- src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 3fcdb14b..44c6bfac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,6 +24,7 @@ use std::convert::TryFrom; use std::fmt::{Display, Formatter, Result as FmtResult}; use rand::{Rng, thread_rng}; +use rand::distributions::Alphanumeric; use regex::Regex; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::de::{Error as SerdeError, Unexpected, Visitor}; @@ -192,7 +193,7 @@ fn display(f: &mut Formatter, sigil: char, localpart: &str, hostname: &Host, por } fn generate_localpart(length: usize) -> String { - thread_rng().gen_ascii_chars().take(length).collect() + thread_rng().sample_iter(&Alphanumeric).take(length).collect() } fn validate_id<'a>(id: &'a str) -> Result<(), Error> { From 7ba979a4116f7acf39bdbbc8ebd3a92537be6b6d Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Thu, 24 May 2018 04:04:19 -0700 Subject: [PATCH 053/140] Derive more traits for Error. --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 44c6bfac..c07190a4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -48,7 +48,7 @@ lazy_static! { } /// An error encountered when trying to parse an invalid ID string. -#[derive(Copy, Clone, Debug, PartialEq)] +#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] pub enum Error { /// The ID's localpart contains invalid characters. /// From 78b0d1e05400524cb67e25b5caa741150e7a6cad Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Sun, 10 Jun 2018 01:05:32 -0700 Subject: [PATCH 054/140] Update Diesel integration to Diesel 1.0. --- src/lib.rs | 267 ++++++++++++++++++++++++++++------------------------- 1 file changed, 141 insertions(+), 126 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index c07190a4..d04d9c21 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,20 +14,21 @@ extern crate serde; extern crate url; #[cfg(feature = "diesel")] +#[macro_use] extern crate diesel; #[cfg(test)] extern crate serde_json; -use std::error::Error as StdError; use std::convert::TryFrom; +use std::error::Error as StdError; use std::fmt::{Display, Formatter, Result as FmtResult}; -use rand::{Rng, thread_rng}; use rand::distributions::Alphanumeric; +use rand::{thread_rng, Rng}; use regex::Regex; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::de::{Error as SerdeError, Unexpected, Visitor}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use url::{ParseError, Url}; pub use url::Host; @@ -81,6 +82,7 @@ pub enum Error { /// ); /// ``` #[derive(Clone, Debug, Eq, Hash, PartialEq)] +#[cfg_attr(feature = "diesel", derive(FromSqlRow))] pub struct EventId { hostname: Host, opaque_id: String, @@ -102,6 +104,7 @@ pub struct EventId { /// ); /// ``` #[derive(Clone, Debug, Eq, Hash, PartialEq)] +#[cfg_attr(feature = "diesel", derive(FromSqlRow))] pub struct RoomAliasId { alias: String, hostname: Host, @@ -123,6 +126,7 @@ pub struct RoomAliasId { /// ); /// ``` #[derive(Clone, Debug, Eq, Hash, PartialEq)] +#[cfg_attr(feature = "diesel", derive(FromSqlRow))] pub struct RoomId { hostname: Host, opaque_id: String, @@ -149,6 +153,7 @@ pub struct RoomId { /// "!n8f893n9:example.com" /// ); #[derive(Clone, Debug, Eq, Hash, PartialEq)] +#[cfg_attr(feature = "diesel", derive(FromSqlRow))] pub enum RoomIdOrAliasId { /// A Matrix room alias ID. RoomAliasId(RoomAliasId), @@ -171,6 +176,7 @@ pub enum RoomIdOrAliasId { /// ); /// ``` #[derive(Clone, Debug, Eq, Hash, PartialEq)] +#[cfg_attr(feature = "diesel", derive(FromSqlRow))] pub struct UserId { hostname: Host, localpart: String, @@ -183,8 +189,13 @@ struct RoomIdVisitor; struct RoomIdOrAliasIdVisitor; struct UserIdVisitor; -fn display(f: &mut Formatter, sigil: char, localpart: &str, hostname: &Host, port: u16) --> FmtResult { +fn display( + f: &mut Formatter, + sigil: char, + localpart: &str, + hostname: &Host, + port: u16, +) -> FmtResult { if port == 443 { write!(f, "{}{}:{}", sigil, localpart, hostname) } else { @@ -193,7 +204,10 @@ fn display(f: &mut Formatter, sigil: char, localpart: &str, hostname: &Host, por } fn generate_localpart(length: usize) -> String { - thread_rng().sample_iter(&Alphanumeric).take(length).collect() + thread_rng() + .sample_iter(&Alphanumeric) + .take(length) + .collect() } fn validate_id<'a>(id: &'a str) -> Result<(), Error> { @@ -406,9 +420,13 @@ impl Display for RoomId { impl Display for RoomIdOrAliasId { fn fmt(&self, f: &mut Formatter) -> FmtResult { match *self { - RoomIdOrAliasId::RoomAliasId(ref room_alias_id) => { - display(f, '#', &room_alias_id.alias, &room_alias_id.hostname, room_alias_id.port) - } + RoomIdOrAliasId::RoomAliasId(ref room_alias_id) => display( + f, + '#', + &room_alias_id.alias, + &room_alias_id.hostname, + room_alias_id.port, + ), RoomIdOrAliasId::RoomId(ref room_id) => { display(f, '!', &room_id.opaque_id, &room_id.hostname, room_id.port) } @@ -423,68 +441,96 @@ impl Display for UserId { } impl Serialize for EventId { - fn serialize(&self, serializer: S) -> Result where S: Serializer { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { serializer.serialize_str(&self.to_string()) } } impl Serialize for RoomAliasId { - fn serialize(&self, serializer: S) -> Result where S: Serializer { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { serializer.serialize_str(&self.to_string()) } } impl Serialize for RoomId { - fn serialize(&self, serializer: S) -> Result where S: Serializer { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { serializer.serialize_str(&self.to_string()) } } impl Serialize for RoomIdOrAliasId { - fn serialize(&self, serializer: S) -> Result where S: Serializer { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { match *self { RoomIdOrAliasId::RoomAliasId(ref room_alias_id) => { serializer.serialize_str(&room_alias_id.to_string()) } - RoomIdOrAliasId::RoomId(ref room_id) => { - serializer.serialize_str(&room_id.to_string()) - } + RoomIdOrAliasId::RoomId(ref room_id) => serializer.serialize_str(&room_id.to_string()), } } } impl Serialize for UserId { - fn serialize(&self, serializer: S) -> Result where S: Serializer { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { serializer.serialize_str(&self.to_string()) } } impl<'de> Deserialize<'de> for EventId { - fn deserialize(deserializer: D) -> Result where D: Deserializer<'de> { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { deserializer.deserialize_any(EventIdVisitor) } } impl<'de> Deserialize<'de> for RoomAliasId { - fn deserialize(deserializer: D) -> Result where D: Deserializer<'de> { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { deserializer.deserialize_any(RoomAliasIdVisitor) } } impl<'de> Deserialize<'de> for RoomId { - fn deserialize(deserializer: D) -> Result where D: Deserializer<'de> { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { deserializer.deserialize_any(RoomIdVisitor) } } impl<'de> Deserialize<'de> for RoomIdOrAliasId { - fn deserialize(deserializer: D) -> Result where D: Deserializer<'de> { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { deserializer.deserialize_any(RoomIdOrAliasIdVisitor) } } impl<'de> Deserialize<'de> for UserId { - fn deserialize(deserializer: D) -> Result where D: Deserializer<'de> { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { deserializer.deserialize_any(UserIdVisitor) } } @@ -567,7 +613,7 @@ impl<'a> TryFrom<&'a str> for RoomIdOrAliasId { let room_id = RoomId::try_from(room_id_or_alias_id)?; Ok(RoomIdOrAliasId::RoomId(room_id)) } - _ => Err(Error::MissingSigil) + _ => Err(Error::MissingSigil), } } } @@ -602,7 +648,10 @@ impl<'de> Visitor<'de> for EventIdVisitor { write!(formatter, "a Matrix event ID as a string") } - fn visit_str(self, v: &str) -> Result where E: SerdeError { + fn visit_str(self, v: &str) -> Result + where + E: SerdeError, + { match EventId::try_from(v) { Ok(event_id) => Ok(event_id), Err(_) => Err(SerdeError::invalid_value(Unexpected::Str(v), &self)), @@ -617,7 +666,10 @@ impl<'de> Visitor<'de> for RoomAliasIdVisitor { write!(formatter, "a Matrix room alias ID as a string") } - fn visit_str(self, v: &str) -> Result where E: SerdeError { + fn visit_str(self, v: &str) -> Result + where + E: SerdeError, + { match RoomAliasId::try_from(v) { Ok(room_alias_id) => Ok(room_alias_id), Err(_) => Err(SerdeError::invalid_value(Unexpected::Str(v), &self)), @@ -632,7 +684,10 @@ impl<'de> Visitor<'de> for RoomIdVisitor { write!(formatter, "a Matrix room ID as a string") } - fn visit_str(self, v: &str) -> Result where E: SerdeError { + fn visit_str(self, v: &str) -> Result + where + E: SerdeError, + { match RoomId::try_from(v) { Ok(room_id) => Ok(room_id), Err(_) => Err(SerdeError::invalid_value(Unexpected::Str(v), &self)), @@ -647,7 +702,10 @@ impl<'de> Visitor<'de> for RoomIdOrAliasIdVisitor { write!(formatter, "a Matrix room ID or room alias ID as a string") } - fn visit_str(self, v: &str) -> Result where E: SerdeError { + fn visit_str(self, v: &str) -> Result + where + E: SerdeError, + { match RoomIdOrAliasId::try_from(v) { Ok(room_id_or_alias_id) => Ok(room_id_or_alias_id), Err(_) => Err(SerdeError::invalid_value(Unexpected::Str(v), &self)), @@ -662,7 +720,10 @@ impl<'de> Visitor<'de> for UserIdVisitor { write!(formatter, "a Matrix user ID as a string") } - fn visit_str(self, v: &str) -> Result where E: SerdeError { + fn visit_str(self, v: &str) -> Result + where + E: SerdeError, + { match UserId::try_from(v) { Ok(user_id) => Ok(user_id), Err(_) => Err(SerdeError::invalid_value(Unexpected::Str(v), &self)), @@ -673,88 +734,37 @@ impl<'de> Visitor<'de> for UserIdVisitor { #[cfg(feature = "diesel")] mod diesel_integration { use std::convert::TryFrom; - use std::error::Error; + use std::error::Error as StdError; use std::io::Write; - use diesel::Queryable; use diesel::backend::Backend; - use diesel::expression::AsExpression; - use diesel::expression::bound::Bound; - use diesel::row::Row; - use diesel::types::{FromSql, FromSqlRow, HasSqlType, IsNull, Nullable, Text, ToSql}; + use diesel::deserialize::{FromSql, Result as DeserializeResult}; + use diesel::serialize::{Output, Result as SerializeResult, ToSql}; + use diesel::sql_types::Text; macro_rules! diesel_impl { ($name:ident) => { - impl FromSql for $crate::$name - where DB: Backend + HasSqlType, String: FromSql { - fn from_sql(bytes: Option<&DB::RawValue>) - -> Result> { - let string = >::from_sql(bytes)?; - - $crate::$name::try_from(&string) - .map_err(|error| Box::new(error) as Box) - } - } - - impl FromSqlRow for $crate::$name - where DB: Backend + HasSqlType, String: FromSql { - fn build_from_row>(row: &mut T) - -> Result> { - FromSql::::from_sql(row.take()) - } - } - - impl ToSql for $crate::$name - where DB: Backend + HasSqlType, String: ToSql { - fn to_sql(&self, out: &mut W) - -> Result> { - self.to_string().to_sql(out) - } - } - - impl Queryable for $crate::$name where - $crate::$name: FromSqlRow, - DB: Backend + HasSqlType, + impl ToSql for $crate::$name + where + DB: Backend, { - type Row = Self; - - fn build(row: Self::Row) -> Self { - row + fn to_sql(&self, out: &mut Output) -> SerializeResult { + ToSql::::to_sql(&self.to_string(), out) } } - impl AsExpression for $crate::$name { - type Expression = Bound; + impl FromSql for $crate::$name + where + DB: Backend, + { + fn from_sql(bytes: Option<&[u8]>) -> DeserializeResult { + let string: String = FromSql::::from_sql(bytes)?; - fn as_expression(self) -> Self::Expression { - Bound::new(self) + $crate::$name::try_from(string.as_str()) + .map_err(|error| Box::new(error) as Box) } } - - impl<'a> AsExpression for &'a $crate::$name { - type Expression = Bound; - - fn as_expression(self) -> Self::Expression { - Bound::new(self) - } - } - - impl AsExpression> for $crate::$name { - type Expression = Bound, Self>; - - fn as_expression(self) -> Self::Expression { - Bound::new(self) - } - } - - impl<'a> AsExpression> for &'a $crate::$name { - type Expression = Bound, Self>; - - fn as_expression(self) -> Self::Expression { - Bound::new(self) - } - } - } + }; } diesel_impl!(EventId); @@ -810,9 +820,8 @@ mod tests { #[test] fn deserialize_valid_event_id() { assert_eq!( - from_str::( - r#""$39hvsi03hlne:example.com""# - ).expect("Failed to convert JSON to EventId"), + from_str::(r#""$39hvsi03hlne:example.com""#) + .expect("Failed to convert JSON to EventId"), EventId::try_from("$39hvsi03hlne:example.com").expect("Failed to create EventId.") ); } @@ -864,7 +873,9 @@ mod tests { #[test] fn invalid_event_id_port() { assert_eq!( - EventId::try_from("$39hvsi03hlne:example.com:notaport").err().unwrap(), + EventId::try_from("$39hvsi03hlne:example.com:notaport") + .err() + .unwrap(), Error::InvalidHost ); } @@ -892,9 +903,8 @@ mod tests { #[test] fn deserialize_valid_room_alias_id() { assert_eq!( - from_str::( - r##""#ruma:example.com""## - ).expect("Failed to convert JSON to RoomAliasId"), + from_str::(r##""#ruma:example.com""##) + .expect("Failed to convert JSON to RoomAliasId"), RoomAliasId::try_from("#ruma:example.com").expect("Failed to create RoomAliasId.") ); } @@ -932,7 +942,9 @@ mod tests { #[test] fn missing_room_alias_id_sigil() { assert_eq!( - RoomAliasId::try_from("39hvsi03hlne:example.com").err().unwrap(), + RoomAliasId::try_from("39hvsi03hlne:example.com") + .err() + .unwrap(), Error::MissingSigil ); } @@ -956,7 +968,9 @@ mod tests { #[test] fn invalid_room_alias_id_port() { assert_eq!( - RoomAliasId::try_from("#ruma:example.com:notaport").err().unwrap(), + RoomAliasId::try_from("#ruma:example.com:notaport") + .err() + .unwrap(), Error::InvalidHost ); } @@ -999,9 +1013,8 @@ mod tests { #[test] fn deserialize_valid_room_id() { assert_eq!( - from_str::( - r#""!29fhd83h92h0:example.com""# - ).expect("Failed to convert JSON to RoomId"), + from_str::(r#""!29fhd83h92h0:example.com""#) + .expect("Failed to convert JSON to RoomId"), RoomId::try_from("!29fhd83h92h0:example.com").expect("Failed to create RoomId.") ); } @@ -1053,7 +1066,9 @@ mod tests { #[test] fn invalid_room_id_port() { assert_eq!( - RoomId::try_from("!29fhd83h92h0:example.com:notaport").err().unwrap(), + RoomId::try_from("!29fhd83h92h0:example.com:notaport") + .err() + .unwrap(), Error::InvalidHost ); } @@ -1090,7 +1105,8 @@ mod tests { fn serialize_valid_room_id_or_alias_id_with_a_room_alias_id() { assert_eq!( to_string( - &RoomIdOrAliasId::try_from("#ruma:example.com").expect("Failed to create RoomAliasId.") + &RoomIdOrAliasId::try_from("#ruma:example.com") + .expect("Failed to create RoomAliasId.") ).expect("Failed to convert RoomAliasId to JSON."), r##""#ruma:example.com""## ); @@ -1100,7 +1116,8 @@ mod tests { fn serialize_valid_room_id_or_alias_id_with_a_room_id() { assert_eq!( to_string( - &RoomIdOrAliasId::try_from("!29fhd83h92h0:example.com").expect("Failed to create RoomId.") + &RoomIdOrAliasId::try_from("!29fhd83h92h0:example.com") + .expect("Failed to create RoomId.") ).expect("Failed to convert RoomId to JSON."), r#""!29fhd83h92h0:example.com""# ); @@ -1109,9 +1126,8 @@ mod tests { #[test] fn deserialize_valid_room_id_or_alias_id_with_a_room_alias_id() { assert_eq!( - from_str::( - r##""#ruma:example.com""## - ).expect("Failed to convert JSON to RoomAliasId"), + from_str::(r##""#ruma:example.com""##) + .expect("Failed to convert JSON to RoomAliasId"), RoomIdOrAliasId::try_from("#ruma:example.com").expect("Failed to create RoomAliasId.") ); } @@ -1119,10 +1135,10 @@ mod tests { #[test] fn deserialize_valid_room_id_or_alias_id_with_a_room_id() { assert_eq!( - from_str::( - r##""!29fhd83h92h0:example.com""## - ).expect("Failed to convert JSON to RoomId"), - RoomIdOrAliasId::try_from("!29fhd83h92h0:example.com").expect("Failed to create RoomAliasId.") + from_str::(r##""!29fhd83h92h0:example.com""##) + .expect("Failed to convert JSON to RoomId"), + RoomIdOrAliasId::try_from("!29fhd83h92h0:example.com") + .expect("Failed to create RoomAliasId.") ); } @@ -1164,9 +1180,8 @@ mod tests { #[test] fn serialize_valid_user_id() { assert_eq!( - to_string( - &UserId::try_from("@carl:example.com").expect("Failed to create UserId.") - ).expect("Failed to convert UserId to JSON."), + to_string(&UserId::try_from("@carl:example.com").expect("Failed to create UserId.")) + .expect("Failed to convert UserId to JSON."), r#""@carl:example.com""# ); } @@ -1174,9 +1189,7 @@ mod tests { #[test] fn deserialize_valid_user_id() { assert_eq!( - from_str::( - r#""@carl:example.com""# - ).expect("Failed to convert JSON to UserId"), + from_str::(r#""@carl:example.com""#).expect("Failed to convert JSON to UserId"), UserId::try_from("@carl:example.com").expect("Failed to create UserId.") ); } @@ -1236,7 +1249,9 @@ mod tests { #[test] fn invalid_user_id_port() { assert_eq!( - UserId::try_from("@carl:example.com:notaport").err().unwrap(), + UserId::try_from("@carl:example.com:notaport") + .err() + .unwrap(), Error::InvalidHost ); } From 05f7e6b0a8ba6fc7050fd8d75218307318065724 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 8 Jan 2019 19:53:25 +0100 Subject: [PATCH 055/140] Configure rustfmt for nested imports, re-run 'cargo fmt' --- .rustfmt.toml | 1 + src/lib.rs | 46 +++++++++++++++++++++++++++------------------- 2 files changed, 28 insertions(+), 19 deletions(-) create mode 100644 .rustfmt.toml diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 00000000..7d2cf549 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1 @@ +merge_imports = true diff --git a/src/lib.rs b/src/lib.rs index d04d9c21..10d10f61 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,15 +20,18 @@ extern crate diesel; #[cfg(test)] extern crate serde_json; -use std::convert::TryFrom; -use std::error::Error as StdError; -use std::fmt::{Display, Formatter, Result as FmtResult}; +use std::{ + convert::TryFrom, + error::Error as StdError, + fmt::{Display, Formatter, Result as FmtResult}, +}; -use rand::distributions::Alphanumeric; -use rand::{thread_rng, Rng}; +use rand::{distributions::Alphanumeric, thread_rng, Rng}; use regex::Regex; -use serde::de::{Error as SerdeError, Unexpected, Visitor}; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use serde::{ + de::{Error as SerdeError, Unexpected, Visitor}, + Deserialize, Deserializer, Serialize, Serializer, +}; use url::{ParseError, Url}; pub use url::Host; @@ -733,14 +736,14 @@ impl<'de> Visitor<'de> for UserIdVisitor { #[cfg(feature = "diesel")] mod diesel_integration { - use std::convert::TryFrom; - use std::error::Error as StdError; - use std::io::Write; + use std::{convert::TryFrom, error::Error as StdError, io::Write}; - use diesel::backend::Backend; - use diesel::deserialize::{FromSql, Result as DeserializeResult}; - use diesel::serialize::{Output, Result as SerializeResult, ToSql}; - use diesel::sql_types::Text; + use diesel::{ + backend::Backend, + deserialize::{FromSql, Result as DeserializeResult}, + serialize::{Output, Result as SerializeResult, ToSql}, + sql_types::Text, + }; macro_rules! diesel_impl { ($name:ident) => { @@ -812,7 +815,8 @@ mod tests { assert_eq!( to_string( &EventId::try_from("$39hvsi03hlne:example.com").expect("Failed to create EventId.") - ).expect("Failed to convert EventId to JSON."), + ) + .expect("Failed to convert EventId to JSON."), r#""$39hvsi03hlne:example.com""# ); } @@ -895,7 +899,8 @@ mod tests { assert_eq!( to_string( &RoomAliasId::try_from("#ruma:example.com").expect("Failed to create RoomAliasId.") - ).expect("Failed to convert RoomAliasId to JSON."), + ) + .expect("Failed to convert RoomAliasId to JSON."), r##""#ruma:example.com""## ); } @@ -1005,7 +1010,8 @@ mod tests { assert_eq!( to_string( &RoomId::try_from("!29fhd83h92h0:example.com").expect("Failed to create RoomId.") - ).expect("Failed to convert RoomId to JSON."), + ) + .expect("Failed to convert RoomId to JSON."), r#""!29fhd83h92h0:example.com""# ); } @@ -1107,7 +1113,8 @@ mod tests { to_string( &RoomIdOrAliasId::try_from("#ruma:example.com") .expect("Failed to create RoomAliasId.") - ).expect("Failed to convert RoomAliasId to JSON."), + ) + .expect("Failed to convert RoomAliasId to JSON."), r##""#ruma:example.com""## ); } @@ -1118,7 +1125,8 @@ mod tests { to_string( &RoomIdOrAliasId::try_from("!29fhd83h92h0:example.com") .expect("Failed to create RoomId.") - ).expect("Failed to convert RoomId to JSON."), + ) + .expect("Failed to convert RoomId to JSON."), r#""!29fhd83h92h0:example.com""# ); } From c8aeee8271800c46dc9d60174a27059fa8a4c6aa Mon Sep 17 00:00:00 2001 From: Andreas Studer Date: Sun, 3 Feb 2019 19:54:12 +0100 Subject: [PATCH 056/140] Allow types to be used as compound keys --- Cargo.toml | 2 +- src/lib.rs | 26 +++++++++++++++++--------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5e6ba4fa..62eac946 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ url = "1.7" [dependencies.diesel] optional = true -version = "1.3" +version = "1.4" [dev-dependencies] serde_json = "1.0" diff --git a/src/lib.rs b/src/lib.rs index 10d10f61..60191ddd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,6 +26,9 @@ use std::{ fmt::{Display, Formatter, Result as FmtResult}, }; +#[cfg(feature = "diesel")] +use diesel::sql_types::Text; + use rand::{distributions::Alphanumeric, thread_rng, Rng}; use regex::Regex; use serde::{ @@ -85,7 +88,8 @@ pub enum Error { /// ); /// ``` #[derive(Clone, Debug, Eq, Hash, PartialEq)] -#[cfg_attr(feature = "diesel", derive(FromSqlRow))] +#[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] +#[cfg_attr(feature = "diesel", sql_type = "Text")] pub struct EventId { hostname: Host, opaque_id: String, @@ -107,7 +111,8 @@ pub struct EventId { /// ); /// ``` #[derive(Clone, Debug, Eq, Hash, PartialEq)] -#[cfg_attr(feature = "diesel", derive(FromSqlRow))] +#[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] +#[cfg_attr(feature = "diesel", sql_type = "Text")] pub struct RoomAliasId { alias: String, hostname: Host, @@ -129,7 +134,8 @@ pub struct RoomAliasId { /// ); /// ``` #[derive(Clone, Debug, Eq, Hash, PartialEq)] -#[cfg_attr(feature = "diesel", derive(FromSqlRow))] +#[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] +#[cfg_attr(feature = "diesel", sql_type = "Text")] pub struct RoomId { hostname: Host, opaque_id: String, @@ -156,7 +162,8 @@ pub struct RoomId { /// "!n8f893n9:example.com" /// ); #[derive(Clone, Debug, Eq, Hash, PartialEq)] -#[cfg_attr(feature = "diesel", derive(FromSqlRow))] +#[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] +#[cfg_attr(feature = "diesel", sql_type = "Text")] pub enum RoomIdOrAliasId { /// A Matrix room alias ID. RoomAliasId(RoomAliasId), @@ -179,7 +186,8 @@ pub enum RoomIdOrAliasId { /// ); /// ``` #[derive(Clone, Debug, Eq, Hash, PartialEq)] -#[cfg_attr(feature = "diesel", derive(FromSqlRow))] +#[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] +#[cfg_attr(feature = "diesel", sql_type = "Text")] pub struct UserId { hostname: Host, localpart: String, @@ -758,11 +766,11 @@ mod diesel_integration { impl FromSql for $crate::$name where - DB: Backend, + String: FromSql, + DB: Backend, { - fn from_sql(bytes: Option<&[u8]>) -> DeserializeResult { - let string: String = FromSql::::from_sql(bytes)?; - + fn from_sql(value: Option<&::RawValue>) -> DeserializeResult { + let string = >::from_sql(value)?; $crate::$name::try_from(string.as_str()) .map_err(|error| Box::new(error) as Box) } From 3159bd8076e906fa28eb35e2008232d3f93290df Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Fri, 8 Feb 2019 13:11:18 -0800 Subject: [PATCH 057/140] Bump version to 0.11.1. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 62eac946..b506d24d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT" name = "ruma-identifiers" readme = "README.md" repository = "https://github.com/ruma/ruma-identifiers" -version = "0.11.0" +version = "0.11.1" [dependencies] lazy_static = "1.0" From 8f130f95e6e23d1337b9dbc7dfe97cf0aeb9450d Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Sun, 13 Jan 2019 22:20:53 +0100 Subject: [PATCH 058/140] Update to Rust 2018, work around a warning for diesel --- Cargo.toml | 3 ++- src/lib.rs | 41 ++++++++++++++--------------------------- 2 files changed, 16 insertions(+), 28 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b506d24d..11dc97c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,9 +9,10 @@ name = "ruma-identifiers" readme = "README.md" repository = "https://github.com/ruma/ruma-identifiers" version = "0.11.1" +edition = "2018" [dependencies] -lazy_static = "1.0" +lazy_static = "1.2" rand = "0.5" regex = "1.0" serde = "1.0" diff --git a/src/lib.rs b/src/lib.rs index 60191ddd..d6ef9d65 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,20 +6,6 @@ #![deny(missing_docs)] #![deny(warnings)] -#[macro_use] -extern crate lazy_static; -extern crate rand; -extern crate regex; -extern crate serde; -extern crate url; - -#[cfg(feature = "diesel")] -#[macro_use] -extern crate diesel; - -#[cfg(test)] -extern crate serde_json; - use std::{ convert::TryFrom, error::Error as StdError, @@ -27,8 +13,9 @@ use std::{ }; #[cfg(feature = "diesel")] -use diesel::sql_types::Text; +use diesel::{FromSqlRow, sql_types::Text}; +use lazy_static::lazy_static; use rand::{distributions::Alphanumeric, thread_rng, Rng}; use regex::Regex; use serde::{ @@ -201,7 +188,7 @@ struct RoomIdOrAliasIdVisitor; struct UserIdVisitor; fn display( - f: &mut Formatter, + f: &mut Formatter<'_>, sigil: char, localpart: &str, hostname: &Host, @@ -261,7 +248,7 @@ fn parse_id<'a>(required_sigil: char, id: &'a str) -> Result<(&'a str, Host, u16 } impl Display for Error { - fn fmt(&self, f: &mut Formatter) -> FmtResult { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { write!(f, "{}", self.description()) } } @@ -411,25 +398,25 @@ impl From for Error { } impl Display for EventId { - fn fmt(&self, f: &mut Formatter) -> FmtResult { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { display(f, '$', &self.opaque_id, &self.hostname, self.port) } } impl Display for RoomAliasId { - fn fmt(&self, f: &mut Formatter) -> FmtResult { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { display(f, '#', &self.alias, &self.hostname, self.port) } } impl Display for RoomId { - fn fmt(&self, f: &mut Formatter) -> FmtResult { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { display(f, '!', &self.opaque_id, &self.hostname, self.port) } } impl Display for RoomIdOrAliasId { - fn fmt(&self, f: &mut Formatter) -> FmtResult { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { match *self { RoomIdOrAliasId::RoomAliasId(ref room_alias_id) => display( f, @@ -446,7 +433,7 @@ impl Display for RoomIdOrAliasId { } impl Display for UserId { - fn fmt(&self, f: &mut Formatter) -> FmtResult { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { display(f, '@', &self.localpart, &self.hostname, self.port) } } @@ -655,7 +642,7 @@ impl<'a> TryFrom<&'a str> for UserId { impl<'de> Visitor<'de> for EventIdVisitor { type Value = EventId; - fn expecting(&self, formatter: &mut Formatter) -> FmtResult { + fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { write!(formatter, "a Matrix event ID as a string") } @@ -673,7 +660,7 @@ impl<'de> Visitor<'de> for EventIdVisitor { impl<'de> Visitor<'de> for RoomAliasIdVisitor { type Value = RoomAliasId; - fn expecting(&self, formatter: &mut Formatter) -> FmtResult { + fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { write!(formatter, "a Matrix room alias ID as a string") } @@ -691,7 +678,7 @@ impl<'de> Visitor<'de> for RoomAliasIdVisitor { impl<'de> Visitor<'de> for RoomIdVisitor { type Value = RoomId; - fn expecting(&self, formatter: &mut Formatter) -> FmtResult { + fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { write!(formatter, "a Matrix room ID as a string") } @@ -709,7 +696,7 @@ impl<'de> Visitor<'de> for RoomIdVisitor { impl<'de> Visitor<'de> for RoomIdOrAliasIdVisitor { type Value = RoomIdOrAliasId; - fn expecting(&self, formatter: &mut Formatter) -> FmtResult { + fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { write!(formatter, "a Matrix room ID or room alias ID as a string") } @@ -727,7 +714,7 @@ impl<'de> Visitor<'de> for RoomIdOrAliasIdVisitor { impl<'de> Visitor<'de> for UserIdVisitor { type Value = UserId; - fn expecting(&self, formatter: &mut Formatter) -> FmtResult { + fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { write!(formatter, "a Matrix user ID as a string") } From 56958147ca3635913e577035f4178485a5dce6f6 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Mon, 8 Apr 2019 17:05:56 -0700 Subject: [PATCH 059/140] Update dependencies, remove Diesel workaround, remove try_from feature. --- Cargo.toml | 14 +++++++------- src/lib.rs | 3 +-- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 11dc97c8..addfc237 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,15 +12,15 @@ version = "0.11.1" edition = "2018" [dependencies] -lazy_static = "1.2" -rand = "0.5" -regex = "1.0" -serde = "1.0" -url = "1.7" +lazy_static = "1.3.0" +rand = "0.6.5" +regex = "1.1.5" +serde = "1.0.90" +url = "1.7.2" [dependencies.diesel] optional = true -version = "1.4" +version = "1.4.2" [dev-dependencies] -serde_json = "1.0" +serde_json = "1.0.39" diff --git a/src/lib.rs b/src/lib.rs index d6ef9d65..9f144095 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,6 @@ //! Crate **ruma_identifiers** contains types for [Matrix](https://matrix.org/) identifiers //! for events, rooms, room aliases, and users. -#![feature(try_from)] #![deny(missing_debug_implementations)] #![deny(missing_docs)] #![deny(warnings)] @@ -13,7 +12,7 @@ use std::{ }; #[cfg(feature = "diesel")] -use diesel::{FromSqlRow, sql_types::Text}; +use diesel::sql_types::Text; use lazy_static::lazy_static; use rand::{distributions::Alphanumeric, thread_rng, Rng}; From 599e292d5c3c4e1611d1bd471092ed3c4bc0c3b5 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Thu, 11 Apr 2019 17:29:15 -0700 Subject: [PATCH 060/140] Add note about minimum Rust version. --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 29c8d640..b8aa880b 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,10 @@ **ruma-identifiers** contains types for [Matrix](https://matrix.org/) identifiers for events, rooms, room aliases, and users. +## Minimum Rust version + +ruma-identifiers requires Rust 1.34 or later. + ## Documentation ruma-identifiers has [comprehensive documentation](https://docs.rs/ruma-identifiers) available on docs.rs. From eda32dfdb540618f3df671ec76029a3d19f3bf0b Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Thu, 11 Apr 2019 17:35:54 -0700 Subject: [PATCH 061/140] Remove try_from feature from doctests. --- src/lib.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 9f144095..584e510e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -65,7 +65,6 @@ pub enum Error { /// into a string as needed. /// /// ``` -/// # #![feature(try_from)] /// # use std::convert::TryFrom; /// # use ruma_identifiers::EventId; /// assert_eq!( @@ -88,7 +87,6 @@ pub struct EventId { /// needed. /// /// ``` -/// # #![feature(try_from)] /// # use std::convert::TryFrom; /// # use ruma_identifiers::RoomAliasId; /// assert_eq!( @@ -111,7 +109,6 @@ pub struct RoomAliasId { /// into a string as needed. /// /// ``` -/// # #![feature(try_from)] /// # use std::convert::TryFrom; /// # use ruma_identifiers::RoomId; /// assert_eq!( @@ -135,7 +132,6 @@ pub struct RoomId { /// string slice, the variant is determined by the leading sigil character. /// /// ``` -/// # #![feature(try_from)] /// # use std::convert::TryFrom; /// # use ruma_identifiers::RoomIdOrAliasId; /// assert_eq!( @@ -163,7 +159,6 @@ pub enum RoomIdOrAliasId { /// into a string as needed. /// /// ``` -/// # #![feature(try_from)] /// # use std::convert::TryFrom; /// # use ruma_identifiers::UserId; /// assert_eq!( From b3eeb25a99bad5451d53ea93640d6431f93a87e4 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Thu, 11 Apr 2019 17:36:47 -0700 Subject: [PATCH 062/140] Bump version to 0.12.0. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index addfc237..3d9163e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT" name = "ruma-identifiers" readme = "README.md" repository = "https://github.com/ruma/ruma-identifiers" -version = "0.11.1" +version = "0.12.0" edition = "2018" [dependencies] From 1c5c1f92d66a1bb622d2d02d4593c3b3d9b1f017 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Fri, 12 Apr 2019 03:54:28 -0700 Subject: [PATCH 063/140] Reintroduce macro_use for Diesel to import custom derives. --- src/lib.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 584e510e..353458b9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,10 @@ #![deny(missing_docs)] #![deny(warnings)] +#[cfg(feature = "diesel")] +#[cfg_attr(feature = "diesel", macro_use)] +extern crate diesel; + use std::{ convert::TryFrom, error::Error as StdError, From a207c6d8f64ba588a5cdeeb62472c69b56bfc49d Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Fri, 12 Apr 2019 03:54:54 -0700 Subject: [PATCH 064/140] Bump version to 0.12.1. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 3d9163e8..26fcc6da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT" name = "ruma-identifiers" readme = "README.md" repository = "https://github.com/ruma/ruma-identifiers" -version = "0.12.0" +version = "0.12.1" edition = "2018" [dependencies] From 7059bc4ff3afe2061a7bb27e8b0a6242082cfe0c Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Sat, 1 Jun 2019 03:06:38 -0700 Subject: [PATCH 065/140] Add missing closing doc test marker. --- src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.rs b/src/lib.rs index 353458b9..729e6787 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -147,6 +147,7 @@ pub struct RoomId { /// RoomIdOrAliasId::try_from("!n8f893n9:example.com").unwrap().to_string(), /// "!n8f893n9:example.com" /// ); +/// ``` #[derive(Clone, Debug, Eq, Hash, PartialEq)] #[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] #[cfg_attr(feature = "diesel", sql_type = "Text")] From 2047c980ef387e15450216486b72894bee23bc4a Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Sun, 2 Jun 2019 09:14:15 -0700 Subject: [PATCH 066/140] Use stable Rust on Travis. --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index a9217dc2..cf686148 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,5 +5,3 @@ notifications: channels: - secure: "eyhVIoItrOoGxIRYXWQp+uqeTlbqn0MW2/+1Gl4raWzHkeZBME8GFezNxBR1bQrse3qlYj+/UXA43vHCDeU2+xBmvYujpBfjWD7f7huj1zIag+dNHP7k9sIQ09fmKSY+D3Vwr6Y9/wj+xrPNzL7xsxXCduL3w37QTZNqFWTcVuFetqqEFasSnNlkSCsM7Gv6QsaLNNBWsLT9q3va7PQdFwYhitisBAtI79tTcGpBoPpH4xegnai02fYu7WNCPetdQF7tyZb+ioRt9b159neqBb/BoftOvLEzEHtpirTMXYizlURdR63am/pYaEk0S7//WSatHLs2UB1JD5qDWjCCldZD7q+Xn2JEdNVwWv0vTtKd4jYfbGNbIYXgEIvOpoBrQoIAkfa4mVd8pOaexydFStdW/Q7P+h8ZSJoituq0Z6tCbeb+zVKi9n5Qx9F+OQcXREV6FKo5YE+L0VQ91QNnoJKPNxj1OMk+R2/xwbu2+qcDsrwzhwC6woaboxw1jLUTD/T3VeMIKB3NvDt4UivkKvWugMN0QR72xD3eU3+3yXaGVbmSZhC+rbaXE8x3ZViukbPlBMKvKtCI6nzuHt9lw7cl5qub/hz3Pt379fj92Z5Q7R1RvlwFwm7dJ7zypAsg2ZCWyKP0q4gvnd8SNxJ972YTK+tHZuq56JzVA7mi2IU=" use_notice: true -rust: - - "nightly" From 1f7ac74adb0ad700b97fbd6e8971b7377c76c687 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Sun, 2 Jun 2019 18:57:28 -0700 Subject: [PATCH 067/140] Add rustfmt and clippy to CI and address clippy warnings. --- .rustfmt.toml | 1 - .travis.yml | 8 +++++ src/lib.rs | 88 ++++++++++++++++++++++++++++++++++++++------------- 3 files changed, 74 insertions(+), 23 deletions(-) delete mode 100644 .rustfmt.toml diff --git a/.rustfmt.toml b/.rustfmt.toml deleted file mode 100644 index 7d2cf549..00000000 --- a/.rustfmt.toml +++ /dev/null @@ -1 +0,0 @@ -merge_imports = true diff --git a/.travis.yml b/.travis.yml index cf686148..6b7f829c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,12 @@ language: "rust" +before_script: + - "rustup component add rustfmt" + - "rustup component add clippy" +script: + - "cargo fmt --all -- --check" + - "cargo clippy --all-targets --all-features -- -D warnings" + - "cargo build --verbose" + - "cargo test --verbose" notifications: email: false irc: diff --git a/src/lib.rs b/src/lib.rs index 729e6787..afd216b4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,31 @@ //! Crate **ruma_identifiers** contains types for [Matrix](https://matrix.org/) identifiers //! for events, rooms, room aliases, and users. -#![deny(missing_debug_implementations)] -#![deny(missing_docs)] -#![deny(warnings)] +#![deny( + missing_copy_implementations, + missing_debug_implementations, + missing_docs, + warnings +)] +#![warn( + clippy::empty_line_after_outer_attr, + clippy::expl_impl_clone_on_copy, + clippy::if_not_else, + clippy::items_after_statements, + clippy::match_same_arms, + clippy::mem_forget, + clippy::missing_docs_in_private_items, + clippy::multiple_inherent_impl, + clippy::mut_mut, + clippy::needless_borrow, + clippy::needless_continue, + clippy::single_match_else, + clippy::unicode_not_nfc, + clippy::use_self, + clippy::used_underscore_binding, + clippy::wrong_pub_self_convention, + clippy::wrong_self_convention +)] #[cfg(feature = "diesel")] #[cfg_attr(feature = "diesel", macro_use)] @@ -80,8 +102,11 @@ pub enum Error { #[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] #[cfg_attr(feature = "diesel", sql_type = "Text")] pub struct EventId { + /// The hostname of the homeserver. hostname: Host, + /// The event's unique ID. opaque_id: String, + /// The network port of the homeserver. port: u16, } @@ -102,8 +127,11 @@ pub struct EventId { #[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] #[cfg_attr(feature = "diesel", sql_type = "Text")] pub struct RoomAliasId { + /// The alias for the room. alias: String, + /// The hostname of the homeserver. hostname: Host, + /// The network port of the homeserver. port: u16, } @@ -124,8 +152,11 @@ pub struct RoomAliasId { #[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] #[cfg_attr(feature = "diesel", sql_type = "Text")] pub struct RoomId { + /// The hostname of the homeserver. hostname: Host, + /// The room's unique ID. opaque_id: String, + /// The network port of the homeserver. port: u16, } @@ -175,17 +206,26 @@ pub enum RoomIdOrAliasId { #[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] #[cfg_attr(feature = "diesel", sql_type = "Text")] pub struct UserId { + /// The hostname of the homeserver. hostname: Host, + /// The user's unique ID. localpart: String, + /// The network port of the homeserver. port: u16, } +/// A serde visitor for `EventId`. struct EventIdVisitor; +/// A serde visitor for `RoomAliasId`. struct RoomAliasIdVisitor; +/// A serde visitor for `RoomId`. struct RoomIdVisitor; +/// A serde visitor for `RoomIdOrAliasId`. struct RoomIdOrAliasIdVisitor; +/// A serde visitor for `UserId`. struct UserIdVisitor; +/// `Display` implementation shared by identifier types. fn display( f: &mut Formatter<'_>, sigil: char, @@ -200,6 +240,7 @@ fn display( } } +/// Generates a random identifier localpart. fn generate_localpart(length: usize) -> String { thread_rng() .sample_iter(&Alphanumeric) @@ -207,7 +248,8 @@ fn generate_localpart(length: usize) -> String { .collect() } -fn validate_id<'a>(id: &'a str) -> Result<(), Error> { +/// Checks if an identifier is within the acceptable byte lengths. +fn validate_id(id: &str) -> Result<(), Error> { if id.len() > MAX_BYTES { return Err(Error::MaximumLengthExceeded); } @@ -219,7 +261,8 @@ fn validate_id<'a>(id: &'a str) -> Result<(), Error> { Ok(()) } -fn parse_id<'a>(required_sigil: char, id: &'a str) -> Result<(&'a str, Host, u16), Error> { +/// Parses the localpart, host, and port from a string identifier. +fn parse_id(required_sigil: char, id: &str) -> Result<(&str, Host, u16), Error> { validate_id(id)?; if !id.starts_with(required_sigil) { @@ -274,10 +317,10 @@ impl EventId { let event_id = format!("${}:{}", generate_localpart(18), server_name); let (opaque_id, host, port) = parse_id('$', &event_id)?; - Ok(EventId { + Ok(Self { hostname: host, opaque_id: opaque_id.to_string(), - port: port, + port, }) } @@ -309,10 +352,10 @@ impl RoomId { let room_id = format!("!{}:{}", generate_localpart(18), server_name); let (opaque_id, host, port) = parse_id('!', &room_id)?; - Ok(RoomId { + Ok(Self { hostname: host, opaque_id: opaque_id.to_string(), - port: port, + port, }) } @@ -364,10 +407,10 @@ impl UserId { let user_id = format!("@{}:{}", generate_localpart(12).to_lowercase(), server_name); let (localpart, host, port) = parse_id('@', &user_id)?; - Ok(UserId { + Ok(Self { hostname: host, localpart: localpart.to_string(), - port: port, + port, }) } @@ -391,7 +434,7 @@ impl UserId { } impl From for Error { - fn from(_: ParseError) -> Error { + fn from(_: ParseError) -> Self { Error::InvalidHost } } @@ -542,10 +585,10 @@ impl<'a> TryFrom<&'a str> for EventId { fn try_from(event_id: &'a str) -> Result { let (opaque_id, host, port) = parse_id('$', event_id)?; - Ok(EventId { + Ok(Self { hostname: host, opaque_id: opaque_id.to_owned(), - port: port, + port, }) } } @@ -560,10 +603,10 @@ impl<'a> TryFrom<&'a str> for RoomAliasId { fn try_from(room_id: &'a str) -> Result { let (alias, host, port) = parse_id('#', room_id)?; - Ok(RoomAliasId { + Ok(Self { alias: alias.to_owned(), hostname: host, - port: port, + port, }) } } @@ -578,10 +621,10 @@ impl<'a> TryFrom<&'a str> for RoomId { fn try_from(room_id: &'a str) -> Result { let (opaque_id, host, port) = parse_id('!', room_id)?; - Ok(RoomId { + Ok(Self { hostname: host, opaque_id: opaque_id.to_owned(), - port: port, + port, }) } } @@ -622,7 +665,7 @@ impl<'a> TryFrom<&'a str> for UserId { /// /// The string must include the leading @ sigil, the localpart, a literal colon, and a valid /// server name. - fn try_from(user_id: &'a str) -> Result { + fn try_from(user_id: &'a str) -> Result { let (localpart, host, port) = parse_id('@', user_id)?; let downcased_localpart = localpart.to_lowercase(); @@ -630,9 +673,9 @@ impl<'a> TryFrom<&'a str> for UserId { return Err(Error::InvalidCharacters); } - Ok(UserId { + Ok(Self { hostname: host, - port: port, + port, localpart: downcased_localpart.to_owned(), }) } @@ -728,6 +771,7 @@ impl<'de> Visitor<'de> for UserIdVisitor { } } +/// Implements traits from Diesel, allowing identifiers to be used as database fields. #[cfg(feature = "diesel")] mod diesel_integration { use std::{convert::TryFrom, error::Error as StdError, io::Write}; @@ -757,7 +801,7 @@ mod diesel_integration { { fn from_sql(value: Option<&::RawValue>) -> DeserializeResult { let string = >::from_sql(value)?; - $crate::$name::try_from(string.as_str()) + Self::try_from(string.as_str()) .map_err(|error| Box::new(error) as Box) } } From 44de3bcb4d24c623f632543bb64c834ed44fb292 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Mon, 3 Jun 2019 08:57:56 -0700 Subject: [PATCH 068/140] Move each type into its own module. --- src/diesel_integration.rs | 41 ++ src/error.rs | 50 ++ src/event_id.rs | 248 +++++++ src/lib.rs | 1211 +------------------------------ src/room_alias_id.rs | 230 ++++++ src/room_id.rs | 248 +++++++ src/room_id_or_room_alias_id.rs | 219 ++++++ src/user_id.rs | 275 +++++++ 8 files changed, 1328 insertions(+), 1194 deletions(-) create mode 100644 src/diesel_integration.rs create mode 100644 src/error.rs create mode 100644 src/event_id.rs create mode 100644 src/room_alias_id.rs create mode 100644 src/room_id.rs create mode 100644 src/room_id_or_room_alias_id.rs create mode 100644 src/user_id.rs diff --git a/src/diesel_integration.rs b/src/diesel_integration.rs new file mode 100644 index 00000000..8ae777e6 --- /dev/null +++ b/src/diesel_integration.rs @@ -0,0 +1,41 @@ +//! Implements traits from Diesel, allowing identifiers to be used as database fields. + +use std::{convert::TryFrom, error::Error as StdError, io::Write}; + +use diesel::{ + backend::Backend, + deserialize::{FromSql, Result as DeserializeResult}, + serialize::{Output, Result as SerializeResult, ToSql}, + sql_types::Text, +}; + +macro_rules! diesel_impl { + ($name:ident) => { + impl ToSql for $crate::$name + where + DB: Backend, + { + fn to_sql(&self, out: &mut Output) -> SerializeResult { + ToSql::::to_sql(&self.to_string(), out) + } + } + + impl FromSql for $crate::$name + where + String: FromSql, + DB: Backend, + { + fn from_sql(value: Option<&::RawValue>) -> DeserializeResult { + let string = >::from_sql(value)?; + Self::try_from(string.as_str()) + .map_err(|error| Box::new(error) as Box) + } + } + }; +} + +diesel_impl!(EventId); +diesel_impl!(RoomAliasId); +diesel_impl!(RoomId); +diesel_impl!(RoomIdOrAliasId); +diesel_impl!(UserId); diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 00000000..a23ece09 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,50 @@ +//! Error conditions. + +use std::{ + error::Error as StdError, + fmt::{Display, Formatter, Result as FmtResult}, +}; + +use url::ParseError; + +/// An error encountered when trying to parse an invalid ID string. +#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] +pub enum Error { + /// The ID's localpart contains invalid characters. + /// + /// Only relevant for user IDs. + InvalidCharacters, + /// The domain part of the the ID string is not a valid IP address or DNS name. + InvalidHost, + /// The ID exceeds 255 bytes. + MaximumLengthExceeded, + /// The ID is less than 4 characters. + MinimumLengthNotSatisfied, + /// The ID is missing the colon delimiter between localpart and server name. + MissingDelimiter, + /// The ID is missing the leading sigil. + MissingSigil, +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + let message = match *self { + Error::InvalidCharacters => "localpart contains invalid characters", + Error::InvalidHost => "server name is not a valid IP address or domain name", + Error::MaximumLengthExceeded => "ID exceeds 255 bytes", + Error::MinimumLengthNotSatisfied => "ID must be at least 4 characters", + Error::MissingDelimiter => "colon is required between localpart and server name", + Error::MissingSigil => "leading sigil is missing", + }; + + write!(f, "{}", message) + } +} + +impl StdError for Error {} + +impl From for Error { + fn from(_: ParseError) -> Self { + Error::InvalidHost + } +} diff --git a/src/event_id.rs b/src/event_id.rs new file mode 100644 index 00000000..2729871f --- /dev/null +++ b/src/event_id.rs @@ -0,0 +1,248 @@ +//! Matrix event identifiers. + +use std::{ + convert::TryFrom, + fmt::{Display, Formatter, Result as FmtResult}, +}; + +#[cfg(feature = "diesel")] +use diesel::sql_types::Text; +use serde::{ + de::{Error as SerdeError, Unexpected, Visitor}, + Deserialize, Deserializer, Serialize, Serializer, +}; +use url::Host; + +use crate::{display, error::Error, generate_localpart, parse_id}; + +/// A Matrix event ID. +/// +/// An `EventId` is generated randomly or converted from a string slice, and can be converted back +/// into a string as needed. +/// +/// ``` +/// # use std::convert::TryFrom; +/// # use ruma_identifiers::EventId; +/// assert_eq!( +/// EventId::try_from("$h29iv0s8:example.com").unwrap().to_string(), +/// "$h29iv0s8:example.com" +/// ); +/// ``` +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +#[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] +#[cfg_attr(feature = "diesel", sql_type = "Text")] +pub struct EventId { + /// The hostname of the homeserver. + hostname: Host, + /// The event's unique ID. + opaque_id: String, + /// The network port of the homeserver. + port: u16, +} + +/// A serde visitor for `EventId`. +struct EventIdVisitor; + +impl EventId { + /// Attempts to generate an `EventId` for the given origin server with a localpart consisting + /// of 18 random ASCII characters. + /// + /// Fails if the given origin server name cannot be parsed as a valid host. + pub fn new(server_name: &str) -> Result { + let event_id = format!("${}:{}", generate_localpart(18), server_name); + let (opaque_id, host, port) = parse_id('$', &event_id)?; + + Ok(Self { + hostname: host, + opaque_id: opaque_id.to_string(), + port, + }) + } + + /// Returns a `Host` for the event ID, containing the server name (minus the port) of the + /// originating homeserver. + /// + /// The host can be either a domain name, an IPv4 address, or an IPv6 address. + pub fn hostname(&self) -> &Host { + &self.hostname + } + + /// Returns the event's opaque ID. + pub fn opaque_id(&self) -> &str { + &self.opaque_id + } + + /// Returns the port the originating homeserver can be accessed on. + pub fn port(&self) -> u16 { + self.port + } +} + +impl Display for EventId { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + display(f, '$', &self.opaque_id, &self.hostname, self.port) + } +} + +impl Serialize for EventId { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for EventId { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_any(EventIdVisitor) + } +} + +impl<'a> TryFrom<&'a str> for EventId { + type Error = Error; + + /// Attempts to create a new Matrix event ID from a string representation. + /// + /// The string must include the leading $ sigil, the opaque ID, a literal colon, and a valid + /// server name. + fn try_from(event_id: &'a str) -> Result { + let (opaque_id, host, port) = parse_id('$', event_id)?; + + Ok(Self { + hostname: host, + opaque_id: opaque_id.to_owned(), + port, + }) + } +} + +impl<'de> Visitor<'de> for EventIdVisitor { + type Value = EventId; + + fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { + write!(formatter, "a Matrix event ID as a string") + } + + fn visit_str(self, v: &str) -> Result + where + E: SerdeError, + { + match EventId::try_from(v) { + Ok(event_id) => Ok(event_id), + Err(_) => Err(SerdeError::invalid_value(Unexpected::Str(v), &self)), + } + } +} + +#[cfg(test)] +mod tests { + use std::convert::TryFrom; + + use serde_json::{from_str, to_string}; + + use super::EventId; + use crate::error::Error; + + #[test] + fn valid_event_id() { + assert_eq!( + EventId::try_from("$39hvsi03hlne:example.com") + .expect("Failed to create EventId.") + .to_string(), + "$39hvsi03hlne:example.com" + ); + } + + #[test] + fn generate_random_valid_event_id() { + let event_id = EventId::new("example.com") + .expect("Failed to generate EventId.") + .to_string(); + + assert!(event_id.to_string().starts_with('$')); + assert_eq!(event_id.len(), 31); + } + + #[test] + fn generate_random_invalid_event_id() { + assert!(EventId::new("").is_err()); + } + + #[test] + fn serialize_valid_event_id() { + assert_eq!( + to_string( + &EventId::try_from("$39hvsi03hlne:example.com").expect("Failed to create EventId.") + ) + .expect("Failed to convert EventId to JSON."), + r#""$39hvsi03hlne:example.com""# + ); + } + + #[test] + fn deserialize_valid_event_id() { + assert_eq!( + from_str::(r#""$39hvsi03hlne:example.com""#) + .expect("Failed to convert JSON to EventId"), + EventId::try_from("$39hvsi03hlne:example.com").expect("Failed to create EventId.") + ); + } + + #[test] + fn valid_event_id_with_explicit_standard_port() { + assert_eq!( + EventId::try_from("$39hvsi03hlne:example.com:443") + .expect("Failed to create EventId.") + .to_string(), + "$39hvsi03hlne:example.com" + ); + } + + #[test] + fn valid_event_id_with_non_standard_port() { + assert_eq!( + EventId::try_from("$39hvsi03hlne:example.com:5000") + .expect("Failed to create EventId.") + .to_string(), + "$39hvsi03hlne:example.com:5000" + ); + } + + #[test] + fn missing_event_id_sigil() { + assert_eq!( + EventId::try_from("39hvsi03hlne:example.com").err().unwrap(), + Error::MissingSigil + ); + } + + #[test] + fn missing_event_id_delimiter() { + assert_eq!( + EventId::try_from("$39hvsi03hlne").err().unwrap(), + Error::MissingDelimiter + ); + } + + #[test] + fn invalid_event_id_host() { + assert_eq!( + EventId::try_from("$39hvsi03hlne:-").err().unwrap(), + Error::InvalidHost + ); + } + + #[test] + fn invalid_event_id_port() { + assert_eq!( + EventId::try_from("$39hvsi03hlne:example.com:notaport") + .err() + .unwrap(), + Error::InvalidHost + ); + } +} diff --git a/src/lib.rs b/src/lib.rs index afd216b4..4672f296 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,25 +31,26 @@ #[cfg_attr(feature = "diesel", macro_use)] extern crate diesel; -use std::{ - convert::TryFrom, - error::Error as StdError, - fmt::{Display, Formatter, Result as FmtResult}, +use std::fmt::{Formatter, Result as FmtResult}; + +use rand::{distributions::Alphanumeric, thread_rng, Rng}; +use url::Url; + +pub use url::Host; + +pub use crate::{ + error::Error, event_id::EventId, room_alias_id::RoomAliasId, room_id::RoomId, + room_id_or_room_alias_id::RoomIdOrAliasId, user_id::UserId, }; #[cfg(feature = "diesel")] -use diesel::sql_types::Text; - -use lazy_static::lazy_static; -use rand::{distributions::Alphanumeric, thread_rng, Rng}; -use regex::Regex; -use serde::{ - de::{Error as SerdeError, Unexpected, Visitor}, - Deserialize, Deserializer, Serialize, Serializer, -}; -use url::{ParseError, Url}; - -pub use url::Host; +mod diesel_integration; +mod error; +mod event_id; +mod room_alias_id; +mod room_id; +mod room_id_or_room_alias_id; +mod user_id; /// All events must be 255 bytes or less. const MAX_BYTES: usize = 255; @@ -61,170 +62,6 @@ const MIN_CHARS: usize = 4; /// The number of bytes in a valid sigil. const SIGIL_BYTES: usize = 1; -lazy_static! { - static ref USER_LOCALPART_PATTERN: Regex = - Regex::new(r"\A[a-z0-9._=-]+\z").expect("Failed to create user localpart regex."); -} - -/// An error encountered when trying to parse an invalid ID string. -#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] -pub enum Error { - /// The ID's localpart contains invalid characters. - /// - /// Only relevant for user IDs. - InvalidCharacters, - /// The domain part of the the ID string is not a valid IP address or DNS name. - InvalidHost, - /// The ID exceeds 255 bytes. - MaximumLengthExceeded, - /// The ID is less than 4 characters. - MinimumLengthNotSatisfied, - /// The ID is missing the colon delimiter between localpart and server name. - MissingDelimiter, - /// The ID is missing the leading sigil. - MissingSigil, -} - -/// A Matrix event ID. -/// -/// An `EventId` is generated randomly or converted from a string slice, and can be converted back -/// into a string as needed. -/// -/// ``` -/// # use std::convert::TryFrom; -/// # use ruma_identifiers::EventId; -/// assert_eq!( -/// EventId::try_from("$h29iv0s8:example.com").unwrap().to_string(), -/// "$h29iv0s8:example.com" -/// ); -/// ``` -#[derive(Clone, Debug, Eq, Hash, PartialEq)] -#[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] -#[cfg_attr(feature = "diesel", sql_type = "Text")] -pub struct EventId { - /// The hostname of the homeserver. - hostname: Host, - /// The event's unique ID. - opaque_id: String, - /// The network port of the homeserver. - port: u16, -} - -/// A Matrix room alias ID. -/// -/// A `RoomAliasId` is converted from a string slice, and can be converted back into a string as -/// needed. -/// -/// ``` -/// # use std::convert::TryFrom; -/// # use ruma_identifiers::RoomAliasId; -/// assert_eq!( -/// RoomAliasId::try_from("#ruma:example.com").unwrap().to_string(), -/// "#ruma:example.com" -/// ); -/// ``` -#[derive(Clone, Debug, Eq, Hash, PartialEq)] -#[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] -#[cfg_attr(feature = "diesel", sql_type = "Text")] -pub struct RoomAliasId { - /// The alias for the room. - alias: String, - /// The hostname of the homeserver. - hostname: Host, - /// The network port of the homeserver. - port: u16, -} - -/// A Matrix room ID. -/// -/// A `RoomId` is generated randomly or converted from a string slice, and can be converted back -/// into a string as needed. -/// -/// ``` -/// # use std::convert::TryFrom; -/// # use ruma_identifiers::RoomId; -/// assert_eq!( -/// RoomId::try_from("!n8f893n9:example.com").unwrap().to_string(), -/// "!n8f893n9:example.com" -/// ); -/// ``` -#[derive(Clone, Debug, Eq, Hash, PartialEq)] -#[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] -#[cfg_attr(feature = "diesel", sql_type = "Text")] -pub struct RoomId { - /// The hostname of the homeserver. - hostname: Host, - /// The room's unique ID. - opaque_id: String, - /// The network port of the homeserver. - port: u16, -} - -/// A Matrix room ID or a Matrix room alias ID. -/// -/// `RoomIdOrAliasId` is useful for APIs that accept either kind of room identifier. It is converted -/// from a string slice, and can be converted back into a string as needed. When converted from a -/// string slice, the variant is determined by the leading sigil character. -/// -/// ``` -/// # use std::convert::TryFrom; -/// # use ruma_identifiers::RoomIdOrAliasId; -/// assert_eq!( -/// RoomIdOrAliasId::try_from("#ruma:example.com").unwrap().to_string(), -/// "#ruma:example.com" -/// ); -/// -/// assert_eq!( -/// RoomIdOrAliasId::try_from("!n8f893n9:example.com").unwrap().to_string(), -/// "!n8f893n9:example.com" -/// ); -/// ``` -#[derive(Clone, Debug, Eq, Hash, PartialEq)] -#[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] -#[cfg_attr(feature = "diesel", sql_type = "Text")] -pub enum RoomIdOrAliasId { - /// A Matrix room alias ID. - RoomAliasId(RoomAliasId), - /// A Matrix room ID. - RoomId(RoomId), -} - -/// A Matrix user ID. -/// -/// A `UserId` is generated randomly or converted from a string slice, and can be converted back -/// into a string as needed. -/// -/// ``` -/// # use std::convert::TryFrom; -/// # use ruma_identifiers::UserId; -/// assert_eq!( -/// UserId::try_from("@carl:example.com").unwrap().to_string(), -/// "@carl:example.com" -/// ); -/// ``` -#[derive(Clone, Debug, Eq, Hash, PartialEq)] -#[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] -#[cfg_attr(feature = "diesel", sql_type = "Text")] -pub struct UserId { - /// The hostname of the homeserver. - hostname: Host, - /// The user's unique ID. - localpart: String, - /// The network port of the homeserver. - port: u16, -} - -/// A serde visitor for `EventId`. -struct EventIdVisitor; -/// A serde visitor for `RoomAliasId`. -struct RoomAliasIdVisitor; -/// A serde visitor for `RoomId`. -struct RoomIdVisitor; -/// A serde visitor for `RoomIdOrAliasId`. -struct RoomIdOrAliasIdVisitor; -/// A serde visitor for `UserId`. -struct UserIdVisitor; - /// `Display` implementation shared by identifier types. fn display( f: &mut Formatter<'_>, @@ -288,1017 +125,3 @@ fn parse_id(required_sigil: char, id: &str) -> Result<(&str, Host, u16), Error> Ok((localpart, host, port)) } - -impl Display for Error { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - write!(f, "{}", self.description()) - } -} - -impl StdError for Error { - fn description(&self) -> &str { - match *self { - Error::InvalidCharacters => "localpart contains invalid characters", - Error::InvalidHost => "server name is not a valid IP address or domain name", - Error::MaximumLengthExceeded => "ID exceeds 255 bytes", - Error::MinimumLengthNotSatisfied => "ID must be at least 4 characters", - Error::MissingDelimiter => "colon is required between localpart and server name", - Error::MissingSigil => "leading sigil is missing", - } - } -} - -impl EventId { - /// Attempts to generate an `EventId` for the given origin server with a localpart consisting - /// of 18 random ASCII characters. - /// - /// Fails if the given origin server name cannot be parsed as a valid host. - pub fn new(server_name: &str) -> Result { - let event_id = format!("${}:{}", generate_localpart(18), server_name); - let (opaque_id, host, port) = parse_id('$', &event_id)?; - - Ok(Self { - hostname: host, - opaque_id: opaque_id.to_string(), - port, - }) - } - - /// Returns a `Host` for the event ID, containing the server name (minus the port) of the - /// originating homeserver. - /// - /// The host can be either a domain name, an IPv4 address, or an IPv6 address. - pub fn hostname(&self) -> &Host { - &self.hostname - } - - /// Returns the event's opaque ID. - pub fn opaque_id(&self) -> &str { - &self.opaque_id - } - - /// Returns the port the originating homeserver can be accessed on. - pub fn port(&self) -> u16 { - self.port - } -} - -impl RoomId { - /// Attempts to generate a `RoomId` for the given origin server with a localpart consisting of - /// 18 random ASCII characters. - /// - /// Fails if the given origin server name cannot be parsed as a valid host. - pub fn new(server_name: &str) -> Result { - let room_id = format!("!{}:{}", generate_localpart(18), server_name); - let (opaque_id, host, port) = parse_id('!', &room_id)?; - - Ok(Self { - hostname: host, - opaque_id: opaque_id.to_string(), - port, - }) - } - - /// Returns a `Host` for the room ID, containing the server name (minus the port) of the - /// originating homeserver. - /// - /// The host can be either a domain name, an IPv4 address, or an IPv6 address. - pub fn hostname(&self) -> &Host { - &self.hostname - } - - /// Returns the event's opaque ID. - pub fn opaque_id(&self) -> &str { - &self.opaque_id - } - - /// Returns the port the originating homeserver can be accessed on. - pub fn port(&self) -> u16 { - self.port - } -} - -impl RoomAliasId { - /// Returns a `Host` for the room alias ID, containing the server name (minus the port) of - /// the originating homeserver. - /// - /// The host can be either a domain name, an IPv4 address, or an IPv6 address. - pub fn hostname(&self) -> &Host { - &self.hostname - } - - /// Returns the room's alias. - pub fn alias(&self) -> &str { - &self.alias - } - - /// Returns the port the originating homeserver can be accessed on. - pub fn port(&self) -> u16 { - self.port - } -} - -impl UserId { - /// Attempts to generate a `UserId` for the given origin server with a localpart consisting of - /// 12 random ASCII characters. - /// - /// Fails if the given origin server name cannot be parsed as a valid host. - pub fn new(server_name: &str) -> Result { - let user_id = format!("@{}:{}", generate_localpart(12).to_lowercase(), server_name); - let (localpart, host, port) = parse_id('@', &user_id)?; - - Ok(Self { - hostname: host, - localpart: localpart.to_string(), - port, - }) - } - - /// Returns a `Host` for the user ID, containing the server name (minus the port) of the - /// originating homeserver. - /// - /// The host can be either a domain name, an IPv4 address, or an IPv6 address. - pub fn hostname(&self) -> &Host { - &self.hostname - } - - /// Returns the user's localpart. - pub fn localpart(&self) -> &str { - &self.localpart - } - - /// Returns the port the originating homeserver can be accessed on. - pub fn port(&self) -> u16 { - self.port - } -} - -impl From for Error { - fn from(_: ParseError) -> Self { - Error::InvalidHost - } -} - -impl Display for EventId { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - display(f, '$', &self.opaque_id, &self.hostname, self.port) - } -} - -impl Display for RoomAliasId { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - display(f, '#', &self.alias, &self.hostname, self.port) - } -} - -impl Display for RoomId { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - display(f, '!', &self.opaque_id, &self.hostname, self.port) - } -} - -impl Display for RoomIdOrAliasId { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - match *self { - RoomIdOrAliasId::RoomAliasId(ref room_alias_id) => display( - f, - '#', - &room_alias_id.alias, - &room_alias_id.hostname, - room_alias_id.port, - ), - RoomIdOrAliasId::RoomId(ref room_id) => { - display(f, '!', &room_id.opaque_id, &room_id.hostname, room_id.port) - } - } - } -} - -impl Display for UserId { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - display(f, '@', &self.localpart, &self.hostname, self.port) - } -} - -impl Serialize for EventId { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -impl Serialize for RoomAliasId { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -impl Serialize for RoomId { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -impl Serialize for RoomIdOrAliasId { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - match *self { - RoomIdOrAliasId::RoomAliasId(ref room_alias_id) => { - serializer.serialize_str(&room_alias_id.to_string()) - } - RoomIdOrAliasId::RoomId(ref room_id) => serializer.serialize_str(&room_id.to_string()), - } - } -} - -impl Serialize for UserId { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -impl<'de> Deserialize<'de> for EventId { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserializer.deserialize_any(EventIdVisitor) - } -} - -impl<'de> Deserialize<'de> for RoomAliasId { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserializer.deserialize_any(RoomAliasIdVisitor) - } -} - -impl<'de> Deserialize<'de> for RoomId { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserializer.deserialize_any(RoomIdVisitor) - } -} - -impl<'de> Deserialize<'de> for RoomIdOrAliasId { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserializer.deserialize_any(RoomIdOrAliasIdVisitor) - } -} - -impl<'de> Deserialize<'de> for UserId { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserializer.deserialize_any(UserIdVisitor) - } -} - -impl<'a> TryFrom<&'a str> for EventId { - type Error = Error; - - /// Attempts to create a new Matrix event ID from a string representation. - /// - /// The string must include the leading $ sigil, the opaque ID, a literal colon, and a valid - /// server name. - fn try_from(event_id: &'a str) -> Result { - let (opaque_id, host, port) = parse_id('$', event_id)?; - - Ok(Self { - hostname: host, - opaque_id: opaque_id.to_owned(), - port, - }) - } -} - -impl<'a> TryFrom<&'a str> for RoomAliasId { - type Error = Error; - - /// 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 valid - /// server name. - fn try_from(room_id: &'a str) -> Result { - let (alias, host, port) = parse_id('#', room_id)?; - - Ok(Self { - alias: alias.to_owned(), - hostname: host, - port, - }) - } -} - -impl<'a> TryFrom<&'a str> for RoomId { - type Error = Error; - - /// Attempts to create a new Matrix room ID from a string representation. - /// - /// The string must include the leading ! sigil, the opaque ID, a literal colon, and a valid - /// server name. - fn try_from(room_id: &'a str) -> Result { - let (opaque_id, host, port) = parse_id('!', room_id)?; - - Ok(Self { - hostname: host, - opaque_id: opaque_id.to_owned(), - port, - }) - } -} - -impl<'a> TryFrom<&'a str> for RoomIdOrAliasId { - type Error = Error; - - /// Attempts to create a new Matrix room ID or a room alias ID from a string representation. - /// - /// The string must either - /// include the leading ! sigil, the opaque ID, a literal colon, and a valid server name or - /// include the leading # sigil, the alias, a literal colon, and a valid server name. - fn try_from(room_id_or_alias_id: &'a str) -> Result { - validate_id(room_id_or_alias_id)?; - - let mut chars = room_id_or_alias_id.chars(); - - let sigil = chars.nth(0).expect("ID missing first character."); - - match sigil { - '#' => { - let room_alias_id = RoomAliasId::try_from(room_id_or_alias_id)?; - Ok(RoomIdOrAliasId::RoomAliasId(room_alias_id)) - } - '!' => { - let room_id = RoomId::try_from(room_id_or_alias_id)?; - Ok(RoomIdOrAliasId::RoomId(room_id)) - } - _ => Err(Error::MissingSigil), - } - } -} - -impl<'a> TryFrom<&'a str> for UserId { - type Error = Error; - - /// Attempts to create a new Matrix user ID from a string representation. - /// - /// The string must include the leading @ sigil, the localpart, a literal colon, and a valid - /// server name. - fn try_from(user_id: &'a str) -> Result { - let (localpart, host, port) = parse_id('@', user_id)?; - let downcased_localpart = localpart.to_lowercase(); - - if !USER_LOCALPART_PATTERN.is_match(&downcased_localpart) { - return Err(Error::InvalidCharacters); - } - - Ok(Self { - hostname: host, - port, - localpart: downcased_localpart.to_owned(), - }) - } -} - -impl<'de> Visitor<'de> for EventIdVisitor { - type Value = EventId; - - fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { - write!(formatter, "a Matrix event ID as a string") - } - - fn visit_str(self, v: &str) -> Result - where - E: SerdeError, - { - match EventId::try_from(v) { - Ok(event_id) => Ok(event_id), - Err(_) => Err(SerdeError::invalid_value(Unexpected::Str(v), &self)), - } - } -} - -impl<'de> Visitor<'de> for RoomAliasIdVisitor { - type Value = RoomAliasId; - - fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { - write!(formatter, "a Matrix room alias ID as a string") - } - - fn visit_str(self, v: &str) -> Result - where - E: SerdeError, - { - match RoomAliasId::try_from(v) { - Ok(room_alias_id) => Ok(room_alias_id), - Err(_) => Err(SerdeError::invalid_value(Unexpected::Str(v), &self)), - } - } -} - -impl<'de> Visitor<'de> for RoomIdVisitor { - type Value = RoomId; - - fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { - write!(formatter, "a Matrix room ID as a string") - } - - fn visit_str(self, v: &str) -> Result - where - E: SerdeError, - { - match RoomId::try_from(v) { - Ok(room_id) => Ok(room_id), - Err(_) => Err(SerdeError::invalid_value(Unexpected::Str(v), &self)), - } - } -} - -impl<'de> Visitor<'de> for RoomIdOrAliasIdVisitor { - type Value = RoomIdOrAliasId; - - fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { - write!(formatter, "a Matrix room ID or room alias ID as a string") - } - - fn visit_str(self, v: &str) -> Result - where - E: SerdeError, - { - match RoomIdOrAliasId::try_from(v) { - Ok(room_id_or_alias_id) => Ok(room_id_or_alias_id), - Err(_) => Err(SerdeError::invalid_value(Unexpected::Str(v), &self)), - } - } -} - -impl<'de> Visitor<'de> for UserIdVisitor { - type Value = UserId; - - fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { - write!(formatter, "a Matrix user ID as a string") - } - - fn visit_str(self, v: &str) -> Result - where - E: SerdeError, - { - match UserId::try_from(v) { - Ok(user_id) => Ok(user_id), - Err(_) => Err(SerdeError::invalid_value(Unexpected::Str(v), &self)), - } - } -} - -/// Implements traits from Diesel, allowing identifiers to be used as database fields. -#[cfg(feature = "diesel")] -mod diesel_integration { - use std::{convert::TryFrom, error::Error as StdError, io::Write}; - - use diesel::{ - backend::Backend, - deserialize::{FromSql, Result as DeserializeResult}, - serialize::{Output, Result as SerializeResult, ToSql}, - sql_types::Text, - }; - - macro_rules! diesel_impl { - ($name:ident) => { - impl ToSql for $crate::$name - where - DB: Backend, - { - fn to_sql(&self, out: &mut Output) -> SerializeResult { - ToSql::::to_sql(&self.to_string(), out) - } - } - - impl FromSql for $crate::$name - where - String: FromSql, - DB: Backend, - { - fn from_sql(value: Option<&::RawValue>) -> DeserializeResult { - let string = >::from_sql(value)?; - Self::try_from(string.as_str()) - .map_err(|error| Box::new(error) as Box) - } - } - }; - } - - diesel_impl!(EventId); - diesel_impl!(RoomAliasId); - diesel_impl!(RoomId); - diesel_impl!(RoomIdOrAliasId); - diesel_impl!(UserId); -} - -#[cfg(test)] -mod tests { - use std::convert::TryFrom; - - use serde_json::{from_str, to_string}; - - use super::{Error, EventId, RoomAliasId, RoomId, RoomIdOrAliasId, UserId}; - - #[test] - fn valid_event_id() { - assert_eq!( - EventId::try_from("$39hvsi03hlne:example.com") - .expect("Failed to create EventId.") - .to_string(), - "$39hvsi03hlne:example.com" - ); - } - - #[test] - fn generate_random_valid_event_id() { - let event_id = EventId::new("example.com") - .expect("Failed to generate EventId.") - .to_string(); - - assert!(event_id.to_string().starts_with('$')); - assert_eq!(event_id.len(), 31); - } - - #[test] - fn generate_random_invalid_event_id() { - assert!(EventId::new("").is_err()); - } - - #[test] - fn serialize_valid_event_id() { - assert_eq!( - to_string( - &EventId::try_from("$39hvsi03hlne:example.com").expect("Failed to create EventId.") - ) - .expect("Failed to convert EventId to JSON."), - r#""$39hvsi03hlne:example.com""# - ); - } - - #[test] - fn deserialize_valid_event_id() { - assert_eq!( - from_str::(r#""$39hvsi03hlne:example.com""#) - .expect("Failed to convert JSON to EventId"), - EventId::try_from("$39hvsi03hlne:example.com").expect("Failed to create EventId.") - ); - } - - #[test] - fn valid_event_id_with_explicit_standard_port() { - assert_eq!( - EventId::try_from("$39hvsi03hlne:example.com:443") - .expect("Failed to create EventId.") - .to_string(), - "$39hvsi03hlne:example.com" - ); - } - - #[test] - fn valid_event_id_with_non_standard_port() { - assert_eq!( - EventId::try_from("$39hvsi03hlne:example.com:5000") - .expect("Failed to create EventId.") - .to_string(), - "$39hvsi03hlne:example.com:5000" - ); - } - - #[test] - fn missing_event_id_sigil() { - assert_eq!( - EventId::try_from("39hvsi03hlne:example.com").err().unwrap(), - Error::MissingSigil - ); - } - - #[test] - fn missing_event_id_delimiter() { - assert_eq!( - EventId::try_from("$39hvsi03hlne").err().unwrap(), - Error::MissingDelimiter - ); - } - - #[test] - fn invalid_event_id_host() { - assert_eq!( - EventId::try_from("$39hvsi03hlne:-").err().unwrap(), - Error::InvalidHost - ); - } - - #[test] - fn invalid_event_id_port() { - assert_eq!( - EventId::try_from("$39hvsi03hlne:example.com:notaport") - .err() - .unwrap(), - Error::InvalidHost - ); - } - - #[test] - fn valid_room_alias_id() { - assert_eq!( - RoomAliasId::try_from("#ruma:example.com") - .expect("Failed to create RoomAliasId.") - .to_string(), - "#ruma:example.com" - ); - } - - #[test] - fn serialize_valid_room_alias_id() { - assert_eq!( - to_string( - &RoomAliasId::try_from("#ruma:example.com").expect("Failed to create RoomAliasId.") - ) - .expect("Failed to convert RoomAliasId to JSON."), - r##""#ruma:example.com""## - ); - } - - #[test] - fn deserialize_valid_room_alias_id() { - assert_eq!( - from_str::(r##""#ruma:example.com""##) - .expect("Failed to convert JSON to RoomAliasId"), - RoomAliasId::try_from("#ruma:example.com").expect("Failed to create RoomAliasId.") - ); - } - - #[test] - fn valid_room_alias_id_with_explicit_standard_port() { - assert_eq!( - RoomAliasId::try_from("#ruma:example.com:443") - .expect("Failed to create RoomAliasId.") - .to_string(), - "#ruma:example.com" - ); - } - - #[test] - fn valid_room_alias_id_with_non_standard_port() { - assert_eq!( - RoomAliasId::try_from("#ruma:example.com:5000") - .expect("Failed to create RoomAliasId.") - .to_string(), - "#ruma:example.com:5000" - ); - } - - #[test] - fn valid_room_alias_id_unicode() { - assert_eq!( - RoomAliasId::try_from("#老虎£я:example.com") - .expect("Failed to create RoomAliasId.") - .to_string(), - "#老虎£я:example.com" - ); - } - - #[test] - fn missing_room_alias_id_sigil() { - assert_eq!( - RoomAliasId::try_from("39hvsi03hlne:example.com") - .err() - .unwrap(), - Error::MissingSigil - ); - } - - #[test] - fn missing_room_alias_id_delimiter() { - assert_eq!( - RoomAliasId::try_from("#ruma").err().unwrap(), - Error::MissingDelimiter - ); - } - - #[test] - fn invalid_room_alias_id_host() { - assert_eq!( - RoomAliasId::try_from("#ruma:-").err().unwrap(), - Error::InvalidHost - ); - } - - #[test] - fn invalid_room_alias_id_port() { - assert_eq!( - RoomAliasId::try_from("#ruma:example.com:notaport") - .err() - .unwrap(), - Error::InvalidHost - ); - } - - #[test] - fn valid_room_id() { - assert_eq!( - RoomId::try_from("!29fhd83h92h0:example.com") - .expect("Failed to create RoomId.") - .to_string(), - "!29fhd83h92h0:example.com" - ); - } - - #[test] - fn generate_random_valid_room_id() { - let room_id = RoomId::new("example.com") - .expect("Failed to generate RoomId.") - .to_string(); - - assert!(room_id.to_string().starts_with('!')); - assert_eq!(room_id.len(), 31); - } - - #[test] - fn generate_random_invalid_room_id() { - assert!(RoomId::new("").is_err()); - } - - #[test] - fn serialize_valid_room_id() { - assert_eq!( - to_string( - &RoomId::try_from("!29fhd83h92h0:example.com").expect("Failed to create RoomId.") - ) - .expect("Failed to convert RoomId to JSON."), - r#""!29fhd83h92h0:example.com""# - ); - } - - #[test] - fn deserialize_valid_room_id() { - assert_eq!( - from_str::(r#""!29fhd83h92h0:example.com""#) - .expect("Failed to convert JSON to RoomId"), - RoomId::try_from("!29fhd83h92h0:example.com").expect("Failed to create RoomId.") - ); - } - - #[test] - fn valid_room_id_with_explicit_standard_port() { - assert_eq!( - RoomId::try_from("!29fhd83h92h0:example.com:443") - .expect("Failed to create RoomId.") - .to_string(), - "!29fhd83h92h0:example.com" - ); - } - - #[test] - fn valid_room_id_with_non_standard_port() { - assert_eq!( - RoomId::try_from("!29fhd83h92h0:example.com:5000") - .expect("Failed to create RoomId.") - .to_string(), - "!29fhd83h92h0:example.com:5000" - ); - } - - #[test] - fn missing_room_id_sigil() { - assert_eq!( - RoomId::try_from("carl:example.com").err().unwrap(), - Error::MissingSigil - ); - } - - #[test] - fn missing_room_id_delimiter() { - assert_eq!( - RoomId::try_from("!29fhd83h92h0").err().unwrap(), - Error::MissingDelimiter - ); - } - - #[test] - fn invalid_room_id_host() { - assert_eq!( - RoomId::try_from("!29fhd83h92h0:-").err().unwrap(), - Error::InvalidHost - ); - } - - #[test] - fn invalid_room_id_port() { - assert_eq!( - RoomId::try_from("!29fhd83h92h0:example.com:notaport") - .err() - .unwrap(), - Error::InvalidHost - ); - } - - #[test] - fn valid_room_id_or_alias_id_with_a_room_alias_id() { - assert_eq!( - RoomIdOrAliasId::try_from("#ruma:example.com") - .expect("Failed to create RoomAliasId.") - .to_string(), - "#ruma:example.com" - ); - } - - #[test] - fn valid_room_id_or_alias_id_with_a_room_id() { - assert_eq!( - RoomIdOrAliasId::try_from("!29fhd83h92h0:example.com") - .expect("Failed to create RoomId.") - .to_string(), - "!29fhd83h92h0:example.com" - ); - } - - #[test] - fn missing_sigil_for_room_id_or_alias_id() { - assert_eq!( - RoomIdOrAliasId::try_from("ruma:example.com").err().unwrap(), - Error::MissingSigil - ); - } - - #[test] - fn serialize_valid_room_id_or_alias_id_with_a_room_alias_id() { - assert_eq!( - to_string( - &RoomIdOrAliasId::try_from("#ruma:example.com") - .expect("Failed to create RoomAliasId.") - ) - .expect("Failed to convert RoomAliasId to JSON."), - r##""#ruma:example.com""## - ); - } - - #[test] - fn serialize_valid_room_id_or_alias_id_with_a_room_id() { - assert_eq!( - to_string( - &RoomIdOrAliasId::try_from("!29fhd83h92h0:example.com") - .expect("Failed to create RoomId.") - ) - .expect("Failed to convert RoomId to JSON."), - r#""!29fhd83h92h0:example.com""# - ); - } - - #[test] - fn deserialize_valid_room_id_or_alias_id_with_a_room_alias_id() { - assert_eq!( - from_str::(r##""#ruma:example.com""##) - .expect("Failed to convert JSON to RoomAliasId"), - RoomIdOrAliasId::try_from("#ruma:example.com").expect("Failed to create RoomAliasId.") - ); - } - - #[test] - fn deserialize_valid_room_id_or_alias_id_with_a_room_id() { - assert_eq!( - from_str::(r##""!29fhd83h92h0:example.com""##) - .expect("Failed to convert JSON to RoomId"), - RoomIdOrAliasId::try_from("!29fhd83h92h0:example.com") - .expect("Failed to create RoomAliasId.") - ); - } - - #[test] - fn valid_user_id() { - assert_eq!( - UserId::try_from("@carl:example.com") - .expect("Failed to create UserId.") - .to_string(), - "@carl:example.com" - ); - } - - #[test] - fn downcase_user_id() { - assert_eq!( - UserId::try_from("@CARL:example.com") - .expect("Failed to create UserId.") - .to_string(), - "@carl:example.com" - ); - } - - #[test] - fn generate_random_valid_user_id() { - let user_id = UserId::new("example.com") - .expect("Failed to generate UserId.") - .to_string(); - - assert!(user_id.to_string().starts_with('@')); - assert_eq!(user_id.len(), 25); - } - - #[test] - fn generate_random_invalid_user_id() { - assert!(UserId::new("").is_err()); - } - - #[test] - fn serialize_valid_user_id() { - assert_eq!( - to_string(&UserId::try_from("@carl:example.com").expect("Failed to create UserId.")) - .expect("Failed to convert UserId to JSON."), - r#""@carl:example.com""# - ); - } - - #[test] - fn deserialize_valid_user_id() { - assert_eq!( - from_str::(r#""@carl:example.com""#).expect("Failed to convert JSON to UserId"), - UserId::try_from("@carl:example.com").expect("Failed to create UserId.") - ); - } - - #[test] - fn valid_user_id_with_explicit_standard_port() { - assert_eq!( - UserId::try_from("@carl:example.com:443") - .expect("Failed to create UserId.") - .to_string(), - "@carl:example.com" - ); - } - - #[test] - fn valid_user_id_with_non_standard_port() { - assert_eq!( - UserId::try_from("@carl:example.com:5000") - .expect("Failed to create UserId.") - .to_string(), - "@carl:example.com:5000" - ); - } - - #[test] - fn invalid_characters_in_user_id_localpart() { - assert_eq!( - UserId::try_from("@%%%:example.com").err().unwrap(), - Error::InvalidCharacters - ); - } - - #[test] - fn missing_user_id_sigil() { - assert_eq!( - UserId::try_from("carl:example.com").err().unwrap(), - Error::MissingSigil - ); - } - - #[test] - fn missing_user_id_delimiter() { - assert_eq!( - UserId::try_from("@carl").err().unwrap(), - Error::MissingDelimiter - ); - } - - #[test] - fn invalid_user_id_host() { - assert_eq!( - UserId::try_from("@carl:-").err().unwrap(), - Error::InvalidHost - ); - } - - #[test] - fn invalid_user_id_port() { - assert_eq!( - UserId::try_from("@carl:example.com:notaport") - .err() - .unwrap(), - Error::InvalidHost - ); - } -} diff --git a/src/room_alias_id.rs b/src/room_alias_id.rs new file mode 100644 index 00000000..1d6c1de4 --- /dev/null +++ b/src/room_alias_id.rs @@ -0,0 +1,230 @@ +//! Matrix room alias identifiers. + +use std::{ + convert::TryFrom, + fmt::{Display, Formatter, Result as FmtResult}, +}; + +#[cfg(feature = "diesel")] +use diesel::sql_types::Text; +use serde::{ + de::{Error as SerdeError, Unexpected, Visitor}, + Deserialize, Deserializer, Serialize, Serializer, +}; +use url::Host; + +use crate::{display, error::Error, parse_id}; + +/// A Matrix room alias ID. +/// +/// A `RoomAliasId` is converted from a string slice, and can be converted back into a string as +/// needed. +/// +/// ``` +/// # use std::convert::TryFrom; +/// # use ruma_identifiers::RoomAliasId; +/// assert_eq!( +/// RoomAliasId::try_from("#ruma:example.com").unwrap().to_string(), +/// "#ruma:example.com" +/// ); +/// ``` +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +#[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] +#[cfg_attr(feature = "diesel", sql_type = "Text")] +pub struct RoomAliasId { + /// The alias for the room. + alias: String, + /// The hostname of the homeserver. + hostname: Host, + /// The network port of the homeserver. + port: u16, +} + +/// A serde visitor for `RoomAliasId`. +struct RoomAliasIdVisitor; + +impl RoomAliasId { + /// Returns a `Host` for the room alias ID, containing the server name (minus the port) of + /// the originating homeserver. + /// + /// The host can be either a domain name, an IPv4 address, or an IPv6 address. + pub fn hostname(&self) -> &Host { + &self.hostname + } + + /// Returns the room's alias. + pub fn alias(&self) -> &str { + &self.alias + } + + /// Returns the port the originating homeserver can be accessed on. + pub fn port(&self) -> u16 { + self.port + } +} + +impl Display for RoomAliasId { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + display(f, '#', &self.alias, &self.hostname, self.port) + } +} + +impl Serialize for RoomAliasId { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for RoomAliasId { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_any(RoomAliasIdVisitor) + } +} + +impl<'a> TryFrom<&'a str> for RoomAliasId { + type Error = Error; + + /// 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 valid + /// server name. + fn try_from(room_id: &'a str) -> Result { + let (alias, host, port) = parse_id('#', room_id)?; + + Ok(Self { + alias: alias.to_owned(), + hostname: host, + port, + }) + } +} + +impl<'de> Visitor<'de> for RoomAliasIdVisitor { + type Value = RoomAliasId; + + fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { + write!(formatter, "a Matrix room alias ID as a string") + } + + fn visit_str(self, v: &str) -> Result + where + E: SerdeError, + { + match RoomAliasId::try_from(v) { + Ok(room_alias_id) => Ok(room_alias_id), + Err(_) => Err(SerdeError::invalid_value(Unexpected::Str(v), &self)), + } + } +} + +#[cfg(test)] +mod tests { + use std::convert::TryFrom; + + use serde_json::{from_str, to_string}; + + use super::RoomAliasId; + use crate::error::Error; + + #[test] + fn valid_room_alias_id() { + assert_eq!( + RoomAliasId::try_from("#ruma:example.com") + .expect("Failed to create RoomAliasId.") + .to_string(), + "#ruma:example.com" + ); + } + + #[test] + fn serialize_valid_room_alias_id() { + assert_eq!( + to_string( + &RoomAliasId::try_from("#ruma:example.com").expect("Failed to create RoomAliasId.") + ) + .expect("Failed to convert RoomAliasId to JSON."), + r##""#ruma:example.com""## + ); + } + + #[test] + fn deserialize_valid_room_alias_id() { + assert_eq!( + from_str::(r##""#ruma:example.com""##) + .expect("Failed to convert JSON to RoomAliasId"), + RoomAliasId::try_from("#ruma:example.com").expect("Failed to create RoomAliasId.") + ); + } + + #[test] + fn valid_room_alias_id_with_explicit_standard_port() { + assert_eq!( + RoomAliasId::try_from("#ruma:example.com:443") + .expect("Failed to create RoomAliasId.") + .to_string(), + "#ruma:example.com" + ); + } + + #[test] + fn valid_room_alias_id_with_non_standard_port() { + assert_eq!( + RoomAliasId::try_from("#ruma:example.com:5000") + .expect("Failed to create RoomAliasId.") + .to_string(), + "#ruma:example.com:5000" + ); + } + + #[test] + fn valid_room_alias_id_unicode() { + assert_eq!( + RoomAliasId::try_from("#老虎£я:example.com") + .expect("Failed to create RoomAliasId.") + .to_string(), + "#老虎£я:example.com" + ); + } + + #[test] + fn missing_room_alias_id_sigil() { + assert_eq!( + RoomAliasId::try_from("39hvsi03hlne:example.com") + .err() + .unwrap(), + Error::MissingSigil + ); + } + + #[test] + fn missing_room_alias_id_delimiter() { + assert_eq!( + RoomAliasId::try_from("#ruma").err().unwrap(), + Error::MissingDelimiter + ); + } + + #[test] + fn invalid_room_alias_id_host() { + assert_eq!( + RoomAliasId::try_from("#ruma:-").err().unwrap(), + Error::InvalidHost + ); + } + + #[test] + fn invalid_room_alias_id_port() { + assert_eq!( + RoomAliasId::try_from("#ruma:example.com:notaport") + .err() + .unwrap(), + Error::InvalidHost + ); + } +} diff --git a/src/room_id.rs b/src/room_id.rs new file mode 100644 index 00000000..6c66c883 --- /dev/null +++ b/src/room_id.rs @@ -0,0 +1,248 @@ +//! Matrix room identifiers. + +use std::{ + convert::TryFrom, + fmt::{Display, Formatter, Result as FmtResult}, +}; + +#[cfg(feature = "diesel")] +use diesel::sql_types::Text; +use serde::{ + de::{Error as SerdeError, Unexpected, Visitor}, + Deserialize, Deserializer, Serialize, Serializer, +}; +use url::Host; + +use crate::{display, error::Error, generate_localpart, parse_id}; + +/// A Matrix room ID. +/// +/// A `RoomId` is generated randomly or converted from a string slice, and can be converted back +/// into a string as needed. +/// +/// ``` +/// # use std::convert::TryFrom; +/// # use ruma_identifiers::RoomId; +/// assert_eq!( +/// RoomId::try_from("!n8f893n9:example.com").unwrap().to_string(), +/// "!n8f893n9:example.com" +/// ); +/// ``` +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +#[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] +#[cfg_attr(feature = "diesel", sql_type = "Text")] +pub struct RoomId { + /// The hostname of the homeserver. + hostname: Host, + /// The room's unique ID. + opaque_id: String, + /// The network port of the homeserver. + port: u16, +} + +/// A serde visitor for `RoomId`. +struct RoomIdVisitor; + +impl RoomId { + /// Attempts to generate a `RoomId` for the given origin server with a localpart consisting of + /// 18 random ASCII characters. + /// + /// Fails if the given origin server name cannot be parsed as a valid host. + pub fn new(server_name: &str) -> Result { + let room_id = format!("!{}:{}", generate_localpart(18), server_name); + let (opaque_id, host, port) = parse_id('!', &room_id)?; + + Ok(Self { + hostname: host, + opaque_id: opaque_id.to_string(), + port, + }) + } + + /// Returns a `Host` for the room ID, containing the server name (minus the port) of the + /// originating homeserver. + /// + /// The host can be either a domain name, an IPv4 address, or an IPv6 address. + pub fn hostname(&self) -> &Host { + &self.hostname + } + + /// Returns the event's opaque ID. + pub fn opaque_id(&self) -> &str { + &self.opaque_id + } + + /// Returns the port the originating homeserver can be accessed on. + pub fn port(&self) -> u16 { + self.port + } +} + +impl Display for RoomId { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + display(f, '!', &self.opaque_id, &self.hostname, self.port) + } +} + +impl Serialize for RoomId { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for RoomId { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_any(RoomIdVisitor) + } +} + +impl<'a> TryFrom<&'a str> for RoomId { + type Error = Error; + + /// Attempts to create a new Matrix room ID from a string representation. + /// + /// The string must include the leading ! sigil, the opaque ID, a literal colon, and a valid + /// server name. + fn try_from(room_id: &'a str) -> Result { + let (opaque_id, host, port) = parse_id('!', room_id)?; + + Ok(Self { + hostname: host, + opaque_id: opaque_id.to_owned(), + port, + }) + } +} + +impl<'de> Visitor<'de> for RoomIdVisitor { + type Value = RoomId; + + fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { + write!(formatter, "a Matrix room ID as a string") + } + + fn visit_str(self, v: &str) -> Result + where + E: SerdeError, + { + match RoomId::try_from(v) { + Ok(room_id) => Ok(room_id), + Err(_) => Err(SerdeError::invalid_value(Unexpected::Str(v), &self)), + } + } +} + +#[cfg(test)] +mod tests { + use std::convert::TryFrom; + + use serde_json::{from_str, to_string}; + + use super::RoomId; + use crate::error::Error; + + #[test] + fn valid_room_id() { + assert_eq!( + RoomId::try_from("!29fhd83h92h0:example.com") + .expect("Failed to create RoomId.") + .to_string(), + "!29fhd83h92h0:example.com" + ); + } + + #[test] + fn generate_random_valid_room_id() { + let room_id = RoomId::new("example.com") + .expect("Failed to generate RoomId.") + .to_string(); + + assert!(room_id.to_string().starts_with('!')); + assert_eq!(room_id.len(), 31); + } + + #[test] + fn generate_random_invalid_room_id() { + assert!(RoomId::new("").is_err()); + } + + #[test] + fn serialize_valid_room_id() { + assert_eq!( + to_string( + &RoomId::try_from("!29fhd83h92h0:example.com").expect("Failed to create RoomId.") + ) + .expect("Failed to convert RoomId to JSON."), + r#""!29fhd83h92h0:example.com""# + ); + } + + #[test] + fn deserialize_valid_room_id() { + assert_eq!( + from_str::(r#""!29fhd83h92h0:example.com""#) + .expect("Failed to convert JSON to RoomId"), + RoomId::try_from("!29fhd83h92h0:example.com").expect("Failed to create RoomId.") + ); + } + + #[test] + fn valid_room_id_with_explicit_standard_port() { + assert_eq!( + RoomId::try_from("!29fhd83h92h0:example.com:443") + .expect("Failed to create RoomId.") + .to_string(), + "!29fhd83h92h0:example.com" + ); + } + + #[test] + fn valid_room_id_with_non_standard_port() { + assert_eq!( + RoomId::try_from("!29fhd83h92h0:example.com:5000") + .expect("Failed to create RoomId.") + .to_string(), + "!29fhd83h92h0:example.com:5000" + ); + } + + #[test] + fn missing_room_id_sigil() { + assert_eq!( + RoomId::try_from("carl:example.com").err().unwrap(), + Error::MissingSigil + ); + } + + #[test] + fn missing_room_id_delimiter() { + assert_eq!( + RoomId::try_from("!29fhd83h92h0").err().unwrap(), + Error::MissingDelimiter + ); + } + + #[test] + fn invalid_room_id_host() { + assert_eq!( + RoomId::try_from("!29fhd83h92h0:-").err().unwrap(), + Error::InvalidHost + ); + } + + #[test] + fn invalid_room_id_port() { + assert_eq!( + RoomId::try_from("!29fhd83h92h0:example.com:notaport") + .err() + .unwrap(), + Error::InvalidHost + ); + } +} diff --git a/src/room_id_or_room_alias_id.rs b/src/room_id_or_room_alias_id.rs new file mode 100644 index 00000000..8ddb85c0 --- /dev/null +++ b/src/room_id_or_room_alias_id.rs @@ -0,0 +1,219 @@ +//! Matrix identifiers for places where a room ID or room alias ID are used interchangeably. + +use std::{ + convert::TryFrom, + fmt::{Display, Formatter, Result as FmtResult}, +}; + +#[cfg(feature = "diesel")] +use diesel::sql_types::Text; +use serde::{ + de::{Error as SerdeError, Unexpected, Visitor}, + Deserialize, Deserializer, Serialize, Serializer, +}; + +use crate::{display, error::Error, room_alias_id::RoomAliasId, room_id::RoomId, validate_id}; + +/// A Matrix room ID or a Matrix room alias ID. +/// +/// `RoomIdOrAliasId` is useful for APIs that accept either kind of room identifier. It is converted +/// from a string slice, and can be converted back into a string as needed. When converted from a +/// string slice, the variant is determined by the leading sigil character. +/// +/// ``` +/// # use std::convert::TryFrom; +/// # use ruma_identifiers::RoomIdOrAliasId; +/// assert_eq!( +/// RoomIdOrAliasId::try_from("#ruma:example.com").unwrap().to_string(), +/// "#ruma:example.com" +/// ); +/// +/// assert_eq!( +/// RoomIdOrAliasId::try_from("!n8f893n9:example.com").unwrap().to_string(), +/// "!n8f893n9:example.com" +/// ); +/// ``` +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +#[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] +#[cfg_attr(feature = "diesel", sql_type = "Text")] +pub enum RoomIdOrAliasId { + /// A Matrix room alias ID. + RoomAliasId(RoomAliasId), + /// A Matrix room ID. + RoomId(RoomId), +} + +/// A serde visitor for `RoomIdOrAliasId`. +struct RoomIdOrAliasIdVisitor; + +impl Display for RoomIdOrAliasId { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + match *self { + RoomIdOrAliasId::RoomAliasId(ref room_alias_id) => display( + f, + '#', + room_alias_id.alias(), + room_alias_id.hostname(), + room_alias_id.port(), + ), + RoomIdOrAliasId::RoomId(ref room_id) => display( + f, + '!', + room_id.opaque_id(), + room_id.hostname(), + room_id.port(), + ), + } + } +} + +impl Serialize for RoomIdOrAliasId { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match *self { + RoomIdOrAliasId::RoomAliasId(ref room_alias_id) => { + serializer.serialize_str(&room_alias_id.to_string()) + } + RoomIdOrAliasId::RoomId(ref room_id) => serializer.serialize_str(&room_id.to_string()), + } + } +} + +impl<'de> Deserialize<'de> for RoomIdOrAliasId { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_any(RoomIdOrAliasIdVisitor) + } +} + +impl<'a> TryFrom<&'a str> for RoomIdOrAliasId { + type Error = Error; + + /// Attempts to create a new Matrix room ID or a room alias ID from a string representation. + /// + /// The string must either + /// include the leading ! sigil, the opaque ID, a literal colon, and a valid server name or + /// include the leading # sigil, the alias, a literal colon, and a valid server name. + fn try_from(room_id_or_alias_id: &'a str) -> Result { + validate_id(room_id_or_alias_id)?; + + let mut chars = room_id_or_alias_id.chars(); + + let sigil = chars.nth(0).expect("ID missing first character."); + + match sigil { + '#' => { + let room_alias_id = RoomAliasId::try_from(room_id_or_alias_id)?; + Ok(RoomIdOrAliasId::RoomAliasId(room_alias_id)) + } + '!' => { + let room_id = RoomId::try_from(room_id_or_alias_id)?; + Ok(RoomIdOrAliasId::RoomId(room_id)) + } + _ => Err(Error::MissingSigil), + } + } +} + +impl<'de> Visitor<'de> for RoomIdOrAliasIdVisitor { + type Value = RoomIdOrAliasId; + + fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { + write!(formatter, "a Matrix room ID or room alias ID as a string") + } + + fn visit_str(self, v: &str) -> Result + where + E: SerdeError, + { + match RoomIdOrAliasId::try_from(v) { + Ok(room_id_or_alias_id) => Ok(room_id_or_alias_id), + Err(_) => Err(SerdeError::invalid_value(Unexpected::Str(v), &self)), + } + } +} + +#[cfg(test)] +mod tests { + use std::convert::TryFrom; + + use serde_json::{from_str, to_string}; + + use super::RoomIdOrAliasId; + use crate::error::Error; + + #[test] + fn valid_room_id_or_alias_id_with_a_room_alias_id() { + assert_eq!( + RoomIdOrAliasId::try_from("#ruma:example.com") + .expect("Failed to create RoomAliasId.") + .to_string(), + "#ruma:example.com" + ); + } + + #[test] + fn valid_room_id_or_alias_id_with_a_room_id() { + assert_eq!( + RoomIdOrAliasId::try_from("!29fhd83h92h0:example.com") + .expect("Failed to create RoomId.") + .to_string(), + "!29fhd83h92h0:example.com" + ); + } + + #[test] + fn missing_sigil_for_room_id_or_alias_id() { + assert_eq!( + RoomIdOrAliasId::try_from("ruma:example.com").err().unwrap(), + Error::MissingSigil + ); + } + + #[test] + fn serialize_valid_room_id_or_alias_id_with_a_room_alias_id() { + assert_eq!( + to_string( + &RoomIdOrAliasId::try_from("#ruma:example.com") + .expect("Failed to create RoomAliasId.") + ) + .expect("Failed to convert RoomAliasId to JSON."), + r##""#ruma:example.com""## + ); + } + + #[test] + fn serialize_valid_room_id_or_alias_id_with_a_room_id() { + assert_eq!( + to_string( + &RoomIdOrAliasId::try_from("!29fhd83h92h0:example.com") + .expect("Failed to create RoomId.") + ) + .expect("Failed to convert RoomId to JSON."), + r#""!29fhd83h92h0:example.com""# + ); + } + + #[test] + fn deserialize_valid_room_id_or_alias_id_with_a_room_alias_id() { + assert_eq!( + from_str::(r##""#ruma:example.com""##) + .expect("Failed to convert JSON to RoomAliasId"), + RoomIdOrAliasId::try_from("#ruma:example.com").expect("Failed to create RoomAliasId.") + ); + } + + #[test] + fn deserialize_valid_room_id_or_alias_id_with_a_room_id() { + assert_eq!( + from_str::(r##""!29fhd83h92h0:example.com""##) + .expect("Failed to convert JSON to RoomId"), + RoomIdOrAliasId::try_from("!29fhd83h92h0:example.com") + .expect("Failed to create RoomAliasId.") + ); + } +} diff --git a/src/user_id.rs b/src/user_id.rs new file mode 100644 index 00000000..81b1ef15 --- /dev/null +++ b/src/user_id.rs @@ -0,0 +1,275 @@ +//! Matrix user identifiers. + +use std::{ + convert::TryFrom, + fmt::{Display, Formatter, Result as FmtResult}, +}; + +#[cfg(feature = "diesel")] +use diesel::sql_types::Text; +use lazy_static::lazy_static; +use regex::Regex; +use serde::{ + de::{Error as SerdeError, Unexpected, Visitor}, + Deserialize, Deserializer, Serialize, Serializer, +}; +use url::Host; + +use crate::{display, error::Error, generate_localpart, parse_id}; + +lazy_static! { + static ref USER_LOCALPART_PATTERN: Regex = + Regex::new(r"\A[a-z0-9._=-]+\z").expect("Failed to create user localpart regex."); +} + +/// A Matrix user ID. +/// +/// A `UserId` is generated randomly or converted from a string slice, and can be converted back +/// into a string as needed. +/// +/// ``` +/// # use std::convert::TryFrom; +/// # use ruma_identifiers::UserId; +/// assert_eq!( +/// UserId::try_from("@carl:example.com").unwrap().to_string(), +/// "@carl:example.com" +/// ); +/// ``` +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +#[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] +#[cfg_attr(feature = "diesel", sql_type = "Text")] +pub struct UserId { + /// The hostname of the homeserver. + hostname: Host, + /// The user's unique ID. + localpart: String, + /// The network port of the homeserver. + port: u16, +} + +/// A serde visitor for `UserId`. +struct UserIdVisitor; + +impl UserId { + /// Attempts to generate a `UserId` for the given origin server with a localpart consisting of + /// 12 random ASCII characters. + /// + /// Fails if the given origin server name cannot be parsed as a valid host. + pub fn new(server_name: &str) -> Result { + let user_id = format!("@{}:{}", generate_localpart(12).to_lowercase(), server_name); + let (localpart, host, port) = parse_id('@', &user_id)?; + + Ok(Self { + hostname: host, + localpart: localpart.to_string(), + port, + }) + } + + /// Returns a `Host` for the user ID, containing the server name (minus the port) of the + /// originating homeserver. + /// + /// The host can be either a domain name, an IPv4 address, or an IPv6 address. + pub fn hostname(&self) -> &Host { + &self.hostname + } + + /// Returns the user's localpart. + pub fn localpart(&self) -> &str { + &self.localpart + } + + /// Returns the port the originating homeserver can be accessed on. + pub fn port(&self) -> u16 { + self.port + } +} + +impl Display for UserId { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + display(f, '@', &self.localpart, &self.hostname, self.port) + } +} + +impl Serialize for UserId { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for UserId { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_any(UserIdVisitor) + } +} + +impl<'a> TryFrom<&'a str> for UserId { + type Error = Error; + + /// Attempts to create a new Matrix user ID from a string representation. + /// + /// The string must include the leading @ sigil, the localpart, a literal colon, and a valid + /// server name. + fn try_from(user_id: &'a str) -> Result { + let (localpart, host, port) = parse_id('@', user_id)?; + let downcased_localpart = localpart.to_lowercase(); + + if !USER_LOCALPART_PATTERN.is_match(&downcased_localpart) { + return Err(Error::InvalidCharacters); + } + + Ok(Self { + hostname: host, + port, + localpart: downcased_localpart.to_owned(), + }) + } +} + +impl<'de> Visitor<'de> for UserIdVisitor { + type Value = UserId; + + fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { + write!(formatter, "a Matrix user ID as a string") + } + + fn visit_str(self, v: &str) -> Result + where + E: SerdeError, + { + match UserId::try_from(v) { + Ok(user_id) => Ok(user_id), + Err(_) => Err(SerdeError::invalid_value(Unexpected::Str(v), &self)), + } + } +} + +#[cfg(test)] +mod tests { + use std::convert::TryFrom; + + use serde_json::{from_str, to_string}; + + use super::UserId; + use crate::error::Error; + + #[test] + fn valid_user_id() { + assert_eq!( + UserId::try_from("@carl:example.com") + .expect("Failed to create UserId.") + .to_string(), + "@carl:example.com" + ); + } + + #[test] + fn downcase_user_id() { + assert_eq!( + UserId::try_from("@CARL:example.com") + .expect("Failed to create UserId.") + .to_string(), + "@carl:example.com" + ); + } + + #[test] + fn generate_random_valid_user_id() { + let user_id = UserId::new("example.com") + .expect("Failed to generate UserId.") + .to_string(); + + assert!(user_id.to_string().starts_with('@')); + assert_eq!(user_id.len(), 25); + } + + #[test] + fn generate_random_invalid_user_id() { + assert!(UserId::new("").is_err()); + } + + #[test] + fn serialize_valid_user_id() { + assert_eq!( + to_string(&UserId::try_from("@carl:example.com").expect("Failed to create UserId.")) + .expect("Failed to convert UserId to JSON."), + r#""@carl:example.com""# + ); + } + + #[test] + fn deserialize_valid_user_id() { + assert_eq!( + from_str::(r#""@carl:example.com""#).expect("Failed to convert JSON to UserId"), + UserId::try_from("@carl:example.com").expect("Failed to create UserId.") + ); + } + + #[test] + fn valid_user_id_with_explicit_standard_port() { + assert_eq!( + UserId::try_from("@carl:example.com:443") + .expect("Failed to create UserId.") + .to_string(), + "@carl:example.com" + ); + } + + #[test] + fn valid_user_id_with_non_standard_port() { + assert_eq!( + UserId::try_from("@carl:example.com:5000") + .expect("Failed to create UserId.") + .to_string(), + "@carl:example.com:5000" + ); + } + + #[test] + fn invalid_characters_in_user_id_localpart() { + assert_eq!( + UserId::try_from("@%%%:example.com").err().unwrap(), + Error::InvalidCharacters + ); + } + + #[test] + fn missing_user_id_sigil() { + assert_eq!( + UserId::try_from("carl:example.com").err().unwrap(), + Error::MissingSigil + ); + } + + #[test] + fn missing_user_id_delimiter() { + assert_eq!( + UserId::try_from("@carl").err().unwrap(), + Error::MissingDelimiter + ); + } + + #[test] + fn invalid_user_id_host() { + assert_eq!( + UserId::try_from("@carl:-").err().unwrap(), + Error::InvalidHost + ); + } + + #[test] + fn invalid_user_id_port() { + assert_eq!( + UserId::try_from("@carl:example.com:notaport") + .err() + .unwrap(), + Error::InvalidHost + ); + } +} From 6d9b3c7bf2a8d26bde4724b5531137abdea71385 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Mon, 3 Jun 2019 10:21:57 -0700 Subject: [PATCH 069/140] Add `RoomVersionId`. --- src/diesel_integration.rs | 1 + src/error.rs | 4 +- src/lib.rs | 7 +- src/room_version_id.rs | 351 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 358 insertions(+), 5 deletions(-) create mode 100644 src/room_version_id.rs diff --git a/src/diesel_integration.rs b/src/diesel_integration.rs index 8ae777e6..65e5ae63 100644 --- a/src/diesel_integration.rs +++ b/src/diesel_integration.rs @@ -38,4 +38,5 @@ diesel_impl!(EventId); diesel_impl!(RoomAliasId); diesel_impl!(RoomId); diesel_impl!(RoomIdOrAliasId); +diesel_impl!(RoomVersionId); diesel_impl!(UserId); diff --git a/src/error.rs b/src/error.rs index a23ece09..397bd7be 100644 --- a/src/error.rs +++ b/src/error.rs @@ -16,9 +16,9 @@ pub enum Error { InvalidCharacters, /// The domain part of the the ID string is not a valid IP address or DNS name. InvalidHost, - /// The ID exceeds 255 bytes. + /// The ID exceeds 255 bytes (or 32 codepoints for a room version ID.) MaximumLengthExceeded, - /// The ID is less than 4 characters. + /// The ID is less than 4 characters (or is an empty room version ID.) MinimumLengthNotSatisfied, /// The ID is missing the colon delimiter between localpart and server name. MissingDelimiter, diff --git a/src/lib.rs b/src/lib.rs index 4672f296..a02bd9b8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,5 @@ //! Crate **ruma_identifiers** contains types for [Matrix](https://matrix.org/) identifiers -//! for events, rooms, room aliases, and users. +//! for events, rooms, room aliases, room versions, and users. #![deny( missing_copy_implementations, @@ -40,7 +40,7 @@ pub use url::Host; pub use crate::{ error::Error, event_id::EventId, room_alias_id::RoomAliasId, room_id::RoomId, - room_id_or_room_alias_id::RoomIdOrAliasId, user_id::UserId, + room_id_or_room_alias_id::RoomIdOrAliasId, room_version_id::RoomVersionId, user_id::UserId, }; #[cfg(feature = "diesel")] @@ -50,9 +50,10 @@ mod event_id; mod room_alias_id; mod room_id; mod room_id_or_room_alias_id; +mod room_version_id; mod user_id; -/// All events must be 255 bytes or less. +/// All identifiers must be 255 bytes or less. const MAX_BYTES: usize = 255; /// The minimum number of characters an ID can be. /// diff --git a/src/room_version_id.rs b/src/room_version_id.rs new file mode 100644 index 00000000..caa374e0 --- /dev/null +++ b/src/room_version_id.rs @@ -0,0 +1,351 @@ +//! Matrix room version identifiers. + +use std::{ + convert::TryFrom, + fmt::{Display, Formatter, Result as FmtResult}, +}; + +#[cfg(feature = "diesel")] +use diesel::sql_types::Text; +use serde::{ + de::{Error as SerdeError, Unexpected, Visitor}, + Deserialize, Deserializer, Serialize, Serializer, +}; + +use crate::error::Error; + +/// Room version identifiers cannot be more than 32 code points. +const MAX_CODE_POINTS: usize = 32; + +/// A Matrix room version ID. +/// +/// A `RoomVersionId` can be or converted or deserialized from a string slice, and can be converted +/// or serialized back into a string as needed. +/// +/// ``` +/// # use std::convert::TryFrom; +/// # use ruma_identifiers::RoomVersionId; +/// assert_eq!(RoomVersionId::try_from("1").unwrap().to_string(), "1"); +/// ``` +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +#[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] +#[cfg_attr(feature = "diesel", sql_type = "Text")] +pub struct RoomVersionId(InnerRoomVersionId); + +/// Possibile values for room version, distinguishing between official Matrix versions and custom +/// versions. +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +enum InnerRoomVersionId { + /// A version 1 room. + Version1, + /// A version 2 room. + Version2, + /// A version 3 room. + Version3, + /// A version 4 room. + Version4, + /// A custom room version. + Custom(String), +} + +/// A serde visitor for `RoomVersionId`. +struct RoomVersionIdVisitor; + +impl RoomVersionId { + /// Creates a version 1 room ID. + pub fn version_1() -> Self { + Self(InnerRoomVersionId::Version1) + } + + /// Creates a version 2 room ID. + pub fn version_2() -> Self { + Self(InnerRoomVersionId::Version2) + } + + /// Creates a version 3 room ID. + pub fn version_3() -> Self { + Self(InnerRoomVersionId::Version3) + } + + /// Creates a version 4 room ID. + pub fn version_4() -> Self { + Self(InnerRoomVersionId::Version4) + } + + /// Creates a custom room version ID from the given string slice. + pub fn custom(id: &str) -> Self { + Self(InnerRoomVersionId::Custom(id.to_string())) + } + + /// Whether or not this room version is an official one specified by the Matrix protocol. + pub fn is_official(&self) -> bool { + !self.is_custom() + } + + /// Whether or not this is a custom room version. + pub fn is_custom(&self) -> bool { + match self.0 { + InnerRoomVersionId::Custom(_) => true, + _ => false, + } + } + + /// Whether or not this is a version 1 room. + pub fn is_version_1(&self) -> bool { + self.0 == InnerRoomVersionId::Version1 + } + + /// Whether or not this is a version 2 room. + pub fn is_version_2(&self) -> bool { + self.0 == InnerRoomVersionId::Version2 + } + + /// Whether or not this is a version 3 room. + pub fn is_version_3(&self) -> bool { + self.0 == InnerRoomVersionId::Version3 + } + + /// Whether or not this is a version 4 room. + pub fn is_version_4(&self) -> bool { + self.0 == InnerRoomVersionId::Version4 + } +} + +impl Display for RoomVersionId { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + let message = match self.0 { + InnerRoomVersionId::Version1 => "1", + InnerRoomVersionId::Version2 => "2", + InnerRoomVersionId::Version3 => "3", + InnerRoomVersionId::Version4 => "4", + InnerRoomVersionId::Custom(ref version) => version, + }; + + write!(f, "{}", message) + } +} + +impl Serialize for RoomVersionId { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl<'de> Deserialize<'de> for RoomVersionId { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_any(RoomVersionIdVisitor) + } +} + +impl<'a> TryFrom<&'a str> for RoomVersionId { + type Error = Error; + + /// Attempts to create a new Matrix room version ID from a string representation. + fn try_from(room_version_id: &'a str) -> Result { + let version = match room_version_id { + "1" => Self(InnerRoomVersionId::Version1), + "2" => Self(InnerRoomVersionId::Version2), + "3" => Self(InnerRoomVersionId::Version3), + "4" => Self(InnerRoomVersionId::Version4), + custom => { + if custom.is_empty() { + return Err(Error::MinimumLengthNotSatisfied); + } else if custom.chars().count() > MAX_CODE_POINTS { + return Err(Error::MaximumLengthExceeded); + } else { + Self(InnerRoomVersionId::Custom(custom.to_string())) + } + } + }; + + Ok(version) + } +} + +impl<'de> Visitor<'de> for RoomVersionIdVisitor { + type Value = RoomVersionId; + + fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { + write!(formatter, "a Matrix room version ID as a string") + } + + fn visit_str(self, v: &str) -> Result + where + E: SerdeError, + { + match RoomVersionId::try_from(v) { + Ok(room_id) => Ok(room_id), + Err(_) => Err(SerdeError::invalid_value(Unexpected::Str(v), &self)), + } + } +} + +#[cfg(test)] +mod tests { + use std::convert::TryFrom; + + use serde_json::{from_str, to_string}; + + use super::RoomVersionId; + use crate::error::Error; + + #[test] + fn valid_version_1_room_version_id() { + assert_eq!( + RoomVersionId::try_from("1") + .expect("Failed to create RoomVersionId.") + .to_string(), + "1" + ); + } + #[test] + fn valid_version_2_room_version_id() { + assert_eq!( + RoomVersionId::try_from("2") + .expect("Failed to create RoomVersionId.") + .to_string(), + "2" + ); + } + #[test] + fn valid_version_3_room_version_id() { + assert_eq!( + RoomVersionId::try_from("3") + .expect("Failed to create RoomVersionId.") + .to_string(), + "3" + ); + } + #[test] + fn valid_version_4_room_version_id() { + assert_eq!( + RoomVersionId::try_from("4") + .expect("Failed to create RoomVersionId.") + .to_string(), + "4" + ); + } + + #[test] + fn valid_custom_room_version_id() { + assert_eq!( + RoomVersionId::try_from("io.ruma.1") + .expect("Failed to create RoomVersionId.") + .to_string(), + "io.ruma.1" + ); + } + + #[test] + fn empty_room_version_id() { + assert_eq!( + RoomVersionId::try_from(""), + Err(Error::MinimumLengthNotSatisfied) + ); + } + + #[test] + fn over_max_code_point_room_version_id() { + assert_eq!( + RoomVersionId::try_from("0123456789012345678901234567890123456789"), + Err(Error::MaximumLengthExceeded) + ); + } + + #[test] + fn serialize_official_room_id() { + assert_eq!( + to_string(&RoomVersionId::try_from("1").expect("Failed to create RoomVersionId.")) + .expect("Failed to convert RoomVersionId to JSON."), + r#""1""# + ); + } + + #[test] + fn deserialize_official_room_id() { + let deserialized = + from_str::(r#""1""#).expect("Failed to convert RoomVersionId to JSON."); + + assert!(deserialized.is_version_1()); + assert!(deserialized.is_official()); + + assert_eq!( + deserialized, + RoomVersionId::try_from("1").expect("Failed to create RoomVersionId.") + ); + } + + #[test] + fn serialize_custom_room_id() { + assert_eq!( + to_string( + &RoomVersionId::try_from("io.ruma.1").expect("Failed to create RoomVersionId.") + ) + .expect("Failed to convert RoomVersionId to JSON."), + r#""io.ruma.1""# + ); + } + + #[test] + fn deserialize_custom_room_id() { + let deserialized = from_str::(r#""io.ruma.1""#) + .expect("Failed to convert RoomVersionId to JSON."); + + assert!(deserialized.is_custom()); + + assert_eq!( + deserialized, + RoomVersionId::try_from("io.ruma.1").expect("Failed to create RoomVersionId.") + ); + } + + #[test] + fn constructors() { + assert!(RoomVersionId::version_1().is_version_1()); + assert!(RoomVersionId::version_2().is_version_2()); + assert!(RoomVersionId::version_3().is_version_3()); + assert!(RoomVersionId::version_4().is_version_4()); + assert!(RoomVersionId::custom("foo").is_custom()); + } + + #[test] + fn predicate_methods() { + let version_1 = RoomVersionId::try_from("1").expect("Failed to create RoomVersionId."); + let version_2 = RoomVersionId::try_from("2").expect("Failed to create RoomVersionId."); + let version_3 = RoomVersionId::try_from("3").expect("Failed to create RoomVersionId."); + let version_4 = RoomVersionId::try_from("4").expect("Failed to create RoomVersionId."); + let custom = RoomVersionId::try_from("io.ruma.1").expect("Failed to create RoomVersionId."); + + assert!(version_1.is_version_1()); + assert!(version_2.is_version_2()); + assert!(version_3.is_version_3()); + assert!(version_4.is_version_4()); + + assert!(!version_1.is_version_2()); + assert!(!version_1.is_version_3()); + assert!(!version_1.is_version_4()); + + assert!(version_1.is_official()); + assert!(version_2.is_official()); + assert!(version_3.is_official()); + assert!(version_4.is_official()); + + assert!(!version_1.is_custom()); + assert!(!version_2.is_custom()); + assert!(!version_3.is_custom()); + assert!(!version_4.is_custom()); + + assert!(custom.is_custom()); + assert!(!custom.is_official()); + assert!(!custom.is_version_1()); + assert!(!custom.is_version_2()); + assert!(!custom.is_version_3()); + assert!(!custom.is_version_4()); + } +} From fb8039ac6d9f206d8bbd5c409dbd79bd40064e4a Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Mon, 3 Jun 2019 11:52:15 -0700 Subject: [PATCH 070/140] Support event ID formats for all room versions. --- src/event_id.rs | 217 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 181 insertions(+), 36 deletions(-) diff --git a/src/event_id.rs b/src/event_id.rs index 2729871f..551d502c 100644 --- a/src/event_id.rs +++ b/src/event_id.rs @@ -20,24 +20,59 @@ use crate::{display, error::Error, generate_localpart, parse_id}; /// An `EventId` is generated randomly or converted from a string slice, and can be converted back /// into a string as needed. /// +/// # Room versions +/// +/// Matrix specifies multiple [room versions](https://matrix.org/docs/spec/#room-versions) and the +/// format of event identifiers differ between them. The original format used by room versions 1 +/// and 2 uses a short pseudorandom "localpart" followed by the hostname and port of the +/// originating homeserver. Later room versions change event identifiers to be a hash of the event +/// encoded with Base64. Some of the methods provided by `EventId` are only relevant to the +/// original event format. +/// /// ``` /// # use std::convert::TryFrom; /// # use ruma_identifiers::EventId; +/// // Original format /// assert_eq!( /// EventId::try_from("$h29iv0s8:example.com").unwrap().to_string(), /// "$h29iv0s8:example.com" /// ); +/// // Room version 3 format +/// assert_eq!( +/// EventId::try_from("$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk").unwrap().to_string(), +/// "$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk" +/// ); +/// // Room version 4 format +/// assert_eq!( +/// EventId::try_from("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg").unwrap().to_string(), +/// "$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg" +/// ); /// ``` #[derive(Clone, Debug, Eq, Hash, PartialEq)] #[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] #[cfg_attr(feature = "diesel", sql_type = "Text")] -pub struct EventId { +pub struct EventId(Format); + +/// Different event ID formats from the different Matrix room versions. +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +enum Format { + /// The original format as used by Matrix room versions 1 and 2. + Original(Original), + /// The format used by Matrix room version 3. + Base64(String), + /// The format used by Matrix room version 4. + UrlSafeBase64(String), +} + +/// An event in the original format as used by Matrix room versions 1 and 2. +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +struct Original { /// The hostname of the homeserver. - hostname: Host, + pub hostname: Host, /// The event's unique ID. - opaque_id: String, + pub localpart: String, /// The network port of the homeserver. - port: u16, + pub port: u16, } /// A serde visitor for `EventId`. @@ -45,42 +80,67 @@ struct EventIdVisitor; impl EventId { /// Attempts to generate an `EventId` for the given origin server with a localpart consisting - /// of 18 random ASCII characters. + /// of 18 random ASCII characters. This should only be used for events in the original format + /// as used by Matrix room versions 1 and 2. /// /// Fails if the given origin server name cannot be parsed as a valid host. pub fn new(server_name: &str) -> Result { let event_id = format!("${}:{}", generate_localpart(18), server_name); - let (opaque_id, host, port) = parse_id('$', &event_id)?; + let (localpart, host, port) = parse_id('$', &event_id)?; - Ok(Self { + Ok(Self(Format::Original(Original { hostname: host, - opaque_id: opaque_id.to_string(), + localpart: localpart.to_string(), port, - }) + }))) } /// Returns a `Host` for the event ID, containing the server name (minus the port) of the - /// originating homeserver. + /// originating homeserver. Only applicable to events in the original format as used by Matrix + /// room versions 1 and 2. /// /// The host can be either a domain name, an IPv4 address, or an IPv6 address. - pub fn hostname(&self) -> &Host { - &self.hostname + pub fn hostname(&self) -> Option<&Host> { + if let Format::Original(original) = &self.0 { + Some(&original.hostname) + } else { + None + } } - /// Returns the event's opaque ID. - pub fn opaque_id(&self) -> &str { - &self.opaque_id + /// Returns the event's unique ID. For the original event format as used by Matrix room + /// versions 1 and 2, this is the "localpart" that precedes the homeserver. For later formats, + /// this is the entire ID without the leading $ sigil. + pub fn localpart(&self) -> &str { + match &self.0 { + Format::Original(original) => &original.localpart, + Format::Base64(id) | Format::UrlSafeBase64(id) => id, + } } - /// Returns the port the originating homeserver can be accessed on. - pub fn port(&self) -> u16 { - self.port + /// Returns the port the originating homeserver can be accessed on. Only applicable to events + /// in the original format as used by Matrix room versions 1 and 2. + pub fn port(&self) -> Option { + if let Format::Original(original) = &self.0 { + Some(original.port) + } else { + None + } } } impl Display for EventId { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - display(f, '$', &self.opaque_id, &self.hostname, self.port) + match &self.0 { + Format::Original(original) => display( + f, + '$', + &original.localpart, + &original.hostname, + original.port, + ), + Format::Base64(id) | Format::UrlSafeBase64(id) => write!(f, "${}", id), + } } } @@ -107,16 +167,25 @@ impl<'a> TryFrom<&'a str> for EventId { /// Attempts to create a new Matrix event ID from a string representation. /// - /// The string must include the leading $ sigil, the opaque ID, a literal colon, and a valid - /// server name. + /// If using the original event format as used by Matrix room versions 1 and 2, the string must + /// include the leading $ sigil, the localpart, a literal colon, and a valid homeserver + /// hostname. fn try_from(event_id: &'a str) -> Result { - let (opaque_id, host, port) = parse_id('$', event_id)?; + if event_id.contains(':') { + let (localpart, host, port) = parse_id('$', event_id)?; - Ok(Self { - hostname: host, - opaque_id: opaque_id.to_owned(), - port, - }) + Ok(Self(Format::Original(Original { + hostname: host, + localpart: localpart.to_owned(), + port, + }))) + } else if !event_id.starts_with('$') { + Err(Error::MissingSigil) + } else if event_id.contains(|chr| chr == '+' || chr == '/') { + Ok(Self(Format::Base64(event_id[1..].to_string()))) + } else { + Ok(Self(Format::UrlSafeBase64(event_id[1..].to_string()))) + } } } @@ -148,7 +217,7 @@ mod tests { use crate::error::Error; #[test] - fn valid_event_id() { + fn valid_original_event_id() { assert_eq!( EventId::try_from("$39hvsi03hlne:example.com") .expect("Failed to create EventId.") @@ -157,6 +226,26 @@ mod tests { ); } + #[test] + fn valid_base64_event_id() { + assert_eq!( + EventId::try_from("$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk") + .expect("Failed to create EventId.") + .to_string(), + "$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk" + ) + } + + #[test] + fn valid_url_safe_base64_event_id() { + assert_eq!( + EventId::try_from("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg") + .expect("Failed to create EventId.") + .to_string(), + "$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg" + ) + } + #[test] fn generate_random_valid_event_id() { let event_id = EventId::new("example.com") @@ -173,7 +262,7 @@ mod tests { } #[test] - fn serialize_valid_event_id() { + fn serialize_valid_original_event_id() { assert_eq!( to_string( &EventId::try_from("$39hvsi03hlne:example.com").expect("Failed to create EventId.") @@ -184,7 +273,31 @@ mod tests { } #[test] - fn deserialize_valid_event_id() { + fn serialize_valid_base64_event_id() { + assert_eq!( + to_string( + &EventId::try_from("$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk") + .expect("Failed to create EventId.") + ) + .expect("Failed to convert EventId to JSON."), + r#""$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk""# + ); + } + + #[test] + fn serialize_valid_url_safe_base64_event_id() { + assert_eq!( + to_string( + &EventId::try_from("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg") + .expect("Failed to create EventId.") + ) + .expect("Failed to convert EventId to JSON."), + r#""$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg""# + ); + } + + #[test] + fn deserialize_valid_original_event_id() { assert_eq!( from_str::(r#""$39hvsi03hlne:example.com""#) .expect("Failed to convert JSON to EventId"), @@ -193,7 +306,27 @@ mod tests { } #[test] - fn valid_event_id_with_explicit_standard_port() { + fn deserialize_valid_base64_event_id() { + assert_eq!( + from_str::(r#""$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk""#) + .expect("Failed to convert JSON to EventId"), + EventId::try_from("$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk") + .expect("Failed to create EventId.") + ); + } + + #[test] + fn deserialize_valid_url_safe_base64_event_id() { + assert_eq!( + from_str::(r#""$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg""#) + .expect("Failed to convert JSON to EventId"), + EventId::try_from("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg") + .expect("Failed to create EventId.") + ); + } + + #[test] + fn valid_original_event_id_with_explicit_standard_port() { assert_eq!( EventId::try_from("$39hvsi03hlne:example.com:443") .expect("Failed to create EventId.") @@ -203,7 +336,7 @@ mod tests { } #[test] - fn valid_event_id_with_non_standard_port() { + fn valid_original_event_id_with_non_standard_port() { assert_eq!( EventId::try_from("$39hvsi03hlne:example.com:5000") .expect("Failed to create EventId.") @@ -213,7 +346,7 @@ mod tests { } #[test] - fn missing_event_id_sigil() { + fn missing_original_event_id_sigil() { assert_eq!( EventId::try_from("39hvsi03hlne:example.com").err().unwrap(), Error::MissingSigil @@ -221,10 +354,22 @@ mod tests { } #[test] - fn missing_event_id_delimiter() { + fn missing_base64_event_id_sigil() { assert_eq!( - EventId::try_from("$39hvsi03hlne").err().unwrap(), - Error::MissingDelimiter + EventId::try_from("acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk") + .err() + .unwrap(), + Error::MissingSigil + ); + } + + #[test] + fn missing_url_safe_base64_event_id_sigil() { + assert_eq!( + EventId::try_from("Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg") + .err() + .unwrap(), + Error::MissingSigil ); } From 9011423eeaf276be6d913ea6ecde90fb65cd8794 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Mon, 3 Jun 2019 12:11:57 -0700 Subject: [PATCH 071/140] Consistently use localpart instead of opaque_id and use homeserver_host instead of server_name. --- src/event_id.rs | 6 +++--- src/room_id.rs | 26 +++++++++++++------------- src/room_id_or_room_alias_id.rs | 8 ++++---- src/user_id.rs | 10 +++++++--- 4 files changed, 27 insertions(+), 23 deletions(-) diff --git a/src/event_id.rs b/src/event_id.rs index 551d502c..606e68c2 100644 --- a/src/event_id.rs +++ b/src/event_id.rs @@ -83,9 +83,9 @@ impl EventId { /// of 18 random ASCII characters. This should only be used for events in the original format /// as used by Matrix room versions 1 and 2. /// - /// Fails if the given origin server name cannot be parsed as a valid host. - pub fn new(server_name: &str) -> Result { - let event_id = format!("${}:{}", generate_localpart(18), server_name); + /// Fails if the homeserver cannot be parsed as a valid host. + pub fn new(homeserver_host: &str) -> Result { + let event_id = format!("${}:{}", generate_localpart(18), homeserver_host); let (localpart, host, port) = parse_id('$', &event_id)?; Ok(Self(Format::Original(Original { diff --git a/src/room_id.rs b/src/room_id.rs index 6c66c883..8aec0959 100644 --- a/src/room_id.rs +++ b/src/room_id.rs @@ -35,7 +35,7 @@ pub struct RoomId { /// The hostname of the homeserver. hostname: Host, /// The room's unique ID. - opaque_id: String, + localpart: String, /// The network port of the homeserver. port: u16, } @@ -47,14 +47,14 @@ impl RoomId { /// Attempts to generate a `RoomId` for the given origin server with a localpart consisting of /// 18 random ASCII characters. /// - /// Fails if the given origin server name cannot be parsed as a valid host. - pub fn new(server_name: &str) -> Result { - let room_id = format!("!{}:{}", generate_localpart(18), server_name); - let (opaque_id, host, port) = parse_id('!', &room_id)?; + /// Fails if the given homeserver cannot be parsed as a valid host. + pub fn new(homeserver_host: &str) -> Result { + let room_id = format!("!{}:{}", generate_localpart(18), homeserver_host); + let (localpart, host, port) = parse_id('!', &room_id)?; Ok(Self { hostname: host, - opaque_id: opaque_id.to_string(), + localpart: localpart.to_string(), port, }) } @@ -67,9 +67,9 @@ impl RoomId { &self.hostname } - /// Returns the event's opaque ID. - pub fn opaque_id(&self) -> &str { - &self.opaque_id + /// Returns the rooms's unique ID. + pub fn localpart(&self) -> &str { + &self.localpart } /// Returns the port the originating homeserver can be accessed on. @@ -80,7 +80,7 @@ impl RoomId { impl Display for RoomId { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - display(f, '!', &self.opaque_id, &self.hostname, self.port) + display(f, '!', &self.localpart, &self.hostname, self.port) } } @@ -107,14 +107,14 @@ impl<'a> TryFrom<&'a str> for RoomId { /// Attempts to create a new Matrix room ID from a string representation. /// - /// The string must include the leading ! sigil, the opaque ID, a literal colon, and a valid + /// The string must include the leading ! sigil, the localpart, a literal colon, and a valid /// server name. fn try_from(room_id: &'a str) -> Result { - let (opaque_id, host, port) = parse_id('!', room_id)?; + let (localpart, host, port) = parse_id('!', room_id)?; Ok(Self { hostname: host, - opaque_id: opaque_id.to_owned(), + localpart: localpart.to_owned(), port, }) } diff --git a/src/room_id_or_room_alias_id.rs b/src/room_id_or_room_alias_id.rs index 8ddb85c0..3a2d7563 100644 --- a/src/room_id_or_room_alias_id.rs +++ b/src/room_id_or_room_alias_id.rs @@ -59,7 +59,7 @@ impl Display for RoomIdOrAliasId { RoomIdOrAliasId::RoomId(ref room_id) => display( f, '!', - room_id.opaque_id(), + room_id.localpart(), room_id.hostname(), room_id.port(), ), @@ -95,9 +95,9 @@ impl<'a> TryFrom<&'a str> for RoomIdOrAliasId { /// Attempts to create a new Matrix room ID or a room alias ID from a string representation. /// - /// The string must either - /// include the leading ! sigil, the opaque ID, a literal colon, and a valid server name or - /// include the leading # sigil, the alias, a literal colon, and a valid server name. + /// The string must either include the leading ! sigil, the localpart, a literal colon, and a + /// valid homeserver host or include the leading # sigil, the alias, a literal colon, and a + /// valid homeserver host. fn try_from(room_id_or_alias_id: &'a str) -> Result { validate_id(room_id_or_alias_id)?; diff --git a/src/user_id.rs b/src/user_id.rs index 81b1ef15..140341ce 100644 --- a/src/user_id.rs +++ b/src/user_id.rs @@ -54,9 +54,13 @@ impl UserId { /// Attempts to generate a `UserId` for the given origin server with a localpart consisting of /// 12 random ASCII characters. /// - /// Fails if the given origin server name cannot be parsed as a valid host. - pub fn new(server_name: &str) -> Result { - let user_id = format!("@{}:{}", generate_localpart(12).to_lowercase(), server_name); + /// Fails if the given homeserver cannot be parsed as a valid host. + pub fn new(homeserver_host: &str) -> Result { + let user_id = format!( + "@{}:{}", + generate_localpart(12).to_lowercase(), + homeserver_host + ); let (localpart, host, port) = parse_id('@', &user_id)?; Ok(Self { From b37bb534952176945d0aa915639ca1a9bbcbe68a Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Mon, 3 Jun 2019 12:13:11 -0700 Subject: [PATCH 072/140] Add `DeviceId`. --- src/device_id.rs | 24 ++++++++++++++++++++++++ src/lib.rs | 3 +++ 2 files changed, 27 insertions(+) create mode 100644 src/device_id.rs diff --git a/src/device_id.rs b/src/device_id.rs new file mode 100644 index 00000000..b0026e74 --- /dev/null +++ b/src/device_id.rs @@ -0,0 +1,24 @@ +//! Matrix device identifiers. + +use crate::generate_localpart; + +/// A Matrix device ID. +/// +/// Device identifiers in Matrix are completely opaque character sequences. This type alias is +/// provided simply for its semantic value. +pub type DeviceId = String; + +/// Generates a random `DeviceId`, suitable for assignment to a new device. +pub fn generate() -> DeviceId { + generate_localpart(8).to_string() +} + +#[cfg(test)] +mod tests { + use super::generate; + + #[test] + fn generate_device_id() { + assert_eq!(generate().len(), 8); + } +} diff --git a/src/lib.rs b/src/lib.rs index a02bd9b8..e5661731 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,11 +38,14 @@ use url::Url; pub use url::Host; +#[doc(inline)] +pub use crate::device_id::DeviceId; pub use crate::{ error::Error, event_id::EventId, room_alias_id::RoomAliasId, room_id::RoomId, room_id_or_room_alias_id::RoomIdOrAliasId, room_version_id::RoomVersionId, user_id::UserId, }; +pub mod device_id; #[cfg(feature = "diesel")] mod diesel_integration; mod error; From 6de32b1b9c3fdfafc40763bc105317aaa9bbf2c8 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Mon, 3 Jun 2019 12:14:34 -0700 Subject: [PATCH 073/140] Bump version to 0.13.0. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 26fcc6da..c2f0e8ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT" name = "ruma-identifiers" readme = "README.md" repository = "https://github.com/ruma/ruma-identifiers" -version = "0.12.1" +version = "0.13.0" edition = "2018" [dependencies] From 9506c565e0fb959804a85d23880ea61a5a67709c Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Sun, 16 Jun 2019 01:29:54 -0700 Subject: [PATCH 074/140] Add support for room version 5. --- src/room_version_id.rs | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/room_version_id.rs b/src/room_version_id.rs index caa374e0..3928c91f 100644 --- a/src/room_version_id.rs +++ b/src/room_version_id.rs @@ -38,12 +38,19 @@ pub struct RoomVersionId(InnerRoomVersionId); enum InnerRoomVersionId { /// A version 1 room. Version1, + /// A version 2 room. Version2, + /// A version 3 room. Version3, + /// A version 4 room. Version4, + + /// A version 5 room. + Version5, + /// A custom room version. Custom(String), } @@ -72,6 +79,11 @@ impl RoomVersionId { Self(InnerRoomVersionId::Version4) } + /// Creates a version 5 room ID. + pub fn version_5() -> Self { + Self(InnerRoomVersionId::Version5) + } + /// Creates a custom room version ID from the given string slice. pub fn custom(id: &str) -> Self { Self(InnerRoomVersionId::Custom(id.to_string())) @@ -109,6 +121,11 @@ impl RoomVersionId { pub fn is_version_4(&self) -> bool { self.0 == InnerRoomVersionId::Version4 } + + /// Whether or not this is a version 5 room. + pub fn is_version_5(&self) -> bool { + self.0 == InnerRoomVersionId::Version5 + } } impl Display for RoomVersionId { @@ -118,6 +135,7 @@ impl Display for RoomVersionId { InnerRoomVersionId::Version2 => "2", InnerRoomVersionId::Version3 => "3", InnerRoomVersionId::Version4 => "4", + InnerRoomVersionId::Version5 => "5", InnerRoomVersionId::Custom(ref version) => version, }; @@ -153,6 +171,7 @@ impl<'a> TryFrom<&'a str> for RoomVersionId { "2" => Self(InnerRoomVersionId::Version2), "3" => Self(InnerRoomVersionId::Version3), "4" => Self(InnerRoomVersionId::Version4), + "5" => Self(InnerRoomVersionId::Version5), custom => { if custom.is_empty() { return Err(Error::MinimumLengthNotSatisfied); @@ -204,6 +223,7 @@ mod tests { "1" ); } + #[test] fn valid_version_2_room_version_id() { assert_eq!( @@ -213,6 +233,7 @@ mod tests { "2" ); } + #[test] fn valid_version_3_room_version_id() { assert_eq!( @@ -222,6 +243,7 @@ mod tests { "3" ); } + #[test] fn valid_version_4_room_version_id() { assert_eq!( @@ -232,6 +254,16 @@ mod tests { ); } + #[test] + fn valid_version_5_room_version_id() { + assert_eq!( + RoomVersionId::try_from("5") + .expect("Failed to create RoomVersionId.") + .to_string(), + "5" + ); + } + #[test] fn valid_custom_room_version_id() { assert_eq!( @@ -311,35 +343,42 @@ mod tests { assert!(RoomVersionId::version_2().is_version_2()); assert!(RoomVersionId::version_3().is_version_3()); assert!(RoomVersionId::version_4().is_version_4()); + assert!(RoomVersionId::version_5().is_version_5()); assert!(RoomVersionId::custom("foo").is_custom()); } #[test] + #[allow(clippy::cognitive_complexity)] fn predicate_methods() { let version_1 = RoomVersionId::try_from("1").expect("Failed to create RoomVersionId."); let version_2 = RoomVersionId::try_from("2").expect("Failed to create RoomVersionId."); let version_3 = RoomVersionId::try_from("3").expect("Failed to create RoomVersionId."); let version_4 = RoomVersionId::try_from("4").expect("Failed to create RoomVersionId."); + let version_5 = RoomVersionId::try_from("5").expect("Failed to create RoomVersionId."); let custom = RoomVersionId::try_from("io.ruma.1").expect("Failed to create RoomVersionId."); assert!(version_1.is_version_1()); assert!(version_2.is_version_2()); assert!(version_3.is_version_3()); assert!(version_4.is_version_4()); + assert!(version_5.is_version_5()); assert!(!version_1.is_version_2()); assert!(!version_1.is_version_3()); assert!(!version_1.is_version_4()); + assert!(!version_1.is_version_5()); assert!(version_1.is_official()); assert!(version_2.is_official()); assert!(version_3.is_official()); assert!(version_4.is_official()); + assert!(version_5.is_official()); assert!(!version_1.is_custom()); assert!(!version_2.is_custom()); assert!(!version_3.is_custom()); assert!(!version_4.is_custom()); + assert!(!version_5.is_custom()); assert!(custom.is_custom()); assert!(!custom.is_official()); @@ -347,5 +386,6 @@ mod tests { assert!(!custom.is_version_2()); assert!(!custom.is_version_3()); assert!(!custom.is_version_4()); + assert!(!custom.is_version_5()); } } From bc7683af96bda933d78a9aaf06308fedca48b06e Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Sun, 16 Jun 2019 01:30:42 -0700 Subject: [PATCH 075/140] Bump version to 0.13.1. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index c2f0e8ad..decea527 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT" name = "ruma-identifiers" readme = "README.md" repository = "https://github.com/ruma/ruma-identifiers" -version = "0.13.0" +version = "0.13.1" edition = "2018" [dependencies] From 77e5976ba65ca4ecf1742e4ffcfcf3e37a37ff0a Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Sun, 16 Jun 2019 16:41:59 -0700 Subject: [PATCH 076/140] Add crates.io categories. [ci skip] --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index decea527..d4717b8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [package] authors = ["Jimmy Cuadra "] +categories = ["api-bindings"] description = "Resource identifiers for Matrix." documentation = "https://docs.rs/ruma-identifiers" homepage = "https://github.com/ruma/ruma-identifiers" From 29a8fa38a98f3236890ffd029582ed359f156b85 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Tue, 23 Jul 2019 09:59:38 -0700 Subject: [PATCH 077/140] Run cargo-audit on CI. --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index 6b7f829c..fda4fc5c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,12 @@ language: "rust" +cache: "cargo" before_script: - "rustup component add rustfmt" - "rustup component add clippy" + - "cargo install --force cargo-audit" + - "cargo generate-lockfile" script: + - "cargo audit" - "cargo fmt --all -- --check" - "cargo clippy --all-targets --all-features -- -D warnings" - "cargo build --verbose" From 4bafdb1ed7ea7ef8e3393bb3287ac502438a5dc0 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 23 Jul 2019 21:34:19 +0200 Subject: [PATCH 078/140] Change invalid host tests from '-' to '/' --- src/event_id.rs | 2 +- src/room_alias_id.rs | 2 +- src/room_id.rs | 2 +- src/user_id.rs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/event_id.rs b/src/event_id.rs index 606e68c2..022449f9 100644 --- a/src/event_id.rs +++ b/src/event_id.rs @@ -376,7 +376,7 @@ mod tests { #[test] fn invalid_event_id_host() { assert_eq!( - EventId::try_from("$39hvsi03hlne:-").err().unwrap(), + EventId::try_from("$39hvsi03hlne:/").err().unwrap(), Error::InvalidHost ); } diff --git a/src/room_alias_id.rs b/src/room_alias_id.rs index 1d6c1de4..52cd06f4 100644 --- a/src/room_alias_id.rs +++ b/src/room_alias_id.rs @@ -213,7 +213,7 @@ mod tests { #[test] fn invalid_room_alias_id_host() { assert_eq!( - RoomAliasId::try_from("#ruma:-").err().unwrap(), + RoomAliasId::try_from("#ruma:/").err().unwrap(), Error::InvalidHost ); } diff --git a/src/room_id.rs b/src/room_id.rs index 8aec0959..c28dbe05 100644 --- a/src/room_id.rs +++ b/src/room_id.rs @@ -231,7 +231,7 @@ mod tests { #[test] fn invalid_room_id_host() { assert_eq!( - RoomId::try_from("!29fhd83h92h0:-").err().unwrap(), + RoomId::try_from("!29fhd83h92h0:/").err().unwrap(), Error::InvalidHost ); } diff --git a/src/user_id.rs b/src/user_id.rs index 140341ce..5297ae79 100644 --- a/src/user_id.rs +++ b/src/user_id.rs @@ -262,7 +262,7 @@ mod tests { #[test] fn invalid_user_id_host() { assert_eq!( - UserId::try_from("@carl:-").err().unwrap(), + UserId::try_from("@carl:/").err().unwrap(), Error::InvalidHost ); } From 59bd63d25eaac41d9308b36125ef258dcf790765 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 23 Jul 2019 21:15:29 +0200 Subject: [PATCH 079/140] Update rand to 0.7, regex to 1.2.0, url to 2.0.0 --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d4717b8b..8ca08ff2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,10 +14,10 @@ edition = "2018" [dependencies] lazy_static = "1.3.0" -rand = "0.6.5" -regex = "1.1.5" +rand = "0.7.0" +regex = "1.2.0" serde = "1.0.90" -url = "1.7.2" +url = "2.0.0" [dependencies.diesel] optional = true From 2edf22157cbfd12ae0705ba9d4089e78e18bfcad Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Thu, 25 Jul 2019 10:33:04 -0700 Subject: [PATCH 080/140] Bump version to 0.14.0. --- Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8ca08ff2..318b44c8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,14 +9,14 @@ license = "MIT" name = "ruma-identifiers" readme = "README.md" repository = "https://github.com/ruma/ruma-identifiers" -version = "0.13.1" +version = "0.14.0" edition = "2018" [dependencies] lazy_static = "1.3.0" rand = "0.7.0" regex = "1.2.0" -serde = "1.0.90" +serde = "1.0.97" url = "2.0.0" [dependencies.diesel] @@ -24,4 +24,4 @@ optional = true version = "1.4.2" [dev-dependencies] -serde_json = "1.0.39" +serde_json = "1.0.40" From b5102db0d4d831ab4644e1ae55b0985135b2d4d6 Mon Sep 17 00:00:00 2001 From: Jimmy Cuadra Date: Sat, 3 Aug 2019 14:02:24 -0700 Subject: [PATCH 081/140] Only build PRs and the master branch on CI. --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index fda4fc5c..6340da38 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,7 @@ script: - "cargo clippy --all-targets --all-features -- -D warnings" - "cargo build --verbose" - "cargo test --verbose" +if: "type != push OR (tag IS blank AND branch = master)" notifications: email: false irc: From fc145097be1bddd30cac7fb905bcb3246a986e7b Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 31 Oct 2019 22:29:48 +0100 Subject: [PATCH 082/140] Update dependencies, use single-line toml syntax --- Cargo.toml | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 318b44c8..13c38242 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,15 +13,12 @@ version = "0.14.0" edition = "2018" [dependencies] -lazy_static = "1.3.0" -rand = "0.7.0" -regex = "1.2.0" -serde = "1.0.97" -url = "2.0.0" - -[dependencies.diesel] -optional = true -version = "1.4.2" +diesel = { version = "1.4.3", optional = true } +lazy_static = "1.4.0" +rand = "0.7.2" +regex = "1.3.1" +serde = "1.0.102" +url = "2.1.0" [dev-dependencies] -serde_json = "1.0.40" +serde_json = "1.0.41" From 859ec7babead7945166e76f7900cb117c551f80b Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 31 Oct 2019 22:32:57 +0100 Subject: [PATCH 083/140] Add 1.34.2, beta, nightly to travis --- .travis.yml | 40 +++++++++++++++++++++++++++++++--------- README.md | 2 +- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6340da38..f300884e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,16 +1,38 @@ language: "rust" cache: "cargo" +rust: + - 1.34.2 + - stable + - beta + - nightly +jobs: + allow_failures: + - rust: nightly + fast_finish: true + before_script: - - "rustup component add rustfmt" - - "rustup component add clippy" - - "cargo install --force cargo-audit" - - "cargo generate-lockfile" + - rustup component add rustfmt + - | + if [ "$TRAVIS_RUST_VERSION" != "1.34.2" ]; then + rustup component add clippy + fi + - | + if [ "$TRAVIS_RUST_VERSION" == "stable" ]; then + cargo install --force cargo-audit + fi + - cargo generate-lockfile script: - - "cargo audit" - - "cargo fmt --all -- --check" - - "cargo clippy --all-targets --all-features -- -D warnings" - - "cargo build --verbose" - - "cargo test --verbose" + - | + if [ "$TRAVIS_RUST_VERSION" == "stable" ]; then + cargo audit + fi + - cargo fmt -- --check + - | + if [ "$TRAVIS_RUST_VERSION" != "1.34.2" ]; then + cargo clippy --all-targets --all-features -- -D warnings + fi + - cargo build --verbose + - cargo test --verbose if: "type != push OR (tag IS blank AND branch = master)" notifications: email: false diff --git a/README.md b/README.md index b8aa880b..3a74093f 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ## Minimum Rust version -ruma-identifiers requires Rust 1.34 or later. +ruma-identifiers requires Rust 1.34.2 or later. ## Documentation From 26068c462d83553a5a43a57711bc1d4988a46003 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 31 Oct 2019 22:33:12 +0100 Subject: [PATCH 084/140] Remove unneeded clones --- src/device_id.rs | 2 +- src/user_id.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/device_id.rs b/src/device_id.rs index b0026e74..1dd27bbf 100644 --- a/src/device_id.rs +++ b/src/device_id.rs @@ -10,7 +10,7 @@ pub type DeviceId = String; /// Generates a random `DeviceId`, suitable for assignment to a new device. pub fn generate() -> DeviceId { - generate_localpart(8).to_string() + generate_localpart(8) } #[cfg(test)] diff --git a/src/user_id.rs b/src/user_id.rs index 5297ae79..069951b8 100644 --- a/src/user_id.rs +++ b/src/user_id.rs @@ -131,7 +131,7 @@ impl<'a> TryFrom<&'a str> for UserId { Ok(Self { hostname: host, port, - localpart: downcased_localpart.to_owned(), + localpart: downcased_localpart, }) } } From 70d4f83ba37b5d09450b81458fe25e5951227347 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 31 Oct 2019 22:33:20 +0100 Subject: [PATCH 085/140] Allow clippy::use_self --- src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index e5661731..3231d8dc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,6 +26,8 @@ clippy::wrong_pub_self_convention, clippy::wrong_self_convention )] +// Since we support Rust 1.34.2, we can't apply this suggestion yet +#![allow(clippy::use_self)] #[cfg(feature = "diesel")] #[cfg_attr(feature = "diesel", macro_use)] From 31b683ab69779c02e1dc7e3d823b38acfb2ddd53 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 31 Oct 2019 22:33:55 +0100 Subject: [PATCH 086/140] =?UTF-8?q?Remove=20#![deny(warnings)],=20#![warn(?= =?UTF-8?q?clippy::=E2=80=A6)]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib.rs | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 3231d8dc..53824ae5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,27 +4,7 @@ #![deny( missing_copy_implementations, missing_debug_implementations, - missing_docs, - warnings -)] -#![warn( - clippy::empty_line_after_outer_attr, - clippy::expl_impl_clone_on_copy, - clippy::if_not_else, - clippy::items_after_statements, - clippy::match_same_arms, - clippy::mem_forget, - clippy::missing_docs_in_private_items, - clippy::multiple_inherent_impl, - clippy::mut_mut, - clippy::needless_borrow, - clippy::needless_continue, - clippy::single_match_else, - clippy::unicode_not_nfc, - clippy::use_self, - clippy::used_underscore_binding, - clippy::wrong_pub_self_convention, - clippy::wrong_self_convention + missing_docs )] // Since we support Rust 1.34.2, we can't apply this suggestion yet #![allow(clippy::use_self)] From f994672db5b6f7db979e165b31abc003886a8d36 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 31 Oct 2019 22:55:24 +0100 Subject: [PATCH 087/140] Replace lazy_static with once_cell --- Cargo.toml | 2 +- src/user_id.rs | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 13c38242..66cd1dd2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ edition = "2018" [dependencies] diesel = { version = "1.4.3", optional = true } -lazy_static = "1.4.0" +once_cell = "1.2.0" rand = "0.7.2" regex = "1.3.1" serde = "1.0.102" diff --git a/src/user_id.rs b/src/user_id.rs index 069951b8..ef480ce9 100644 --- a/src/user_id.rs +++ b/src/user_id.rs @@ -7,7 +7,7 @@ use std::{ #[cfg(feature = "diesel")] use diesel::sql_types::Text; -use lazy_static::lazy_static; +use once_cell::sync::Lazy; use regex::Regex; use serde::{ de::{Error as SerdeError, Unexpected, Visitor}, @@ -17,10 +17,8 @@ use url::Host; use crate::{display, error::Error, generate_localpart, parse_id}; -lazy_static! { - static ref USER_LOCALPART_PATTERN: Regex = - Regex::new(r"\A[a-z0-9._=-]+\z").expect("Failed to create user localpart regex."); -} +static USER_LOCALPART_PATTERN: Lazy = + Lazy::new(|| Regex::new(r"\A[a-z0-9._=-]+\z").expect("Failed to create user localpart regex.")); /// A Matrix user ID. /// From 3e3e7cb7b33aa43fa3364dc9f0fdcad11df73459 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Fri, 1 Nov 2019 11:11:05 +0100 Subject: [PATCH 088/140] Fix missing --- src/diesel_integration.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/diesel_integration.rs b/src/diesel_integration.rs index 65e5ae63..e83b20cb 100644 --- a/src/diesel_integration.rs +++ b/src/diesel_integration.rs @@ -28,7 +28,7 @@ macro_rules! diesel_impl { fn from_sql(value: Option<&::RawValue>) -> DeserializeResult { let string = >::from_sql(value)?; Self::try_from(string.as_str()) - .map_err(|error| Box::new(error) as Box) + .map_err(|error| Box::new(error) as Box) } } }; From 3b10faad0ead09d8adfaeef77b983ffba1b99bf8 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 12 Nov 2019 01:24:57 +0100 Subject: [PATCH 089/140] Add #![warn(rust_2018_idioms)] --- src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.rs b/src/lib.rs index 53824ae5..45320370 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ //! Crate **ruma_identifiers** contains types for [Matrix](https://matrix.org/) identifiers //! for events, rooms, room aliases, room versions, and users. +#![warn(rust_2018_idioms)] #![deny( missing_copy_implementations, missing_debug_implementations, From 3141f3e9a1ef18c000493885124687c92dd4c6c3 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 12 Nov 2019 01:53:18 +0100 Subject: [PATCH 090/140] Fix Rust 2018 idiom warning with `cargo fix --all-features` --- src/diesel_integration.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/diesel_integration.rs b/src/diesel_integration.rs index e83b20cb..2daf80c4 100644 --- a/src/diesel_integration.rs +++ b/src/diesel_integration.rs @@ -15,7 +15,7 @@ macro_rules! diesel_impl { where DB: Backend, { - fn to_sql(&self, out: &mut Output) -> SerializeResult { + fn to_sql(&self, out: &mut Output<'_, W, DB>) -> SerializeResult { ToSql::::to_sql(&self.to_string(), out) } } From f84b7f2b7f5e0b7a4bacb86ec8b49ecd8be3b58c Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Fri, 22 Nov 2019 11:08:53 +0100 Subject: [PATCH 091/140] Bump MSRV to 1.36.0 --- .travis.yml | 6 +++--- CHANGELOG.md | 5 +++++ README.md | 2 +- src/lib.rs | 2 +- 4 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 CHANGELOG.md diff --git a/.travis.yml b/.travis.yml index f300884e..d8cbf9a9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: "rust" cache: "cargo" rust: - - 1.34.2 + - 1.36.0 - stable - beta - nightly @@ -13,7 +13,7 @@ jobs: before_script: - rustup component add rustfmt - | - if [ "$TRAVIS_RUST_VERSION" != "1.34.2" ]; then + if [ "$TRAVIS_RUST_VERSION" != "1.36.0" ]; then rustup component add clippy fi - | @@ -28,7 +28,7 @@ script: fi - cargo fmt -- --check - | - if [ "$TRAVIS_RUST_VERSION" != "1.34.2" ]; then + if [ "$TRAVIS_RUST_VERSION" != "1.36.0" ]; then cargo clippy --all-targets --all-features -- -D warnings fi - cargo build --verbose diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..360ddec5 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# [unreleased] + +Breaking changes: + +* Our Minimum Supported Rust Version is now 1.36.0 diff --git a/README.md b/README.md index 3a74093f..585b7880 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ## Minimum Rust version -ruma-identifiers requires Rust 1.34.2 or later. +ruma-identifiers requires Rust 1.36.0 or later. ## Documentation diff --git a/src/lib.rs b/src/lib.rs index 45320370..baab2bc1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,7 @@ missing_debug_implementations, missing_docs )] -// Since we support Rust 1.34.2, we can't apply this suggestion yet +// Since we support Rust 1.36.0, we can't apply this suggestion yet #![allow(clippy::use_self)] #[cfg(feature = "diesel")] From 7fa1407d7669312d0178b7bc5746b4152a781713 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Mon, 9 Dec 2019 17:43:15 +0100 Subject: [PATCH 092/140] Update set of allowed characters in UserId localpart --- src/user_id.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/user_id.rs b/src/user_id.rs index ef480ce9..9bb8fb28 100644 --- a/src/user_id.rs +++ b/src/user_id.rs @@ -17,8 +17,10 @@ use url::Host; use crate::{display, error::Error, generate_localpart, parse_id}; -static USER_LOCALPART_PATTERN: Lazy = - Lazy::new(|| Regex::new(r"\A[a-z0-9._=-]+\z").expect("Failed to create user localpart regex.")); +// See https://matrix.org/docs/spec/appendices#user-identifiers +static USER_LOCALPART_PATTERN: Lazy = Lazy::new(|| { + Regex::new(r"\A[a-z0-9\-.=_/]+\z").expect("Failed to create user localpart regex.") +}); /// A Matrix user ID. /// From 11841f91b81e30284cc046a4b14b24af73c235c8 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 10 Dec 2019 11:41:30 +0100 Subject: [PATCH 093/140] Remove regex, once_cell deps --- Cargo.toml | 2 -- src/user_id.rs | 13 +++++-------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 66cd1dd2..6d448013 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,9 +14,7 @@ edition = "2018" [dependencies] diesel = { version = "1.4.3", optional = true } -once_cell = "1.2.0" rand = "0.7.2" -regex = "1.3.1" serde = "1.0.102" url = "2.1.0" diff --git a/src/user_id.rs b/src/user_id.rs index 9bb8fb28..255ae416 100644 --- a/src/user_id.rs +++ b/src/user_id.rs @@ -7,8 +7,6 @@ use std::{ #[cfg(feature = "diesel")] use diesel::sql_types::Text; -use once_cell::sync::Lazy; -use regex::Regex; use serde::{ de::{Error as SerdeError, Unexpected, Visitor}, Deserialize, Deserializer, Serialize, Serializer, @@ -17,11 +15,6 @@ use url::Host; use crate::{display, error::Error, generate_localpart, parse_id}; -// See https://matrix.org/docs/spec/appendices#user-identifiers -static USER_LOCALPART_PATTERN: Lazy = Lazy::new(|| { - Regex::new(r"\A[a-z0-9\-.=_/]+\z").expect("Failed to create user localpart regex.") -}); - /// A Matrix user ID. /// /// A `UserId` is generated randomly or converted from a string slice, and can be converted back @@ -124,7 +117,11 @@ impl<'a> TryFrom<&'a str> for UserId { let (localpart, host, port) = parse_id('@', user_id)?; let downcased_localpart = localpart.to_lowercase(); - if !USER_LOCALPART_PATTERN.is_match(&downcased_localpart) { + // See https://matrix.org/docs/spec/appendices#user-identifiers + if downcased_localpart.bytes().any(|b| match b { + b'0'..=b'9' | b'a'..=b'z' | b'-' | b'.' | b'=' | b'_' | b'/' => false, + _ => true, + }) { return Err(Error::InvalidCharacters); } From d120f77635a96a13e7d851cc24803d50662aea15 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 10 Dec 2019 11:54:09 +0100 Subject: [PATCH 094/140] Simplify deserialization --- src/event_id.rs | 26 ++++++++------------------ src/room_alias_id.rs | 26 ++++++++------------------ src/room_id.rs | 26 ++++++++------------------ src/room_id_or_room_alias_id.rs | 27 +++++++++------------------ src/room_version_id.rs | 26 ++++++++------------------ src/user_id.rs | 26 ++++++++------------------ 6 files changed, 49 insertions(+), 108 deletions(-) diff --git a/src/event_id.rs b/src/event_id.rs index 022449f9..fc582526 100644 --- a/src/event_id.rs +++ b/src/event_id.rs @@ -8,7 +8,7 @@ use std::{ #[cfg(feature = "diesel")] use diesel::sql_types::Text; use serde::{ - de::{Error as SerdeError, Unexpected, Visitor}, + de::{Error as SerdeError, Expected, Unexpected}, Deserialize, Deserializer, Serialize, Serializer, }; use url::Host; @@ -75,9 +75,6 @@ struct Original { pub port: u16, } -/// A serde visitor for `EventId`. -struct EventIdVisitor; - impl EventId { /// Attempts to generate an `EventId` for the given origin server with a localpart consisting /// of 18 random ASCII characters. This should only be used for events in the original format @@ -158,7 +155,10 @@ impl<'de> Deserialize<'de> for EventId { where D: Deserializer<'de>, { - deserializer.deserialize_any(EventIdVisitor) + String::deserialize(deserializer).and_then(|v| { + EventId::try_from(&v as &str) + .map_err(|_| SerdeError::invalid_value(Unexpected::Str(&v), &ExpectedEventId)) + }) } } @@ -189,22 +189,12 @@ impl<'a> TryFrom<&'a str> for EventId { } } -impl<'de> Visitor<'de> for EventIdVisitor { - type Value = EventId; +struct ExpectedEventId; - fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { +impl Expected for ExpectedEventId { + fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult { write!(formatter, "a Matrix event ID as a string") } - - fn visit_str(self, v: &str) -> Result - where - E: SerdeError, - { - match EventId::try_from(v) { - Ok(event_id) => Ok(event_id), - Err(_) => Err(SerdeError::invalid_value(Unexpected::Str(v), &self)), - } - } } #[cfg(test)] diff --git a/src/room_alias_id.rs b/src/room_alias_id.rs index 52cd06f4..5267e1e1 100644 --- a/src/room_alias_id.rs +++ b/src/room_alias_id.rs @@ -8,7 +8,7 @@ use std::{ #[cfg(feature = "diesel")] use diesel::sql_types::Text; use serde::{ - de::{Error as SerdeError, Unexpected, Visitor}, + de::{Error as SerdeError, Expected, Unexpected}, Deserialize, Deserializer, Serialize, Serializer, }; use url::Host; @@ -40,9 +40,6 @@ pub struct RoomAliasId { port: u16, } -/// A serde visitor for `RoomAliasId`. -struct RoomAliasIdVisitor; - impl RoomAliasId { /// Returns a `Host` for the room alias ID, containing the server name (minus the port) of /// the originating homeserver. @@ -83,7 +80,10 @@ impl<'de> Deserialize<'de> for RoomAliasId { where D: Deserializer<'de>, { - deserializer.deserialize_any(RoomAliasIdVisitor) + String::deserialize(deserializer).and_then(|v| { + RoomAliasId::try_from(&v as &str) + .map_err(|_| SerdeError::invalid_value(Unexpected::Str(&v), &ExpectedRoomAliasId)) + }) } } @@ -105,22 +105,12 @@ impl<'a> TryFrom<&'a str> for RoomAliasId { } } -impl<'de> Visitor<'de> for RoomAliasIdVisitor { - type Value = RoomAliasId; +struct ExpectedRoomAliasId; - fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { +impl Expected for ExpectedRoomAliasId { + fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult { write!(formatter, "a Matrix room alias ID as a string") } - - fn visit_str(self, v: &str) -> Result - where - E: SerdeError, - { - match RoomAliasId::try_from(v) { - Ok(room_alias_id) => Ok(room_alias_id), - Err(_) => Err(SerdeError::invalid_value(Unexpected::Str(v), &self)), - } - } } #[cfg(test)] diff --git a/src/room_id.rs b/src/room_id.rs index c28dbe05..09de6ca2 100644 --- a/src/room_id.rs +++ b/src/room_id.rs @@ -8,7 +8,7 @@ use std::{ #[cfg(feature = "diesel")] use diesel::sql_types::Text; use serde::{ - de::{Error as SerdeError, Unexpected, Visitor}, + de::{Error as SerdeError, Expected, Unexpected}, Deserialize, Deserializer, Serialize, Serializer, }; use url::Host; @@ -40,9 +40,6 @@ pub struct RoomId { port: u16, } -/// A serde visitor for `RoomId`. -struct RoomIdVisitor; - impl RoomId { /// Attempts to generate a `RoomId` for the given origin server with a localpart consisting of /// 18 random ASCII characters. @@ -98,7 +95,10 @@ impl<'de> Deserialize<'de> for RoomId { where D: Deserializer<'de>, { - deserializer.deserialize_any(RoomIdVisitor) + String::deserialize(deserializer).and_then(|v| { + RoomId::try_from(&v as &str) + .map_err(|_| SerdeError::invalid_value(Unexpected::Str(&v), &ExpectedRoomId)) + }) } } @@ -120,22 +120,12 @@ impl<'a> TryFrom<&'a str> for RoomId { } } -impl<'de> Visitor<'de> for RoomIdVisitor { - type Value = RoomId; +struct ExpectedRoomId; - fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { +impl Expected for ExpectedRoomId { + fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult { write!(formatter, "a Matrix room ID as a string") } - - fn visit_str(self, v: &str) -> Result - where - E: SerdeError, - { - match RoomId::try_from(v) { - Ok(room_id) => Ok(room_id), - Err(_) => Err(SerdeError::invalid_value(Unexpected::Str(v), &self)), - } - } } #[cfg(test)] diff --git a/src/room_id_or_room_alias_id.rs b/src/room_id_or_room_alias_id.rs index 3a2d7563..f453131b 100644 --- a/src/room_id_or_room_alias_id.rs +++ b/src/room_id_or_room_alias_id.rs @@ -8,7 +8,7 @@ use std::{ #[cfg(feature = "diesel")] use diesel::sql_types::Text; use serde::{ - de::{Error as SerdeError, Unexpected, Visitor}, + de::{Error as SerdeError, Expected, Unexpected}, Deserialize, Deserializer, Serialize, Serializer, }; @@ -43,9 +43,6 @@ pub enum RoomIdOrAliasId { RoomId(RoomId), } -/// A serde visitor for `RoomIdOrAliasId`. -struct RoomIdOrAliasIdVisitor; - impl Display for RoomIdOrAliasId { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { match *self { @@ -86,7 +83,11 @@ impl<'de> Deserialize<'de> for RoomIdOrAliasId { where D: Deserializer<'de>, { - deserializer.deserialize_any(RoomIdOrAliasIdVisitor) + String::deserialize(deserializer).and_then(|v| { + RoomIdOrAliasId::try_from(&v as &str).map_err(|_| { + SerdeError::invalid_value(Unexpected::Str(&v), &ExpectedRoomIdOrAliasId) + }) + }) } } @@ -119,22 +120,12 @@ impl<'a> TryFrom<&'a str> for RoomIdOrAliasId { } } -impl<'de> Visitor<'de> for RoomIdOrAliasIdVisitor { - type Value = RoomIdOrAliasId; +struct ExpectedRoomIdOrAliasId; - fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { +impl Expected for ExpectedRoomIdOrAliasId { + fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult { write!(formatter, "a Matrix room ID or room alias ID as a string") } - - fn visit_str(self, v: &str) -> Result - where - E: SerdeError, - { - match RoomIdOrAliasId::try_from(v) { - Ok(room_id_or_alias_id) => Ok(room_id_or_alias_id), - Err(_) => Err(SerdeError::invalid_value(Unexpected::Str(v), &self)), - } - } } #[cfg(test)] diff --git a/src/room_version_id.rs b/src/room_version_id.rs index 3928c91f..44b59c2a 100644 --- a/src/room_version_id.rs +++ b/src/room_version_id.rs @@ -8,7 +8,7 @@ use std::{ #[cfg(feature = "diesel")] use diesel::sql_types::Text; use serde::{ - de::{Error as SerdeError, Unexpected, Visitor}, + de::{Error as SerdeError, Expected, Unexpected}, Deserialize, Deserializer, Serialize, Serializer, }; @@ -55,9 +55,6 @@ enum InnerRoomVersionId { Custom(String), } -/// A serde visitor for `RoomVersionId`. -struct RoomVersionIdVisitor; - impl RoomVersionId { /// Creates a version 1 room ID. pub fn version_1() -> Self { @@ -157,7 +154,10 @@ impl<'de> Deserialize<'de> for RoomVersionId { where D: Deserializer<'de>, { - deserializer.deserialize_any(RoomVersionIdVisitor) + String::deserialize(deserializer).and_then(|v| { + RoomVersionId::try_from(&v as &str) + .map_err(|_| SerdeError::invalid_value(Unexpected::Str(&v), &ExpectedRoomVersionId)) + }) } } @@ -187,22 +187,12 @@ impl<'a> TryFrom<&'a str> for RoomVersionId { } } -impl<'de> Visitor<'de> for RoomVersionIdVisitor { - type Value = RoomVersionId; +struct ExpectedRoomVersionId; - fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { +impl Expected for ExpectedRoomVersionId { + fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult { write!(formatter, "a Matrix room version ID as a string") } - - fn visit_str(self, v: &str) -> Result - where - E: SerdeError, - { - match RoomVersionId::try_from(v) { - Ok(room_id) => Ok(room_id), - Err(_) => Err(SerdeError::invalid_value(Unexpected::Str(v), &self)), - } - } } #[cfg(test)] diff --git a/src/user_id.rs b/src/user_id.rs index 255ae416..005ecf21 100644 --- a/src/user_id.rs +++ b/src/user_id.rs @@ -8,7 +8,7 @@ use std::{ #[cfg(feature = "diesel")] use diesel::sql_types::Text; use serde::{ - de::{Error as SerdeError, Unexpected, Visitor}, + de::{Error as SerdeError, Expected, Unexpected}, Deserialize, Deserializer, Serialize, Serializer, }; use url::Host; @@ -40,9 +40,6 @@ pub struct UserId { port: u16, } -/// A serde visitor for `UserId`. -struct UserIdVisitor; - impl UserId { /// Attempts to generate a `UserId` for the given origin server with a localpart consisting of /// 12 random ASCII characters. @@ -102,7 +99,10 @@ impl<'de> Deserialize<'de> for UserId { where D: Deserializer<'de>, { - deserializer.deserialize_any(UserIdVisitor) + String::deserialize(deserializer).and_then(|v| { + UserId::try_from(&v as &str) + .map_err(|_| SerdeError::invalid_value(Unexpected::Str(&v), &ExpectedUserId)) + }) } } @@ -133,22 +133,12 @@ impl<'a> TryFrom<&'a str> for UserId { } } -impl<'de> Visitor<'de> for UserIdVisitor { - type Value = UserId; +struct ExpectedUserId; - fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult { +impl Expected for ExpectedUserId { + fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult { write!(formatter, "a Matrix user ID as a string") } - - fn visit_str(self, v: &str) -> Result - where - E: SerdeError, - { - match UserId::try_from(v) { - Ok(user_id) => Ok(user_id), - Err(_) => Err(SerdeError::invalid_value(Unexpected::Str(v), &self)), - } - } } #[cfg(test)] From 32733cf7825944582f154fe94bd8a507f7206f19 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Tue, 10 Dec 2019 22:42:46 +0100 Subject: [PATCH 095/140] Further simplify deserialization --- src/event_id.rs | 20 +++----------------- src/lib.rs | 19 ++++++++++++++++++- src/room_alias_id.rs | 20 +++----------------- src/room_id.rs | 20 +++----------------- src/room_id_or_room_alias_id.rs | 26 ++++++++------------------ src/room_version_id.rs | 20 +++----------------- src/user_id.rs | 20 +++----------------- 7 files changed, 41 insertions(+), 104 deletions(-) diff --git a/src/event_id.rs b/src/event_id.rs index fc582526..f70cd50c 100644 --- a/src/event_id.rs +++ b/src/event_id.rs @@ -7,13 +7,10 @@ use std::{ #[cfg(feature = "diesel")] use diesel::sql_types::Text; -use serde::{ - de::{Error as SerdeError, Expected, Unexpected}, - Deserialize, Deserializer, Serialize, Serializer, -}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use url::Host; -use crate::{display, error::Error, generate_localpart, parse_id}; +use crate::{deserialize_id, display, error::Error, generate_localpart, parse_id}; /// A Matrix event ID. /// @@ -155,10 +152,7 @@ impl<'de> Deserialize<'de> for EventId { where D: Deserializer<'de>, { - String::deserialize(deserializer).and_then(|v| { - EventId::try_from(&v as &str) - .map_err(|_| SerdeError::invalid_value(Unexpected::Str(&v), &ExpectedEventId)) - }) + deserialize_id(deserializer, "a Matrix event ID as a string") } } @@ -189,14 +183,6 @@ impl<'a> TryFrom<&'a str> for EventId { } } -struct ExpectedEventId; - -impl Expected for ExpectedEventId { - fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult { - write!(formatter, "a Matrix event ID as a string") - } -} - #[cfg(test)] mod tests { use std::convert::TryFrom; diff --git a/src/lib.rs b/src/lib.rs index baab2bc1..6a7d9277 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,9 +14,13 @@ #[cfg_attr(feature = "diesel", macro_use)] extern crate diesel; -use std::fmt::{Formatter, Result as FmtResult}; +use std::{ + convert::TryFrom, + fmt::{Formatter, Result as FmtResult}, +}; use rand::{distributions::Alphanumeric, thread_rng, Rng}; +use serde::de::{self, Deserialize as _, Deserializer, Unexpected}; use url::Url; pub use url::Host; @@ -112,3 +116,16 @@ fn parse_id(required_sigil: char, id: &str) -> Result<(&str, Host, u16), Error> Ok((localpart, host, port)) } + +/// Deserializes any type of id using the provided TryFrom implementation. +/// +/// This is a helper function to reduce the boilerplate of the Deserialize implementations. +fn deserialize_id<'de, D, T>(deserializer: D, expected_str: &str) -> Result +where + D: Deserializer<'de>, + T: for<'a> TryFrom<&'a str>, +{ + String::deserialize(deserializer).and_then(|v| { + T::try_from(&v).map_err(|_| de::Error::invalid_value(Unexpected::Str(&v), &expected_str)) + }) +} diff --git a/src/room_alias_id.rs b/src/room_alias_id.rs index 5267e1e1..0883f293 100644 --- a/src/room_alias_id.rs +++ b/src/room_alias_id.rs @@ -7,13 +7,10 @@ use std::{ #[cfg(feature = "diesel")] use diesel::sql_types::Text; -use serde::{ - de::{Error as SerdeError, Expected, Unexpected}, - Deserialize, Deserializer, Serialize, Serializer, -}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use url::Host; -use crate::{display, error::Error, parse_id}; +use crate::{deserialize_id, display, error::Error, parse_id}; /// A Matrix room alias ID. /// @@ -80,10 +77,7 @@ impl<'de> Deserialize<'de> for RoomAliasId { where D: Deserializer<'de>, { - String::deserialize(deserializer).and_then(|v| { - RoomAliasId::try_from(&v as &str) - .map_err(|_| SerdeError::invalid_value(Unexpected::Str(&v), &ExpectedRoomAliasId)) - }) + deserialize_id(deserializer, "a Matrix room alias ID as a string") } } @@ -105,14 +99,6 @@ impl<'a> TryFrom<&'a str> for RoomAliasId { } } -struct ExpectedRoomAliasId; - -impl Expected for ExpectedRoomAliasId { - fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult { - write!(formatter, "a Matrix room alias ID as a string") - } -} - #[cfg(test)] mod tests { use std::convert::TryFrom; diff --git a/src/room_id.rs b/src/room_id.rs index 09de6ca2..8c17bfb7 100644 --- a/src/room_id.rs +++ b/src/room_id.rs @@ -7,13 +7,10 @@ use std::{ #[cfg(feature = "diesel")] use diesel::sql_types::Text; -use serde::{ - de::{Error as SerdeError, Expected, Unexpected}, - Deserialize, Deserializer, Serialize, Serializer, -}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use url::Host; -use crate::{display, error::Error, generate_localpart, parse_id}; +use crate::{deserialize_id, display, error::Error, generate_localpart, parse_id}; /// A Matrix room ID. /// @@ -95,10 +92,7 @@ impl<'de> Deserialize<'de> for RoomId { where D: Deserializer<'de>, { - String::deserialize(deserializer).and_then(|v| { - RoomId::try_from(&v as &str) - .map_err(|_| SerdeError::invalid_value(Unexpected::Str(&v), &ExpectedRoomId)) - }) + deserialize_id(deserializer, "a Matrix room ID as a string") } } @@ -120,14 +114,6 @@ impl<'a> TryFrom<&'a str> for RoomId { } } -struct ExpectedRoomId; - -impl Expected for ExpectedRoomId { - fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult { - write!(formatter, "a Matrix room ID as a string") - } -} - #[cfg(test)] mod tests { use std::convert::TryFrom; diff --git a/src/room_id_or_room_alias_id.rs b/src/room_id_or_room_alias_id.rs index f453131b..687cf3e1 100644 --- a/src/room_id_or_room_alias_id.rs +++ b/src/room_id_or_room_alias_id.rs @@ -7,12 +7,11 @@ use std::{ #[cfg(feature = "diesel")] use diesel::sql_types::Text; -use serde::{ - de::{Error as SerdeError, Expected, Unexpected}, - Deserialize, Deserializer, Serialize, Serializer, -}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use crate::{display, error::Error, room_alias_id::RoomAliasId, room_id::RoomId, validate_id}; +use crate::{ + deserialize_id, display, error::Error, room_alias_id::RoomAliasId, room_id::RoomId, validate_id, +}; /// A Matrix room ID or a Matrix room alias ID. /// @@ -83,11 +82,10 @@ impl<'de> Deserialize<'de> for RoomIdOrAliasId { where D: Deserializer<'de>, { - String::deserialize(deserializer).and_then(|v| { - RoomIdOrAliasId::try_from(&v as &str).map_err(|_| { - SerdeError::invalid_value(Unexpected::Str(&v), &ExpectedRoomIdOrAliasId) - }) - }) + deserialize_id( + deserializer, + "a Matrix room ID or room alias ID as a string", + ) } } @@ -120,14 +118,6 @@ impl<'a> TryFrom<&'a str> for RoomIdOrAliasId { } } -struct ExpectedRoomIdOrAliasId; - -impl Expected for ExpectedRoomIdOrAliasId { - fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult { - write!(formatter, "a Matrix room ID or room alias ID as a string") - } -} - #[cfg(test)] mod tests { use std::convert::TryFrom; diff --git a/src/room_version_id.rs b/src/room_version_id.rs index 44b59c2a..6277199b 100644 --- a/src/room_version_id.rs +++ b/src/room_version_id.rs @@ -7,12 +7,9 @@ use std::{ #[cfg(feature = "diesel")] use diesel::sql_types::Text; -use serde::{ - de::{Error as SerdeError, Expected, Unexpected}, - Deserialize, Deserializer, Serialize, Serializer, -}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use crate::error::Error; +use crate::{deserialize_id, error::Error}; /// Room version identifiers cannot be more than 32 code points. const MAX_CODE_POINTS: usize = 32; @@ -154,10 +151,7 @@ impl<'de> Deserialize<'de> for RoomVersionId { where D: Deserializer<'de>, { - String::deserialize(deserializer).and_then(|v| { - RoomVersionId::try_from(&v as &str) - .map_err(|_| SerdeError::invalid_value(Unexpected::Str(&v), &ExpectedRoomVersionId)) - }) + deserialize_id(deserializer, "a Matrix room version ID as a string") } } @@ -187,14 +181,6 @@ impl<'a> TryFrom<&'a str> for RoomVersionId { } } -struct ExpectedRoomVersionId; - -impl Expected for ExpectedRoomVersionId { - fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult { - write!(formatter, "a Matrix room version ID as a string") - } -} - #[cfg(test)] mod tests { use std::convert::TryFrom; diff --git a/src/user_id.rs b/src/user_id.rs index 005ecf21..c0e415d8 100644 --- a/src/user_id.rs +++ b/src/user_id.rs @@ -7,13 +7,10 @@ use std::{ #[cfg(feature = "diesel")] use diesel::sql_types::Text; -use serde::{ - de::{Error as SerdeError, Expected, Unexpected}, - Deserialize, Deserializer, Serialize, Serializer, -}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use url::Host; -use crate::{display, error::Error, generate_localpart, parse_id}; +use crate::{deserialize_id, display, error::Error, generate_localpart, parse_id}; /// A Matrix user ID. /// @@ -99,10 +96,7 @@ impl<'de> Deserialize<'de> for UserId { where D: Deserializer<'de>, { - String::deserialize(deserializer).and_then(|v| { - UserId::try_from(&v as &str) - .map_err(|_| SerdeError::invalid_value(Unexpected::Str(&v), &ExpectedUserId)) - }) + deserialize_id(deserializer, "a Matrix user ID as a string") } } @@ -133,14 +127,6 @@ impl<'a> TryFrom<&'a str> for UserId { } } -struct ExpectedUserId; - -impl Expected for ExpectedUserId { - fn fmt(&self, formatter: &mut Formatter<'_>) -> FmtResult { - write!(formatter, "a Matrix user ID as a string") - } -} - #[cfg(test)] mod tests { use std::convert::TryFrom; From d19d9c054feb7e83993c25f399c256ba520e5142 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Sat, 14 Dec 2019 15:58:23 +0100 Subject: [PATCH 096/140] Support historical user IDs --- src/user_id.rs | 59 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 42 insertions(+), 17 deletions(-) diff --git a/src/user_id.rs b/src/user_id.rs index c0e415d8..4e31a9ca 100644 --- a/src/user_id.rs +++ b/src/user_id.rs @@ -35,6 +35,12 @@ pub struct UserId { localpart: String, /// The network port of the homeserver. port: u16, + /// Whether this user id is a historical one. + /// + /// A historical user id is one that is not legal per the regular user id rules, but was + /// accepted by previous versions of the spec and thus has to be supported because users with + /// these kinds of ids still exist. + is_historical: bool, } impl UserId { @@ -54,6 +60,7 @@ impl UserId { hostname: host, localpart: localpart.to_string(), port, + is_historical: false, }) } @@ -74,6 +81,13 @@ impl UserId { pub fn port(&self) -> u16 { self.port } + + /// Whether this user ID is a historical one, i.e. one that doesn't conform to the latest + /// specification of the user ID grammar but is still accepted because it was previously + /// allowed. + pub fn is_historical(&self) -> bool { + self.is_historical + } } impl Display for UserId { @@ -112,10 +126,19 @@ impl<'a> TryFrom<&'a str> for UserId { let downcased_localpart = localpart.to_lowercase(); // See https://matrix.org/docs/spec/appendices#user-identifiers - if downcased_localpart.bytes().any(|b| match b { - b'0'..=b'9' | b'a'..=b'z' | b'-' | b'.' | b'=' | b'_' | b'/' => false, - _ => true, - }) { + let is_fully_conforming = downcased_localpart.bytes().all(|b| match b { + b'0'..=b'9' | b'a'..=b'z' | b'-' | b'.' | b'=' | b'_' | b'/' => true, + _ => false, + }); + + // 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 + && downcased_localpart + .bytes() + .any(|b| b < 0x21 || b == b':' || b > 0x7E) + { return Err(Error::InvalidCharacters); } @@ -123,6 +146,7 @@ impl<'a> TryFrom<&'a str> for UserId { hostname: host, port, localpart: downcased_localpart, + is_historical: !is_fully_conforming, }) } } @@ -138,12 +162,16 @@ mod tests { #[test] fn valid_user_id() { - assert_eq!( - UserId::try_from("@carl:example.com") - .expect("Failed to create UserId.") - .to_string(), - "@carl:example.com" - ); + let user_id = UserId::try_from("@carl:example.com").expect("Failed to create UserId."); + assert_eq!(user_id.to_string(), "@carl:example.com"); + assert!(!user_id.is_historical()); + } + + #[test] + fn valid_historical_user_id() { + let user_id = UserId::try_from("@a%b[irc]:example.com").expect("Failed to create UserId."); + assert_eq!(user_id.to_string(), "@a%b[irc]:example.com"); + assert!(user_id.is_historical()); } #[test] @@ -200,18 +228,15 @@ mod tests { #[test] fn valid_user_id_with_non_standard_port() { - assert_eq!( - UserId::try_from("@carl:example.com:5000") - .expect("Failed to create UserId.") - .to_string(), - "@carl:example.com:5000" - ); + let user_id = UserId::try_from("@carl:example.com:5000").expect("Failed to create UserId."); + assert_eq!(user_id.to_string(), "@carl:example.com:5000"); + assert!(!user_id.is_historical()); } #[test] fn invalid_characters_in_user_id_localpart() { assert_eq!( - UserId::try_from("@%%%:example.com").err().unwrap(), + UserId::try_from("@te\nst:example.com").err().unwrap(), Error::InvalidCharacters ); } From 874b32a53b4997b0682cd9f6dd8cc29ddd97527d Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Sat, 14 Dec 2019 15:59:05 +0100 Subject: [PATCH 097/140] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 360ddec5..e2de86b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,3 +3,7 @@ Breaking changes: * Our Minimum Supported Rust Version is now 1.36.0 + +Improvements: + +* We now support [historical user IDs](https://matrix.org/docs/spec/appendices#historical-user-ids) From f21f4b4733358292cacedb405429e9425a8a73dd Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Sat, 14 Dec 2019 16:00:11 +0100 Subject: [PATCH 098/140] Replace `.err().unwrap()` with `.unwrap_err()` --- src/event_id.rs | 16 +++++----------- src/room_alias_id.rs | 12 ++++-------- src/room_id.rs | 10 ++++------ src/room_id_or_room_alias_id.rs | 2 +- src/user_id.rs | 15 +++++---------- 5 files changed, 19 insertions(+), 36 deletions(-) diff --git a/src/event_id.rs b/src/event_id.rs index f70cd50c..1b39685c 100644 --- a/src/event_id.rs +++ b/src/event_id.rs @@ -324,7 +324,7 @@ mod tests { #[test] fn missing_original_event_id_sigil() { assert_eq!( - EventId::try_from("39hvsi03hlne:example.com").err().unwrap(), + EventId::try_from("39hvsi03hlne:example.com").unwrap_err(), Error::MissingSigil ); } @@ -332,9 +332,7 @@ mod tests { #[test] fn missing_base64_event_id_sigil() { assert_eq!( - EventId::try_from("acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk") - .err() - .unwrap(), + EventId::try_from("acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk").unwrap_err(), Error::MissingSigil ); } @@ -342,9 +340,7 @@ mod tests { #[test] fn missing_url_safe_base64_event_id_sigil() { assert_eq!( - EventId::try_from("Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg") - .err() - .unwrap(), + EventId::try_from("Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg").unwrap_err(), Error::MissingSigil ); } @@ -352,7 +348,7 @@ mod tests { #[test] fn invalid_event_id_host() { assert_eq!( - EventId::try_from("$39hvsi03hlne:/").err().unwrap(), + EventId::try_from("$39hvsi03hlne:/").unwrap_err(), Error::InvalidHost ); } @@ -360,9 +356,7 @@ mod tests { #[test] fn invalid_event_id_port() { assert_eq!( - EventId::try_from("$39hvsi03hlne:example.com:notaport") - .err() - .unwrap(), + EventId::try_from("$39hvsi03hlne:example.com:notaport").unwrap_err(), Error::InvalidHost ); } diff --git a/src/room_alias_id.rs b/src/room_alias_id.rs index 0883f293..418b0878 100644 --- a/src/room_alias_id.rs +++ b/src/room_alias_id.rs @@ -171,9 +171,7 @@ mod tests { #[test] fn missing_room_alias_id_sigil() { assert_eq!( - RoomAliasId::try_from("39hvsi03hlne:example.com") - .err() - .unwrap(), + RoomAliasId::try_from("39hvsi03hlne:example.com").unwrap_err(), Error::MissingSigil ); } @@ -181,7 +179,7 @@ mod tests { #[test] fn missing_room_alias_id_delimiter() { assert_eq!( - RoomAliasId::try_from("#ruma").err().unwrap(), + RoomAliasId::try_from("#ruma").unwrap_err(), Error::MissingDelimiter ); } @@ -189,7 +187,7 @@ mod tests { #[test] fn invalid_room_alias_id_host() { assert_eq!( - RoomAliasId::try_from("#ruma:/").err().unwrap(), + RoomAliasId::try_from("#ruma:/").unwrap_err(), Error::InvalidHost ); } @@ -197,9 +195,7 @@ mod tests { #[test] fn invalid_room_alias_id_port() { assert_eq!( - RoomAliasId::try_from("#ruma:example.com:notaport") - .err() - .unwrap(), + RoomAliasId::try_from("#ruma:example.com:notaport").unwrap_err(), Error::InvalidHost ); } diff --git a/src/room_id.rs b/src/room_id.rs index 8c17bfb7..2ffc61bb 100644 --- a/src/room_id.rs +++ b/src/room_id.rs @@ -191,7 +191,7 @@ mod tests { #[test] fn missing_room_id_sigil() { assert_eq!( - RoomId::try_from("carl:example.com").err().unwrap(), + RoomId::try_from("carl:example.com").unwrap_err(), Error::MissingSigil ); } @@ -199,7 +199,7 @@ mod tests { #[test] fn missing_room_id_delimiter() { assert_eq!( - RoomId::try_from("!29fhd83h92h0").err().unwrap(), + RoomId::try_from("!29fhd83h92h0").unwrap_err(), Error::MissingDelimiter ); } @@ -207,7 +207,7 @@ mod tests { #[test] fn invalid_room_id_host() { assert_eq!( - RoomId::try_from("!29fhd83h92h0:/").err().unwrap(), + RoomId::try_from("!29fhd83h92h0:/").unwrap_err(), Error::InvalidHost ); } @@ -215,9 +215,7 @@ mod tests { #[test] fn invalid_room_id_port() { assert_eq!( - RoomId::try_from("!29fhd83h92h0:example.com:notaport") - .err() - .unwrap(), + RoomId::try_from("!29fhd83h92h0:example.com:notaport").unwrap_err(), Error::InvalidHost ); } diff --git a/src/room_id_or_room_alias_id.rs b/src/room_id_or_room_alias_id.rs index 687cf3e1..cc20b18e 100644 --- a/src/room_id_or_room_alias_id.rs +++ b/src/room_id_or_room_alias_id.rs @@ -150,7 +150,7 @@ mod tests { #[test] fn missing_sigil_for_room_id_or_alias_id() { assert_eq!( - RoomIdOrAliasId::try_from("ruma:example.com").err().unwrap(), + RoomIdOrAliasId::try_from("ruma:example.com").unwrap_err(), Error::MissingSigil ); } diff --git a/src/user_id.rs b/src/user_id.rs index 4e31a9ca..f01c03bc 100644 --- a/src/user_id.rs +++ b/src/user_id.rs @@ -236,7 +236,7 @@ mod tests { #[test] fn invalid_characters_in_user_id_localpart() { assert_eq!( - UserId::try_from("@te\nst:example.com").err().unwrap(), + UserId::try_from("@te\nst:example.com").unwrap_err(), Error::InvalidCharacters ); } @@ -244,7 +244,7 @@ mod tests { #[test] fn missing_user_id_sigil() { assert_eq!( - UserId::try_from("carl:example.com").err().unwrap(), + UserId::try_from("carl:example.com").unwrap_err(), Error::MissingSigil ); } @@ -252,25 +252,20 @@ mod tests { #[test] fn missing_user_id_delimiter() { assert_eq!( - UserId::try_from("@carl").err().unwrap(), + UserId::try_from("@carl").unwrap_err(), Error::MissingDelimiter ); } #[test] fn invalid_user_id_host() { - assert_eq!( - UserId::try_from("@carl:/").err().unwrap(), - Error::InvalidHost - ); + assert_eq!(UserId::try_from("@carl:/").unwrap_err(), Error::InvalidHost); } #[test] fn invalid_user_id_port() { assert_eq!( - UserId::try_from("@carl:example.com:notaport") - .err() - .unwrap(), + UserId::try_from("@carl:example.com:notaport").unwrap_err(), Error::InvalidHost ); } From 88fcdb6f9bf76b527b2f1ea2c9ecc27e84f60687 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Sat, 14 Dec 2019 18:19:30 +0100 Subject: [PATCH 099/140] Update changelog, release 0.14.1 --- CHANGELOG.md | 8 ++++++++ Cargo.toml | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2de86b6..492b632f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,17 @@ # [unreleased] +# 0.14.1 + Breaking changes: * Our Minimum Supported Rust Version is now 1.36.0 + * This is done in a patch version because it is only a documentation change. Practially, a new + project using even ruma-identifiers 0.14 won't build out of the box on older versions of Rust + because of an MSRV bump in a minor release of an indirect dependency. Using ruma-identifiers + with older versions of Rust will potentially continue to work with some crates pinned to older + versions, but won't be tested in CI. Improvements: +* Remove the dependency on `lazy_static` and `regex` * We now support [historical user IDs](https://matrix.org/docs/spec/appendices#historical-user-ids) diff --git a/Cargo.toml b/Cargo.toml index 6d448013..3842f5d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ license = "MIT" name = "ruma-identifiers" readme = "README.md" repository = "https://github.com/ruma/ruma-identifiers" -version = "0.14.0" +version = "0.14.1" edition = "2018" [dependencies] From 2a0414cd60592a716770dcb2718051a902843176 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Sun, 15 Dec 2019 20:39:03 +0100 Subject: [PATCH 100/140] Remove unnecessary explicit lifetimes --- src/event_id.rs | 4 ++-- src/room_alias_id.rs | 4 ++-- src/room_id.rs | 4 ++-- src/room_id_or_room_alias_id.rs | 4 ++-- src/room_version_id.rs | 4 ++-- src/user_id.rs | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/event_id.rs b/src/event_id.rs index 1b39685c..e9eb73e6 100644 --- a/src/event_id.rs +++ b/src/event_id.rs @@ -156,7 +156,7 @@ impl<'de> Deserialize<'de> for EventId { } } -impl<'a> TryFrom<&'a str> for EventId { +impl TryFrom<&str> for EventId { type Error = Error; /// Attempts to create a new Matrix event ID from a string representation. @@ -164,7 +164,7 @@ impl<'a> TryFrom<&'a str> for EventId { /// If using the original event format as used by Matrix room versions 1 and 2, the string must /// include the leading $ sigil, the localpart, a literal colon, and a valid homeserver /// hostname. - fn try_from(event_id: &'a str) -> Result { + fn try_from(event_id: &str) -> Result { if event_id.contains(':') { let (localpart, host, port) = parse_id('$', event_id)?; diff --git a/src/room_alias_id.rs b/src/room_alias_id.rs index 418b0878..480b94e1 100644 --- a/src/room_alias_id.rs +++ b/src/room_alias_id.rs @@ -81,14 +81,14 @@ impl<'de> Deserialize<'de> for RoomAliasId { } } -impl<'a> TryFrom<&'a str> for RoomAliasId { +impl TryFrom<&str> for RoomAliasId { type Error = Error; /// 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 valid /// server name. - fn try_from(room_id: &'a str) -> Result { + fn try_from(room_id: &str) -> Result { let (alias, host, port) = parse_id('#', room_id)?; Ok(Self { diff --git a/src/room_id.rs b/src/room_id.rs index 2ffc61bb..606ba6ae 100644 --- a/src/room_id.rs +++ b/src/room_id.rs @@ -96,14 +96,14 @@ impl<'de> Deserialize<'de> for RoomId { } } -impl<'a> TryFrom<&'a str> for RoomId { +impl TryFrom<&str> for RoomId { type Error = Error; /// Attempts to create a new Matrix room ID from a string representation. /// /// The string must include the leading ! sigil, the localpart, a literal colon, and a valid /// server name. - fn try_from(room_id: &'a str) -> Result { + fn try_from(room_id: &str) -> Result { let (localpart, host, port) = parse_id('!', room_id)?; Ok(Self { diff --git a/src/room_id_or_room_alias_id.rs b/src/room_id_or_room_alias_id.rs index cc20b18e..c9a768e4 100644 --- a/src/room_id_or_room_alias_id.rs +++ b/src/room_id_or_room_alias_id.rs @@ -89,7 +89,7 @@ impl<'de> Deserialize<'de> for RoomIdOrAliasId { } } -impl<'a> TryFrom<&'a str> for RoomIdOrAliasId { +impl TryFrom<&str> for RoomIdOrAliasId { type Error = Error; /// Attempts to create a new Matrix room ID or a room alias ID from a string representation. @@ -97,7 +97,7 @@ impl<'a> TryFrom<&'a str> for RoomIdOrAliasId { /// The string must either include the leading ! sigil, the localpart, a literal colon, and a /// valid homeserver host or include the leading # sigil, the alias, a literal colon, and a /// valid homeserver host. - fn try_from(room_id_or_alias_id: &'a str) -> Result { + fn try_from(room_id_or_alias_id: &str) -> Result { validate_id(room_id_or_alias_id)?; let mut chars = room_id_or_alias_id.chars(); diff --git a/src/room_version_id.rs b/src/room_version_id.rs index 6277199b..85772263 100644 --- a/src/room_version_id.rs +++ b/src/room_version_id.rs @@ -155,11 +155,11 @@ impl<'de> Deserialize<'de> for RoomVersionId { } } -impl<'a> TryFrom<&'a str> for RoomVersionId { +impl TryFrom<&str> for RoomVersionId { type Error = Error; /// Attempts to create a new Matrix room version ID from a string representation. - fn try_from(room_version_id: &'a str) -> Result { + fn try_from(room_version_id: &str) -> Result { let version = match room_version_id { "1" => Self(InnerRoomVersionId::Version1), "2" => Self(InnerRoomVersionId::Version2), diff --git a/src/user_id.rs b/src/user_id.rs index f01c03bc..990ccb10 100644 --- a/src/user_id.rs +++ b/src/user_id.rs @@ -114,14 +114,14 @@ impl<'de> Deserialize<'de> for UserId { } } -impl<'a> TryFrom<&'a str> for UserId { +impl TryFrom<&str> for UserId { type Error = Error; /// Attempts to create a new Matrix user ID from a string representation. /// /// The string must include the leading @ sigil, the localpart, a literal colon, and a valid /// server name. - fn try_from(user_id: &'a str) -> Result { + fn try_from(user_id: &str) -> Result { let (localpart, host, port) = parse_id('@', user_id)?; let downcased_localpart = localpart.to_lowercase(); From 7d3cfd769e1b1bf9b823456572e9dfaf6e5151ac Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Sun, 15 Dec 2019 20:40:51 +0100 Subject: [PATCH 101/140] ID deserialization: Borrow from the deserializer if possible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit … by using Cow<'_, str> instead of String --- src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 6a7d9277..1fdcd391 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,6 +15,7 @@ extern crate diesel; use std::{ + borrow::Cow, convert::TryFrom, fmt::{Formatter, Result as FmtResult}, }; @@ -125,7 +126,7 @@ where D: Deserializer<'de>, T: for<'a> TryFrom<&'a str>, { - String::deserialize(deserializer).and_then(|v| { + Cow::<'_, str>::deserialize(deserializer).and_then(|v| { T::try_from(&v).map_err(|_| de::Error::invalid_value(Unexpected::Str(&v), &expected_str)) }) } From 03555a3aed2907fcff28f3d36740d6b873e3bb53 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Mon, 17 Feb 2020 01:08:15 +0100 Subject: [PATCH 102/140] Replace nth(0) with next() --- src/room_id_or_room_alias_id.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/room_id_or_room_alias_id.rs b/src/room_id_or_room_alias_id.rs index c9a768e4..93b7e4bb 100644 --- a/src/room_id_or_room_alias_id.rs +++ b/src/room_id_or_room_alias_id.rs @@ -102,7 +102,7 @@ impl TryFrom<&str> for RoomIdOrAliasId { let mut chars = room_id_or_alias_id.chars(); - let sigil = chars.nth(0).expect("ID missing first character."); + let sigil = chars.next().expect("ID missing first character."); match sigil { '#' => { From 81fb260a4693e87611b0c852515067f6e0d2c25e Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 17 Feb 2020 09:31:46 +0100 Subject: [PATCH 103/140] Add support for historical uppercase user IDs --- CHANGELOG.md | 4 ++++ src/user_id.rs | 20 ++++++-------------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 492b632f..c1bb10c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # [unreleased] +Improvements: + +* Add support for historical uppercase MXIDs + # 0.14.1 Breaking changes: diff --git a/src/user_id.rs b/src/user_id.rs index 990ccb10..a07dd134 100644 --- a/src/user_id.rs +++ b/src/user_id.rs @@ -123,10 +123,9 @@ impl TryFrom<&str> for UserId { /// server name. fn try_from(user_id: &str) -> Result { let (localpart, host, port) = parse_id('@', user_id)?; - let downcased_localpart = localpart.to_lowercase(); // See https://matrix.org/docs/spec/appendices#user-identifiers - let is_fully_conforming = downcased_localpart.bytes().all(|b| match b { + let is_fully_conforming = localpart.bytes().all(|b| match b { b'0'..=b'9' | b'a'..=b'z' | b'-' | b'.' | b'=' | b'_' | b'/' => true, _ => false, }); @@ -134,18 +133,14 @@ impl TryFrom<&str> for UserId { // 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 - && downcased_localpart - .bytes() - .any(|b| b < 0x21 || b == b':' || b > 0x7E) - { + if !is_fully_conforming && localpart.bytes().any(|b| b < 0x21 || b == b':' || b > 0x7E) { return Err(Error::InvalidCharacters); } Ok(Self { hostname: host, port, - localpart: downcased_localpart, + localpart: localpart.to_owned(), is_historical: !is_fully_conforming, }) } @@ -176,12 +171,9 @@ mod tests { #[test] fn downcase_user_id() { - assert_eq!( - UserId::try_from("@CARL:example.com") - .expect("Failed to create UserId.") - .to_string(), - "@carl:example.com" - ); + let user_id = UserId::try_from("@CARL:example.com").expect("Failed to create UserId."); + assert_eq!(user_id.to_string(), "@CARL:example.com"); + assert!(user_id.is_historical()); } #[test] From 04e03db41d3df01e186a0e164cde3223e53f6534 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Fri, 3 Apr 2020 18:54:31 +0200 Subject: [PATCH 104/140] Remove redundant spaces --- src/device_id.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/device_id.rs b/src/device_id.rs index 1dd27bbf..549ec2cc 100644 --- a/src/device_id.rs +++ b/src/device_id.rs @@ -2,10 +2,10 @@ use crate::generate_localpart; -/// A Matrix device ID. +/// A Matrix device ID. /// -/// Device identifiers in Matrix are completely opaque character sequences. This type alias is -/// provided simply for its semantic value. +/// Device identifiers in Matrix are completely opaque character sequences. This type alias is +/// provided simply for its semantic value. pub type DeviceId = String; /// Generates a random `DeviceId`, suitable for assignment to a new device. From cc6a4dbc9183fb576cfd233a357d48755bc55e72 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Fri, 3 Apr 2020 19:10:45 +0200 Subject: [PATCH 105/140] CI: Disable irc notifications --- .travis.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index d8cbf9a9..7d848609 100644 --- a/.travis.yml +++ b/.travis.yml @@ -34,9 +34,3 @@ script: - cargo build --verbose - cargo test --verbose if: "type != push OR (tag IS blank AND branch = master)" -notifications: - email: false - irc: - channels: - - secure: "eyhVIoItrOoGxIRYXWQp+uqeTlbqn0MW2/+1Gl4raWzHkeZBME8GFezNxBR1bQrse3qlYj+/UXA43vHCDeU2+xBmvYujpBfjWD7f7huj1zIag+dNHP7k9sIQ09fmKSY+D3Vwr6Y9/wj+xrPNzL7xsxXCduL3w37QTZNqFWTcVuFetqqEFasSnNlkSCsM7Gv6QsaLNNBWsLT9q3va7PQdFwYhitisBAtI79tTcGpBoPpH4xegnai02fYu7WNCPetdQF7tyZb+ioRt9b159neqBb/BoftOvLEzEHtpirTMXYizlURdR63am/pYaEk0S7//WSatHLs2UB1JD5qDWjCCldZD7q+Xn2JEdNVwWv0vTtKd4jYfbGNbIYXgEIvOpoBrQoIAkfa4mVd8pOaexydFStdW/Q7P+h8ZSJoituq0Z6tCbeb+zVKi9n5Qx9F+OQcXREV6FKo5YE+L0VQ91QNnoJKPNxj1OMk+R2/xwbu2+qcDsrwzhwC6woaboxw1jLUTD/T3VeMIKB3NvDt4UivkKvWugMN0QR72xD3eU3+3yXaGVbmSZhC+rbaXE8x3ZViukbPlBMKvKtCI6nzuHt9lw7cl5qub/hz3Pt379fj92Z5Q7R1RvlwFwm7dJ7zypAsg2ZCWyKP0q4gvnd8SNxJ972YTK+tHZuq56JzVA7mi2IU=" - use_notice: true From 791b5107602213c33a2392ef0161506d6f81c576 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Fri, 10 Apr 2020 14:06:31 +0200 Subject: [PATCH 106/140] Update README.md --- README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 585b7880..fe9daca0 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ # ruma-identifiers -[![Build Status](https://travis-ci.org/ruma/ruma-identifiers.svg?branch=master)](https://travis-ci.org/ruma/ruma-identifiers) +[![crates.io page](https://img.shields.io/crates/v/ruma-identifiers.svg)](https://crates.io/crates/ruma-identifiers) +[![docs.rs page](https://docs.rs/ruma-identifiers/badge.svg)](https://docs.rs/ruma-identifiers/) +[![build status](https://travis-ci.org/ruma/ruma-identifiers.svg?branch=master)](https://travis-ci.org/ruma/ruma-identifiers) +![license: MIT](https://img.shields.io/crates/l/ruma-identifiers.svg) **ruma-identifiers** contains types for [Matrix](https://matrix.org/) identifiers for events, rooms, room aliases, and users. @@ -11,7 +14,3 @@ ruma-identifiers requires Rust 1.36.0 or later. ## Documentation ruma-identifiers has [comprehensive documentation](https://docs.rs/ruma-identifiers) available on docs.rs. - -## License - -[MIT](http://opensource.org/licenses/MIT) From 3945f88e1031f09c6fea8e4ea15b53b1c867d6ca Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Wed, 15 Apr 2020 13:54:31 +0200 Subject: [PATCH 107/140] New representation for identifiers Identifiers now always own just a single allocation, and implement AsRef. PartialOrd and Ord implementations were also added at the same time. All identifiers except RoomVersionId can now also be converted to String without allocation. --- Cargo.toml | 1 - src/diesel_integration.rs | 2 +- src/error.rs | 22 ++-- src/event_id.rs | 178 ++++++++++---------------------- src/lib.rs | 71 ++++--------- src/macros.rs | 84 +++++++++++++++ src/room_alias_id.rs | 90 +++++----------- src/room_id.rs | 110 ++++++-------------- src/room_id_or_room_alias_id.rs | 128 +++++++++-------------- src/room_version_id.rs | 80 +++++++++----- src/user_id.rs | 112 +++++++------------- 11 files changed, 369 insertions(+), 509 deletions(-) create mode 100644 src/macros.rs diff --git a/Cargo.toml b/Cargo.toml index 3842f5d3..1c34e993 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,6 @@ edition = "2018" diesel = { version = "1.4.3", optional = true } rand = "0.7.2" serde = "1.0.102" -url = "2.1.0" [dev-dependencies] serde_json = "1.0.41" diff --git a/src/diesel_integration.rs b/src/diesel_integration.rs index 2daf80c4..92194179 100644 --- a/src/diesel_integration.rs +++ b/src/diesel_integration.rs @@ -16,7 +16,7 @@ macro_rules! diesel_impl { DB: Backend, { fn to_sql(&self, out: &mut Output<'_, W, DB>) -> SerializeResult { - ToSql::::to_sql(&self.to_string(), out) + ToSql::::to_sql(self.as_ref(), out) } } diff --git a/src/error.rs b/src/error.rs index 397bd7be..087a8200 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,11 +1,6 @@ //! Error conditions. -use std::{ - error::Error as StdError, - fmt::{Display, Formatter, Result as FmtResult}, -}; - -use url::ParseError; +use std::fmt::{self, Display, Formatter}; /// An error encountered when trying to parse an invalid ID string. #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] @@ -14,6 +9,8 @@ pub enum Error { /// /// Only relevant for user IDs. InvalidCharacters, + /// The localpart of the ID string is not valid (because it is empty). + InvalidLocalPart, /// The domain part of the the ID string is not a valid IP address or DNS name. InvalidHost, /// The ID exceeds 255 bytes (or 32 codepoints for a room version ID.) @@ -27,9 +24,10 @@ pub enum Error { } impl Display for Error { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - let message = match *self { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let message = match self { Error::InvalidCharacters => "localpart contains invalid characters", + Error::InvalidLocalPart => "localpart is empty", Error::InvalidHost => "server name is not a valid IP address or domain name", Error::MaximumLengthExceeded => "ID exceeds 255 bytes", Error::MinimumLengthNotSatisfied => "ID must be at least 4 characters", @@ -41,10 +39,4 @@ impl Display for Error { } } -impl StdError for Error {} - -impl From for Error { - fn from(_: ParseError) -> Self { - Error::InvalidHost - } -} +impl std::error::Error for Error {} diff --git a/src/event_id.rs b/src/event_id.rs index e9eb73e6..97c1545a 100644 --- a/src/event_id.rs +++ b/src/event_id.rs @@ -1,16 +1,11 @@ //! Matrix event identifiers. -use std::{ - convert::TryFrom, - fmt::{Display, Formatter, Result as FmtResult}, -}; +use std::{borrow::Cow, convert::TryFrom, num::NonZeroU8}; #[cfg(feature = "diesel")] use diesel::sql_types::Text; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use url::Host; -use crate::{deserialize_id, display, error::Error, generate_localpart, parse_id}; +use crate::{error::Error, generate_localpart, parse_id, validate_id}; /// A Matrix event ID. /// @@ -31,45 +26,26 @@ use crate::{deserialize_id, display, error::Error, generate_localpart, parse_id} /// # use ruma_identifiers::EventId; /// // Original format /// assert_eq!( -/// EventId::try_from("$h29iv0s8:example.com").unwrap().to_string(), +/// EventId::try_from("$h29iv0s8:example.com").unwrap().as_ref(), /// "$h29iv0s8:example.com" /// ); /// // Room version 3 format /// assert_eq!( -/// EventId::try_from("$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk").unwrap().to_string(), +/// EventId::try_from("$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk").unwrap().as_ref(), /// "$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk" /// ); /// // Room version 4 format /// assert_eq!( -/// EventId::try_from("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg").unwrap().to_string(), +/// EventId::try_from("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg").unwrap().as_ref(), /// "$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg" /// ); /// ``` -#[derive(Clone, Debug, Eq, Hash, PartialEq)] +#[derive(Clone, Debug)] #[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] #[cfg_attr(feature = "diesel", sql_type = "Text")] -pub struct EventId(Format); - -/// Different event ID formats from the different Matrix room versions. -#[derive(Clone, Debug, Eq, Hash, PartialEq)] -enum Format { - /// The original format as used by Matrix room versions 1 and 2. - Original(Original), - /// The format used by Matrix room version 3. - Base64(String), - /// The format used by Matrix room version 4. - UrlSafeBase64(String), -} - -/// An event in the original format as used by Matrix room versions 1 and 2. -#[derive(Clone, Debug, Eq, Hash, PartialEq)] -struct Original { - /// The hostname of the homeserver. - pub hostname: Host, - /// The event's unique ID. - pub localpart: String, - /// The network port of the homeserver. - pub port: u16, +pub struct EventId { + full_id: String, + colon_idx: Option, } impl EventId { @@ -77,86 +53,39 @@ impl EventId { /// of 18 random ASCII characters. This should only be used for events in the original format /// as used by Matrix room versions 1 and 2. /// - /// Fails if the homeserver cannot be parsed as a valid host. + /// Does not currently ever fail, but may fail in the future if the homeserver cannot be parsed + /// parsed as a valid host. pub fn new(homeserver_host: &str) -> Result { - let event_id = format!("${}:{}", generate_localpart(18), homeserver_host); - let (localpart, host, port) = parse_id('$', &event_id)?; + let full_id = format!("${}:{}", generate_localpart(18), homeserver_host); - Ok(Self(Format::Original(Original { - hostname: host, - localpart: localpart.to_string(), - port, - }))) + Ok(Self { + full_id, + colon_idx: NonZeroU8::new(19), + }) } - /// Returns a `Host` for the event ID, containing the server name (minus the port) of the + /// Returns the host of the event ID, containing the server name (including the port) of the /// originating homeserver. Only applicable to events in the original format as used by Matrix /// room versions 1 and 2. - /// - /// The host can be either a domain name, an IPv4 address, or an IPv6 address. - pub fn hostname(&self) -> Option<&Host> { - if let Format::Original(original) = &self.0 { - Some(&original.hostname) - } else { - None - } + pub fn hostname(&self) -> Option<&str> { + self.colon_idx + .map(|idx| &self.full_id[idx.get() as usize + 1..]) } /// Returns the event's unique ID. For the original event format as used by Matrix room /// versions 1 and 2, this is the "localpart" that precedes the homeserver. For later formats, /// this is the entire ID without the leading $ sigil. pub fn localpart(&self) -> &str { - match &self.0 { - Format::Original(original) => &original.localpart, - Format::Base64(id) | Format::UrlSafeBase64(id) => id, - } - } + let idx = match self.colon_idx { + Some(idx) => idx.get() as usize, + None => self.full_id.len(), + }; - /// Returns the port the originating homeserver can be accessed on. Only applicable to events - /// in the original format as used by Matrix room versions 1 and 2. - pub fn port(&self) -> Option { - if let Format::Original(original) = &self.0 { - Some(original.port) - } else { - None - } + &self.full_id[1..idx] } } -impl Display for EventId { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - match &self.0 { - Format::Original(original) => display( - f, - '$', - &original.localpart, - &original.hostname, - original.port, - ), - Format::Base64(id) | Format::UrlSafeBase64(id) => write!(f, "${}", id), - } - } -} - -impl Serialize for EventId { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -impl<'de> Deserialize<'de> for EventId { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserialize_id(deserializer, "a Matrix event ID as a string") - } -} - -impl TryFrom<&str> for EventId { +impl TryFrom> for EventId { type Error = Error; /// Attempts to create a new Matrix event ID from a string representation. @@ -164,25 +93,27 @@ impl TryFrom<&str> for EventId { /// If using the original event format as used by Matrix room versions 1 and 2, the string must /// include the leading $ sigil, the localpart, a literal colon, and a valid homeserver /// hostname. - fn try_from(event_id: &str) -> Result { + fn try_from(event_id: Cow<'_, str>) -> Result { if event_id.contains(':') { - let (localpart, host, port) = parse_id('$', event_id)?; + let colon_idx = parse_id(&event_id, &['$'])?; - Ok(Self(Format::Original(Original { - hostname: host, - localpart: localpart.to_owned(), - port, - }))) - } else if !event_id.starts_with('$') { - Err(Error::MissingSigil) - } else if event_id.contains(|chr| chr == '+' || chr == '/') { - Ok(Self(Format::Base64(event_id[1..].to_string()))) + Ok(Self { + full_id: event_id.into_owned(), + colon_idx: Some(colon_idx), + }) } else { - Ok(Self(Format::UrlSafeBase64(event_id[1..].to_string()))) + validate_id(&event_id, &['$'])?; + + Ok(Self { + full_id: event_id.into_owned(), + colon_idx: None, + }) } } } +common_impls!(EventId, "a Matrix event ID"); + #[cfg(test)] mod tests { use std::convert::TryFrom; @@ -197,7 +128,7 @@ mod tests { assert_eq!( EventId::try_from("$39hvsi03hlne:example.com") .expect("Failed to create EventId.") - .to_string(), + .as_ref(), "$39hvsi03hlne:example.com" ); } @@ -207,7 +138,7 @@ mod tests { assert_eq!( EventId::try_from("$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk") .expect("Failed to create EventId.") - .to_string(), + .as_ref(), "$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk" ) } @@ -217,25 +148,24 @@ mod tests { assert_eq!( EventId::try_from("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg") .expect("Failed to create EventId.") - .to_string(), + .as_ref(), "$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg" ) } #[test] fn generate_random_valid_event_id() { - let event_id = EventId::new("example.com") - .expect("Failed to generate EventId.") - .to_string(); + let event_id = EventId::new("example.com").expect("Failed to generate EventId."); + let id_str: &str = event_id.as_ref(); - assert!(event_id.to_string().starts_with('$')); - assert_eq!(event_id.len(), 31); + assert!(id_str.starts_with('$')); + assert_eq!(id_str.len(), 31); } - #[test] + /*#[test] fn generate_random_invalid_event_id() { assert!(EventId::new("").is_err()); - } + }*/ #[test] fn serialize_valid_original_event_id() { @@ -306,8 +236,8 @@ mod tests { assert_eq!( EventId::try_from("$39hvsi03hlne:example.com:443") .expect("Failed to create EventId.") - .to_string(), - "$39hvsi03hlne:example.com" + .as_ref(), + "$39hvsi03hlne:example.com:443" ); } @@ -316,7 +246,7 @@ mod tests { assert_eq!( EventId::try_from("$39hvsi03hlne:example.com:5000") .expect("Failed to create EventId.") - .to_string(), + .as_ref(), "$39hvsi03hlne:example.com:5000" ); } @@ -345,7 +275,7 @@ mod tests { ); } - #[test] + /*#[test] fn invalid_event_id_host() { assert_eq!( EventId::try_from("$39hvsi03hlne:/").unwrap_err(), @@ -359,5 +289,5 @@ mod tests { EventId::try_from("$39hvsi03hlne:example.com:notaport").unwrap_err(), Error::InvalidHost ); - } + }*/ } diff --git a/src/lib.rs b/src/lib.rs index 1fdcd391..666799e0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,17 +14,10 @@ #[cfg_attr(feature = "diesel", macro_use)] extern crate diesel; -use std::{ - borrow::Cow, - convert::TryFrom, - fmt::{Formatter, Result as FmtResult}, -}; +use std::{borrow::Cow, convert::TryFrom, num::NonZeroU8}; use rand::{distributions::Alphanumeric, thread_rng, Rng}; use serde::de::{self, Deserialize as _, Deserializer, Unexpected}; -use url::Url; - -pub use url::Host; #[doc(inline)] pub use crate::device_id::DeviceId; @@ -33,6 +26,9 @@ pub use crate::{ room_id_or_room_alias_id::RoomIdOrAliasId, room_version_id::RoomVersionId, user_id::UserId, }; +#[macro_use] +mod macros; + pub mod device_id; #[cfg(feature = "diesel")] mod diesel_integration; @@ -51,23 +47,6 @@ const MAX_BYTES: usize = 255; /// 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; -/// The number of bytes in a valid sigil. -const SIGIL_BYTES: usize = 1; - -/// `Display` implementation shared by identifier types. -fn display( - f: &mut Formatter<'_>, - sigil: char, - localpart: &str, - hostname: &Host, - port: u16, -) -> FmtResult { - if port == 443 { - write!(f, "{}{}:{}", sigil, localpart, hostname) - } else { - write!(f, "{}{}:{}:{}", sigil, localpart, hostname, port) - } -} /// Generates a random identifier localpart. fn generate_localpart(length: usize) -> String { @@ -77,8 +56,8 @@ fn generate_localpart(length: usize) -> String { .collect() } -/// Checks if an identifier is within the acceptable byte lengths. -fn validate_id(id: &str) -> Result<(), Error> { +/// 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); } @@ -87,35 +66,27 @@ fn validate_id(id: &str) -> Result<(), Error> { return Err(Error::MinimumLengthNotSatisfied); } - Ok(()) -} - -/// Parses the localpart, host, and port from a string identifier. -fn parse_id(required_sigil: char, id: &str) -> Result<(&str, Host, u16), Error> { - validate_id(id)?; - - if !id.starts_with(required_sigil) { + if !valid_sigils.contains(&id.chars().next().unwrap()) { return Err(Error::MissingSigil); } - let delimiter_index = match id.find(':') { - Some(index) => index, - None => return Err(Error::MissingDelimiter), - }; + Ok(()) +} - let localpart = &id[1..delimiter_index]; - let raw_host = &id[delimiter_index + SIGIL_BYTES..]; - let url_string = format!("https://{}", raw_host); - let url = Url::parse(&url_string)?; +/// 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 host = match url.host() { - Some(host) => host.to_owned(), - None => return Err(Error::InvalidHost), - }; + let colon_idx = id.find(':').ok_or(Error::MissingDelimiter)?; + if colon_idx == id.len() - 1 { + return Err(Error::InvalidHost); + } - let port = url.port().unwrap_or(443); - - Ok((localpart, host, port)) + match NonZeroU8::new(colon_idx as u8) { + Some(idx) => Ok(idx), + None => Err(Error::InvalidLocalPart), + } } /// Deserializes any type of id using the provided TryFrom implementation. diff --git a/src/macros.rs b/src/macros.rs new file mode 100644 index 00000000..8289823d --- /dev/null +++ b/src/macros.rs @@ -0,0 +1,84 @@ +macro_rules! common_impls { + ($id:ident, $desc:literal) => { + impl ::std::convert::From<$id> for ::std::string::String { + fn from(id: $id) -> Self { + id.full_id + } + } + + impl ::std::convert::TryFrom<&str> for $id { + type Error = crate::error::Error; + + fn try_from(s: &str) -> Result { + Self::try_from(::std::borrow::Cow::Borrowed(s)) + } + } + + impl ::std::convert::TryFrom for $id { + type Error = crate::error::Error; + + fn try_from(s: String) -> Result { + Self::try_from(::std::borrow::Cow::Owned(s)) + } + } + + impl ::std::convert::AsRef for $id { + fn as_ref(&self) -> &str { + &self.full_id + } + } + + impl ::std::fmt::Display for $id { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + write!(f, "{}", self.full_id) + } + } + + impl ::std::cmp::PartialEq for $id { + fn eq(&self, other: &Self) -> bool { + self.full_id == other.full_id + } + } + + impl ::std::cmp::Eq for $id {} + + impl ::std::cmp::PartialOrd for $id { + fn partial_cmp(&self, other: &Self) -> Option<::std::cmp::Ordering> { + <::std::string::String as ::std::cmp::PartialOrd>::partial_cmp( + &self.full_id, + &other.full_id, + ) + } + } + + impl ::std::cmp::Ord for $id { + fn cmp(&self, other: &Self) -> ::std::cmp::Ordering { + <::std::string::String as ::std::cmp::Ord>::cmp(&self.full_id, &other.full_id) + } + } + + impl ::std::hash::Hash for $id { + fn hash(&self, state: &mut H) { + self.full_id.hash(state); + } + } + + impl ::serde::Serialize for $id { + fn serialize(&self, serializer: S) -> Result + where + S: ::serde::Serializer, + { + serializer.serialize_str(&self.full_id) + } + } + + impl<'de> ::serde::Deserialize<'de> for $id { + fn deserialize(deserializer: D) -> Result + where + D: ::serde::Deserializer<'de>, + { + crate::deserialize_id(deserializer, $desc) + } + } + }; +} diff --git a/src/room_alias_id.rs b/src/room_alias_id.rs index 480b94e1..54c05a37 100644 --- a/src/room_alias_id.rs +++ b/src/room_alias_id.rs @@ -1,16 +1,11 @@ //! Matrix room alias identifiers. -use std::{ - convert::TryFrom, - fmt::{Display, Formatter, Result as FmtResult}, -}; +use std::{borrow::Cow, convert::TryFrom, num::NonZeroU8}; #[cfg(feature = "diesel")] use diesel::sql_types::Text; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use url::Host; -use crate::{deserialize_id, display, error::Error, parse_id}; +use crate::{error::Error, parse_id}; /// A Matrix room alias ID. /// @@ -21,84 +16,49 @@ use crate::{deserialize_id, display, error::Error, parse_id}; /// # use std::convert::TryFrom; /// # use ruma_identifiers::RoomAliasId; /// assert_eq!( -/// RoomAliasId::try_from("#ruma:example.com").unwrap().to_string(), +/// RoomAliasId::try_from("#ruma:example.com").unwrap().as_ref(), /// "#ruma:example.com" /// ); /// ``` -#[derive(Clone, Debug, Eq, Hash, PartialEq)] +#[derive(Clone, Debug)] #[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] #[cfg_attr(feature = "diesel", sql_type = "Text")] pub struct RoomAliasId { - /// The alias for the room. - alias: String, - /// The hostname of the homeserver. - hostname: Host, - /// The network port of the homeserver. - port: u16, + full_id: String, + colon_idx: NonZeroU8, } impl RoomAliasId { - /// Returns a `Host` for the room alias ID, containing the server name (minus the port) of + /// Returns the host of the room alias ID, containing the server name (including the port) of /// the originating homeserver. - /// - /// The host can be either a domain name, an IPv4 address, or an IPv6 address. - pub fn hostname(&self) -> &Host { - &self.hostname + pub fn hostname(&self) -> &str { + &self.full_id[self.colon_idx.get() as usize + 1..] } /// Returns the room's alias. pub fn alias(&self) -> &str { - &self.alias - } - - /// Returns the port the originating homeserver can be accessed on. - pub fn port(&self) -> u16 { - self.port + &self.full_id[1..self.colon_idx.get() as usize] } } -impl Display for RoomAliasId { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - display(f, '#', &self.alias, &self.hostname, self.port) - } -} - -impl Serialize for RoomAliasId { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -impl<'de> Deserialize<'de> for RoomAliasId { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserialize_id(deserializer, "a Matrix room alias ID as a string") - } -} - -impl TryFrom<&str> for RoomAliasId { +impl TryFrom> for RoomAliasId { type Error = Error; /// 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 valid - /// server name. - fn try_from(room_id: &str) -> Result { - let (alias, host, port) = parse_id('#', room_id)?; + /// The string must include the leading # sigil, the alias, a literal colon, and a server name. + fn try_from(room_id: Cow<'_, str>) -> Result { + let colon_idx = parse_id(&room_id, &['#'])?; Ok(Self { - alias: alias.to_owned(), - hostname: host, - port, + full_id: room_id.into_owned(), + colon_idx, }) } } +common_impls!(RoomAliasId, "a Matrix room alias ID"); + #[cfg(test)] mod tests { use std::convert::TryFrom; @@ -113,7 +73,7 @@ mod tests { assert_eq!( RoomAliasId::try_from("#ruma:example.com") .expect("Failed to create RoomAliasId.") - .to_string(), + .as_ref(), "#ruma:example.com" ); } @@ -143,8 +103,8 @@ mod tests { assert_eq!( RoomAliasId::try_from("#ruma:example.com:443") .expect("Failed to create RoomAliasId.") - .to_string(), - "#ruma:example.com" + .as_ref(), + "#ruma:example.com:443" ); } @@ -153,7 +113,7 @@ mod tests { assert_eq!( RoomAliasId::try_from("#ruma:example.com:5000") .expect("Failed to create RoomAliasId.") - .to_string(), + .as_ref(), "#ruma:example.com:5000" ); } @@ -163,7 +123,7 @@ mod tests { assert_eq!( RoomAliasId::try_from("#老虎£я:example.com") .expect("Failed to create RoomAliasId.") - .to_string(), + .as_ref(), "#老虎£я:example.com" ); } @@ -184,7 +144,7 @@ mod tests { ); } - #[test] + /*#[test] fn invalid_room_alias_id_host() { assert_eq!( RoomAliasId::try_from("#ruma:/").unwrap_err(), @@ -198,5 +158,5 @@ mod tests { RoomAliasId::try_from("#ruma:example.com:notaport").unwrap_err(), Error::InvalidHost ); - } + }*/ } diff --git a/src/room_id.rs b/src/room_id.rs index 606ba6ae..c80d3c54 100644 --- a/src/room_id.rs +++ b/src/room_id.rs @@ -1,16 +1,11 @@ //! Matrix room identifiers. -use std::{ - convert::TryFrom, - fmt::{Display, Formatter, Result as FmtResult}, -}; +use std::{borrow::Cow, convert::TryFrom, num::NonZeroU8}; #[cfg(feature = "diesel")] use diesel::sql_types::Text; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use url::Host; -use crate::{deserialize_id, display, error::Error, generate_localpart, parse_id}; +use crate::{error::Error, generate_localpart, parse_id}; /// A Matrix room ID. /// @@ -21,20 +16,16 @@ use crate::{deserialize_id, display, error::Error, generate_localpart, parse_id} /// # use std::convert::TryFrom; /// # use ruma_identifiers::RoomId; /// assert_eq!( -/// RoomId::try_from("!n8f893n9:example.com").unwrap().to_string(), +/// RoomId::try_from("!n8f893n9:example.com").unwrap().as_ref(), /// "!n8f893n9:example.com" /// ); /// ``` -#[derive(Clone, Debug, Eq, Hash, PartialEq)] +#[derive(Clone, Debug)] #[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] #[cfg_attr(feature = "diesel", sql_type = "Text")] pub struct RoomId { - /// The hostname of the homeserver. - hostname: Host, - /// The room's unique ID. - localpart: String, - /// The network port of the homeserver. - port: u16, + full_id: String, + colon_idx: NonZeroU8, } impl RoomId { @@ -43,77 +34,45 @@ impl RoomId { /// /// Fails if the given homeserver cannot be parsed as a valid host. pub fn new(homeserver_host: &str) -> Result { - let room_id = format!("!{}:{}", generate_localpart(18), homeserver_host); - let (localpart, host, port) = parse_id('!', &room_id)?; + let full_id = format!("!{}:{}", generate_localpart(18), homeserver_host); Ok(Self { - hostname: host, - localpart: localpart.to_string(), - port, + full_id, + colon_idx: NonZeroU8::new(19).unwrap(), }) } - /// Returns a `Host` for the room ID, containing the server name (minus the port) of the + /// Returns the host of the room ID, containing the server name (including the port) of the /// originating homeserver. - /// - /// The host can be either a domain name, an IPv4 address, or an IPv6 address. - pub fn hostname(&self) -> &Host { - &self.hostname + pub fn hostname(&self) -> &str { + &self.full_id[self.colon_idx.get() as usize + 1..] } /// Returns the rooms's unique ID. pub fn localpart(&self) -> &str { - &self.localpart - } - - /// Returns the port the originating homeserver can be accessed on. - pub fn port(&self) -> u16 { - self.port + &self.full_id[1..self.colon_idx.get() as usize] } } -impl Display for RoomId { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - display(f, '!', &self.localpart, &self.hostname, self.port) - } -} - -impl Serialize for RoomId { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -impl<'de> Deserialize<'de> for RoomId { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserialize_id(deserializer, "a Matrix room ID as a string") - } -} - -impl TryFrom<&str> for RoomId { +impl TryFrom> for RoomId { type Error = Error; /// Attempts to create a new Matrix room ID from a string representation. /// - /// The string must include the leading ! sigil, the localpart, a literal colon, and a valid - /// server name. - fn try_from(room_id: &str) -> Result { - let (localpart, host, port) = parse_id('!', room_id)?; + /// The string must include the leading ! sigil, the localpart, a literal colon, and a server + /// name. + fn try_from(room_id: Cow<'_, str>) -> Result { + let colon_idx = parse_id(&room_id, &['!'])?; Ok(Self { - hostname: host, - localpart: localpart.to_owned(), - port, + full_id: room_id.into_owned(), + colon_idx, }) } } +common_impls!(RoomId, "a Matrix room ID"); + #[cfg(test)] mod tests { use std::convert::TryFrom; @@ -128,25 +87,24 @@ mod tests { assert_eq!( RoomId::try_from("!29fhd83h92h0:example.com") .expect("Failed to create RoomId.") - .to_string(), + .as_ref(), "!29fhd83h92h0:example.com" ); } #[test] fn generate_random_valid_room_id() { - let room_id = RoomId::new("example.com") - .expect("Failed to generate RoomId.") - .to_string(); + let room_id = RoomId::new("example.com").expect("Failed to generate RoomId."); + let id_str: &str = room_id.as_ref(); - assert!(room_id.to_string().starts_with('!')); - assert_eq!(room_id.len(), 31); + assert!(id_str.starts_with('!')); + assert_eq!(id_str.len(), 31); } - #[test] + /*#[test] fn generate_random_invalid_room_id() { assert!(RoomId::new("").is_err()); - } + }*/ #[test] fn serialize_valid_room_id() { @@ -173,8 +131,8 @@ mod tests { assert_eq!( RoomId::try_from("!29fhd83h92h0:example.com:443") .expect("Failed to create RoomId.") - .to_string(), - "!29fhd83h92h0:example.com" + .as_ref(), + "!29fhd83h92h0:example.com:443" ); } @@ -183,7 +141,7 @@ mod tests { assert_eq!( RoomId::try_from("!29fhd83h92h0:example.com:5000") .expect("Failed to create RoomId.") - .to_string(), + .as_ref(), "!29fhd83h92h0:example.com:5000" ); } @@ -204,7 +162,7 @@ mod tests { ); } - #[test] + /*#[test] fn invalid_room_id_host() { assert_eq!( RoomId::try_from("!29fhd83h92h0:/").unwrap_err(), @@ -218,5 +176,5 @@ mod tests { RoomId::try_from("!29fhd83h92h0:example.com:notaport").unwrap_err(), Error::InvalidHost ); - } + }*/ } diff --git a/src/room_id_or_room_alias_id.rs b/src/room_id_or_room_alias_id.rs index 93b7e4bb..a83b019e 100644 --- a/src/room_id_or_room_alias_id.rs +++ b/src/room_id_or_room_alias_id.rs @@ -1,17 +1,11 @@ //! Matrix identifiers for places where a room ID or room alias ID are used interchangeably. -use std::{ - convert::TryFrom, - fmt::{Display, Formatter, Result as FmtResult}, -}; +use std::{borrow::Cow, convert::TryFrom, hint::unreachable_unchecked, num::NonZeroU8}; #[cfg(feature = "diesel")] use diesel::sql_types::Text; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use crate::{ - deserialize_id, display, error::Error, room_alias_id::RoomAliasId, room_id::RoomId, validate_id, -}; +use crate::{error::Error, parse_id}; /// A Matrix room ID or a Matrix room alias ID. /// @@ -23,73 +17,61 @@ use crate::{ /// # use std::convert::TryFrom; /// # use ruma_identifiers::RoomIdOrAliasId; /// assert_eq!( -/// RoomIdOrAliasId::try_from("#ruma:example.com").unwrap().to_string(), +/// RoomIdOrAliasId::try_from("#ruma:example.com").unwrap().as_ref(), /// "#ruma:example.com" /// ); /// /// assert_eq!( -/// RoomIdOrAliasId::try_from("!n8f893n9:example.com").unwrap().to_string(), +/// RoomIdOrAliasId::try_from("!n8f893n9:example.com").unwrap().as_ref(), /// "!n8f893n9:example.com" /// ); /// ``` -#[derive(Clone, Debug, Eq, Hash, PartialEq)] +#[derive(Clone, Debug)] #[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] #[cfg_attr(feature = "diesel", sql_type = "Text")] -pub enum RoomIdOrAliasId { - /// A Matrix room alias ID. - RoomAliasId(RoomAliasId), - /// A Matrix room ID. - RoomId(RoomId), +pub struct RoomIdOrAliasId { + full_id: String, + colon_idx: NonZeroU8, } -impl Display for RoomIdOrAliasId { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - match *self { - RoomIdOrAliasId::RoomAliasId(ref room_alias_id) => display( - f, - '#', - room_alias_id.alias(), - room_alias_id.hostname(), - room_alias_id.port(), - ), - RoomIdOrAliasId::RoomId(ref room_id) => display( - f, - '!', - room_id.localpart(), - room_id.hostname(), - room_id.port(), - ), +impl RoomIdOrAliasId { + /// Returns the host of the room (alias) ID, containing the server name (including the port) of + /// the originating homeserver. + pub fn hostname(&self) -> &str { + &self.full_id[self.colon_idx.get() as usize + 1..] + } + + /// Returns the local part (everything after the `!` or `#` and before the first colon). + pub fn localpart(&self) -> &str { + &self.full_id[1..self.colon_idx.get() as usize] + } + + /// Whether this is a room id (starts with `'!'`) + pub fn is_room_id(&self) -> bool { + self.variant() == Variant::RoomId + } + + /// Whether this is a room alias id (starts with `'#'`) + pub fn is_room_alias_id(&self) -> bool { + self.variant() == Variant::RoomAliasId + } + + fn variant(&self) -> Variant { + match self.full_id.bytes().next() { + Some(b'!') => Variant::RoomId, + Some(b'#') => Variant::RoomAliasId, + _ => unsafe { unreachable_unchecked() }, } } } -impl Serialize for RoomIdOrAliasId { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - match *self { - RoomIdOrAliasId::RoomAliasId(ref room_alias_id) => { - serializer.serialize_str(&room_alias_id.to_string()) - } - RoomIdOrAliasId::RoomId(ref room_id) => serializer.serialize_str(&room_id.to_string()), - } - } +#[derive(PartialEq)] +enum Variant { + RoomId, + RoomAliasId, } -impl<'de> Deserialize<'de> for RoomIdOrAliasId { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserialize_id( - deserializer, - "a Matrix room ID or room alias ID as a string", - ) - } -} - -impl TryFrom<&str> for RoomIdOrAliasId { +impl TryFrom> for RoomIdOrAliasId { type Error = Error; /// Attempts to create a new Matrix room ID or a room alias ID from a string representation. @@ -97,27 +79,17 @@ impl TryFrom<&str> for RoomIdOrAliasId { /// The string must either include the leading ! sigil, the localpart, a literal colon, and a /// valid homeserver host or include the leading # sigil, the alias, a literal colon, and a /// valid homeserver host. - fn try_from(room_id_or_alias_id: &str) -> Result { - validate_id(room_id_or_alias_id)?; - - let mut chars = room_id_or_alias_id.chars(); - - let sigil = chars.next().expect("ID missing first character."); - - match sigil { - '#' => { - let room_alias_id = RoomAliasId::try_from(room_id_or_alias_id)?; - Ok(RoomIdOrAliasId::RoomAliasId(room_alias_id)) - } - '!' => { - let room_id = RoomId::try_from(room_id_or_alias_id)?; - Ok(RoomIdOrAliasId::RoomId(room_id)) - } - _ => Err(Error::MissingSigil), - } + fn try_from(room_id_or_alias_id: Cow<'_, str>) -> Result { + let colon_idx = parse_id(&room_id_or_alias_id, &['#', '!'])?; + Ok(Self { + full_id: room_id_or_alias_id.into_owned(), + colon_idx, + }) } } +common_impls!(RoomIdOrAliasId, "a Matrix room ID or room alias ID"); + #[cfg(test)] mod tests { use std::convert::TryFrom; @@ -132,7 +104,7 @@ mod tests { assert_eq!( RoomIdOrAliasId::try_from("#ruma:example.com") .expect("Failed to create RoomAliasId.") - .to_string(), + .as_ref(), "#ruma:example.com" ); } @@ -142,7 +114,7 @@ mod tests { assert_eq!( RoomIdOrAliasId::try_from("!29fhd83h92h0:example.com") .expect("Failed to create RoomId.") - .to_string(), + .as_ref(), "!29fhd83h92h0:example.com" ); } diff --git a/src/room_version_id.rs b/src/room_version_id.rs index 85772263..77de552e 100644 --- a/src/room_version_id.rs +++ b/src/room_version_id.rs @@ -1,8 +1,9 @@ //! Matrix room version identifiers. use std::{ + borrow::Cow, convert::TryFrom, - fmt::{Display, Formatter, Result as FmtResult}, + fmt::{self, Display, Formatter}, }; #[cfg(feature = "diesel")] @@ -22,7 +23,7 @@ const MAX_CODE_POINTS: usize = 32; /// ``` /// # use std::convert::TryFrom; /// # use ruma_identifiers::RoomVersionId; -/// assert_eq!(RoomVersionId::try_from("1").unwrap().to_string(), "1"); +/// assert_eq!(RoomVersionId::try_from("1").unwrap().as_ref(), "1"); /// ``` #[derive(Clone, Debug, Eq, Hash, PartialEq)] #[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] @@ -31,7 +32,7 @@ pub struct RoomVersionId(InnerRoomVersionId); /// Possibile values for room version, distinguishing between official Matrix versions and custom /// versions. -#[derive(Clone, Debug, Eq, Hash, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] enum InnerRoomVersionId { /// A version 1 room. Version1, @@ -79,8 +80,8 @@ impl RoomVersionId { } /// Creates a custom room version ID from the given string slice. - pub fn custom(id: &str) -> Self { - Self(InnerRoomVersionId::Custom(id.to_string())) + pub fn custom(id: String) -> Self { + Self(InnerRoomVersionId::Custom(id)) } /// Whether or not this room version is an official one specified by the Matrix protocol. @@ -122,18 +123,35 @@ impl RoomVersionId { } } -impl Display for RoomVersionId { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - let message = match self.0 { +impl From for String { + fn from(id: RoomVersionId) -> Self { + match id.0 { + InnerRoomVersionId::Version1 => "1".to_owned(), + InnerRoomVersionId::Version2 => "2".to_owned(), + InnerRoomVersionId::Version3 => "3".to_owned(), + InnerRoomVersionId::Version4 => "4".to_owned(), + InnerRoomVersionId::Version5 => "5".to_owned(), + InnerRoomVersionId::Custom(version) => version, + } + } +} + +impl AsRef for RoomVersionId { + fn as_ref(&self) -> &str { + match &self.0 { InnerRoomVersionId::Version1 => "1", InnerRoomVersionId::Version2 => "2", InnerRoomVersionId::Version3 => "3", InnerRoomVersionId::Version4 => "4", InnerRoomVersionId::Version5 => "5", - InnerRoomVersionId::Custom(ref version) => version, - }; + InnerRoomVersionId::Custom(version) => version, + } + } +} - write!(f, "{}", message) +impl Display for RoomVersionId { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.as_ref()) } } @@ -142,7 +160,7 @@ impl Serialize for RoomVersionId { where S: Serializer, { - serializer.serialize_str(&self.to_string()) + serializer.serialize_str(self.as_ref()) } } @@ -155,12 +173,12 @@ impl<'de> Deserialize<'de> for RoomVersionId { } } -impl TryFrom<&str> for RoomVersionId { +impl TryFrom> for RoomVersionId { type Error = Error; /// Attempts to create a new Matrix room version ID from a string representation. - fn try_from(room_version_id: &str) -> Result { - let version = match room_version_id { + fn try_from(room_version_id: Cow<'_, str>) -> Result { + let version = match &room_version_id as &str { "1" => Self(InnerRoomVersionId::Version1), "2" => Self(InnerRoomVersionId::Version2), "3" => Self(InnerRoomVersionId::Version3), @@ -172,7 +190,7 @@ impl TryFrom<&str> for RoomVersionId { } else if custom.chars().count() > MAX_CODE_POINTS { return Err(Error::MaximumLengthExceeded); } else { - Self(InnerRoomVersionId::Custom(custom.to_string())) + Self(InnerRoomVersionId::Custom(room_version_id.into_owned())) } } }; @@ -181,6 +199,22 @@ impl TryFrom<&str> for RoomVersionId { } } +impl TryFrom<&str> for RoomVersionId { + type Error = crate::error::Error; + + fn try_from(s: &str) -> Result { + Self::try_from(Cow::Borrowed(s)) + } +} + +impl TryFrom for RoomVersionId { + type Error = crate::error::Error; + + fn try_from(s: String) -> Result { + Self::try_from(Cow::Owned(s)) + } +} + #[cfg(test)] mod tests { use std::convert::TryFrom; @@ -195,7 +229,7 @@ mod tests { assert_eq!( RoomVersionId::try_from("1") .expect("Failed to create RoomVersionId.") - .to_string(), + .as_ref(), "1" ); } @@ -205,7 +239,7 @@ mod tests { assert_eq!( RoomVersionId::try_from("2") .expect("Failed to create RoomVersionId.") - .to_string(), + .as_ref(), "2" ); } @@ -215,7 +249,7 @@ mod tests { assert_eq!( RoomVersionId::try_from("3") .expect("Failed to create RoomVersionId.") - .to_string(), + .as_ref(), "3" ); } @@ -225,7 +259,7 @@ mod tests { assert_eq!( RoomVersionId::try_from("4") .expect("Failed to create RoomVersionId.") - .to_string(), + .as_ref(), "4" ); } @@ -235,7 +269,7 @@ mod tests { assert_eq!( RoomVersionId::try_from("5") .expect("Failed to create RoomVersionId.") - .to_string(), + .as_ref(), "5" ); } @@ -245,7 +279,7 @@ mod tests { assert_eq!( RoomVersionId::try_from("io.ruma.1") .expect("Failed to create RoomVersionId.") - .to_string(), + .as_ref(), "io.ruma.1" ); } @@ -320,7 +354,7 @@ mod tests { assert!(RoomVersionId::version_3().is_version_3()); assert!(RoomVersionId::version_4().is_version_4()); assert!(RoomVersionId::version_5().is_version_5()); - assert!(RoomVersionId::custom("foo").is_custom()); + assert!(RoomVersionId::custom("foo".into()).is_custom()); } #[test] diff --git a/src/user_id.rs b/src/user_id.rs index a07dd134..d0fb1ea5 100644 --- a/src/user_id.rs +++ b/src/user_id.rs @@ -1,16 +1,11 @@ //! Matrix user identifiers. -use std::{ - convert::TryFrom, - fmt::{Display, Formatter, Result as FmtResult}, -}; +use std::{borrow::Cow, convert::TryFrom, num::NonZeroU8}; #[cfg(feature = "diesel")] use diesel::sql_types::Text; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use url::Host; -use crate::{deserialize_id, display, error::Error, generate_localpart, parse_id}; +use crate::{error::Error, generate_localpart, parse_id}; /// A Matrix user ID. /// @@ -21,20 +16,16 @@ use crate::{deserialize_id, display, error::Error, generate_localpart, parse_id} /// # use std::convert::TryFrom; /// # use ruma_identifiers::UserId; /// assert_eq!( -/// UserId::try_from("@carl:example.com").unwrap().to_string(), +/// UserId::try_from("@carl:example.com").unwrap().as_ref(), /// "@carl:example.com" /// ); /// ``` -#[derive(Clone, Debug, Eq, Hash, PartialEq)] +#[derive(Clone, Debug)] #[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] #[cfg_attr(feature = "diesel", sql_type = "Text")] pub struct UserId { - /// The hostname of the homeserver. - hostname: Host, - /// The user's unique ID. - localpart: String, - /// The network port of the homeserver. - port: u16, + full_id: String, + colon_idx: NonZeroU8, /// Whether this user id is a historical one. /// /// A historical user id is one that is not legal per the regular user id rules, but was @@ -49,37 +40,29 @@ impl UserId { /// /// Fails if the given homeserver cannot be parsed as a valid host. pub fn new(homeserver_host: &str) -> Result { - let user_id = format!( + let full_id = format!( "@{}:{}", generate_localpart(12).to_lowercase(), homeserver_host ); - let (localpart, host, port) = parse_id('@', &user_id)?; + let colon_idx = parse_id(&full_id, &['@'])?; Ok(Self { - hostname: host, - localpart: localpart.to_string(), - port, + full_id, + colon_idx, is_historical: false, }) } - /// Returns a `Host` for the user ID, containing the server name (minus the port) of the + /// Returns the host of the user ID, containing the server name (including the port) of the /// originating homeserver. - /// - /// The host can be either a domain name, an IPv4 address, or an IPv6 address. - pub fn hostname(&self) -> &Host { - &self.hostname + pub fn hostname(&self) -> &str { + &self.full_id[self.colon_idx.get() as usize + 1..] } /// Returns the user's localpart. pub fn localpart(&self) -> &str { - &self.localpart - } - - /// Returns the port the originating homeserver can be accessed on. - pub fn port(&self) -> u16 { - self.port + &self.full_id[1..self.colon_idx.get() as usize] } /// Whether this user ID is a historical one, i.e. one that doesn't conform to the latest @@ -90,39 +73,16 @@ impl UserId { } } -impl Display for UserId { - fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { - display(f, '@', &self.localpart, &self.hostname, self.port) - } -} - -impl Serialize for UserId { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -impl<'de> Deserialize<'de> for UserId { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserialize_id(deserializer, "a Matrix user ID as a string") - } -} - -impl TryFrom<&str> for UserId { +impl TryFrom> for UserId { type Error = Error; /// Attempts to create a new Matrix user ID from a string representation. /// - /// The string must include the leading @ sigil, the localpart, a literal colon, and a valid - /// server name. - fn try_from(user_id: &str) -> Result { - let (localpart, host, port) = parse_id('@', user_id)?; + /// The string must include the leading @ sigil, the localpart, a literal colon, and a server + /// name. + fn try_from(user_id: Cow<'_, str>) -> Result { + let colon_idx = parse_id(&user_id, &['@'])?; + let localpart = &user_id[1..colon_idx.get() as usize]; // See https://matrix.org/docs/spec/appendices#user-identifiers let is_fully_conforming = localpart.bytes().all(|b| match b { @@ -138,14 +98,15 @@ impl TryFrom<&str> for UserId { } Ok(Self { - hostname: host, - port, - localpart: localpart.to_owned(), + full_id: user_id.into_owned(), + colon_idx, is_historical: !is_fully_conforming, }) } } +common_impls!(UserId, "a Matrix user ID"); + #[cfg(test)] mod tests { use std::convert::TryFrom; @@ -158,32 +119,31 @@ mod tests { #[test] fn valid_user_id() { let user_id = UserId::try_from("@carl:example.com").expect("Failed to create UserId."); - assert_eq!(user_id.to_string(), "@carl:example.com"); + assert_eq!(user_id.as_ref(), "@carl:example.com"); assert!(!user_id.is_historical()); } #[test] fn valid_historical_user_id() { let user_id = UserId::try_from("@a%b[irc]:example.com").expect("Failed to create UserId."); - assert_eq!(user_id.to_string(), "@a%b[irc]:example.com"); + assert_eq!(user_id.as_ref(), "@a%b[irc]:example.com"); assert!(user_id.is_historical()); } #[test] fn downcase_user_id() { let user_id = UserId::try_from("@CARL:example.com").expect("Failed to create UserId."); - assert_eq!(user_id.to_string(), "@CARL:example.com"); + assert_eq!(user_id.as_ref(), "@CARL:example.com"); assert!(user_id.is_historical()); } #[test] fn generate_random_valid_user_id() { - let user_id = UserId::new("example.com") - .expect("Failed to generate UserId.") - .to_string(); + let user_id = UserId::new("example.com").expect("Failed to generate UserId."); + let id_str: &str = user_id.as_ref(); - assert!(user_id.to_string().starts_with('@')); - assert_eq!(user_id.len(), 25); + assert!(id_str.starts_with('@')); + assert_eq!(id_str.len(), 25); } #[test] @@ -213,15 +173,15 @@ mod tests { assert_eq!( UserId::try_from("@carl:example.com:443") .expect("Failed to create UserId.") - .to_string(), - "@carl:example.com" + .as_ref(), + "@carl:example.com:443" ); } #[test] fn valid_user_id_with_non_standard_port() { let user_id = UserId::try_from("@carl:example.com:5000").expect("Failed to create UserId."); - assert_eq!(user_id.to_string(), "@carl:example.com:5000"); + assert_eq!(user_id.as_ref(), "@carl:example.com:5000"); assert!(!user_id.is_historical()); } @@ -249,7 +209,7 @@ mod tests { ); } - #[test] + /*#[test] fn invalid_user_id_host() { assert_eq!(UserId::try_from("@carl:/").unwrap_err(), Error::InvalidHost); } @@ -260,5 +220,5 @@ mod tests { UserId::try_from("@carl:example.com:notaport").unwrap_err(), Error::InvalidHost ); - } + }*/ } From fe70d158e76541a7c720cc35fb0f393c020f55cf Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 16 Apr 2020 13:53:17 +0200 Subject: [PATCH 108/140] Implement string comparison for identifier types --- src/macros.rs | 24 ++++++++++++++++++++++++ src/room_version_id.rs | 24 ++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/src/macros.rs b/src/macros.rs index 8289823d..06f32306 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -80,5 +80,29 @@ macro_rules! common_impls { crate::deserialize_id(deserializer, $desc) } } + + impl ::std::cmp::PartialEq for $id { + fn eq(&self, other: &str) -> bool { + self.full_id == other + } + } + + impl ::std::cmp::PartialEq<$id> for str { + fn eq(&self, other: &$id) -> bool { + self == other.full_id + } + } + + impl ::std::cmp::PartialEq<::std::string::String> for $id { + fn eq(&self, other: &::std::string::String) -> bool { + &self.full_id == other + } + } + + impl ::std::cmp::PartialEq<$id> for ::string::String { + fn eq(&self, other: &$id) -> bool { + self == &other.full_id + } + } }; } diff --git a/src/room_version_id.rs b/src/room_version_id.rs index 77de552e..5e8d8d35 100644 --- a/src/room_version_id.rs +++ b/src/room_version_id.rs @@ -215,6 +215,30 @@ impl TryFrom for RoomVersionId { } } +impl PartialEq for RoomVersionId { + fn eq(&self, other: &str) -> bool { + self.as_ref() == other + } +} + +impl PartialEq for str { + fn eq(&self, other: &RoomVersionId) -> bool { + self == other.as_ref() + } +} + +impl PartialEq for RoomVersionId { + fn eq(&self, other: &String) -> bool { + self.as_ref() == other + } +} + +impl PartialEq for String { + fn eq(&self, other: &RoomVersionId) -> bool { + self == other.as_ref() + } +} + #[cfg(test)] mod tests { use std::convert::TryFrom; From a9aa8a7d0d45354a645266c969b5c09cdf4715e6 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 16 Apr 2020 14:00:04 +0200 Subject: [PATCH 109/140] Update change log --- CHANGELOG.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1bb10c7..1cd27f7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,25 @@ # [unreleased] +Breaking changes: + +* All identifiers now allocate at maximum one string (localpart and host are no longer stored + separately) + * Because of this, these traits are now implemented for them and only allocate in the obvious + case: + * `impl From<…Id> for String` + * `impl AsRef for …Id` + * `impl TryFrom> for …Id` + * `impl TryFrom for …Id` + * `PartialEq` for `String`s and string slices + * Additionally, the `Hash` implementations will now yield the same hashes as hashing the string + representation + * Note that hashes are generally only guaranteed consistent in the lifetime of the program + though, so do not persist them! + * Methods returning `url::Host` have been removed or updated to return string slices + Improvements: -* Add support for historical uppercase MXIDs +* Add support for historical uppercase MXIDs # 0.14.1 From c3ed37b33789219ff337749018efddd1773be4c2 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 16 Apr 2020 16:15:27 +0200 Subject: [PATCH 110/140] Fix `String` path in macros.rs --- src/macros.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/macros.rs b/src/macros.rs index 06f32306..8daf46f1 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -99,7 +99,7 @@ macro_rules! common_impls { } } - impl ::std::cmp::PartialEq<$id> for ::string::String { + impl ::std::cmp::PartialEq<$id> for ::std::string::String { fn eq(&self, other: &$id) -> bool { self == &other.full_id } From b48235f496182134c2ed0fbe8a6ad4c978ca4d69 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Fri, 17 Apr 2020 11:14:54 +0200 Subject: [PATCH 111/140] Update change log --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1cd27f7c..757acd5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ Breaking changes: representation * Note that hashes are generally only guaranteed consistent in the lifetime of the program though, so do not persist them! - * Methods returning `url::Host` have been removed or updated to return string slices + * The `hostname` methods have been updated to return string slices instead of `&url::Host` Improvements: From e8f7bd9f6d4dc8ef7bfd274940999334bafdf3ce Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Fri, 17 Apr 2020 12:06:40 +0200 Subject: [PATCH 112/140] Restore server name validation --- CHANGELOG.md | 2 + src/error.rs | 6 +-- src/event_id.rs | 12 +++--- src/lib.rs | 8 ++-- src/room_alias_id.rs | 8 ++-- src/room_id.rs | 12 +++--- src/server_name.rs | 96 ++++++++++++++++++++++++++++++++++++++++++++ src/user_id.rs | 11 +++-- 8 files changed, 129 insertions(+), 26 deletions(-) create mode 100644 src/server_name.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 757acd5f..6320241b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ Breaking changes: * Note that hashes are generally only guaranteed consistent in the lifetime of the program though, so do not persist them! * The `hostname` methods have been updated to return string slices instead of `&url::Host` +* `Error::InvalidHost` has been renamed to `Error::InvalidServerName`, because it also covers errors + in the port, not just the host part section of the server name Improvements: diff --git a/src/error.rs b/src/error.rs index 087a8200..0ea68c2f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -11,8 +11,8 @@ pub enum Error { InvalidCharacters, /// The localpart of the ID string is not valid (because it is empty). InvalidLocalPart, - /// The domain part of the the ID string is not a valid IP address or DNS name. - InvalidHost, + /// The server name part of the the ID string is not a valid server name. + InvalidServerName, /// The ID exceeds 255 bytes (or 32 codepoints for a room version ID.) MaximumLengthExceeded, /// The ID is less than 4 characters (or is an empty room version ID.) @@ -28,7 +28,7 @@ impl Display for Error { let message = match self { Error::InvalidCharacters => "localpart contains invalid characters", Error::InvalidLocalPart => "localpart is empty", - Error::InvalidHost => "server name is not a valid IP address or domain name", + Error::InvalidServerName => "server name is not a valid IP address or domain name", Error::MaximumLengthExceeded => "ID exceeds 255 bytes", Error::MinimumLengthNotSatisfied => "ID must be at least 4 characters", Error::MissingDelimiter => "colon is required between localpart and server name", diff --git a/src/event_id.rs b/src/event_id.rs index 97c1545a..cd536fd1 100644 --- a/src/event_id.rs +++ b/src/event_id.rs @@ -162,10 +162,10 @@ mod tests { assert_eq!(id_str.len(), 31); } - /*#[test] + #[test] fn generate_random_invalid_event_id() { assert!(EventId::new("").is_err()); - }*/ + } #[test] fn serialize_valid_original_event_id() { @@ -275,11 +275,11 @@ mod tests { ); } - /*#[test] + #[test] fn invalid_event_id_host() { assert_eq!( EventId::try_from("$39hvsi03hlne:/").unwrap_err(), - Error::InvalidHost + Error::InvalidServerName ); } @@ -287,7 +287,7 @@ mod tests { fn invalid_event_id_port() { assert_eq!( EventId::try_from("$39hvsi03hlne:example.com:notaport").unwrap_err(), - Error::InvalidHost + Error::InvalidServerName ); - }*/ + } } diff --git a/src/lib.rs b/src/lib.rs index 666799e0..6c5d75ea 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,7 +23,8 @@ use serde::de::{self, Deserialize as _, Deserializer, Unexpected}; pub use crate::device_id::DeviceId; pub use crate::{ error::Error, event_id::EventId, room_alias_id::RoomAliasId, room_id::RoomId, - room_id_or_room_alias_id::RoomIdOrAliasId, room_version_id::RoomVersionId, user_id::UserId, + room_id_or_room_alias_id::RoomIdOrAliasId, room_version_id::RoomVersionId, + server_name::is_valid_server_name, user_id::UserId, }; #[macro_use] @@ -38,6 +39,7 @@ mod room_alias_id; mod room_id; mod room_id_or_room_alias_id; mod room_version_id; +mod server_name; mod user_id; /// All identifiers must be 255 bytes or less. @@ -79,8 +81,8 @@ 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 == id.len() - 1 { - return Err(Error::InvalidHost); + if !is_valid_server_name(&id[colon_idx + 1..]) { + return Err(Error::InvalidServerName); } match NonZeroU8::new(colon_idx as u8) { diff --git a/src/room_alias_id.rs b/src/room_alias_id.rs index 54c05a37..8b3445b3 100644 --- a/src/room_alias_id.rs +++ b/src/room_alias_id.rs @@ -144,11 +144,11 @@ mod tests { ); } - /*#[test] + #[test] fn invalid_room_alias_id_host() { assert_eq!( RoomAliasId::try_from("#ruma:/").unwrap_err(), - Error::InvalidHost + Error::InvalidServerName ); } @@ -156,7 +156,7 @@ mod tests { fn invalid_room_alias_id_port() { assert_eq!( RoomAliasId::try_from("#ruma:example.com:notaport").unwrap_err(), - Error::InvalidHost + Error::InvalidServerName ); - }*/ + } } diff --git a/src/room_id.rs b/src/room_id.rs index c80d3c54..75aa62c9 100644 --- a/src/room_id.rs +++ b/src/room_id.rs @@ -101,10 +101,10 @@ mod tests { assert_eq!(id_str.len(), 31); } - /*#[test] + #[test] fn generate_random_invalid_room_id() { assert!(RoomId::new("").is_err()); - }*/ + } #[test] fn serialize_valid_room_id() { @@ -162,11 +162,11 @@ mod tests { ); } - /*#[test] + #[test] fn invalid_room_id_host() { assert_eq!( RoomId::try_from("!29fhd83h92h0:/").unwrap_err(), - Error::InvalidHost + Error::InvalidServerName ); } @@ -174,7 +174,7 @@ mod tests { fn invalid_room_id_port() { assert_eq!( RoomId::try_from("!29fhd83h92h0:example.com:notaport").unwrap_err(), - Error::InvalidHost + Error::InvalidServerName ); - }*/ + } } diff --git a/src/server_name.rs b/src/server_name.rs new file mode 100644 index 00000000..b0b2d15e --- /dev/null +++ b/src/server_name.rs @@ -0,0 +1,96 @@ +/// Check whether a given string is a valid server name according to [the specification][]. +/// +/// [the specification]: https://matrix.org/docs/spec/appendices#server-name +pub fn is_valid_server_name(name: &str) -> bool { + use std::net::Ipv6Addr; + + let end_of_host = if name.starts_with('[') { + let end_of_ipv6 = match name.find(']') { + Some(idx) => idx, + None => return false, + }; + + if name[1..end_of_ipv6].parse::().is_err() { + return false; + } + + end_of_ipv6 + 1 + } else { + let end_of_host = name.find(':').unwrap_or_else(|| name.len()); + + if name[..end_of_host] + .bytes() + .any(|byte| !(byte.is_ascii_alphanumeric() || byte == b'-' || byte == b'.')) + { + return false; + } + + end_of_host + }; + + if name.len() == end_of_host { + true + } else if name.as_bytes()[end_of_host] != b':' { + // hostname is followed by something other than ":port" + false + } else { + // are the remaining characters after ':' a valid port? + name[end_of_host + 1..].parse::().is_ok() + } +} + +#[cfg(test)] +mod tests { + use super::is_valid_server_name; + + #[test] + fn ipv4_host() { + assert!(is_valid_server_name("127.0.0.1")); + } + + #[test] + fn ipv4_host_and_port() { + assert!(is_valid_server_name("1.1.1.1:12000")); + } + + #[test] + fn ipv6() { + assert!(is_valid_server_name("[::1]")); + } + + #[test] + fn ipv6_with_port() { + assert!(is_valid_server_name("[1234:5678::abcd]:5678")); + } + + #[test] + fn dns_name() { + assert!(is_valid_server_name("example.com")); + } + + #[test] + fn dns_name_with_port() { + assert!(is_valid_server_name("ruma.io:8080")); + } + + #[test] + fn invalid_ipv6() { + assert!(!is_valid_server_name("[test::1]")); + } + + #[test] + fn ipv4_with_invalid_port() { + assert!(!is_valid_server_name("127.0.0.1:")); + } + + #[test] + fn ipv6_with_invalid_port() { + assert!(!is_valid_server_name("[fe80::1]:100000")); + assert!(!is_valid_server_name("[fe80::1]!")); + } + + #[test] + fn dns_name_with_invalid_port() { + assert!(!is_valid_server_name("matrix.org:hello")); + } +} diff --git a/src/user_id.rs b/src/user_id.rs index d0fb1ea5..5b3fd7d2 100644 --- a/src/user_id.rs +++ b/src/user_id.rs @@ -209,16 +209,19 @@ mod tests { ); } - /*#[test] + #[test] fn invalid_user_id_host() { - assert_eq!(UserId::try_from("@carl:/").unwrap_err(), Error::InvalidHost); + assert_eq!( + UserId::try_from("@carl:/").unwrap_err(), + Error::InvalidServerName + ); } #[test] fn invalid_user_id_port() { assert_eq!( UserId::try_from("@carl:example.com:notaport").unwrap_err(), - Error::InvalidHost + Error::InvalidServerName ); - }*/ + } } From ad48d3972e53aa83fe623ea5cd1cb2f33e499eaf Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Fri, 17 Apr 2020 12:19:41 +0200 Subject: [PATCH 113/140] Error on invalid localpart --- src/lib.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 6c5d75ea..45bcf63d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -81,14 +81,15 @@ 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); + } + if !is_valid_server_name(&id[colon_idx + 1..]) { return Err(Error::InvalidServerName); } - match NonZeroU8::new(colon_idx as u8) { - Some(idx) => Ok(idx), - None => Err(Error::InvalidLocalPart), - } + Ok(NonZeroU8::new(colon_idx as u8).unwrap()) } /// Deserializes any type of id using the provided TryFrom implementation. From cb857db71e43401103b53f96e81dd338ea2b997c Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Fri, 17 Apr 2020 12:28:58 +0200 Subject: [PATCH 114/140] Fix tests, some refactoring --- CHANGELOG.md | 3 ++- src/event_id.rs | 25 ++++++++++++++----------- src/room_alias_id.rs | 11 +++++------ src/room_id.rs | 20 +++++++++++--------- src/room_id_or_room_alias_id.rs | 11 +++++------ src/server_name.rs | 9 +++++++++ src/user_id.rs | 30 +++++++++++++++--------------- 7 files changed, 61 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6320241b..e9e40bc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,8 @@ Breaking changes: representation * Note that hashes are generally only guaranteed consistent in the lifetime of the program though, so do not persist them! - * The `hostname` methods have been updated to return string slices instead of `&url::Host` + * The `hostname` methods have been rename to `server_name` and updated to return string slices + instead of `&url::Host` * `Error::InvalidHost` has been renamed to `Error::InvalidServerName`, because it also covers errors in the port, not just the host part section of the server name diff --git a/src/event_id.rs b/src/event_id.rs index cd536fd1..40b1ef37 100644 --- a/src/event_id.rs +++ b/src/event_id.rs @@ -5,7 +5,7 @@ use std::{borrow::Cow, convert::TryFrom, num::NonZeroU8}; #[cfg(feature = "diesel")] use diesel::sql_types::Text; -use crate::{error::Error, generate_localpart, parse_id, validate_id}; +use crate::{error::Error, generate_localpart, is_valid_server_name, parse_id, validate_id}; /// A Matrix event ID. /// @@ -55,8 +55,11 @@ impl EventId { /// /// Does not currently ever fail, but may fail in the future if the homeserver cannot be parsed /// parsed as a valid host. - pub fn new(homeserver_host: &str) -> Result { - let full_id = format!("${}:{}", generate_localpart(18), homeserver_host); + pub fn new(server_name: &str) -> Result { + if !is_valid_server_name(server_name) { + return Err(Error::InvalidServerName); + } + let full_id = format!("${}:{}", generate_localpart(18), server_name); Ok(Self { full_id, @@ -64,14 +67,6 @@ impl EventId { }) } - /// Returns the host of the event ID, containing the server name (including the port) of the - /// originating homeserver. Only applicable to events in the original format as used by Matrix - /// room versions 1 and 2. - pub fn hostname(&self) -> Option<&str> { - self.colon_idx - .map(|idx| &self.full_id[idx.get() as usize + 1..]) - } - /// Returns the event's unique ID. For the original event format as used by Matrix room /// versions 1 and 2, this is the "localpart" that precedes the homeserver. For later formats, /// this is the entire ID without the leading $ sigil. @@ -83,6 +78,14 @@ impl EventId { &self.full_id[1..idx] } + + /// Returns the server name of the event ID. + /// + /// Only applicable to events in the original format as used by Matrix room versions 1 and 2. + pub fn server_name(&self) -> Option<&str> { + self.colon_idx + .map(|idx| &self.full_id[idx.get() as usize + 1..]) + } } impl TryFrom> for EventId { diff --git a/src/room_alias_id.rs b/src/room_alias_id.rs index 8b3445b3..6bf23924 100644 --- a/src/room_alias_id.rs +++ b/src/room_alias_id.rs @@ -29,16 +29,15 @@ pub struct RoomAliasId { } impl RoomAliasId { - /// Returns the host of the room alias ID, containing the server name (including the port) of - /// the originating homeserver. - pub fn hostname(&self) -> &str { - &self.full_id[self.colon_idx.get() as usize + 1..] - } - /// Returns the room's alias. pub fn alias(&self) -> &str { &self.full_id[1..self.colon_idx.get() as usize] } + + /// Returns the server name of the room alias ID. + pub fn server_name(&self) -> &str { + &self.full_id[self.colon_idx.get() as usize + 1..] + } } impl TryFrom> for RoomAliasId { diff --git a/src/room_id.rs b/src/room_id.rs index 75aa62c9..9505ac3e 100644 --- a/src/room_id.rs +++ b/src/room_id.rs @@ -5,7 +5,7 @@ use std::{borrow::Cow, convert::TryFrom, num::NonZeroU8}; #[cfg(feature = "diesel")] use diesel::sql_types::Text; -use crate::{error::Error, generate_localpart, parse_id}; +use crate::{error::Error, generate_localpart, is_valid_server_name, parse_id}; /// A Matrix room ID. /// @@ -33,8 +33,11 @@ impl RoomId { /// 18 random ASCII characters. /// /// Fails if the given homeserver cannot be parsed as a valid host. - pub fn new(homeserver_host: &str) -> Result { - let full_id = format!("!{}:{}", generate_localpart(18), homeserver_host); + pub fn new(server_name: &str) -> Result { + if !is_valid_server_name(server_name) { + return Err(Error::InvalidServerName); + } + let full_id = format!("!{}:{}", generate_localpart(18), server_name); Ok(Self { full_id, @@ -42,16 +45,15 @@ impl RoomId { }) } - /// Returns the host of the room ID, containing the server name (including the port) of the - /// originating homeserver. - pub fn hostname(&self) -> &str { - &self.full_id[self.colon_idx.get() as usize + 1..] - } - /// Returns the rooms's unique ID. pub fn localpart(&self) -> &str { &self.full_id[1..self.colon_idx.get() as usize] } + + /// Returns the server name of the room ID. + pub fn server_name(&self) -> &str { + &self.full_id[self.colon_idx.get() as usize + 1..] + } } impl TryFrom> for RoomId { diff --git a/src/room_id_or_room_alias_id.rs b/src/room_id_or_room_alias_id.rs index a83b019e..9023824c 100644 --- a/src/room_id_or_room_alias_id.rs +++ b/src/room_id_or_room_alias_id.rs @@ -35,17 +35,16 @@ pub struct RoomIdOrAliasId { } impl RoomIdOrAliasId { - /// Returns the host of the room (alias) ID, containing the server name (including the port) of - /// the originating homeserver. - pub fn hostname(&self) -> &str { - &self.full_id[self.colon_idx.get() as usize + 1..] - } - /// Returns the local part (everything after the `!` or `#` and before the first colon). pub fn localpart(&self) -> &str { &self.full_id[1..self.colon_idx.get() as usize] } + /// Returns the server name of the room (alias) ID. + pub fn server_name(&self) -> &str { + &self.full_id[self.colon_idx.get() as usize + 1..] + } + /// Whether this is a room id (starts with `'!'`) pub fn is_room_id(&self) -> bool { self.variant() == Variant::RoomId diff --git a/src/server_name.rs b/src/server_name.rs index b0b2d15e..65a75596 100644 --- a/src/server_name.rs +++ b/src/server_name.rs @@ -4,6 +4,10 @@ pub fn is_valid_server_name(name: &str) -> bool { use std::net::Ipv6Addr; + if name.is_empty() { + return false; + } + let end_of_host = if name.starts_with('[') { let end_of_ipv6 = match name.find(']') { Some(idx) => idx, @@ -73,6 +77,11 @@ mod tests { assert!(is_valid_server_name("ruma.io:8080")); } + #[test] + fn empty_string() { + assert!(!is_valid_server_name("")); + } + #[test] fn invalid_ipv6() { assert!(!is_valid_server_name("[test::1]")); diff --git a/src/user_id.rs b/src/user_id.rs index 5b3fd7d2..63532ccb 100644 --- a/src/user_id.rs +++ b/src/user_id.rs @@ -5,7 +5,7 @@ use std::{borrow::Cow, convert::TryFrom, num::NonZeroU8}; #[cfg(feature = "diesel")] use diesel::sql_types::Text; -use crate::{error::Error, generate_localpart, parse_id}; +use crate::{error::Error, generate_localpart, is_valid_server_name, parse_id}; /// A Matrix user ID. /// @@ -39,32 +39,29 @@ impl UserId { /// 12 random ASCII characters. /// /// Fails if the given homeserver cannot be parsed as a valid host. - pub fn new(homeserver_host: &str) -> Result { - let full_id = format!( - "@{}:{}", - generate_localpart(12).to_lowercase(), - homeserver_host - ); - let colon_idx = parse_id(&full_id, &['@'])?; + pub fn new(server_name: &str) -> Result { + if !is_valid_server_name(server_name) { + return Err(Error::InvalidServerName); + } + let full_id = format!("@{}:{}", generate_localpart(12).to_lowercase(), server_name); Ok(Self { full_id, - colon_idx, + colon_idx: NonZeroU8::new(13).unwrap(), is_historical: false, }) } - /// Returns the host of the user ID, containing the server name (including the port) of the - /// originating homeserver. - pub fn hostname(&self) -> &str { - &self.full_id[self.colon_idx.get() as usize + 1..] - } - /// Returns the user's localpart. pub fn localpart(&self) -> &str { &self.full_id[1..self.colon_idx.get() as usize] } + /// Returns the server name of the user ID. + pub fn server_name(&self) -> &str { + &self.full_id[self.colon_idx.get() as usize + 1..] + } + /// Whether this user ID is a historical one, i.e. one that doesn't conform to the latest /// specification of the user ID grammar but is still accepted because it was previously /// allowed. @@ -140,6 +137,9 @@ mod tests { #[test] fn generate_random_valid_user_id() { let user_id = UserId::new("example.com").expect("Failed to generate UserId."); + assert_eq!(user_id.localpart().len(), 12); + assert_eq!(user_id.server_name(), "example.com"); + let id_str: &str = user_id.as_ref(); assert!(id_str.starts_with('@')); From 025e29827484e78cc57a6eb4385821fbab50ecae Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Fri, 17 Apr 2020 12:40:50 +0200 Subject: [PATCH 115/140] Update dependencies --- Cargo.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1c34e993..34d31961 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,9 +13,9 @@ version = "0.14.1" edition = "2018" [dependencies] -diesel = { version = "1.4.3", optional = true } -rand = "0.7.2" -serde = "1.0.102" +diesel = { version = "1.4.4", optional = true } +rand = "0.7.3" +serde = "1.0.106" [dev-dependencies] -serde_json = "1.0.41" +serde_json = "1.0.51" From 76a5a487d210a195254757560cb47aee9c3cedf8 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Fri, 17 Apr 2020 12:52:53 +0200 Subject: [PATCH 116/140] Make rand and serde dependencies optional --- .travis.yml | 3 ++- CHANGELOG.md | 4 ++++ Cargo.toml | 7 +++++-- src/device_id.rs | 4 +++- src/event_id.rs | 14 +++++++++++++- src/lib.rs | 15 +++++++++------ src/macros.rs | 2 ++ src/room_alias_id.rs | 3 +++ src/room_id.rs | 10 +++++++++- src/room_id_or_room_alias_id.rs | 5 +++++ src/room_version_id.rs | 12 ++++++++++-- src/user_id.rs | 10 +++++++++- 12 files changed, 74 insertions(+), 15 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7d848609..129d203c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,5 +32,6 @@ script: cargo clippy --all-targets --all-features -- -D warnings fi - cargo build --verbose - - cargo test --verbose + - cargo test --no-default-features --verbose + - cargo test --all-features --verbose if: "type != push OR (tag IS blank AND branch = master)" diff --git a/CHANGELOG.md b/CHANGELOG.md index e9e40bc5..07fe41d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,10 +19,14 @@ Breaking changes: instead of `&url::Host` * `Error::InvalidHost` has been renamed to `Error::InvalidServerName`, because it also covers errors in the port, not just the host part section of the server name +* The random identifier generation functions (`Id::new`) are now only available if the `rand` + feature of this crate is enabled Improvements: * Add support for historical uppercase MXIDs +* Made all dependencies optional + * `serde` is the only one that is enabled by default # 0.14.1 diff --git a/Cargo.toml b/Cargo.toml index 34d31961..bcaf4d11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,10 +12,13 @@ repository = "https://github.com/ruma/ruma-identifiers" version = "0.14.1" edition = "2018" +[features] +default = ["serde"] + [dependencies] diesel = { version = "1.4.4", optional = true } -rand = "0.7.3" -serde = "1.0.106" +rand = { version = "0.7.3", optional = true } +serde = { version = "1.0.106", optional = true } [dev-dependencies] serde_json = "1.0.51" diff --git a/src/device_id.rs b/src/device_id.rs index 549ec2cc..8ee95378 100644 --- a/src/device_id.rs +++ b/src/device_id.rs @@ -1,5 +1,6 @@ //! Matrix device identifiers. +#[cfg(feature = "rand")] use crate::generate_localpart; /// A Matrix device ID. @@ -9,11 +10,12 @@ use crate::generate_localpart; pub type DeviceId = String; /// Generates a random `DeviceId`, suitable for assignment to a new device. +#[cfg(feature = "rand")] pub fn generate() -> DeviceId { generate_localpart(8) } -#[cfg(test)] +#[cfg(all(test, feature = "rand"))] mod tests { use super::generate; diff --git a/src/event_id.rs b/src/event_id.rs index 40b1ef37..b93688d6 100644 --- a/src/event_id.rs +++ b/src/event_id.rs @@ -5,7 +5,7 @@ use std::{borrow::Cow, convert::TryFrom, num::NonZeroU8}; #[cfg(feature = "diesel")] use diesel::sql_types::Text; -use crate::{error::Error, generate_localpart, is_valid_server_name, parse_id, validate_id}; +use crate::{error::Error, parse_id, validate_id}; /// A Matrix event ID. /// @@ -55,7 +55,10 @@ impl EventId { /// /// Does not currently ever fail, but may fail in the future if the homeserver cannot be parsed /// parsed as a valid host. + #[cfg(feature = "rand")] pub fn new(server_name: &str) -> Result { + use crate::{generate_localpart, is_valid_server_name}; + if !is_valid_server_name(server_name) { return Err(Error::InvalidServerName); } @@ -121,6 +124,7 @@ common_impls!(EventId, "a Matrix event ID"); mod tests { use std::convert::TryFrom; + #[cfg(feature = "serde")] use serde_json::{from_str, to_string}; use super::EventId; @@ -156,6 +160,7 @@ mod tests { ) } + #[cfg(feature = "rand")] #[test] fn generate_random_valid_event_id() { let event_id = EventId::new("example.com").expect("Failed to generate EventId."); @@ -165,11 +170,13 @@ mod tests { assert_eq!(id_str.len(), 31); } + #[cfg(feature = "rand")] #[test] fn generate_random_invalid_event_id() { assert!(EventId::new("").is_err()); } + #[cfg(feature = "serde")] #[test] fn serialize_valid_original_event_id() { assert_eq!( @@ -181,6 +188,7 @@ mod tests { ); } + #[cfg(feature = "serde")] #[test] fn serialize_valid_base64_event_id() { assert_eq!( @@ -193,6 +201,7 @@ mod tests { ); } + #[cfg(feature = "serde")] #[test] fn serialize_valid_url_safe_base64_event_id() { assert_eq!( @@ -205,6 +214,7 @@ mod tests { ); } + #[cfg(feature = "serde")] #[test] fn deserialize_valid_original_event_id() { assert_eq!( @@ -214,6 +224,7 @@ mod tests { ); } + #[cfg(feature = "serde")] #[test] fn deserialize_valid_base64_event_id() { assert_eq!( @@ -224,6 +235,7 @@ mod tests { ); } + #[cfg(feature = "serde")] #[test] fn deserialize_valid_url_safe_base64_event_id() { assert_eq!( diff --git a/src/lib.rs b/src/lib.rs index 45bcf63d..8ea4364e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,9 +14,9 @@ #[cfg_attr(feature = "diesel", macro_use)] extern crate diesel; -use std::{borrow::Cow, convert::TryFrom, num::NonZeroU8}; +use std::num::NonZeroU8; -use rand::{distributions::Alphanumeric, thread_rng, Rng}; +#[cfg(feature = "serde")] use serde::de::{self, Deserialize as _, Deserializer, Unexpected}; #[doc(inline)] @@ -51,9 +51,11 @@ const MAX_BYTES: usize = 255; const MIN_CHARS: usize = 4; /// Generates a random identifier localpart. +#[cfg(feature = "rand")] fn generate_localpart(length: usize) -> String { - thread_rng() - .sample_iter(&Alphanumeric) + use rand::Rng as _; + rand::thread_rng() + .sample_iter(&rand::distributions::Alphanumeric) .take(length) .collect() } @@ -95,12 +97,13 @@ fn parse_id(id: &str, valid_sigils: &[char]) -> Result { /// Deserializes any type of id using the provided TryFrom implementation. /// /// This is a helper function to reduce the boilerplate of the Deserialize implementations. +#[cfg(feature = "serde")] fn deserialize_id<'de, D, T>(deserializer: D, expected_str: &str) -> Result where D: Deserializer<'de>, - T: for<'a> TryFrom<&'a str>, + T: for<'a> std::convert::TryFrom<&'a str>, { - Cow::<'_, str>::deserialize(deserializer).and_then(|v| { + std::borrow::Cow::<'_, str>::deserialize(deserializer).and_then(|v| { T::try_from(&v).map_err(|_| de::Error::invalid_value(Unexpected::Str(&v), &expected_str)) }) } diff --git a/src/macros.rs b/src/macros.rs index 8daf46f1..b823ff20 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -63,6 +63,7 @@ macro_rules! common_impls { } } + #[cfg(feature = "serde")] impl ::serde::Serialize for $id { fn serialize(&self, serializer: S) -> Result where @@ -72,6 +73,7 @@ macro_rules! common_impls { } } + #[cfg(feature = "serde")] impl<'de> ::serde::Deserialize<'de> for $id { fn deserialize(deserializer: D) -> Result where diff --git a/src/room_alias_id.rs b/src/room_alias_id.rs index 6bf23924..2649621b 100644 --- a/src/room_alias_id.rs +++ b/src/room_alias_id.rs @@ -62,6 +62,7 @@ common_impls!(RoomAliasId, "a Matrix room alias ID"); mod tests { use std::convert::TryFrom; + #[cfg(feature = "serde")] use serde_json::{from_str, to_string}; use super::RoomAliasId; @@ -77,6 +78,7 @@ mod tests { ); } + #[cfg(feature = "serde")] #[test] fn serialize_valid_room_alias_id() { assert_eq!( @@ -88,6 +90,7 @@ mod tests { ); } + #[cfg(feature = "serde")] #[test] fn deserialize_valid_room_alias_id() { assert_eq!( diff --git a/src/room_id.rs b/src/room_id.rs index 9505ac3e..9d2ae762 100644 --- a/src/room_id.rs +++ b/src/room_id.rs @@ -5,7 +5,7 @@ use std::{borrow::Cow, convert::TryFrom, num::NonZeroU8}; #[cfg(feature = "diesel")] use diesel::sql_types::Text; -use crate::{error::Error, generate_localpart, is_valid_server_name, parse_id}; +use crate::{error::Error, parse_id}; /// A Matrix room ID. /// @@ -33,7 +33,10 @@ impl RoomId { /// 18 random ASCII characters. /// /// Fails if the given homeserver cannot be parsed as a valid host. + #[cfg(feature = "rand")] pub fn new(server_name: &str) -> Result { + use crate::{generate_localpart, is_valid_server_name}; + if !is_valid_server_name(server_name) { return Err(Error::InvalidServerName); } @@ -79,6 +82,7 @@ common_impls!(RoomId, "a Matrix room ID"); mod tests { use std::convert::TryFrom; + #[cfg(feature = "serde")] use serde_json::{from_str, to_string}; use super::RoomId; @@ -94,6 +98,7 @@ mod tests { ); } + #[cfg(feature = "rand")] #[test] fn generate_random_valid_room_id() { let room_id = RoomId::new("example.com").expect("Failed to generate RoomId."); @@ -103,11 +108,13 @@ mod tests { assert_eq!(id_str.len(), 31); } + #[cfg(feature = "rand")] #[test] fn generate_random_invalid_room_id() { assert!(RoomId::new("").is_err()); } + #[cfg(feature = "serde")] #[test] fn serialize_valid_room_id() { assert_eq!( @@ -119,6 +126,7 @@ mod tests { ); } + #[cfg(feature = "serde")] #[test] fn deserialize_valid_room_id() { assert_eq!( diff --git a/src/room_id_or_room_alias_id.rs b/src/room_id_or_room_alias_id.rs index 9023824c..fe7ab364 100644 --- a/src/room_id_or_room_alias_id.rs +++ b/src/room_id_or_room_alias_id.rs @@ -93,6 +93,7 @@ common_impls!(RoomIdOrAliasId, "a Matrix room ID or room alias ID"); mod tests { use std::convert::TryFrom; + #[cfg(feature = "serde")] use serde_json::{from_str, to_string}; use super::RoomIdOrAliasId; @@ -126,6 +127,7 @@ mod tests { ); } + #[cfg(feature = "serde")] #[test] fn serialize_valid_room_id_or_alias_id_with_a_room_alias_id() { assert_eq!( @@ -138,6 +140,7 @@ mod tests { ); } + #[cfg(feature = "serde")] #[test] fn serialize_valid_room_id_or_alias_id_with_a_room_id() { assert_eq!( @@ -150,6 +153,7 @@ mod tests { ); } + #[cfg(feature = "serde")] #[test] fn deserialize_valid_room_id_or_alias_id_with_a_room_alias_id() { assert_eq!( @@ -159,6 +163,7 @@ mod tests { ); } + #[cfg(feature = "serde")] #[test] fn deserialize_valid_room_id_or_alias_id_with_a_room_id() { assert_eq!( diff --git a/src/room_version_id.rs b/src/room_version_id.rs index 5e8d8d35..603a5085 100644 --- a/src/room_version_id.rs +++ b/src/room_version_id.rs @@ -8,9 +8,10 @@ use std::{ #[cfg(feature = "diesel")] use diesel::sql_types::Text; +#[cfg(feature = "serde")] use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use crate::{deserialize_id, error::Error}; +use crate::error::Error; /// Room version identifiers cannot be more than 32 code points. const MAX_CODE_POINTS: usize = 32; @@ -155,6 +156,7 @@ impl Display for RoomVersionId { } } +#[cfg(feature = "serde")] impl Serialize for RoomVersionId { fn serialize(&self, serializer: S) -> Result where @@ -164,12 +166,13 @@ impl Serialize for RoomVersionId { } } +#[cfg(feature = "serde")] impl<'de> Deserialize<'de> for RoomVersionId { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, { - deserialize_id(deserializer, "a Matrix room version ID as a string") + crate::deserialize_id(deserializer, "a Matrix room version ID as a string") } } @@ -243,6 +246,7 @@ impl PartialEq for String { mod tests { use std::convert::TryFrom; + #[cfg(feature = "serde")] use serde_json::{from_str, to_string}; use super::RoomVersionId; @@ -324,6 +328,7 @@ mod tests { ); } + #[cfg(feature = "serde")] #[test] fn serialize_official_room_id() { assert_eq!( @@ -333,6 +338,7 @@ mod tests { ); } + #[cfg(feature = "serde")] #[test] fn deserialize_official_room_id() { let deserialized = @@ -347,6 +353,7 @@ mod tests { ); } + #[cfg(feature = "serde")] #[test] fn serialize_custom_room_id() { assert_eq!( @@ -358,6 +365,7 @@ mod tests { ); } + #[cfg(feature = "serde")] #[test] fn deserialize_custom_room_id() { let deserialized = from_str::(r#""io.ruma.1""#) diff --git a/src/user_id.rs b/src/user_id.rs index 63532ccb..31a67100 100644 --- a/src/user_id.rs +++ b/src/user_id.rs @@ -5,7 +5,7 @@ use std::{borrow::Cow, convert::TryFrom, num::NonZeroU8}; #[cfg(feature = "diesel")] use diesel::sql_types::Text; -use crate::{error::Error, generate_localpart, is_valid_server_name, parse_id}; +use crate::{error::Error, parse_id}; /// A Matrix user ID. /// @@ -39,7 +39,10 @@ impl UserId { /// 12 random ASCII characters. /// /// Fails if the given homeserver cannot be parsed as a valid host. + #[cfg(feature = "rand")] pub fn new(server_name: &str) -> Result { + use crate::{generate_localpart, is_valid_server_name}; + if !is_valid_server_name(server_name) { return Err(Error::InvalidServerName); } @@ -108,6 +111,7 @@ common_impls!(UserId, "a Matrix user ID"); mod tests { use std::convert::TryFrom; + #[cfg(feature = "serde")] use serde_json::{from_str, to_string}; use super::UserId; @@ -134,6 +138,7 @@ mod tests { assert!(user_id.is_historical()); } + #[cfg(feature = "rand")] #[test] fn generate_random_valid_user_id() { let user_id = UserId::new("example.com").expect("Failed to generate UserId."); @@ -146,11 +151,13 @@ mod tests { assert_eq!(id_str.len(), 25); } + #[cfg(feature = "rand")] #[test] fn generate_random_invalid_user_id() { assert!(UserId::new("").is_err()); } + #[cfg(feature = "serde")] #[test] fn serialize_valid_user_id() { assert_eq!( @@ -160,6 +167,7 @@ mod tests { ); } + #[cfg(feature = "serde")] #[test] fn deserialize_valid_user_id() { assert_eq!( From 93f75353a7480237a501a72b53cdce5f311e2880 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Fri, 17 Apr 2020 13:12:38 +0200 Subject: [PATCH 117/140] Move user ID localpart classification into a public function --- CHANGELOG.md | 3 +++ src/lib.rs | 7 +++---- src/user_id.rs | 40 +++++++++++++++++++++++++++------------- 3 files changed, 33 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07fe41d8..c50abcc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,9 @@ Improvements: * Add support for historical uppercase MXIDs * Made all dependencies optional * `serde` is the only one that is enabled by default +* The `user_id` module is now public and contains `fn localpart_is_fully_conforming` + * This function can be used to determine whether a user name (the localpart of a user ID) is valid + without actually constructing a full user ID first # 0.14.1 diff --git a/src/lib.rs b/src/lib.rs index 8ea4364e..c008e968 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,10 +20,9 @@ use std::num::NonZeroU8; use serde::de::{self, Deserialize as _, Deserializer, Unexpected}; #[doc(inline)] -pub use crate::device_id::DeviceId; pub use crate::{ - error::Error, event_id::EventId, room_alias_id::RoomAliasId, room_id::RoomId, - room_id_or_room_alias_id::RoomIdOrAliasId, room_version_id::RoomVersionId, + device_id::DeviceId, error::Error, event_id::EventId, room_alias_id::RoomAliasId, + room_id::RoomId, room_id_or_room_alias_id::RoomIdOrAliasId, room_version_id::RoomVersionId, server_name::is_valid_server_name, user_id::UserId, }; @@ -40,7 +39,7 @@ mod room_id; mod room_id_or_room_alias_id; mod room_version_id; mod server_name; -mod user_id; +pub mod user_id; /// All identifiers must be 255 bytes or less. const MAX_BYTES: usize = 255; diff --git a/src/user_id.rs b/src/user_id.rs index 31a67100..96e44a07 100644 --- a/src/user_id.rs +++ b/src/user_id.rs @@ -84,29 +84,43 @@ impl TryFrom> for UserId { let colon_idx = parse_id(&user_id, &['@'])?; let localpart = &user_id[1..colon_idx.get() as usize]; - // See https://matrix.org/docs/spec/appendices#user-identifiers - let is_fully_conforming = localpart.bytes().all(|b| match b { - b'0'..=b'9' | b'a'..=b'z' | b'-' | b'.' | b'=' | b'_' | b'/' => true, - _ => false, - }); - - // 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) { - return Err(Error::InvalidCharacters); - } + let is_historical = localpart_is_fully_comforming(localpart)?; Ok(Self { full_id: user_id.into_owned(), colon_idx, - is_historical: !is_fully_conforming, + is_historical: !is_historical, }) } } common_impls!(UserId, "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| match b { + b'0'..=b'9' | b'a'..=b'z' | b'-' | b'.' | b'=' | b'_' | b'/' => true, + _ => false, + }); + + // 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) + } +} + #[cfg(test)] mod tests { use std::convert::TryFrom; From ccaffcf10732b844a2cda4056f9a024a0fb1b7df Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Fri, 17 Apr 2020 13:31:53 +0200 Subject: [PATCH 118/140] Add UserId::parse_with_server_name --- CHANGELOG.md | 1 + src/user_id.rs | 78 ++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 76 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c50abcc1..2cfd92b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ Improvements: * The `user_id` module is now public and contains `fn localpart_is_fully_conforming` * This function can be used to determine whether a user name (the localpart of a user ID) is valid without actually constructing a full user ID first +* Add `UserId::parse_with_server_name` # 0.14.1 diff --git a/src/user_id.rs b/src/user_id.rs index 96e44a07..19aec566 100644 --- a/src/user_id.rs +++ b/src/user_id.rs @@ -5,7 +5,7 @@ use std::{borrow::Cow, convert::TryFrom, num::NonZeroU8}; #[cfg(feature = "diesel")] use diesel::sql_types::Text; -use crate::{error::Error, parse_id}; +use crate::{error::Error, is_valid_server_name, parse_id}; /// A Matrix user ID. /// @@ -41,7 +41,7 @@ impl UserId { /// Fails if the given homeserver cannot be parsed as a valid host. #[cfg(feature = "rand")] pub fn new(server_name: &str) -> Result { - use crate::{generate_localpart, is_valid_server_name}; + use crate::generate_localpart; if !is_valid_server_name(server_name) { return Err(Error::InvalidServerName); @@ -55,6 +55,34 @@ impl UserId { }) } + /// Attempts to complete a user ID, by adding the colon + server name and `@` prefix, if not + /// present already. + /// + /// This is a convenience function for the login API, where a user can supply either their full + /// user ID or just the localpart. It only supports a valid user ID or a valid user ID + /// localpart, not the localpart plus the `@` prefix, or the localpart plus server name without + /// the `@` prefix. + pub fn parse_with_server_name<'a>( + id: impl Into>, + server_name: &str, + ) -> Result { + let id = id.into(); + if id.starts_with('@') { + Self::try_from(id) + } else { + let is_fully_conforming = localpart_is_fully_comforming(&id)?; + if !is_valid_server_name(server_name) { + return Err(Error::InvalidServerName); + } + + Ok(Self { + full_id: format!("@{}:{}", id, server_name), + colon_idx: NonZeroU8::new(id.len() as u8 + 1).unwrap(), + is_historical: !is_fully_conforming, + }) + } + } + /// Returns the user's localpart. pub fn localpart(&self) -> &str { &self.full_id[1..self.colon_idx.get() as usize] @@ -132,9 +160,31 @@ mod tests { use crate::error::Error; #[test] - fn valid_user_id() { + fn valid_user_id_from_str() { let user_id = UserId::try_from("@carl:example.com").expect("Failed to create UserId."); assert_eq!(user_id.as_ref(), "@carl:example.com"); + assert_eq!(user_id.localpart(), "carl"); + assert_eq!(user_id.server_name(), "example.com"); + assert!(!user_id.is_historical()); + } + + #[test] + fn parse_valid_user_id() { + let user_id = UserId::parse_with_server_name("@carl:example.com", "example.com") + .expect("Failed to create UserId."); + assert_eq!(user_id.as_ref(), "@carl:example.com"); + assert_eq!(user_id.localpart(), "carl"); + assert_eq!(user_id.server_name(), "example.com"); + assert!(!user_id.is_historical()); + } + + #[test] + fn parse_valid_user_id_parts() { + let user_id = UserId::parse_with_server_name("carl", "example.com") + .expect("Failed to create UserId."); + assert_eq!(user_id.as_ref(), "@carl:example.com"); + assert_eq!(user_id.localpart(), "carl"); + assert_eq!(user_id.server_name(), "example.com"); assert!(!user_id.is_historical()); } @@ -142,6 +192,28 @@ mod tests { fn valid_historical_user_id() { let user_id = UserId::try_from("@a%b[irc]:example.com").expect("Failed to create UserId."); assert_eq!(user_id.as_ref(), "@a%b[irc]:example.com"); + assert_eq!(user_id.localpart(), "a%b[irc]"); + assert_eq!(user_id.server_name(), "example.com"); + assert!(user_id.is_historical()); + } + + #[test] + fn parse_valid_historical_user_id() { + let user_id = UserId::parse_with_server_name("@a%b[irc]:example.com", "example.com") + .expect("Failed to create UserId."); + assert_eq!(user_id.as_ref(), "@a%b[irc]:example.com"); + assert_eq!(user_id.localpart(), "a%b[irc]"); + assert_eq!(user_id.server_name(), "example.com"); + assert!(user_id.is_historical()); + } + + #[test] + fn parse_valid_historical_user_id_parts() { + let user_id = UserId::parse_with_server_name("a%b[irc]", "example.com") + .expect("Failed to create UserId."); + assert_eq!(user_id.as_ref(), "@a%b[irc]:example.com"); + assert_eq!(user_id.localpart(), "a%b[irc]"); + assert_eq!(user_id.server_name(), "example.com"); assert!(user_id.is_historical()); } From 8f9bcd3a65b8ad49594db1c80b21d783c9f79cb8 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Fri, 17 Apr 2020 14:01:57 +0200 Subject: [PATCH 119/140] Make docsrs show items for all features with indicators what needs to be enabled to use each conditional item --- Cargo.toml | 4 ++++ src/device_id.rs | 1 + src/event_id.rs | 1 + src/lib.rs | 2 ++ src/room_id.rs | 1 + src/user_id.rs | 1 + 6 files changed, 10 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index bcaf4d11..a714c398 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,10 @@ repository = "https://github.com/ruma/ruma-identifiers" version = "0.14.1" edition = "2018" +[package.metadata.docs.rs] +features = ["all"] +rustdoc-args = ["--cfg", "docsrs"] + [features] default = ["serde"] diff --git a/src/device_id.rs b/src/device_id.rs index 8ee95378..d526f066 100644 --- a/src/device_id.rs +++ b/src/device_id.rs @@ -11,6 +11,7 @@ pub type DeviceId = String; /// Generates a random `DeviceId`, suitable for assignment to a new device. #[cfg(feature = "rand")] +#[cfg_attr(docsrs, doc(cfg(feature = "rand")))] pub fn generate() -> DeviceId { generate_localpart(8) } diff --git a/src/event_id.rs b/src/event_id.rs index b93688d6..e0af1492 100644 --- a/src/event_id.rs +++ b/src/event_id.rs @@ -56,6 +56,7 @@ impl EventId { /// Does not currently ever fail, but may fail in the future if the homeserver cannot be parsed /// parsed as a valid host. #[cfg(feature = "rand")] + #[cfg_attr(docsrs, doc(cfg(feature = "rand")))] pub fn new(server_name: &str) -> Result { use crate::{generate_localpart, is_valid_server_name}; diff --git a/src/lib.rs b/src/lib.rs index c008e968..1f044b51 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,7 @@ )] // Since we support Rust 1.36.0, we can't apply this suggestion yet #![allow(clippy::use_self)] +#![cfg_attr(docsrs, feature(doc_cfg))] #[cfg(feature = "diesel")] #[cfg_attr(feature = "diesel", macro_use)] @@ -31,6 +32,7 @@ mod macros; pub mod device_id; #[cfg(feature = "diesel")] +#[cfg_attr(docsrs, doc(cfg(feature = "diesel")))] mod diesel_integration; mod error; mod event_id; diff --git a/src/room_id.rs b/src/room_id.rs index 9d2ae762..88fb82fe 100644 --- a/src/room_id.rs +++ b/src/room_id.rs @@ -34,6 +34,7 @@ impl RoomId { /// /// Fails if the given homeserver cannot be parsed as a valid host. #[cfg(feature = "rand")] + #[cfg_attr(docsrs, doc(cfg(feature = "rand")))] pub fn new(server_name: &str) -> Result { use crate::{generate_localpart, is_valid_server_name}; diff --git a/src/user_id.rs b/src/user_id.rs index 19aec566..e504e47d 100644 --- a/src/user_id.rs +++ b/src/user_id.rs @@ -40,6 +40,7 @@ impl UserId { /// /// Fails if the given homeserver cannot be parsed as a valid host. #[cfg(feature = "rand")] + #[cfg_attr(docsrs, doc(cfg(feature = "rand")))] pub fn new(server_name: &str) -> Result { use crate::generate_localpart; From 0ca9c4c5138a39e439ebd3c6e8711c6db6ca698a Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Fri, 17 Apr 2020 14:02:35 +0200 Subject: [PATCH 120/140] Bump version --- CHANGELOG.md | 2 ++ Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cfd92b5..84383752 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # [unreleased] +# 0.15.0 + Breaking changes: * All identifiers now allocate at maximum one string (localpart and host are no longer stored diff --git a/Cargo.toml b/Cargo.toml index a714c398..5557558e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ license = "MIT" name = "ruma-identifiers" readme = "README.md" repository = "https://github.com/ruma/ruma-identifiers" -version = "0.14.1" +version = "0.15.0" edition = "2018" [package.metadata.docs.rs] From 50cc35f1fec231d32fb6ddc50f19bddd6fc0fc31 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Fri, 17 Apr 2020 14:30:12 +0200 Subject: [PATCH 121/140] Bump version again... --- CHANGELOG.md | 6 ++++++ Cargo.toml | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84383752..ffd0e901 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # [unreleased] +# 0.15.1 + +Bug fixes: + +* Fix docs.rs build + # 0.15.0 Breaking changes: diff --git a/Cargo.toml b/Cargo.toml index 5557558e..39decbca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,11 +9,11 @@ license = "MIT" name = "ruma-identifiers" readme = "README.md" repository = "https://github.com/ruma/ruma-identifiers" -version = "0.15.0" +version = "0.15.1" edition = "2018" [package.metadata.docs.rs] -features = ["all"] +all-features = true rustdoc-args = ["--cfg", "docsrs"] [features] From acb02f96bcfb9c78818fbb534dec54d28a999de0 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Fri, 17 Apr 2020 14:53:45 +0200 Subject: [PATCH 122/140] Update parse_with_server_name's id bounds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit … from `Into>` to `AsRef + Into` --- src/user_id.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/user_id.rs b/src/user_id.rs index e504e47d..26519b61 100644 --- a/src/user_id.rs +++ b/src/user_id.rs @@ -63,22 +63,23 @@ impl UserId { /// user ID or just the localpart. It only supports a valid user ID or a valid user ID /// localpart, not the localpart plus the `@` prefix, or the localpart plus server name without /// the `@` prefix. - pub fn parse_with_server_name<'a>( - id: impl Into>, + pub fn parse_with_server_name( + id: impl AsRef + Into, server_name: &str, ) -> Result { - let id = id.into(); - if id.starts_with('@') { - Self::try_from(id) + let id_str = id.as_ref(); + + if id_str.starts_with('@') { + Self::try_from(id.into()) } else { - let is_fully_conforming = localpart_is_fully_comforming(&id)?; + let is_fully_conforming = localpart_is_fully_comforming(id_str)?; if !is_valid_server_name(server_name) { return Err(Error::InvalidServerName); } Ok(Self { - full_id: format!("@{}:{}", id, server_name), - colon_idx: NonZeroU8::new(id.len() as u8 + 1).unwrap(), + full_id: format!("@{}:{}", id_str, server_name), + colon_idx: NonZeroU8::new(id_str.len() as u8 + 1).unwrap(), is_historical: !is_fully_conforming, }) } From 6ad614ceb8afcd4d18cfcccddb6e6a2943c839b5 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Fri, 17 Apr 2020 18:15:23 +0200 Subject: [PATCH 123/140] Rename a test --- src/user_id.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/user_id.rs b/src/user_id.rs index 26519b61..0b0be22d 100644 --- a/src/user_id.rs +++ b/src/user_id.rs @@ -220,7 +220,7 @@ mod tests { } #[test] - fn downcase_user_id() { + fn uppercase_user_id() { let user_id = UserId::try_from("@CARL:example.com").expect("Failed to create UserId."); assert_eq!(user_id.as_ref(), "@CARL:example.com"); assert!(user_id.is_historical()); From 05c9eac6abd73110556ca65ae121f558bf7bac21 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Fri, 17 Apr 2020 18:18:14 +0200 Subject: [PATCH 124/140] Add tests for empty localparts --- src/room_alias_id.rs | 8 ++++++++ src/user_id.rs | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/src/room_alias_id.rs b/src/room_alias_id.rs index 2649621b..e612d655 100644 --- a/src/room_alias_id.rs +++ b/src/room_alias_id.rs @@ -138,6 +138,14 @@ mod tests { ); } + #[test] + fn missing_localpart() { + assert_eq!( + RoomAliasId::try_from("#:example.com").unwrap_err(), + Error::InvalidLocalPart + ); + } + #[test] fn missing_room_alias_id_delimiter() { assert_eq!( diff --git a/src/user_id.rs b/src/user_id.rs index 0b0be22d..f020dd46 100644 --- a/src/user_id.rs +++ b/src/user_id.rs @@ -297,6 +297,14 @@ mod tests { ); } + #[test] + fn missing_localpart() { + assert_eq!( + UserId::try_from("@:example.com").unwrap_err(), + Error::InvalidLocalPart + ); + } + #[test] fn missing_user_id_delimiter() { assert_eq!( From 6e6a51e11a5bec7e82191f53dee58a3a22fce1db Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Mon, 20 Apr 2020 15:38:19 +0200 Subject: [PATCH 125/140] Implement conversion functinos for RoomIdOrAliasId --- CHANGELOG.md | 14 ++++++++++ src/room_alias_id.rs | 4 +-- src/room_id.rs | 4 +-- src/room_id_or_room_alias_id.rs | 48 ++++++++++++++++++++++++++++++++- 4 files changed, 65 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffd0e901..f4210fb4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # [unreleased] +Breaking changes: + +* Update `RoomId::parse_with_server_name`s bounds from `Into>` to + `AsRef + Into`. While this is a breaking change, it is not expected to actually + require code changes. + +Improvements: + +* Add conversion functions for `RoomIdOrAliasId` + * `impl From for RoomIdOrAliasId` + * `impl From for RoomIdOrAliasId` + * `impl TryFrom for RoomId` + * `impl TryFrom for RoomAliasId` + # 0.15.1 Bug fixes: diff --git a/src/room_alias_id.rs b/src/room_alias_id.rs index e612d655..0293bfb5 100644 --- a/src/room_alias_id.rs +++ b/src/room_alias_id.rs @@ -24,8 +24,8 @@ use crate::{error::Error, parse_id}; #[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] #[cfg_attr(feature = "diesel", sql_type = "Text")] pub struct RoomAliasId { - full_id: String, - colon_idx: NonZeroU8, + pub(crate) full_id: String, + pub(crate) colon_idx: NonZeroU8, } impl RoomAliasId { diff --git a/src/room_id.rs b/src/room_id.rs index 88fb82fe..fa5e8841 100644 --- a/src/room_id.rs +++ b/src/room_id.rs @@ -24,8 +24,8 @@ use crate::{error::Error, parse_id}; #[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] #[cfg_attr(feature = "diesel", sql_type = "Text")] pub struct RoomId { - full_id: String, - colon_idx: NonZeroU8, + pub(crate) full_id: String, + pub(crate) colon_idx: NonZeroU8, } impl RoomId { diff --git a/src/room_id_or_room_alias_id.rs b/src/room_id_or_room_alias_id.rs index fe7ab364..fd02007f 100644 --- a/src/room_id_or_room_alias_id.rs +++ b/src/room_id_or_room_alias_id.rs @@ -5,7 +5,7 @@ use std::{borrow::Cow, convert::TryFrom, hint::unreachable_unchecked, num::NonZe #[cfg(feature = "diesel")] use diesel::sql_types::Text; -use crate::{error::Error, parse_id}; +use crate::{error::Error, parse_id, RoomAliasId, RoomId}; /// A Matrix room ID or a Matrix room alias ID. /// @@ -89,6 +89,52 @@ impl TryFrom> for RoomIdOrAliasId { common_impls!(RoomIdOrAliasId, "a Matrix room ID or room alias ID"); +impl From for RoomIdOrAliasId { + fn from(RoomId { full_id, colon_idx }: RoomId) -> Self { + Self { full_id, colon_idx } + } +} + +impl From for RoomIdOrAliasId { + fn from(RoomAliasId { full_id, colon_idx }: RoomAliasId) -> Self { + Self { full_id, colon_idx } + } +} + +impl TryFrom for RoomId { + type Error = RoomAliasId; + + fn try_from(id: RoomIdOrAliasId) -> Result { + match id.variant() { + Variant::RoomId => Ok(RoomId { + full_id: id.full_id, + colon_idx: id.colon_idx, + }), + Variant::RoomAliasId => Err(RoomAliasId { + full_id: id.full_id, + colon_idx: id.colon_idx, + }), + } + } +} + +impl TryFrom for RoomAliasId { + type Error = RoomId; + + fn try_from(id: RoomIdOrAliasId) -> Result { + match id.variant() { + Variant::RoomAliasId => Ok(RoomAliasId { + full_id: id.full_id, + colon_idx: id.colon_idx, + }), + Variant::RoomId => Err(RoomId { + full_id: id.full_id, + colon_idx: id.colon_idx, + }), + } + } +} + #[cfg(test)] mod tests { use std::convert::TryFrom; From e21633ef53e677d1877d5b9b431bcaaa4247463a Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Mon, 20 Apr 2020 15:41:58 +0200 Subject: [PATCH 126/140] Add RoomIdOrAliasId::into_either --- CHANGELOG.md | 2 ++ Cargo.toml | 1 + src/room_id_or_room_alias_id.rs | 16 ++++++++++++++++ 3 files changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4210fb4..0fa4de8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ Improvements: * `impl From for RoomIdOrAliasId` * `impl TryFrom for RoomId` * `impl TryFrom for RoomAliasId` + * `RoomIdOrAliasId::into_either` (if the optional dependency `either` is activated with the + identically named feature) # 0.15.1 diff --git a/Cargo.toml b/Cargo.toml index 39decbca..b46c2dd1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ default = ["serde"] [dependencies] diesel = { version = "1.4.4", optional = true } +either = { version = "1.5.3", optional = true } rand = { version = "0.7.3", optional = true } serde = { version = "1.0.106", optional = true } diff --git a/src/room_id_or_room_alias_id.rs b/src/room_id_or_room_alias_id.rs index fd02007f..f0be719e 100644 --- a/src/room_id_or_room_alias_id.rs +++ b/src/room_id_or_room_alias_id.rs @@ -55,6 +55,22 @@ impl RoomIdOrAliasId { self.variant() == Variant::RoomAliasId } + /// Turn this `RoomIdOrAliasId` into `Either` + #[cfg(feature = "either")] + #[cfg_attr(docsrs, doc(cfg(feature = "either")))] + pub fn into_either(self) -> either::Either { + match self.variant() { + Variant::RoomId => either::Either::Left(RoomId { + full_id: self.full_id, + colon_idx: self.colon_idx, + }), + Variant::RoomAliasId => either::Either::Right(RoomAliasId { + full_id: self.full_id, + colon_idx: self.colon_idx, + }), + } + } + fn variant(&self) -> Variant { match self.full_id.bytes().next() { Some(b'!') => Variant::RoomId, From e9d69be874377ea68bd47c9665ff11a293417cdc Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Mon, 20 Apr 2020 15:43:07 +0200 Subject: [PATCH 127/140] Bump versions to 0.16.0 --- CHANGELOG.md | 2 ++ Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fa4de8b..2d3d31c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # [unreleased] +# 0.16.0 + Breaking changes: * Update `RoomId::parse_with_server_name`s bounds from `Into>` to diff --git a/Cargo.toml b/Cargo.toml index b46c2dd1..e9c35ce6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ license = "MIT" name = "ruma-identifiers" readme = "README.md" repository = "https://github.com/ruma/ruma-identifiers" -version = "0.15.1" +version = "0.16.0" edition = "2018" [package.metadata.docs.rs] From a67ba7a729fdef3d76f5c199809fc43a37e13cbb Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Fri, 24 Apr 2020 21:54:27 +0200 Subject: [PATCH 128/140] Switch CI from travis to builds.sr.ht --- .builds/beta.yml | 30 ++++++++++++++++++++++++++++++ .builds/msrv.yml | 26 ++++++++++++++++++++++++++ .builds/nightly.yml | 32 ++++++++++++++++++++++++++++++++ .builds/stable.yml | 32 ++++++++++++++++++++++++++++++++ .travis.yml | 37 ------------------------------------- README.md | 1 - 6 files changed, 120 insertions(+), 38 deletions(-) create mode 100644 .builds/beta.yml create mode 100644 .builds/msrv.yml create mode 100644 .builds/nightly.yml create mode 100644 .builds/stable.yml delete mode 100644 .travis.yml diff --git a/.builds/beta.yml b/.builds/beta.yml new file mode 100644 index 00000000..ad2e33a7 --- /dev/null +++ b/.builds/beta.yml @@ -0,0 +1,30 @@ +image: archlinux +packages: + - rustup +sources: + - https://github.com/ruma/ruma-identifiers +tasks: + - rustup: | + # We specify --profile minimal because we'd otherwise download docs + rustup toolchain install beta --profile minimal -c rustfmt -c clippy + rustup default beta + - test: | + cd ruma-identifiers + + # We don't want the build to stop on individual failure of independent + # tools, so capture tool exit codes and set the task exit code manually + set +e + + cargo fmt -- --check + fmt_exit=$? + + cargo clippy --all-targets --all-features -- -D warnings + clippy_exit=$? + + cargo test --no-default-features --verbose + test1_exit=$? + + cargo test --all-features --verbose + test2_exit=$? + + exit $(( $fmt_exit || $clippy_exit || $test1_exit || $test2_exit )) diff --git a/.builds/msrv.yml b/.builds/msrv.yml new file mode 100644 index 00000000..0f98a39f --- /dev/null +++ b/.builds/msrv.yml @@ -0,0 +1,26 @@ +image: archlinux +packages: + - rustup +sources: + - https://github.com/ruma/ruma-identifiers +tasks: + - rustup: | + # We specify --profile minimal because we'd otherwise download docs + rustup toolchain install 1.36.0 --profile minimal + rustup default 1.36.0 + - test: | + cd ruma-identifiers + + # We don't want the build to stop on individual failure of independent + # tools, so capture tool exit codes and set the task exit code manually + set +e + + # Only make sure the code builds with the MSRV. Tests can require later + # Rust versions, don't compile or run them. + cargo build --no-default-features --verbose + build1_exit=$? + + cargo build --all-features --verbose + build2_exit=$? + + exit $(( $build1_exit || $build2_exit )) diff --git a/.builds/nightly.yml b/.builds/nightly.yml new file mode 100644 index 00000000..e119912a --- /dev/null +++ b/.builds/nightly.yml @@ -0,0 +1,32 @@ +image: archlinux +packages: + - rustup +sources: + - https://github.com/ruma/ruma-identifiers +tasks: + - rustup: | + rustup toolchain install nightly --profile minimal + rustup default nightly + + # Try installing rustfmt & clippy for nightly, but don't fail the build + # if they are not available + rustup component add rustfmt || true + rustup component add clippy || true + - test: | + cd ruma-identifiers + + # We don't want the build to stop on individual failure of independent + # tools, so capture tool exit codes and set the task exit code manually + set +e + + if ( rustup component list | grep -q rustfmt ); then + cargo fmt -- --check + fi + fmt_exit=$? + + if ( rustup component list | grep -q clippy ); then + cargo clippy --all-targets --all-features -- -D warnings + fi + clippy_exit=$? + + exit $(( $fmt_exit || $clippy_exit )) diff --git a/.builds/stable.yml b/.builds/stable.yml new file mode 100644 index 00000000..57620ca8 --- /dev/null +++ b/.builds/stable.yml @@ -0,0 +1,32 @@ +image: archlinux +packages: + - rustup +sources: + - https://github.com/ruma/ruma-identifiers +tasks: + - rustup: | + # We specify --profile minimal because we'd otherwise download docs + rustup toolchain install stable --profile minimal -c rustfmt -c clippy + rustup default stable + - test: | + cd ruma-identifiers + + # We don't want the build to stop on individual failure of independent + # tools, so capture tool exit codes and set the task exit code manually + set +e + + cargo fmt -- --check + fmt_exit=$? + + cargo clippy --all-targets --all-features -- -D warnings + clippy_exit=$? + + cargo test --no-default-features --verbose + test1_exit=$? + + cargo test --all-features --verbose + test2_exit=$? + + exit $(( $fmt_exit || $clippy_exit || $test1_exit || $test2_exit )) + # TODO: Add audit task once cargo-audit binary releases are available. + # See https://github.com/RustSec/cargo-audit/issues/66 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 129d203c..00000000 --- a/.travis.yml +++ /dev/null @@ -1,37 +0,0 @@ -language: "rust" -cache: "cargo" -rust: - - 1.36.0 - - stable - - beta - - nightly -jobs: - allow_failures: - - rust: nightly - fast_finish: true - -before_script: - - rustup component add rustfmt - - | - if [ "$TRAVIS_RUST_VERSION" != "1.36.0" ]; then - rustup component add clippy - fi - - | - if [ "$TRAVIS_RUST_VERSION" == "stable" ]; then - cargo install --force cargo-audit - fi - - cargo generate-lockfile -script: - - | - if [ "$TRAVIS_RUST_VERSION" == "stable" ]; then - cargo audit - fi - - cargo fmt -- --check - - | - if [ "$TRAVIS_RUST_VERSION" != "1.36.0" ]; then - cargo clippy --all-targets --all-features -- -D warnings - fi - - cargo build --verbose - - cargo test --no-default-features --verbose - - cargo test --all-features --verbose -if: "type != push OR (tag IS blank AND branch = master)" diff --git a/README.md b/README.md index fe9daca0..34a9f2ed 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ [![crates.io page](https://img.shields.io/crates/v/ruma-identifiers.svg)](https://crates.io/crates/ruma-identifiers) [![docs.rs page](https://docs.rs/ruma-identifiers/badge.svg)](https://docs.rs/ruma-identifiers/) -[![build status](https://travis-ci.org/ruma/ruma-identifiers.svg?branch=master)](https://travis-ci.org/ruma/ruma-identifiers) ![license: MIT](https://img.shields.io/crates/l/ruma-identifiers.svg) **ruma-identifiers** contains types for [Matrix](https://matrix.org/) identifiers for events, rooms, room aliases, and users. From 89f0594a68b9eff97d4390ccad6ad7bcf97249be Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Fri, 1 May 2020 22:58:26 +0200 Subject: [PATCH 129/140] Make string comparison PartialEq impls work --- CHANGELOG.md | 8 ++++++++ Cargo.toml | 2 +- src/macros.rs | 10 +++++----- src/room_version_id.rs | 10 +++++----- 4 files changed, 19 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d3d31c7..be2aed48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # [unreleased] +# 0.16.1 + +Bug fixes: + +* Change `PartialEq` implementations to compare IDs with string literals from `str` to `&str` + * This is technically a breaking change, but the previous implementations were extremely + unlikely to actually be used + # 0.16.0 Breaking changes: diff --git a/Cargo.toml b/Cargo.toml index e9c35ce6..d19f2d69 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ license = "MIT" name = "ruma-identifiers" readme = "README.md" repository = "https://github.com/ruma/ruma-identifiers" -version = "0.16.0" +version = "0.16.1" edition = "2018" [package.metadata.docs.rs] diff --git a/src/macros.rs b/src/macros.rs index b823ff20..881f5292 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -83,15 +83,15 @@ macro_rules! common_impls { } } - impl ::std::cmp::PartialEq for $id { - fn eq(&self, other: &str) -> bool { - self.full_id == other + impl ::std::cmp::PartialEq<&str> for $id { + fn eq(&self, other: &&str) -> bool { + self.full_id == *other } } - impl ::std::cmp::PartialEq<$id> for str { + impl ::std::cmp::PartialEq<$id> for &str { fn eq(&self, other: &$id) -> bool { - self == other.full_id + *self == other.full_id } } diff --git a/src/room_version_id.rs b/src/room_version_id.rs index 603a5085..02cd7bf4 100644 --- a/src/room_version_id.rs +++ b/src/room_version_id.rs @@ -218,15 +218,15 @@ impl TryFrom for RoomVersionId { } } -impl PartialEq for RoomVersionId { - fn eq(&self, other: &str) -> bool { - self.as_ref() == other +impl PartialEq<&str> for RoomVersionId { + fn eq(&self, other: &&str) -> bool { + self.as_ref() == *other } } -impl PartialEq for str { +impl PartialEq for &str { fn eq(&self, other: &RoomVersionId) -> bool { - self == other.as_ref() + *self == other.as_ref() } } From 96ca63847416b2de1b752eae9a5520122b82c8f2 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Sat, 30 May 2020 18:31:08 +0200 Subject: [PATCH 130/140] Change most usages of String to Box --- src/event_id.rs | 8 ++++---- src/macros.rs | 17 +++++++---------- src/room_alias_id.rs | 4 ++-- src/room_id.rs | 6 +++--- src/room_id_or_room_alias_id.rs | 4 ++-- src/room_version_id.rs | 10 ++++++---- src/user_id.rs | 8 ++++---- 7 files changed, 28 insertions(+), 29 deletions(-) diff --git a/src/event_id.rs b/src/event_id.rs index e0af1492..d4b9811e 100644 --- a/src/event_id.rs +++ b/src/event_id.rs @@ -44,7 +44,7 @@ use crate::{error::Error, parse_id, validate_id}; #[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] #[cfg_attr(feature = "diesel", sql_type = "Text")] pub struct EventId { - full_id: String, + full_id: Box, colon_idx: Option, } @@ -63,7 +63,7 @@ impl EventId { if !is_valid_server_name(server_name) { return Err(Error::InvalidServerName); } - let full_id = format!("${}:{}", generate_localpart(18), server_name); + let full_id = format!("${}:{}", generate_localpart(18), server_name).into(); Ok(Self { full_id, @@ -105,14 +105,14 @@ impl TryFrom> for EventId { let colon_idx = parse_id(&event_id, &['$'])?; Ok(Self { - full_id: event_id.into_owned(), + full_id: event_id.into_owned().into(), colon_idx: Some(colon_idx), }) } else { validate_id(&event_id, &['$'])?; Ok(Self { - full_id: event_id.into_owned(), + full_id: event_id.into_owned().into(), colon_idx: None, }) } diff --git a/src/macros.rs b/src/macros.rs index 881f5292..09cb7e8a 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -2,7 +2,7 @@ macro_rules! common_impls { ($id:ident, $desc:literal) => { impl ::std::convert::From<$id> for ::std::string::String { fn from(id: $id) -> Self { - id.full_id + id.full_id.into() } } @@ -44,16 +44,13 @@ macro_rules! common_impls { impl ::std::cmp::PartialOrd for $id { fn partial_cmp(&self, other: &Self) -> Option<::std::cmp::Ordering> { - <::std::string::String as ::std::cmp::PartialOrd>::partial_cmp( - &self.full_id, - &other.full_id, - ) + ::std::cmp::PartialOrd::partial_cmp(&self.full_id, &other.full_id) } } impl ::std::cmp::Ord for $id { fn cmp(&self, other: &Self) -> ::std::cmp::Ordering { - <::std::string::String as ::std::cmp::Ord>::cmp(&self.full_id, &other.full_id) + ::std::cmp::Ord::cmp(&self.full_id, &other.full_id) } } @@ -85,25 +82,25 @@ macro_rules! common_impls { impl ::std::cmp::PartialEq<&str> for $id { fn eq(&self, other: &&str) -> bool { - self.full_id == *other + &self.full_id[..] == *other } } impl ::std::cmp::PartialEq<$id> for &str { fn eq(&self, other: &$id) -> bool { - *self == other.full_id + *self == &other.full_id[..] } } impl ::std::cmp::PartialEq<::std::string::String> for $id { fn eq(&self, other: &::std::string::String) -> bool { - &self.full_id == other + &self.full_id[..] == &other[..] } } impl ::std::cmp::PartialEq<$id> for ::std::string::String { fn eq(&self, other: &$id) -> bool { - self == &other.full_id + &self[..] == &other.full_id[..] } } }; diff --git a/src/room_alias_id.rs b/src/room_alias_id.rs index 0293bfb5..dacce28e 100644 --- a/src/room_alias_id.rs +++ b/src/room_alias_id.rs @@ -24,7 +24,7 @@ use crate::{error::Error, parse_id}; #[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] #[cfg_attr(feature = "diesel", sql_type = "Text")] pub struct RoomAliasId { - pub(crate) full_id: String, + pub(crate) full_id: Box, pub(crate) colon_idx: NonZeroU8, } @@ -50,7 +50,7 @@ impl TryFrom> for RoomAliasId { let colon_idx = parse_id(&room_id, &['#'])?; Ok(Self { - full_id: room_id.into_owned(), + full_id: room_id.into_owned().into(), colon_idx, }) } diff --git a/src/room_id.rs b/src/room_id.rs index fa5e8841..c349ee84 100644 --- a/src/room_id.rs +++ b/src/room_id.rs @@ -24,7 +24,7 @@ use crate::{error::Error, parse_id}; #[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] #[cfg_attr(feature = "diesel", sql_type = "Text")] pub struct RoomId { - pub(crate) full_id: String, + pub(crate) full_id: Box, pub(crate) colon_idx: NonZeroU8, } @@ -41,7 +41,7 @@ impl RoomId { if !is_valid_server_name(server_name) { return Err(Error::InvalidServerName); } - let full_id = format!("!{}:{}", generate_localpart(18), server_name); + let full_id = format!("!{}:{}", generate_localpart(18), server_name).into(); Ok(Self { full_id, @@ -71,7 +71,7 @@ impl TryFrom> for RoomId { let colon_idx = parse_id(&room_id, &['!'])?; Ok(Self { - full_id: room_id.into_owned(), + full_id: room_id.into_owned().into(), colon_idx, }) } diff --git a/src/room_id_or_room_alias_id.rs b/src/room_id_or_room_alias_id.rs index f0be719e..7b356d4a 100644 --- a/src/room_id_or_room_alias_id.rs +++ b/src/room_id_or_room_alias_id.rs @@ -30,7 +30,7 @@ use crate::{error::Error, parse_id, RoomAliasId, RoomId}; #[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] #[cfg_attr(feature = "diesel", sql_type = "Text")] pub struct RoomIdOrAliasId { - full_id: String, + full_id: Box, colon_idx: NonZeroU8, } @@ -97,7 +97,7 @@ impl TryFrom> for RoomIdOrAliasId { fn try_from(room_id_or_alias_id: Cow<'_, str>) -> Result { let colon_idx = parse_id(&room_id_or_alias_id, &['#', '!'])?; Ok(Self { - full_id: room_id_or_alias_id.into_owned(), + full_id: room_id_or_alias_id.into_owned().into(), colon_idx, }) } diff --git a/src/room_version_id.rs b/src/room_version_id.rs index 02cd7bf4..216b59f0 100644 --- a/src/room_version_id.rs +++ b/src/room_version_id.rs @@ -51,7 +51,7 @@ enum InnerRoomVersionId { Version5, /// A custom room version. - Custom(String), + Custom(Box), } impl RoomVersionId { @@ -82,7 +82,7 @@ impl RoomVersionId { /// Creates a custom room version ID from the given string slice. pub fn custom(id: String) -> Self { - Self(InnerRoomVersionId::Custom(id)) + Self(InnerRoomVersionId::Custom(id.into())) } /// Whether or not this room version is an official one specified by the Matrix protocol. @@ -132,7 +132,7 @@ impl From for String { InnerRoomVersionId::Version3 => "3".to_owned(), InnerRoomVersionId::Version4 => "4".to_owned(), InnerRoomVersionId::Version5 => "5".to_owned(), - InnerRoomVersionId::Custom(version) => version, + InnerRoomVersionId::Custom(version) => version.into(), } } } @@ -193,7 +193,9 @@ impl TryFrom> for RoomVersionId { } else if custom.chars().count() > MAX_CODE_POINTS { return Err(Error::MaximumLengthExceeded); } else { - Self(InnerRoomVersionId::Custom(room_version_id.into_owned())) + Self(InnerRoomVersionId::Custom( + room_version_id.into_owned().into(), + )) } } }; diff --git a/src/user_id.rs b/src/user_id.rs index f020dd46..43e499bd 100644 --- a/src/user_id.rs +++ b/src/user_id.rs @@ -24,7 +24,7 @@ use crate::{error::Error, is_valid_server_name, parse_id}; #[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] #[cfg_attr(feature = "diesel", sql_type = "Text")] pub struct UserId { - full_id: String, + full_id: Box, colon_idx: NonZeroU8, /// Whether this user id is a historical one. /// @@ -47,7 +47,7 @@ impl UserId { if !is_valid_server_name(server_name) { return Err(Error::InvalidServerName); } - let full_id = format!("@{}:{}", generate_localpart(12).to_lowercase(), server_name); + let full_id = format!("@{}:{}", generate_localpart(12).to_lowercase(), server_name).into(); Ok(Self { full_id, @@ -78,7 +78,7 @@ impl UserId { } Ok(Self { - full_id: format!("@{}:{}", id_str, server_name), + full_id: format!("@{}:{}", id_str, server_name).into(), colon_idx: NonZeroU8::new(id_str.len() as u8 + 1).unwrap(), is_historical: !is_fully_conforming, }) @@ -117,7 +117,7 @@ impl TryFrom> for UserId { let is_historical = localpart_is_fully_comforming(localpart)?; Ok(Self { - full_id: user_id.into_owned(), + full_id: user_id.into_owned().into(), colon_idx, is_historical: !is_historical, }) From 5861457f3bd39134199e11f1e0273e2bda5f9533 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Sat, 30 May 2020 19:10:21 +0200 Subject: [PATCH 131/140] Remove diesel integration --- CHANGELOG.md | 5 ++++ Cargo.toml | 1 - src/diesel_integration.rs | 42 --------------------------------- src/event_id.rs | 5 ---- src/lib.rs | 7 ------ src/room_alias_id.rs | 5 ---- src/room_id.rs | 5 ---- src/room_id_or_room_alias_id.rs | 5 ---- src/room_version_id.rs | 4 ---- src/user_id.rs | 5 ---- 10 files changed, 5 insertions(+), 79 deletions(-) delete mode 100644 src/diesel_integration.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index be2aed48..6eb3c139 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # [unreleased] +Breaking changes + +* Removed diesel integration. If you were using it, please comment on the corresponding issue: + https://github.com/ruma/ruma-identifiers/issues/22 + # 0.16.1 Bug fixes: diff --git a/Cargo.toml b/Cargo.toml index d19f2d69..9e40aac5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,6 @@ rustdoc-args = ["--cfg", "docsrs"] default = ["serde"] [dependencies] -diesel = { version = "1.4.4", optional = true } either = { version = "1.5.3", optional = true } rand = { version = "0.7.3", optional = true } serde = { version = "1.0.106", optional = true } diff --git a/src/diesel_integration.rs b/src/diesel_integration.rs deleted file mode 100644 index 92194179..00000000 --- a/src/diesel_integration.rs +++ /dev/null @@ -1,42 +0,0 @@ -//! Implements traits from Diesel, allowing identifiers to be used as database fields. - -use std::{convert::TryFrom, error::Error as StdError, io::Write}; - -use diesel::{ - backend::Backend, - deserialize::{FromSql, Result as DeserializeResult}, - serialize::{Output, Result as SerializeResult, ToSql}, - sql_types::Text, -}; - -macro_rules! diesel_impl { - ($name:ident) => { - impl ToSql for $crate::$name - where - DB: Backend, - { - fn to_sql(&self, out: &mut Output<'_, W, DB>) -> SerializeResult { - ToSql::::to_sql(self.as_ref(), out) - } - } - - impl FromSql for $crate::$name - where - String: FromSql, - DB: Backend, - { - fn from_sql(value: Option<&::RawValue>) -> DeserializeResult { - let string = >::from_sql(value)?; - Self::try_from(string.as_str()) - .map_err(|error| Box::new(error) as Box) - } - } - }; -} - -diesel_impl!(EventId); -diesel_impl!(RoomAliasId); -diesel_impl!(RoomId); -diesel_impl!(RoomIdOrAliasId); -diesel_impl!(RoomVersionId); -diesel_impl!(UserId); diff --git a/src/event_id.rs b/src/event_id.rs index d4b9811e..9cbfb259 100644 --- a/src/event_id.rs +++ b/src/event_id.rs @@ -2,9 +2,6 @@ use std::{borrow::Cow, convert::TryFrom, num::NonZeroU8}; -#[cfg(feature = "diesel")] -use diesel::sql_types::Text; - use crate::{error::Error, parse_id, validate_id}; /// A Matrix event ID. @@ -41,8 +38,6 @@ use crate::{error::Error, parse_id, validate_id}; /// ); /// ``` #[derive(Clone, Debug)] -#[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] -#[cfg_attr(feature = "diesel", sql_type = "Text")] pub struct EventId { full_id: Box, colon_idx: Option, diff --git a/src/lib.rs b/src/lib.rs index 1f044b51..16b6dea6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,10 +11,6 @@ #![allow(clippy::use_self)] #![cfg_attr(docsrs, feature(doc_cfg))] -#[cfg(feature = "diesel")] -#[cfg_attr(feature = "diesel", macro_use)] -extern crate diesel; - use std::num::NonZeroU8; #[cfg(feature = "serde")] @@ -31,9 +27,6 @@ pub use crate::{ mod macros; pub mod device_id; -#[cfg(feature = "diesel")] -#[cfg_attr(docsrs, doc(cfg(feature = "diesel")))] -mod diesel_integration; mod error; mod event_id; mod room_alias_id; diff --git a/src/room_alias_id.rs b/src/room_alias_id.rs index dacce28e..a6e873eb 100644 --- a/src/room_alias_id.rs +++ b/src/room_alias_id.rs @@ -2,9 +2,6 @@ use std::{borrow::Cow, convert::TryFrom, num::NonZeroU8}; -#[cfg(feature = "diesel")] -use diesel::sql_types::Text; - use crate::{error::Error, parse_id}; /// A Matrix room alias ID. @@ -21,8 +18,6 @@ use crate::{error::Error, parse_id}; /// ); /// ``` #[derive(Clone, Debug)] -#[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] -#[cfg_attr(feature = "diesel", sql_type = "Text")] pub struct RoomAliasId { pub(crate) full_id: Box, pub(crate) colon_idx: NonZeroU8, diff --git a/src/room_id.rs b/src/room_id.rs index c349ee84..ed120543 100644 --- a/src/room_id.rs +++ b/src/room_id.rs @@ -2,9 +2,6 @@ use std::{borrow::Cow, convert::TryFrom, num::NonZeroU8}; -#[cfg(feature = "diesel")] -use diesel::sql_types::Text; - use crate::{error::Error, parse_id}; /// A Matrix room ID. @@ -21,8 +18,6 @@ use crate::{error::Error, parse_id}; /// ); /// ``` #[derive(Clone, Debug)] -#[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] -#[cfg_attr(feature = "diesel", sql_type = "Text")] pub struct RoomId { pub(crate) full_id: Box, pub(crate) colon_idx: NonZeroU8, diff --git a/src/room_id_or_room_alias_id.rs b/src/room_id_or_room_alias_id.rs index 7b356d4a..82787e6b 100644 --- a/src/room_id_or_room_alias_id.rs +++ b/src/room_id_or_room_alias_id.rs @@ -2,9 +2,6 @@ use std::{borrow::Cow, convert::TryFrom, hint::unreachable_unchecked, num::NonZeroU8}; -#[cfg(feature = "diesel")] -use diesel::sql_types::Text; - use crate::{error::Error, parse_id, RoomAliasId, RoomId}; /// A Matrix room ID or a Matrix room alias ID. @@ -27,8 +24,6 @@ use crate::{error::Error, parse_id, RoomAliasId, RoomId}; /// ); /// ``` #[derive(Clone, Debug)] -#[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] -#[cfg_attr(feature = "diesel", sql_type = "Text")] pub struct RoomIdOrAliasId { full_id: Box, colon_idx: NonZeroU8, diff --git a/src/room_version_id.rs b/src/room_version_id.rs index 216b59f0..2c4337d3 100644 --- a/src/room_version_id.rs +++ b/src/room_version_id.rs @@ -6,8 +6,6 @@ use std::{ fmt::{self, Display, Formatter}, }; -#[cfg(feature = "diesel")] -use diesel::sql_types::Text; #[cfg(feature = "serde")] use serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -27,8 +25,6 @@ const MAX_CODE_POINTS: usize = 32; /// assert_eq!(RoomVersionId::try_from("1").unwrap().as_ref(), "1"); /// ``` #[derive(Clone, Debug, Eq, Hash, PartialEq)] -#[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] -#[cfg_attr(feature = "diesel", sql_type = "Text")] pub struct RoomVersionId(InnerRoomVersionId); /// Possibile values for room version, distinguishing between official Matrix versions and custom diff --git a/src/user_id.rs b/src/user_id.rs index 43e499bd..e34e6627 100644 --- a/src/user_id.rs +++ b/src/user_id.rs @@ -2,9 +2,6 @@ use std::{borrow::Cow, convert::TryFrom, num::NonZeroU8}; -#[cfg(feature = "diesel")] -use diesel::sql_types::Text; - use crate::{error::Error, is_valid_server_name, parse_id}; /// A Matrix user ID. @@ -21,8 +18,6 @@ use crate::{error::Error, is_valid_server_name, parse_id}; /// ); /// ``` #[derive(Clone, Debug)] -#[cfg_attr(feature = "diesel", derive(FromSqlRow, QueryId, AsExpression, SqlType))] -#[cfg_attr(feature = "diesel", sql_type = "Text")] pub struct UserId { full_id: Box, colon_idx: NonZeroU8, From 7b30c2bb3ee93834387a4d60b8c086f0579d3805 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Sat, 30 May 2020 19:28:06 +0200 Subject: [PATCH 132/140] Update how TryFrom implementation are generated MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit … and remove TryFrom> implementations --- CHANGELOG.md | 4 +++ src/event_id.rs | 46 ++++++++++++++++----------------- src/macros.rs | 6 ++--- src/room_alias_id.rs | 29 ++++++++++----------- src/room_id.rs | 30 ++++++++++----------- src/room_id_or_room_alias_id.rs | 37 ++++++++++++++------------ src/user_id.rs | 40 ++++++++++++++-------------- 7 files changed, 96 insertions(+), 96 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6eb3c139..8ae312cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ Breaking changes * Removed diesel integration. If you were using it, please comment on the corresponding issue: https://github.com/ruma/ruma-identifiers/issues/22 +* Remove `TryFrom>` implementations for identifier types +* Update `parse_with_server_name`s signature (instead of `Into` it now requires + `Into>` of the id type). This is technically a breaking change, but extremely unlikely + to affect any existing code. # 0.16.1 diff --git a/src/event_id.rs b/src/event_id.rs index 9cbfb259..c0c9c1f1 100644 --- a/src/event_id.rs +++ b/src/event_id.rs @@ -1,6 +1,6 @@ //! Matrix event identifiers. -use std::{borrow::Cow, convert::TryFrom, num::NonZeroU8}; +use std::num::NonZeroU8; use crate::{error::Error, parse_id, validate_id}; @@ -87,34 +87,32 @@ impl EventId { } } -impl TryFrom> for EventId { - type Error = Error; +/// Attempts to create a new Matrix event ID from a string representation. +/// +/// If using the original event format as used by Matrix room versions 1 and 2, the string must +/// include the leading $ sigil, the localpart, a literal colon, and a valid homeserver hostname. +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(), &['$'])?; - /// Attempts to create a new Matrix event ID from a string representation. - /// - /// If using the original event format as used by Matrix room versions 1 and 2, the string must - /// include the leading $ sigil, the localpart, a literal colon, and a valid homeserver - /// hostname. - fn try_from(event_id: Cow<'_, str>) -> Result { - if event_id.contains(':') { - let colon_idx = parse_id(&event_id, &['$'])?; + Ok(EventId { + full_id: event_id.into(), + colon_idx: Some(colon_idx), + }) + } else { + validate_id(event_id.as_ref(), &['$'])?; - Ok(Self { - full_id: event_id.into_owned().into(), - colon_idx: Some(colon_idx), - }) - } else { - validate_id(&event_id, &['$'])?; - - Ok(Self { - full_id: event_id.into_owned().into(), - colon_idx: None, - }) - } + Ok(EventId { + full_id: event_id.into(), + colon_idx: None, + }) } } -common_impls!(EventId, "a Matrix event ID"); +common_impls!(EventId, try_from, "a Matrix event ID"); #[cfg(test)] mod tests { diff --git a/src/macros.rs b/src/macros.rs index 09cb7e8a..5ea40a4e 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -1,5 +1,5 @@ macro_rules! common_impls { - ($id:ident, $desc:literal) => { + ($id:ident, $try_from:ident, $desc:literal) => { impl ::std::convert::From<$id> for ::std::string::String { fn from(id: $id) -> Self { id.full_id.into() @@ -10,7 +10,7 @@ macro_rules! common_impls { type Error = crate::error::Error; fn try_from(s: &str) -> Result { - Self::try_from(::std::borrow::Cow::Borrowed(s)) + $try_from(s) } } @@ -18,7 +18,7 @@ macro_rules! common_impls { type Error = crate::error::Error; fn try_from(s: String) -> Result { - Self::try_from(::std::borrow::Cow::Owned(s)) + $try_from(s) } } diff --git a/src/room_alias_id.rs b/src/room_alias_id.rs index a6e873eb..b8ca6684 100644 --- a/src/room_alias_id.rs +++ b/src/room_alias_id.rs @@ -1,6 +1,6 @@ //! Matrix room alias identifiers. -use std::{borrow::Cow, convert::TryFrom, num::NonZeroU8}; +use std::num::NonZeroU8; use crate::{error::Error, parse_id}; @@ -35,23 +35,22 @@ impl RoomAliasId { } } -impl TryFrom> for RoomAliasId { - type Error = Error; +/// 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 +where + S: AsRef + Into>, +{ + let colon_idx = parse_id(room_id.as_ref(), &['#'])?; - /// 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: Cow<'_, str>) -> Result { - let colon_idx = parse_id(&room_id, &['#'])?; - - Ok(Self { - full_id: room_id.into_owned().into(), - colon_idx, - }) - } + Ok(RoomAliasId { + full_id: room_id.into(), + colon_idx, + }) } -common_impls!(RoomAliasId, "a Matrix room alias ID"); +common_impls!(RoomAliasId, try_from, "a Matrix room alias ID"); #[cfg(test)] mod tests { diff --git a/src/room_id.rs b/src/room_id.rs index ed120543..4187cc9d 100644 --- a/src/room_id.rs +++ b/src/room_id.rs @@ -1,6 +1,6 @@ //! Matrix room identifiers. -use std::{borrow::Cow, convert::TryFrom, num::NonZeroU8}; +use std::num::NonZeroU8; use crate::{error::Error, parse_id}; @@ -55,24 +55,22 @@ impl RoomId { } } -impl TryFrom> for RoomId { - type Error = Error; +/// Attempts to create a new Matrix room ID from a string representation. +/// +/// The string must include the leading ! sigil, the localpart, a literal colon, and a server name. +fn try_from(room_id: S) -> Result +where + S: AsRef + Into>, +{ + let colon_idx = parse_id(room_id.as_ref(), &['!'])?; - /// Attempts to create a new Matrix room ID from a string representation. - /// - /// The string must include the leading ! sigil, the localpart, a literal colon, and a server - /// name. - fn try_from(room_id: Cow<'_, str>) -> Result { - let colon_idx = parse_id(&room_id, &['!'])?; - - Ok(Self { - full_id: room_id.into_owned().into(), - colon_idx, - }) - } + Ok(RoomId { + full_id: room_id.into(), + colon_idx, + }) } -common_impls!(RoomId, "a Matrix room ID"); +common_impls!(RoomId, try_from, "a Matrix room ID"); #[cfg(test)] mod tests { diff --git a/src/room_id_or_room_alias_id.rs b/src/room_id_or_room_alias_id.rs index 82787e6b..00018c82 100644 --- a/src/room_id_or_room_alias_id.rs +++ b/src/room_id_or_room_alias_id.rs @@ -1,6 +1,6 @@ //! Matrix identifiers for places where a room ID or room alias ID are used interchangeably. -use std::{borrow::Cow, convert::TryFrom, hint::unreachable_unchecked, num::NonZeroU8}; +use std::{convert::TryFrom, hint::unreachable_unchecked, num::NonZeroU8}; use crate::{error::Error, parse_id, RoomAliasId, RoomId}; @@ -81,24 +81,27 @@ enum Variant { RoomAliasId, } -impl TryFrom> for RoomIdOrAliasId { - type Error = Error; - - /// Attempts to create a new Matrix room ID or a room alias ID from a string representation. - /// - /// The string must either include the leading ! sigil, the localpart, a literal colon, and a - /// valid homeserver host or include the leading # sigil, the alias, a literal colon, and a - /// valid homeserver host. - fn try_from(room_id_or_alias_id: Cow<'_, str>) -> Result { - let colon_idx = parse_id(&room_id_or_alias_id, &['#', '!'])?; - Ok(Self { - full_id: room_id_or_alias_id.into_owned().into(), - colon_idx, - }) - } +/// Attempts to create a new Matrix room ID or a room alias ID from a string representation. +/// +/// The string must either include the leading ! sigil, the localpart, a literal colon, and a +/// valid homeserver host or include the leading # sigil, the alias, a literal colon, and a +/// valid homeserver host. +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(), &['#', '!'])?; + Ok(RoomIdOrAliasId { + full_id: room_id_or_alias_id.into(), + colon_idx, + }) } -common_impls!(RoomIdOrAliasId, "a Matrix room ID or room alias ID"); +common_impls!( + RoomIdOrAliasId, + try_from, + "a Matrix room ID or room alias ID" +); impl From for RoomIdOrAliasId { fn from(RoomId { full_id, colon_idx }: RoomId) -> Self { diff --git a/src/user_id.rs b/src/user_id.rs index e34e6627..5bb8047c 100644 --- a/src/user_id.rs +++ b/src/user_id.rs @@ -1,6 +1,6 @@ //! Matrix user identifiers. -use std::{borrow::Cow, convert::TryFrom, num::NonZeroU8}; +use std::num::NonZeroU8; use crate::{error::Error, is_valid_server_name, parse_id}; @@ -59,13 +59,13 @@ impl UserId { /// localpart, not the localpart plus the `@` prefix, or the localpart plus server name without /// the `@` prefix. pub fn parse_with_server_name( - id: impl AsRef + Into, + id: impl AsRef + Into>, server_name: &str, ) -> Result { let id_str = id.as_ref(); if id_str.starts_with('@') { - Self::try_from(id.into()) + try_from(id.into()) } else { let is_fully_conforming = localpart_is_fully_comforming(id_str)?; if !is_valid_server_name(server_name) { @@ -98,28 +98,26 @@ impl UserId { } } -impl TryFrom> for UserId { - type Error = Error; +/// Attempts to create a new Matrix user ID from a string representation. +/// +/// The string must include the leading @ sigil, the localpart, a literal colon, and a server name. +fn try_from(user_id: S) -> Result +where + S: AsRef + Into>, +{ + let colon_idx = parse_id(user_id.as_ref(), &['@'])?; + let localpart = &user_id.as_ref()[1..colon_idx.get() as usize]; - /// Attempts to create a new Matrix user ID from a string representation. - /// - /// The string must include the leading @ sigil, the localpart, a literal colon, and a server - /// name. - fn try_from(user_id: Cow<'_, str>) -> Result { - let colon_idx = parse_id(&user_id, &['@'])?; - let localpart = &user_id[1..colon_idx.get() as usize]; + let is_historical = localpart_is_fully_comforming(localpart)?; - let is_historical = localpart_is_fully_comforming(localpart)?; - - Ok(Self { - full_id: user_id.into_owned().into(), - colon_idx, - is_historical: !is_historical, - }) - } + Ok(UserId { + full_id: user_id.into(), + colon_idx, + is_historical: !is_historical, + }) } -common_impls!(UserId, "a Matrix user ID"); +common_impls!(UserId, try_from, "a Matrix user ID"); /// Check whether the given user id localpart is valid and fully conforming /// From cf1a1de151bc49ce3168fc5f50950c40568dfc1b Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Sat, 30 May 2020 20:00:34 +0200 Subject: [PATCH 133/140] Support room version 6 --- CHANGELOG.md | 6 +++++- src/room_version_id.rs | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ae312cc..c5405b89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # [unreleased] -Breaking changes +Breaking changes: * Removed diesel integration. If you were using it, please comment on the corresponding issue: https://github.com/ruma/ruma-identifiers/issues/22 @@ -9,6 +9,10 @@ Breaking changes `Into>` of the id type). This is technically a breaking change, but extremely unlikely to affect any existing code. +Improvements: + +* Add `RoomVersionId::version_6` and `RoomVersionId::is_version_6` + # 0.16.1 Bug fixes: diff --git a/src/room_version_id.rs b/src/room_version_id.rs index 2c4337d3..efd59979 100644 --- a/src/room_version_id.rs +++ b/src/room_version_id.rs @@ -46,6 +46,9 @@ enum InnerRoomVersionId { /// A version 5 room. Version5, + /// A version 6 room. + Version6, + /// A custom room version. Custom(Box), } @@ -76,6 +79,11 @@ impl RoomVersionId { Self(InnerRoomVersionId::Version5) } + /// Creates a version 6 room ID. + pub fn version_6() -> Self { + Self(InnerRoomVersionId::Version6) + } + /// Creates a custom room version ID from the given string slice. pub fn custom(id: String) -> Self { Self(InnerRoomVersionId::Custom(id.into())) @@ -118,6 +126,11 @@ impl RoomVersionId { pub fn is_version_5(&self) -> bool { self.0 == InnerRoomVersionId::Version5 } + + /// Whether or not this is a version 6 room. + pub fn is_version_6(&self) -> bool { + self.0 == InnerRoomVersionId::Version5 + } } impl From for String { @@ -128,6 +141,7 @@ impl From for String { InnerRoomVersionId::Version3 => "3".to_owned(), InnerRoomVersionId::Version4 => "4".to_owned(), InnerRoomVersionId::Version5 => "5".to_owned(), + InnerRoomVersionId::Version6 => "6".to_owned(), InnerRoomVersionId::Custom(version) => version.into(), } } @@ -141,6 +155,7 @@ impl AsRef for RoomVersionId { InnerRoomVersionId::Version3 => "3", InnerRoomVersionId::Version4 => "4", InnerRoomVersionId::Version5 => "5", + InnerRoomVersionId::Version6 => "6", InnerRoomVersionId::Custom(version) => version, } } From 07040a18c8a14115674943436db9c8990c8f9e0a Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Sat, 30 May 2020 20:04:19 +0200 Subject: [PATCH 134/140] Fixup for 7b30c2b (forgot RoomVersionId) --- src/room_version_id.rs | 54 +++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/src/room_version_id.rs b/src/room_version_id.rs index efd59979..b5d1f58a 100644 --- a/src/room_version_id.rs +++ b/src/room_version_id.rs @@ -1,7 +1,6 @@ //! Matrix room version identifiers. use std::{ - borrow::Cow, convert::TryFrom, fmt::{self, Display, Formatter}, }; @@ -187,47 +186,44 @@ impl<'de> Deserialize<'de> for RoomVersionId { } } -impl TryFrom> for RoomVersionId { - type Error = Error; - - /// Attempts to create a new Matrix room version ID from a string representation. - fn try_from(room_version_id: Cow<'_, str>) -> Result { - let version = match &room_version_id as &str { - "1" => Self(InnerRoomVersionId::Version1), - "2" => Self(InnerRoomVersionId::Version2), - "3" => Self(InnerRoomVersionId::Version3), - "4" => Self(InnerRoomVersionId::Version4), - "5" => Self(InnerRoomVersionId::Version5), - custom => { - if custom.is_empty() { - return Err(Error::MinimumLengthNotSatisfied); - } else if custom.chars().count() > MAX_CODE_POINTS { - return Err(Error::MaximumLengthExceeded); - } else { - Self(InnerRoomVersionId::Custom( - room_version_id.into_owned().into(), - )) - } +/// Attempts to create a new Matrix room version ID from a string representation. +fn try_from(room_version_id: S) -> Result +where + S: AsRef + Into>, +{ + let version = match room_version_id.as_ref() { + "1" => RoomVersionId(InnerRoomVersionId::Version1), + "2" => RoomVersionId(InnerRoomVersionId::Version2), + "3" => RoomVersionId(InnerRoomVersionId::Version3), + "4" => RoomVersionId(InnerRoomVersionId::Version4), + "5" => RoomVersionId(InnerRoomVersionId::Version5), + custom => { + if custom.is_empty() { + return Err(Error::MinimumLengthNotSatisfied); + } else if custom.chars().count() > MAX_CODE_POINTS { + return Err(Error::MaximumLengthExceeded); + } else { + RoomVersionId(InnerRoomVersionId::Custom(room_version_id.into())) } - }; + } + }; - Ok(version) - } + Ok(version) } impl TryFrom<&str> for RoomVersionId { type Error = crate::error::Error; - fn try_from(s: &str) -> Result { - Self::try_from(Cow::Borrowed(s)) + fn try_from(s: &str) -> Result { + try_from(s) } } impl TryFrom for RoomVersionId { type Error = crate::error::Error; - fn try_from(s: String) -> Result { - Self::try_from(Cow::Owned(s)) + fn try_from(s: String) -> Result { + try_from(s) } } From f6f0b58a1c22721ca913c87b6781b51efbb5e204 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Mon, 1 Jun 2020 00:44:46 +0200 Subject: [PATCH 135/140] Update change log --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5405b89..dab12c02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Breaking changes: Improvements: +* Update the internal representation of identifiers to be more compact * Add `RoomVersionId::version_6` and `RoomVersionId::is_version_6` # 0.16.1 From 1093c2f84d3f4e62ff2c534aabfc88ae0b7f151a Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Mon, 1 Jun 2020 01:19:03 +0200 Subject: [PATCH 136/140] Add PartialOrd and Ord implementations for RoomVersionId --- CHANGELOG.md | 1 + src/room_version_id.rs | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dab12c02..1f7f35b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Improvements: * Update the internal representation of identifiers to be more compact * Add `RoomVersionId::version_6` and `RoomVersionId::is_version_6` +* Add `PartialOrd` and `Ord` implementations for `RoomVersionId` # 0.16.1 diff --git a/src/room_version_id.rs b/src/room_version_id.rs index b5d1f58a..bcffda3d 100644 --- a/src/room_version_id.rs +++ b/src/room_version_id.rs @@ -1,6 +1,7 @@ //! Matrix room version identifiers. use std::{ + cmp::Ordering, convert::TryFrom, fmt::{self, Display, Formatter}, }; @@ -23,7 +24,7 @@ const MAX_CODE_POINTS: usize = 32; /// # use ruma_identifiers::RoomVersionId; /// assert_eq!(RoomVersionId::try_from("1").unwrap().as_ref(), "1"); /// ``` -#[derive(Clone, Debug, Eq, Hash, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct RoomVersionId(InnerRoomVersionId); /// Possibile values for room version, distinguishing between official Matrix versions and custom @@ -166,6 +167,18 @@ impl Display for RoomVersionId { } } +impl PartialOrd for RoomVersionId { + fn partial_cmp(&self, other: &RoomVersionId) -> Option { + self.as_ref().partial_cmp(other.as_ref()) + } +} + +impl Ord for RoomVersionId { + fn cmp(&self, other: &Self) -> Ordering { + self.as_ref().cmp(other.as_ref()) + } +} + #[cfg(feature = "serde")] impl Serialize for RoomVersionId { fn serialize(&self, serializer: S) -> Result From 06c52c2abbd32eb18dca8ca0b0788d22969fef43 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Mon, 1 Jun 2020 01:23:00 +0200 Subject: [PATCH 137/140] Update change log --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f7f35b3..3b65e54c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ Breaking changes: `Into>` of the id type). This is technically a breaking change, but extremely unlikely to affect any existing code. +# 0.16.2 + Improvements: * Update the internal representation of identifiers to be more compact From 622d69884ccdae70bb830a851c8d55e7abf4c848 Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Sat, 30 May 2020 21:01:03 +0200 Subject: [PATCH 138/140] Add borrowed id types --- .builds/msrv.yml | 4 +- src/event_id.rs | 36 +++++++++------ src/lib.rs | 51 ++++++++++++++++----- src/macros.rs | 58 +++++++++++++----------- src/room_alias_id.rs | 19 ++++---- src/room_id.rs | 34 +++++++++----- src/room_id_or_room_alias_id.rs | 45 ++++++++++--------- src/room_version_id.rs | 80 +++++++++++++++++++-------------- src/user_id.rs | 43 +++++++++++------- 9 files changed, 226 insertions(+), 144 deletions(-) diff --git a/.builds/msrv.yml b/.builds/msrv.yml index 0f98a39f..4e7eb631 100644 --- a/.builds/msrv.yml +++ b/.builds/msrv.yml @@ -6,8 +6,8 @@ sources: tasks: - rustup: | # We specify --profile minimal because we'd otherwise download docs - rustup toolchain install 1.36.0 --profile minimal - rustup default 1.36.0 + rustup toolchain install 1.42.0 --profile minimal + rustup default 1.42.0 - test: | cd ruma-identifiers diff --git a/src/event_id.rs b/src/event_id.rs index c0c9c1f1..aa95f931 100644 --- a/src/event_id.rs +++ b/src/event_id.rs @@ -37,13 +37,13 @@ use crate::{error::Error, parse_id, validate_id}; /// "$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg" /// ); /// ``` -#[derive(Clone, Debug)] -pub struct EventId { - full_id: Box, +#[derive(Clone, Copy, Debug)] +pub struct EventId { + full_id: T, colon_idx: Option, } -impl EventId { +impl EventId { /// Attempts to generate an `EventId` for the given origin server with a localpart consisting /// of 18 random ASCII characters. This should only be used for events in the original format /// as used by Matrix room versions 1 and 2. @@ -52,7 +52,10 @@ impl EventId { /// parsed as a valid host. #[cfg(feature = "rand")] #[cfg_attr(docsrs, doc(cfg(feature = "rand")))] - pub fn new(server_name: &str) -> Result { + pub fn new(server_name: &str) -> Result + where + String: Into, + { use crate::{generate_localpart, is_valid_server_name}; if !is_valid_server_name(server_name) { @@ -69,21 +72,27 @@ impl EventId { /// Returns the event's unique ID. For the original event format as used by Matrix room /// versions 1 and 2, this is the "localpart" that precedes the homeserver. For later formats, /// this is the entire ID without the leading $ sigil. - pub fn localpart(&self) -> &str { + pub fn localpart(&self) -> &str + where + T: AsRef, + { let idx = match self.colon_idx { Some(idx) => idx.get() as usize, - None => self.full_id.len(), + None => self.full_id.as_ref().len(), }; - &self.full_id[1..idx] + &self.full_id.as_ref()[1..idx] } /// Returns the server name of the event ID. /// /// Only applicable to events in the original format as used by Matrix room versions 1 and 2. - pub fn server_name(&self) -> Option<&str> { + pub fn server_name(&self) -> Option<&str> + where + T: AsRef, + { self.colon_idx - .map(|idx| &self.full_id[idx.get() as usize + 1..]) + .map(|idx| &self.full_id.as_ref()[idx.get() as usize + 1..]) } } @@ -91,9 +100,9 @@ impl EventId { /// /// If using the original event format as used by Matrix room versions 1 and 2, the string must /// include the leading $ sigil, the localpart, a literal colon, and a valid homeserver hostname. -fn try_from(event_id: S) -> Result +fn try_from(event_id: S) -> Result, Error> where - S: AsRef + Into>, + S: AsRef + Into, { if event_id.as_ref().contains(':') { let colon_idx = parse_id(event_id.as_ref(), &['$'])?; @@ -121,9 +130,10 @@ mod tests { #[cfg(feature = "serde")] use serde_json::{from_str, to_string}; - use super::EventId; use crate::error::Error; + type EventId = super::EventId>; + #[test] fn valid_original_event_id() { assert_eq!( diff --git a/src/lib.rs b/src/lib.rs index 16b6dea6..ae770d04 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,7 +5,7 @@ #![deny( missing_copy_implementations, missing_debug_implementations, - missing_docs + //missing_docs )] // Since we support Rust 1.36.0, we can't apply this suggestion yet #![allow(clippy::use_self)] @@ -17,25 +17,52 @@ use std::num::NonZeroU8; use serde::de::{self, Deserialize as _, Deserializer, Unexpected}; #[doc(inline)] -pub use crate::{ - device_id::DeviceId, error::Error, event_id::EventId, room_alias_id::RoomAliasId, - room_id::RoomId, room_id_or_room_alias_id::RoomIdOrAliasId, room_version_id::RoomVersionId, - server_name::is_valid_server_name, user_id::UserId, -}; +pub use crate::{error::Error, server_name::is_valid_server_name}; #[macro_use] mod macros; -pub mod device_id; mod error; -mod event_id; -mod room_alias_id; -mod room_id; -mod room_id_or_room_alias_id; -mod room_version_id; mod server_name; + +pub mod device_id; +pub mod event_id; +pub mod room_alias_id; +pub mod room_id; +pub mod room_id_or_room_alias_id; +pub mod room_version_id; pub mod user_id; +/// An owned event ID. +pub type EventId = event_id::EventId>; +/// A reference to an event ID. +pub type EventIdRef<'a> = event_id::EventId<&'a str>; + +/// An owned room alias ID. +pub type RoomAliasId = room_alias_id::RoomAliasId>; +/// A reference to a room alias ID. +pub type RoomAliasIdRef<'a> = room_alias_id::RoomAliasId<&'a str>; + +/// An owned room ID. +pub type RoomId = room_id::RoomId>; +/// A reference to a room ID. +pub type RoomIdRef<'a> = room_id::RoomId<&'a str>; + +/// An owned room alias ID or room ID. +pub type RoomIdOrAliasId = room_id_or_room_alias_id::RoomIdOrAliasId>; +/// A reference to a room alias ID or room ID. +pub type RoomIdOrAliasIdRef<'a> = room_id_or_room_alias_id::RoomIdOrAliasId<&'a str>; + +/// An owned room version ID. +pub type RoomVersionId = room_version_id::RoomVersionId>; +/// A reference to a room version ID. +pub type RoomVersionIdRef<'a> = room_version_id::RoomVersionId<&'a str>; + +/// An owned user ID. +pub type UserId = user_id::UserId>; +/// A reference to a user ID. +pub type UserIdRef<'a> = user_id::UserId<&'a str>; + /// All identifiers must be 255 bytes or less. const MAX_BYTES: usize = 255; /// The minimum number of characters an ID can be. diff --git a/src/macros.rs b/src/macros.rs index 5ea40a4e..a072a6aa 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -1,12 +1,20 @@ macro_rules! common_impls { ($id:ident, $try_from:ident, $desc:literal) => { - impl ::std::convert::From<$id> for ::std::string::String { - fn from(id: $id) -> Self { + impl ::std::convert::From<$id>> for ::std::string::String { + fn from(id: $id>) -> Self { id.full_id.into() } } - impl ::std::convert::TryFrom<&str> for $id { + impl<'a> ::std::convert::TryFrom<&'a str> for $id<&'a str> { + type Error = crate::error::Error; + + fn try_from(s: &'a str) -> Result { + $try_from(s) + } + } + + impl ::std::convert::TryFrom<&str> for $id> { type Error = crate::error::Error; fn try_from(s: &str) -> Result { @@ -14,7 +22,7 @@ macro_rules! common_impls { } } - impl ::std::convert::TryFrom for $id { + impl ::std::convert::TryFrom for $id> { type Error = crate::error::Error; fn try_from(s: String) -> Result { @@ -22,56 +30,56 @@ macro_rules! common_impls { } } - impl ::std::convert::AsRef for $id { + impl> ::std::convert::AsRef for $id { fn as_ref(&self) -> &str { - &self.full_id + self.full_id.as_ref() } } - impl ::std::fmt::Display for $id { + impl ::std::fmt::Display for $id { fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { write!(f, "{}", self.full_id) } } - impl ::std::cmp::PartialEq for $id { + impl ::std::cmp::PartialEq for $id { fn eq(&self, other: &Self) -> bool { self.full_id == other.full_id } } - impl ::std::cmp::Eq for $id {} + impl ::std::cmp::Eq for $id {} - impl ::std::cmp::PartialOrd for $id { + impl ::std::cmp::PartialOrd for $id { fn partial_cmp(&self, other: &Self) -> Option<::std::cmp::Ordering> { ::std::cmp::PartialOrd::partial_cmp(&self.full_id, &other.full_id) } } - impl ::std::cmp::Ord for $id { + impl ::std::cmp::Ord for $id { fn cmp(&self, other: &Self) -> ::std::cmp::Ordering { ::std::cmp::Ord::cmp(&self.full_id, &other.full_id) } } - impl ::std::hash::Hash for $id { + impl ::std::hash::Hash for $id { fn hash(&self, state: &mut H) { self.full_id.hash(state); } } #[cfg(feature = "serde")] - impl ::serde::Serialize for $id { + impl> ::serde::Serialize for $id { fn serialize(&self, serializer: S) -> Result where S: ::serde::Serializer, { - serializer.serialize_str(&self.full_id) + serializer.serialize_str(self.full_id.as_ref()) } } #[cfg(feature = "serde")] - impl<'de> ::serde::Deserialize<'de> for $id { + impl<'de> ::serde::Deserialize<'de> for $id> { fn deserialize(deserializer: D) -> Result where D: ::serde::Deserializer<'de>, @@ -80,27 +88,27 @@ macro_rules! common_impls { } } - impl ::std::cmp::PartialEq<&str> for $id { + impl> ::std::cmp::PartialEq<&str> for $id { fn eq(&self, other: &&str) -> bool { - &self.full_id[..] == *other + self.full_id.as_ref() == *other } } - impl ::std::cmp::PartialEq<$id> for &str { - fn eq(&self, other: &$id) -> bool { - *self == &other.full_id[..] + impl> ::std::cmp::PartialEq<$id> for &str { + fn eq(&self, other: &$id) -> bool { + *self == other.full_id.as_ref() } } - impl ::std::cmp::PartialEq<::std::string::String> for $id { + impl> ::std::cmp::PartialEq<::std::string::String> for $id { fn eq(&self, other: &::std::string::String) -> bool { - &self.full_id[..] == &other[..] + self.full_id.as_ref() == &other[..] } } - impl ::std::cmp::PartialEq<$id> for ::std::string::String { - fn eq(&self, other: &$id) -> bool { - &self[..] == &other.full_id[..] + impl> ::std::cmp::PartialEq<$id> for ::std::string::String { + fn eq(&self, other: &$id) -> bool { + &self[..] == other.full_id.as_ref() } } }; diff --git a/src/room_alias_id.rs b/src/room_alias_id.rs index b8ca6684..d8358631 100644 --- a/src/room_alias_id.rs +++ b/src/room_alias_id.rs @@ -17,30 +17,30 @@ use crate::{error::Error, parse_id}; /// "#ruma:example.com" /// ); /// ``` -#[derive(Clone, Debug)] -pub struct RoomAliasId { - pub(crate) full_id: Box, +#[derive(Clone, Copy, Debug)] +pub struct RoomAliasId { + pub(crate) full_id: T, pub(crate) colon_idx: NonZeroU8, } -impl RoomAliasId { +impl> RoomAliasId { /// Returns the room's alias. pub fn alias(&self) -> &str { - &self.full_id[1..self.colon_idx.get() as usize] + &self.full_id.as_ref()[1..self.colon_idx.get() as usize] } /// Returns the server name of the room alias ID. pub fn server_name(&self) -> &str { - &self.full_id[self.colon_idx.get() as usize + 1..] + &self.full_id.as_ref()[self.colon_idx.get() as usize + 1..] } } /// 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_id: S) -> Result, Error> where - S: AsRef + Into>, + S: AsRef + Into, { let colon_idx = parse_id(room_id.as_ref(), &['#'])?; @@ -59,9 +59,10 @@ mod tests { #[cfg(feature = "serde")] use serde_json::{from_str, to_string}; - use super::RoomAliasId; use crate::error::Error; + type RoomAliasId = super::RoomAliasId>; + #[test] fn valid_room_alias_id() { assert_eq!( diff --git a/src/room_id.rs b/src/room_id.rs index 4187cc9d..82a4416a 100644 --- a/src/room_id.rs +++ b/src/room_id.rs @@ -17,20 +17,23 @@ use crate::{error::Error, parse_id}; /// "!n8f893n9:example.com" /// ); /// ``` -#[derive(Clone, Debug)] -pub struct RoomId { - pub(crate) full_id: Box, +#[derive(Clone, Copy, Debug)] +pub struct RoomId { + pub(crate) full_id: T, pub(crate) colon_idx: NonZeroU8, } -impl RoomId { +impl RoomId { /// Attempts to generate a `RoomId` for the given origin server with a localpart consisting of /// 18 random ASCII characters. /// /// Fails if the given homeserver cannot be parsed as a valid host. #[cfg(feature = "rand")] #[cfg_attr(docsrs, doc(cfg(feature = "rand")))] - pub fn new(server_name: &str) -> Result { + pub fn new(server_name: &str) -> Result + where + String: Into, + { use crate::{generate_localpart, is_valid_server_name}; if !is_valid_server_name(server_name) { @@ -45,22 +48,28 @@ impl RoomId { } /// Returns the rooms's unique ID. - pub fn localpart(&self) -> &str { - &self.full_id[1..self.colon_idx.get() as usize] + pub fn localpart(&self) -> &str + where + T: AsRef, + { + &self.full_id.as_ref()[1..self.colon_idx.get() as usize] } /// Returns the server name of the room ID. - pub fn server_name(&self) -> &str { - &self.full_id[self.colon_idx.get() as usize + 1..] + pub fn server_name(&self) -> &str + where + T: AsRef, + { + &self.full_id.as_ref()[self.colon_idx.get() as usize + 1..] } } /// Attempts to create a new Matrix room ID from a string representation. /// /// The string must include the leading ! sigil, the localpart, a literal colon, and a server name. -fn try_from(room_id: S) -> Result +fn try_from(room_id: S) -> Result, Error> where - S: AsRef + Into>, + S: AsRef + Into, { let colon_idx = parse_id(room_id.as_ref(), &['!'])?; @@ -79,9 +88,10 @@ mod tests { #[cfg(feature = "serde")] use serde_json::{from_str, to_string}; - use super::RoomId; use crate::error::Error; + type RoomId = super::RoomId>; + #[test] fn valid_room_id() { assert_eq!( diff --git a/src/room_id_or_room_alias_id.rs b/src/room_id_or_room_alias_id.rs index 00018c82..67c93c80 100644 --- a/src/room_id_or_room_alias_id.rs +++ b/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, RoomAliasId, RoomId}; +use crate::{error::Error, parse_id, room_alias_id::RoomAliasId, room_id::RoomId}; /// A Matrix room ID or a Matrix room alias ID. /// @@ -23,21 +23,21 @@ use crate::{error::Error, parse_id, RoomAliasId, RoomId}; /// "!n8f893n9:example.com" /// ); /// ``` -#[derive(Clone, Debug)] -pub struct RoomIdOrAliasId { - full_id: Box, +#[derive(Clone, Copy, Debug)] +pub struct RoomIdOrAliasId { + full_id: T, colon_idx: NonZeroU8, } -impl RoomIdOrAliasId { +impl> RoomIdOrAliasId { /// Returns the local part (everything after the `!` or `#` and before the first colon). pub fn localpart(&self) -> &str { - &self.full_id[1..self.colon_idx.get() as usize] + &self.full_id.as_ref()[1..self.colon_idx.get() as usize] } /// Returns the server name of the room (alias) ID. pub fn server_name(&self) -> &str { - &self.full_id[self.colon_idx.get() as usize + 1..] + &self.full_id.as_ref()[self.colon_idx.get() as usize + 1..] } /// Whether this is a room id (starts with `'!'`) @@ -53,7 +53,7 @@ impl RoomIdOrAliasId { /// Turn this `RoomIdOrAliasId` into `Either` #[cfg(feature = "either")] #[cfg_attr(docsrs, doc(cfg(feature = "either")))] - pub fn into_either(self) -> either::Either { + pub fn into_either(self) -> either::Either, RoomAliasId> { match self.variant() { Variant::RoomId => either::Either::Left(RoomId { full_id: self.full_id, @@ -67,7 +67,7 @@ impl RoomIdOrAliasId { } fn variant(&self) -> Variant { - match self.full_id.bytes().next() { + match self.full_id.as_ref().bytes().next() { Some(b'!') => Variant::RoomId, Some(b'#') => Variant::RoomAliasId, _ => unsafe { unreachable_unchecked() }, @@ -86,9 +86,9 @@ enum Variant { /// The string must either include the leading ! sigil, the localpart, a literal colon, and a /// valid homeserver host or include the leading # sigil, the alias, a literal colon, and a /// valid homeserver host. -fn try_from(room_id_or_alias_id: S) -> Result +fn try_from(room_id_or_alias_id: S) -> Result, Error> where - S: AsRef + Into>, + S: AsRef + Into, { let colon_idx = parse_id(room_id_or_alias_id.as_ref(), &['#', '!'])?; Ok(RoomIdOrAliasId { @@ -103,22 +103,22 @@ common_impls!( "a Matrix room ID or room alias ID" ); -impl From for RoomIdOrAliasId { - fn from(RoomId { full_id, colon_idx }: RoomId) -> Self { +impl From> for RoomIdOrAliasId { + fn from(RoomId { full_id, colon_idx }: RoomId) -> Self { Self { full_id, colon_idx } } } -impl From for RoomIdOrAliasId { - fn from(RoomAliasId { full_id, colon_idx }: RoomAliasId) -> Self { +impl From> for RoomIdOrAliasId { + fn from(RoomAliasId { full_id, colon_idx }: RoomAliasId) -> Self { Self { full_id, colon_idx } } } -impl TryFrom for RoomId { - type Error = RoomAliasId; +impl> TryFrom> for RoomId { + type Error = RoomAliasId; - fn try_from(id: RoomIdOrAliasId) -> Result { + fn try_from(id: RoomIdOrAliasId) -> Result, RoomAliasId> { match id.variant() { Variant::RoomId => Ok(RoomId { full_id: id.full_id, @@ -132,10 +132,10 @@ impl TryFrom for RoomId { } } -impl TryFrom for RoomAliasId { - type Error = RoomId; +impl> TryFrom> for RoomAliasId { + type Error = RoomId; - fn try_from(id: RoomIdOrAliasId) -> Result { + fn try_from(id: RoomIdOrAliasId) -> Result, RoomId> { match id.variant() { Variant::RoomAliasId => Ok(RoomAliasId { full_id: id.full_id, @@ -156,9 +156,10 @@ mod tests { #[cfg(feature = "serde")] use serde_json::{from_str, to_string}; - use super::RoomIdOrAliasId; use crate::error::Error; + type RoomIdOrAliasId = super::RoomIdOrAliasId>; + #[test] fn valid_room_id_or_alias_id_with_a_room_alias_id() { assert_eq!( diff --git a/src/room_version_id.rs b/src/room_version_id.rs index bcffda3d..9b7239e1 100644 --- a/src/room_version_id.rs +++ b/src/room_version_id.rs @@ -24,13 +24,13 @@ const MAX_CODE_POINTS: usize = 32; /// # use ruma_identifiers::RoomVersionId; /// assert_eq!(RoomVersionId::try_from("1").unwrap().as_ref(), "1"); /// ``` -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct RoomVersionId(InnerRoomVersionId); +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct RoomVersionId(InnerRoomVersionId); /// Possibile values for room version, distinguishing between official Matrix versions and custom /// versions. -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -enum InnerRoomVersionId { +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +enum InnerRoomVersionId { /// A version 1 room. Version1, @@ -50,10 +50,10 @@ enum InnerRoomVersionId { Version6, /// A custom room version. - Custom(Box), + Custom(T), } -impl RoomVersionId { +impl RoomVersionId { /// Creates a version 1 room ID. pub fn version_1() -> Self { Self(InnerRoomVersionId::Version1) @@ -85,7 +85,10 @@ impl RoomVersionId { } /// Creates a custom room version ID from the given string slice. - pub fn custom(id: String) -> Self { + pub fn custom(id: String) -> Self + where + String: Into, + { Self(InnerRoomVersionId::Custom(id.into())) } @@ -104,37 +107,37 @@ impl RoomVersionId { /// Whether or not this is a version 1 room. pub fn is_version_1(&self) -> bool { - self.0 == InnerRoomVersionId::Version1 + matches!(self.0, InnerRoomVersionId::Version1) } /// Whether or not this is a version 2 room. pub fn is_version_2(&self) -> bool { - self.0 == InnerRoomVersionId::Version2 + matches!(self.0, InnerRoomVersionId::Version2) } /// Whether or not this is a version 3 room. pub fn is_version_3(&self) -> bool { - self.0 == InnerRoomVersionId::Version3 + matches!(self.0, InnerRoomVersionId::Version3) } /// Whether or not this is a version 4 room. pub fn is_version_4(&self) -> bool { - self.0 == InnerRoomVersionId::Version4 + matches!(self.0, InnerRoomVersionId::Version4) } /// Whether or not this is a version 5 room. pub fn is_version_5(&self) -> bool { - self.0 == InnerRoomVersionId::Version5 + matches!(self.0, InnerRoomVersionId::Version5) } /// Whether or not this is a version 6 room. pub fn is_version_6(&self) -> bool { - self.0 == InnerRoomVersionId::Version5 + matches!(self.0, InnerRoomVersionId::Version5) } } -impl From for String { - fn from(id: RoomVersionId) -> Self { +impl From>> for String { + fn from(id: RoomVersionId>) -> Self { match id.0 { InnerRoomVersionId::Version1 => "1".to_owned(), InnerRoomVersionId::Version2 => "2".to_owned(), @@ -147,7 +150,7 @@ impl From for String { } } -impl AsRef for RoomVersionId { +impl> AsRef for RoomVersionId { fn as_ref(&self) -> &str { match &self.0 { InnerRoomVersionId::Version1 => "1", @@ -156,31 +159,31 @@ impl AsRef for RoomVersionId { InnerRoomVersionId::Version4 => "4", InnerRoomVersionId::Version5 => "5", InnerRoomVersionId::Version6 => "6", - InnerRoomVersionId::Custom(version) => version, + InnerRoomVersionId::Custom(version) => version.as_ref(), } } } -impl Display for RoomVersionId { +impl> Display for RoomVersionId { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "{}", self.as_ref()) } } -impl PartialOrd for RoomVersionId { - fn partial_cmp(&self, other: &RoomVersionId) -> Option { +impl> PartialOrd for RoomVersionId { + fn partial_cmp(&self, other: &RoomVersionId) -> Option { self.as_ref().partial_cmp(other.as_ref()) } } -impl Ord for RoomVersionId { +impl> Ord for RoomVersionId { fn cmp(&self, other: &Self) -> Ordering { self.as_ref().cmp(other.as_ref()) } } #[cfg(feature = "serde")] -impl Serialize for RoomVersionId { +impl> Serialize for RoomVersionId { fn serialize(&self, serializer: S) -> Result where S: Serializer, @@ -190,7 +193,7 @@ impl Serialize for RoomVersionId { } #[cfg(feature = "serde")] -impl<'de> Deserialize<'de> for RoomVersionId { +impl<'de> Deserialize<'de> for RoomVersionId> { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, @@ -200,9 +203,9 @@ impl<'de> Deserialize<'de> for RoomVersionId { } /// Attempts to create a new Matrix room version ID from a string representation. -fn try_from(room_version_id: S) -> Result +fn try_from(room_version_id: S) -> Result, Error> where - S: AsRef + Into>, + S: AsRef + Into, { let version = match room_version_id.as_ref() { "1" => RoomVersionId(InnerRoomVersionId::Version1), @@ -224,7 +227,15 @@ where Ok(version) } -impl TryFrom<&str> for RoomVersionId { +impl<'a> TryFrom<&'a str> for RoomVersionId<&'a str> { + type Error = crate::error::Error; + + fn try_from(s: &'a str) -> Result { + try_from(s) + } +} + +impl TryFrom<&str> for RoomVersionId> { type Error = crate::error::Error; fn try_from(s: &str) -> Result { @@ -232,7 +243,7 @@ impl TryFrom<&str> for RoomVersionId { } } -impl TryFrom for RoomVersionId { +impl TryFrom for RoomVersionId> { type Error = crate::error::Error; fn try_from(s: String) -> Result { @@ -240,26 +251,26 @@ impl TryFrom for RoomVersionId { } } -impl PartialEq<&str> for RoomVersionId { +impl> PartialEq<&str> for RoomVersionId { fn eq(&self, other: &&str) -> bool { self.as_ref() == *other } } -impl PartialEq for &str { - fn eq(&self, other: &RoomVersionId) -> bool { +impl> PartialEq> for &str { + fn eq(&self, other: &RoomVersionId) -> bool { *self == other.as_ref() } } -impl PartialEq for RoomVersionId { +impl> PartialEq for RoomVersionId { fn eq(&self, other: &String) -> bool { self.as_ref() == other } } -impl PartialEq for String { - fn eq(&self, other: &RoomVersionId) -> bool { +impl> PartialEq> for String { + fn eq(&self, other: &RoomVersionId) -> bool { self == other.as_ref() } } @@ -271,9 +282,10 @@ mod tests { #[cfg(feature = "serde")] use serde_json::{from_str, to_string}; - use super::RoomVersionId; use crate::error::Error; + type RoomVersionId = super::RoomVersionId>; + #[test] fn valid_version_1_room_version_id() { assert_eq!( diff --git a/src/user_id.rs b/src/user_id.rs index 5bb8047c..f4b2962c 100644 --- a/src/user_id.rs +++ b/src/user_id.rs @@ -17,9 +17,9 @@ use crate::{error::Error, is_valid_server_name, parse_id}; /// "@carl:example.com" /// ); /// ``` -#[derive(Clone, Debug)] -pub struct UserId { - full_id: Box, +#[derive(Clone, Copy, Debug)] +pub struct UserId { + full_id: T, colon_idx: NonZeroU8, /// Whether this user id is a historical one. /// @@ -29,14 +29,17 @@ pub struct UserId { is_historical: bool, } -impl UserId { +impl UserId { /// Attempts to generate a `UserId` for the given origin server with a localpart consisting of /// 12 random ASCII characters. /// /// Fails if the given homeserver cannot be parsed as a valid host. #[cfg(feature = "rand")] #[cfg_attr(docsrs, doc(cfg(feature = "rand")))] - pub fn new(server_name: &str) -> Result { + pub fn new(server_name: &str) -> Result + where + String: Into, + { use crate::generate_localpart; if !is_valid_server_name(server_name) { @@ -59,13 +62,16 @@ impl UserId { /// localpart, not the localpart plus the `@` prefix, or the localpart plus server name without /// the `@` prefix. pub fn parse_with_server_name( - id: impl AsRef + Into>, + id: impl AsRef + Into, server_name: &str, - ) -> Result { + ) -> Result + where + String: Into, + { let id_str = id.as_ref(); if id_str.starts_with('@') { - try_from(id.into()) + try_from(id) } else { let is_fully_conforming = localpart_is_fully_comforming(id_str)?; if !is_valid_server_name(server_name) { @@ -81,13 +87,19 @@ impl UserId { } /// Returns the user's localpart. - pub fn localpart(&self) -> &str { - &self.full_id[1..self.colon_idx.get() as usize] + pub fn localpart(&self) -> &str + where + T: AsRef, + { + &self.full_id.as_ref()[1..self.colon_idx.get() as usize] } /// Returns the server name of the user ID. - pub fn server_name(&self) -> &str { - &self.full_id[self.colon_idx.get() as usize + 1..] + pub fn server_name(&self) -> &str + where + T: AsRef, + { + &self.full_id.as_ref()[self.colon_idx.get() as usize + 1..] } /// Whether this user ID is a historical one, i.e. one that doesn't conform to the latest @@ -101,9 +113,9 @@ impl UserId { /// Attempts to create a new Matrix user ID from a string representation. /// /// The string must include the leading @ sigil, the localpart, a literal colon, and a server name. -fn try_from(user_id: S) -> Result +fn try_from(user_id: S) -> Result, Error> where - S: AsRef + Into>, + S: AsRef + Into, { let colon_idx = parse_id(user_id.as_ref(), &['@'])?; let localpart = &user_id.as_ref()[1..colon_idx.get() as usize]; @@ -151,9 +163,10 @@ mod tests { #[cfg(feature = "serde")] use serde_json::{from_str, to_string}; - use super::UserId; use crate::error::Error; + type UserId = super::UserId>; + #[test] fn valid_user_id_from_str() { let user_id = UserId::try_from("@carl:example.com").expect("Failed to create UserId."); From 3746f1d3316caa9aa560db089cf64f34d59dd68d Mon Sep 17 00:00:00 2001 From: Jonas Platte Date: Thu, 4 Jun 2020 16:38:38 +0200 Subject: [PATCH 139/140] Improve documentation --- src/event_id.rs | 3 +++ src/lib.rs | 38 +++++++++++++++++++++++++++++++++ src/room_alias_id.rs | 3 +++ src/room_id.rs | 3 +++ src/room_id_or_room_alias_id.rs | 3 +++ src/room_version_id.rs | 3 +++ src/user_id.rs | 3 +++ 7 files changed, 56 insertions(+) diff --git a/src/event_id.rs b/src/event_id.rs index aa95f931..77d31857 100644 --- a/src/event_id.rs +++ b/src/event_id.rs @@ -9,6 +9,9 @@ use crate::{error::Error, parse_id, validate_id}; /// An `EventId` is generated randomly or converted from a string slice, and can be converted back /// into a string as needed. /// +/// It is discouraged to use this type directly – instead use one of the aliases (`EventId` and +/// `EventIdRef`) in the crate root. +/// /// # Room versions /// /// Matrix specifies multiple [room versions](https://matrix.org/docs/spec/#room-versions) and the diff --git a/src/lib.rs b/src/lib.rs index ae770d04..2fa264cb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,33 +34,71 @@ pub mod room_version_id; pub mod user_id; /// An owned event ID. +/// +/// Can be created via `new` (if the `rand` feature is enabled) and `TryFrom` + +/// `TryFrom<&str>`, implements `Serialize` and `Deserialize` if the `serde` feature is enabled. pub type EventId = event_id::EventId>; + /// A reference to an event ID. +/// +/// Can be created via `TryFrom<&str>`, implements `Serialize` if the `serde` feature is enabled. pub type EventIdRef<'a> = event_id::EventId<&'a str>; /// An owned room alias ID. +/// +/// Can be created via `TryFrom` and `TryFrom<&str>`, implements `Serialize` and +/// `Deserialize` if the `serde` feature is enabled. pub type RoomAliasId = room_alias_id::RoomAliasId>; + /// A reference to a room alias ID. +/// +/// Can be created via `TryFrom<&str>`, implements `Serialize` if the `serde` feature is enabled. pub type RoomAliasIdRef<'a> = room_alias_id::RoomAliasId<&'a str>; /// An owned room ID. +/// +/// Can be created via `new` (if the `rand` feature is enabled) and `TryFrom` + +/// `TryFrom<&str>`, implements `Serialize` and `Deserialize` if the `serde` feature is enabled. pub type RoomId = room_id::RoomId>; + /// A reference to a room ID. +/// +/// Can be created via `TryFrom<&str>`, implements `Serialize` if the `serde` feature is enabled. pub type RoomIdRef<'a> = room_id::RoomId<&'a str>; /// An owned room alias ID or room ID. +/// +/// Can be created via `TryFrom`, `TryFrom<&str>`, `From` and `From`; +/// implements `Serialize` and `Deserialize` if the `serde` feature is enabled. pub type RoomIdOrAliasId = room_id_or_room_alias_id::RoomIdOrAliasId>; + /// A reference to a room alias ID or room ID. +/// +/// Can be created via `TryFrom<&str>`, `From` and `From`; implements +/// `Serialize` if the `serde` feature is enabled. pub type RoomIdOrAliasIdRef<'a> = room_id_or_room_alias_id::RoomIdOrAliasId<&'a str>; /// An owned room version ID. +/// +/// Can be created using the `version_N` constructor functions, `TryFrom` and +/// `TryFrom<&str>`; implements `Serialize` and `Deserialize` if the `serde` feature is enabled. pub type RoomVersionId = room_version_id::RoomVersionId>; + /// A reference to a room version ID. +/// +/// Can be created using the `version_N` constructor functions and via `TryFrom<&str>`, implements +/// `Serialize` if the `serde` feature is enabled. pub type RoomVersionIdRef<'a> = room_version_id::RoomVersionId<&'a str>; /// An owned user ID. +/// +/// Can be created via `new` (if the `rand` feature is enabled) and `TryFrom` + +/// `TryFrom<&str>`, implements `Serialize` and `Deserialize` if the `serde` feature is enabled. pub type UserId = user_id::UserId>; + /// A reference to a user ID. +/// +/// Can be created via `TryFrom<&str>`, implements `Serialize` if the `serde` feature is enabled. pub type UserIdRef<'a> = user_id::UserId<&'a str>; /// All identifiers must be 255 bytes or less. diff --git a/src/room_alias_id.rs b/src/room_alias_id.rs index d8358631..b18ccc00 100644 --- a/src/room_alias_id.rs +++ b/src/room_alias_id.rs @@ -6,6 +6,9 @@ use crate::{error::Error, parse_id}; /// A Matrix room alias ID. /// +/// It is discouraged to use this type directly – instead use one of the aliases (`RoomAliasId` and +/// `RoomAliasIdRef`) in the crate root. +/// /// A `RoomAliasId` is converted from a string slice, and can be converted back into a string as /// needed. /// diff --git a/src/room_id.rs b/src/room_id.rs index 82a4416a..07fdfee6 100644 --- a/src/room_id.rs +++ b/src/room_id.rs @@ -9,6 +9,9 @@ use crate::{error::Error, parse_id}; /// A `RoomId` is generated randomly or converted from a string slice, and can be converted back /// into a string as needed. /// +/// It is discouraged to use this type directly – instead use one of the aliases (`RoomId` and +/// `RoomIdRef`) in the crate root. +/// /// ``` /// # use std::convert::TryFrom; /// # use ruma_identifiers::RoomId; diff --git a/src/room_id_or_room_alias_id.rs b/src/room_id_or_room_alias_id.rs index 67c93c80..5e1dcf0c 100644 --- a/src/room_id_or_room_alias_id.rs +++ b/src/room_id_or_room_alias_id.rs @@ -10,6 +10,9 @@ use crate::{error::Error, parse_id, room_alias_id::RoomAliasId, room_id::RoomId} /// from a string slice, and can be converted back into a string as needed. When converted from a /// string slice, the variant is determined by the leading sigil character. /// +/// It is discouraged to use this type directly – instead use one of the aliases +/// (`RoomIdOrRoomAliasId` and `RoomIdOrRoomAliasIdRef`) in the crate root. +/// /// ``` /// # use std::convert::TryFrom; /// # use ruma_identifiers::RoomIdOrAliasId; diff --git a/src/room_version_id.rs b/src/room_version_id.rs index 9b7239e1..742b2cc8 100644 --- a/src/room_version_id.rs +++ b/src/room_version_id.rs @@ -19,6 +19,9 @@ const MAX_CODE_POINTS: usize = 32; /// A `RoomVersionId` can be or converted or deserialized from a string slice, and can be converted /// or serialized back into a string as needed. /// +/// It is discouraged to use this type directly – instead use one of the aliases (`RoomVersionId` +/// and `RoomVersionIdRef`) in the crate root. +/// /// ``` /// # use std::convert::TryFrom; /// # use ruma_identifiers::RoomVersionId; diff --git a/src/user_id.rs b/src/user_id.rs index f4b2962c..2e8cb77c 100644 --- a/src/user_id.rs +++ b/src/user_id.rs @@ -9,6 +9,9 @@ use crate::{error::Error, is_valid_server_name, parse_id}; /// A `UserId` is generated randomly or converted from a string slice, and can be converted back /// into a string as needed. /// +/// It is discouraged to use this type directly – instead use one of the aliases (`UserId` and +/// `UserIdRef`) in the crate root. +/// /// ``` /// # use std::convert::TryFrom; /// # use ruma_identifiers::UserId; From c0a1d8bd440c7cde0fa4ab5e22898ddb26bb706d Mon Sep 17 00:00:00 2001 From: iinuwa Date: Sun, 7 Jun 2020 10:03:49 -0500 Subject: [PATCH 140/140] Add key identifiers --- CHANGELOG.md | 4 ++ Cargo.toml | 3 +- src/device_key_id.rs | 136 ++++++++++++++++++++++++++++++++++++++++++ src/error.rs | 12 ++++ src/key_algorithms.rs | 33 ++++++++++ src/lib.rs | 3 + src/server_key_id.rs | 125 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 315 insertions(+), 1 deletion(-) create mode 100644 src/device_key_id.rs create mode 100644 src/key_algorithms.rs create mode 100644 src/server_key_id.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b65e54c..275e87f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ Breaking changes: `Into>` of the id type). This is technically a breaking change, but extremely unlikely to affect any existing code. +Improvements: + +* Add `DeviceKeyId`, `KeyAlgorithm`, and `ServerKeyId` + # 0.16.2 Improvements: diff --git a/Cargo.toml b/Cargo.toml index 9e40aac5..79c77ff0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,8 @@ default = ["serde"] [dependencies] either = { version = "1.5.3", optional = true } rand = { version = "0.7.3", optional = true } -serde = { version = "1.0.106", optional = true } +serde = { version = "1.0.106", optional = true, features = ["derive"] } +strum = { version = "0.18.0", features = ["derive"] } [dev-dependencies] serde_json = "1.0.51" diff --git a/src/device_key_id.rs b/src/device_key_id.rs new file mode 100644 index 00000000..419c9398 --- /dev/null +++ b/src/device_key_id.rs @@ -0,0 +1,136 @@ +//! Identifiers for device keys for end-to-end encryption. + +use crate::{device_id::DeviceId, error::Error, key_algorithms::DeviceKeyAlgorithm}; +use std::num::NonZeroU8; +use std::str::FromStr; + +/// A key algorithm and a device id, combined with a ':' +#[derive(Clone, Debug)] +pub struct DeviceKeyId { + full_id: T, + colon_idx: NonZeroU8, +} + +impl DeviceKeyId { + /// Returns key algorithm of the device key ID. + pub fn algorithm(&self) -> DeviceKeyAlgorithm + where + T: AsRef, + { + DeviceKeyAlgorithm::from_str(&self.full_id.as_ref()[..self.colon_idx.get() as usize]) + .unwrap() + } + + /// Returns device ID of the device key ID. + pub fn device_id(&self) -> DeviceId + where + T: AsRef, + { + DeviceId::from(&self.full_id.as_ref()[self.colon_idx.get() as usize + 1..]) + } +} + +fn try_from(key_id: S) -> Result, Error> +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)?; + + Ok(DeviceKeyId { + full_id: key_id.into(), + colon_idx, + }) +} + +common_impls!( + DeviceKeyId, + try_from, + "Device key ID with algorithm and device ID" +); + +#[cfg(test)] +mod test { + use std::convert::TryFrom; + + #[cfg(feature = "serde")] + use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; + + use super::DeviceKeyId; + use crate::{device_id::DeviceId, error::Error, key_algorithms::DeviceKeyAlgorithm}; + + #[test] + fn convert_device_key_id() { + assert_eq!( + DeviceKeyId::<&str>::try_from("ed25519:JLAFKJWSCS") + .expect("Failed to create device key ID.") + .as_ref(), + "ed25519:JLAFKJWSCS" + ); + } + + #[cfg(feature = "serde")] + #[test] + fn serialize_device_key_id() { + let device_key_id = DeviceKeyId::<&str>::try_from("ed25519:JLAFKJWSCS").unwrap(); + let serialized = to_json_value(device_key_id).unwrap(); + + let expected = json!("ed25519:JLAFKJWSCS"); + assert_eq!(serialized, expected); + } + + #[cfg(feature = "serde")] + #[test] + fn deserialize_device_key_id() { + let deserialized: DeviceKeyId<_> = from_json_value(json!("ed25519:JLAFKJWSCS")).unwrap(); + + let expected = DeviceKeyId::try_from("ed25519:JLAFKJWSCS").unwrap(); + assert_eq!(deserialized, expected); + } + + #[test] + fn missing_key_algorithm() { + assert_eq!( + DeviceKeyId::<&str>::try_from(":JLAFKJWSCS").unwrap_err(), + Error::UnknownKeyAlgorithm + ); + } + + #[test] + fn missing_delimiter() { + assert_eq!( + DeviceKeyId::<&str>::try_from("ed25519|JLAFKJWSCS").unwrap_err(), + Error::MissingDeviceKeyDelimiter, + ); + } + + #[test] + fn unknown_key_algorithm() { + assert_eq!( + DeviceKeyId::<&str>::try_from("signed_curve25510:JLAFKJWSCS").unwrap_err(), + Error::UnknownKeyAlgorithm, + ); + } + + #[test] + fn empty_device_id_ok() { + assert!(DeviceKeyId::<&str>::try_from("ed25519:").is_ok()); + } + + #[test] + fn valid_key_algorithm() { + let device_key_id = DeviceKeyId::<&str>::try_from("ed25519:JLAFKJWSCS").unwrap(); + assert_eq!(device_key_id.algorithm(), DeviceKeyAlgorithm::Ed25519); + } + + #[test] + fn valid_device_id() { + let device_key_id = DeviceKeyId::<&str>::try_from("ed25519:JLAFKJWSCS").unwrap(); + assert_eq!(device_key_id.device_id(), DeviceId::from("JLAFKJWSCS")); + } +} diff --git a/src/error.rs b/src/error.rs index 0ea68c2f..d61017f8 100644 --- a/src/error.rs +++ b/src/error.rs @@ -9,6 +9,8 @@ pub enum Error { /// /// Only relevant for user IDs. InvalidCharacters, + /// The key version contains outside of [a-zA-Z0-9_]. + InvalidKeyVersion, /// The localpart of the ID string is not valid (because it is empty). InvalidLocalPart, /// The server name part of the the ID string is not a valid server name. @@ -19,20 +21,30 @@ pub enum Error { MinimumLengthNotSatisfied, /// The ID is missing the colon delimiter between localpart and server name. MissingDelimiter, + /// The ID is missing the colon delimiter between key algorithm and device ID. + MissingDeviceKeyDelimiter, + /// The ID is missing the colon delimiter between key algorithm and version. + MissingServerKeyDelimiter, /// The ID is missing the leading sigil. MissingSigil, + /// The key algorithm is not recognized. + UnknownKeyAlgorithm, } impl Display for Error { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { let message = match self { Error::InvalidCharacters => "localpart contains invalid characters", + Error::InvalidKeyVersion => "key id version contains invalid characters", Error::InvalidLocalPart => "localpart is empty", Error::InvalidServerName => "server name is not a valid IP address or domain name", Error::MaximumLengthExceeded => "ID exceeds 255 bytes", Error::MinimumLengthNotSatisfied => "ID must be at least 4 characters", Error::MissingDelimiter => "colon is required between localpart and server name", + Error::MissingDeviceKeyDelimiter => "colon is required between algorithm and device ID", + Error::MissingServerKeyDelimiter => "colon is required between algorithm and version", Error::MissingSigil => "leading sigil is missing", + Error::UnknownKeyAlgorithm => "unknown key algorithm specified", }; write!(f, "{}", message) diff --git a/src/key_algorithms.rs b/src/key_algorithms.rs new file mode 100644 index 00000000..a4879b6e --- /dev/null +++ b/src/key_algorithms.rs @@ -0,0 +1,33 @@ +//! Key algorithms used in Matrix spec. + +#[cfg(feature = "serde")] +use serde::{Deserialize, Serialize}; + +use strum::{AsRefStr, Display, EnumString}; + +/// The basic key algorithms in the specification +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, AsRefStr, Display, EnumString)] +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +#[non_exhaustive] +pub enum DeviceKeyAlgorithm { + /// The Ed25519 signature algorithm. + #[strum(to_string = "ed25519")] + Ed25519, + + /// The Curve25519 ECDH algorithm. + #[strum(to_string = "curve25519")] + Curve25519, + + /// The Curve25519 ECDH algorithm, but the key also contains signatures + #[strum(to_string = "signed_curve25519")] + SignedCurve25519, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, AsRefStr, Display, EnumString)] +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +#[non_exhaustive] +pub enum ServerKeyAlgorithm { + /// The Ed25519 signature algorithm. + #[strum(to_string = "ed25519")] + Ed25519, +} diff --git a/src/lib.rs b/src/lib.rs index 2fa264cb..09f378e6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,11 +26,14 @@ mod error; mod server_name; pub mod device_id; +pub mod device_key_id; pub mod event_id; +pub mod key_algorithms; pub mod room_alias_id; pub mod room_id; pub mod room_id_or_room_alias_id; pub mod room_version_id; +pub mod server_key_id; pub mod user_id; /// An owned event ID. diff --git a/src/server_key_id.rs b/src/server_key_id.rs new file mode 100644 index 00000000..336a052b --- /dev/null +++ b/src/server_key_id.rs @@ -0,0 +1,125 @@ +//! Identifiers for homeserver signing keys used for federation. + +use std::{num::NonZeroU8, str::FromStr}; + +use crate::{error::Error, key_algorithms::ServerKeyAlgorithm}; + +/// Key identifiers used for homeserver signing keys. +#[derive(Clone, Debug)] +pub struct ServerKeyId { + full_id: T, + colon_idx: NonZeroU8, +} + +impl ServerKeyId { + /// Returns key algorithm of the server key ID. + pub fn algorithm(&self) -> ServerKeyAlgorithm + where + T: AsRef, + { + ServerKeyAlgorithm::from_str(&self.full_id.as_ref()[..self.colon_idx.get() as usize]) + .unwrap() + } + + /// Returns the version of the server key ID. + pub fn version(&self) -> &str + where + T: AsRef, + { + &self.full_id.as_ref()[self.colon_idx.get() as usize + 1..] + } +} + +fn try_from(key_id: S) -> Result, Error> +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..])?; + + 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; + + #[cfg(feature = "serde")] + use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; + + use super::ServerKeyId; + use crate::error::Error; + + #[cfg(feature = "serde")] + use crate::key_algorithms::ServerKeyAlgorithm; + + #[cfg(feature = "serde")] + #[test] + fn deserialize_id() { + let server_key_id: ServerKeyId<_> = from_json_value(json!("ed25519:Abc_1")).unwrap(); + assert_eq!(server_key_id.algorithm(), ServerKeyAlgorithm::Ed25519); + assert_eq!(server_key_id.version(), "Abc_1"); + } + + #[cfg(feature = "serde")] + #[test] + fn serialize_id() { + let server_key_id: ServerKeyId<&str> = ServerKeyId::try_from("ed25519:abc123").unwrap(); + assert_eq!( + to_json_value(&server_key_id).unwrap(), + json!("ed25519:abc123") + ); + } + + #[test] + fn invalid_version_characters() { + assert_eq!( + ServerKeyId::<&str>::try_from("ed25519:Abc-1").unwrap_err(), + Error::InvalidCharacters, + ); + } + + #[test] + fn invalid_key_algorithm() { + assert_eq!( + ServerKeyId::<&str>::try_from("signed_curve25519:Abc-1").unwrap_err(), + Error::UnknownKeyAlgorithm, + ); + } + + #[test] + fn missing_delimiter() { + assert_eq!( + ServerKeyId::<&str>::try_from("ed25519|Abc_1").unwrap_err(), + Error::MissingServerKeyDelimiter, + ); + } +}