Move ruma-identifiers validation logic into a new crate

This commit is contained in:
Jonas Platte 2020-08-04 23:23:39 +02:00
parent 6c4589d642
commit 1881e45eee
No known key found for this signature in database
GPG Key ID: 7D261D771D915378
26 changed files with 321 additions and 214 deletions

View File

@ -0,0 +1,16 @@
[package]
name = "ruma-identifiers-validation"
description = "Validation logic for ruma-identifiers and ruma-identifiers-macros"
documentation = "https://docs.rs/ruma-identifiers"
homepage = "https://www.ruma.io/"
repository = "https://github.com/ruma/ruma"
authors = [
"Jimmy Cuadra <jimmy@jimmycuadra.com>",
"Jonas Platte <jplatte@posteo.de>",
]
license = "MIT"
version = "0.1.0"
edition = "2018"
[dependencies]
strum = { version = "0.18.0", features = ["derive"] }

View File

@ -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.

View File

@ -0,0 +1,13 @@
use std::{num::NonZeroU8, str::FromStr};
use crate::{key_algorithms::DeviceKeyAlgorithm, Error};
pub fn validate(s: &str) -> Result<NonZeroU8, Error> {
let colon_idx = NonZeroU8::new(s.find(':').ok_or(Error::MissingDeviceKeyDelimiter)? as u8)
.ok_or(Error::UnknownKeyAlgorithm)?;
DeviceKeyAlgorithm::from_str(&s[0..colon_idx.get() as usize])
.map_err(|_| Error::UnknownKeyAlgorithm)?;
Ok(colon_idx)
}

View File

@ -0,0 +1,13 @@
use std::num::NonZeroU8;
use crate::{parse_id, validate_id, Error};
pub fn validate(s: &str) -> Result<Option<NonZeroU8>, Error> {
Ok(match s.contains(':') {
true => Some(parse_id(s, &['$'])?),
false => {
validate_id(s, &['$'])?;
None
}
})
}

View File

@ -0,0 +1,56 @@
pub mod device_key_id;
pub mod error;
pub mod event_id;
pub mod key_algorithms;
pub mod room_alias_id;
pub mod room_id;
pub mod room_id_or_alias_id;
pub mod room_version_id;
pub mod server_key_id;
pub mod server_name;
pub mod user_id;
use std::num::NonZeroU8;
pub use error::Error;
/// All identifiers must be 255 bytes or less.
const MAX_BYTES: usize = 255;
/// The minimum number of characters an ID can be.
///
/// This is an optimization and not required by the spec. The shortest possible valid ID is a sigil
/// + a single character local ID + a colon + a single character hostname.
const MIN_CHARS: usize = 4;
/// Checks if an identifier is valid.
fn validate_id(id: &str, valid_sigils: &[char]) -> Result<(), Error> {
if id.len() > MAX_BYTES {
return Err(Error::MaximumLengthExceeded);
}
if id.len() < MIN_CHARS {
return Err(Error::MinimumLengthNotSatisfied);
}
if !valid_sigils.contains(&id.chars().next().unwrap()) {
return Err(Error::MissingSigil);
}
Ok(())
}
/// Checks an identifier that contains a localpart and hostname for validity,
/// and returns the index of the colon that separates the two.
fn parse_id(id: &str, valid_sigils: &[char]) -> Result<NonZeroU8, Error> {
validate_id(id, valid_sigils)?;
let colon_idx = id.find(':').ok_or(Error::MissingDelimiter)?;
if colon_idx < 2 {
return Err(Error::InvalidLocalPart);
}
server_name::validate(&id[colon_idx + 1..])?;
Ok(NonZeroU8::new(colon_idx as u8).unwrap())
}

View File

@ -0,0 +1,7 @@
use std::num::NonZeroU8;
use crate::{parse_id, Error};
pub fn validate(s: &str) -> Result<NonZeroU8, Error> {
parse_id(s, &['#'])
}

View File

@ -0,0 +1,7 @@
use std::num::NonZeroU8;
use crate::{parse_id, Error};
pub fn validate(s: &str) -> Result<NonZeroU8, Error> {
parse_id(s, &['!'])
}

View File

@ -0,0 +1,7 @@
use std::num::NonZeroU8;
use crate::{parse_id, Error};
pub fn validate(s: &str) -> Result<NonZeroU8, Error> {
parse_id(s, &['#', '!'])
}

View File

@ -0,0 +1,14 @@
use crate::Error;
/// Room version identifiers cannot be more than 32 code points.
const MAX_CODE_POINTS: usize = 32;
pub fn validate(s: &str) -> Result<(), Error> {
if s.is_empty() {
Err(Error::MinimumLengthNotSatisfied)
} else if s.chars().count() > MAX_CODE_POINTS {
Err(Error::MaximumLengthExceeded)
} else {
Ok(())
}
}

View File

@ -0,0 +1,30 @@
use std::{num::NonZeroU8, str::FromStr};
use crate::{key_algorithms::ServerKeyAlgorithm, Error};
pub fn validate(s: &str) -> Result<NonZeroU8, Error> {
let colon_idx = NonZeroU8::new(s.find(':').ok_or(Error::MissingServerKeyDelimiter)? as u8)
.ok_or(Error::UnknownKeyAlgorithm)?;
validate_server_key_algorithm(&s[..colon_idx.get() as usize])?;
validate_version(&s[colon_idx.get() as usize + 1..])?;
Ok(colon_idx)
}
fn validate_version(version: &str) -> Result<(), Error> {
if version.is_empty() {
return Err(Error::MinimumLengthNotSatisfied);
} else if !version.chars().all(|c| c.is_alphanumeric() || c == '_') {
return Err(Error::InvalidCharacters);
}
Ok(())
}
fn validate_server_key_algorithm(algorithm: &str) -> Result<(), Error> {
match ServerKeyAlgorithm::from_str(algorithm) {
Ok(_) => Ok(()),
Err(_) => Err(Error::UnknownKeyAlgorithm),
}
}

View File

@ -0,0 +1,46 @@
use crate::error::Error;
pub fn validate(server_name: &str) -> Result<(), Error> {
use std::net::Ipv6Addr;
if server_name.is_empty() {
return Err(Error::InvalidServerName);
}
let end_of_host = if server_name.starts_with('[') {
let end_of_ipv6 = match server_name.find(']') {
Some(idx) => idx,
None => return Err(Error::InvalidServerName),
};
if server_name[1..end_of_ipv6].parse::<Ipv6Addr>().is_err() {
return Err(Error::InvalidServerName);
}
end_of_ipv6 + 1
} else {
let end_of_host = server_name.find(':').unwrap_or_else(|| server_name.len());
if server_name[..end_of_host]
.bytes()
.any(|byte| !(byte.is_ascii_alphanumeric() || byte == b'-' || byte == b'.'))
{
return Err(Error::InvalidServerName);
}
end_of_host
};
if server_name.len() != end_of_host
&& (
// hostname is followed by something other than ":port"
server_name.as_bytes()[end_of_host] != b':'
// the remaining characters after ':' are not a valid port
|| server_name[end_of_host + 1..].parse::<u16>().is_err()
)
{
Err(Error::InvalidServerName)
} else {
Ok(())
}
}

View File

@ -0,0 +1,35 @@
use std::num::NonZeroU8;
use crate::{parse_id, Error};
pub fn validate(s: &str) -> Result<(NonZeroU8, bool), Error> {
let colon_idx = parse_id(s, &['@'])?;
let localpart = &s[1..colon_idx.get() as usize];
let is_historical = localpart_is_fully_comforming(localpart)?;
Ok((colon_idx, is_historical))
}
/// Check whether the given user id localpart is valid and fully conforming
///
/// Returns an `Err` for invalid user ID localparts, `Ok(false)` for historical user ID localparts
/// and `Ok(true)` for fully conforming user ID localparts.
pub fn localpart_is_fully_comforming(localpart: &str) -> Result<bool, Error> {
if localpart.is_empty() {
return Err(Error::InvalidLocalPart);
}
// See https://matrix.org/docs/spec/appendices#user-identifiers
let is_fully_conforming = localpart
.bytes()
.all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'z' | b'-' | b'.' | b'=' | b'_' | b'/'));
// If it's not fully conforming, check if it contains characters that are also disallowed
// for historical user IDs. If there are, return an error.
// See https://matrix.org/docs/spec/appendices#historical-user-ids
if !is_fully_conforming && localpart.bytes().any(|b| b < 0x21 || b == b':' || b > 0x7E) {
Err(Error::InvalidCharacters)
} else {
Ok(is_fully_conforming)
}
}

View File

@ -1,5 +1,8 @@
[package]
authors = ["Jimmy Cuadra <jimmy@jimmycuadra.com>"]
authors = [
"Jimmy Cuadra <jimmy@jimmycuadra.com>",
"Jonas Platte <jplatte@posteo.de>",
]
categories = ["api-bindings"]
description = "Resource identifiers for Matrix."
documentation = "https://docs.rs/ruma-identifiers"
@ -22,9 +25,10 @@ default = ["serde"]
[dependencies]
either = { version = "1.5.3", optional = true }
rand = { version = "0.7.3", optional = true }
ruma-identifiers-validation = { version = "0.1.0", path = "../ruma-identifiers-validation" }
serde = { version = "1.0.114", optional = true, features = ["derive"] }
strum = { version = "0.18.0", features = ["derive"] }
[dev-dependencies]
matches = "0.1.8"
serde_json = "1.0.56"
serde_json = "1.0.57"

View File

@ -1,8 +1,11 @@
//! Identifiers for device keys for end-to-end encryption.
use crate::{error::Error, key_algorithms::DeviceKeyAlgorithm, DeviceId};
use std::{num::NonZeroU8, str::FromStr};
use ruma_identifiers_validation::{key_algorithms::DeviceKeyAlgorithm, Error};
use crate::DeviceId;
/// A key algorithm and a device id, combined with a ':'
#[derive(Clone, Debug)]
pub struct DeviceKeyId {
@ -26,14 +29,7 @@ fn try_from<S>(key_id: S) -> Result<DeviceKeyId, Error>
where
S: AsRef<str> + Into<Box<str>>,
{
let key_str = key_id.as_ref();
let colon_idx =
NonZeroU8::new(key_str.find(':').ok_or(Error::MissingDeviceKeyDelimiter)? as u8)
.ok_or(Error::UnknownKeyAlgorithm)?;
DeviceKeyAlgorithm::from_str(&key_str[0..colon_idx.get() as usize])
.map_err(|_| Error::UnknownKeyAlgorithm)?;
let colon_idx = ruma_identifiers_validation::device_key_id::validate(key_id.as_ref())?;
Ok(DeviceKeyId { full_id: key_id.into(), colon_idx })
}
@ -43,11 +39,11 @@ common_impls!(DeviceKeyId, try_from, "Device key ID with algorithm and device ID
mod test {
use std::convert::TryFrom;
use ruma_identifiers_validation::{key_algorithms::DeviceKeyAlgorithm, Error};
#[cfg(feature = "serde")]
use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
use super::DeviceKeyId;
use crate::{error::Error, key_algorithms::DeviceKeyAlgorithm};
#[test]
fn convert_device_key_id() {

View File

@ -2,7 +2,7 @@
use std::{convert::TryFrom, num::NonZeroU8};
use crate::{error::Error, parse_id, validate_id, ServerName};
use crate::{Error, ServerName};
/// A Matrix event ID.
///
@ -91,15 +91,8 @@ fn try_from<S>(event_id: S) -> Result<EventId, Error>
where
S: AsRef<str> + Into<Box<str>>,
{
if event_id.as_ref().contains(':') {
let colon_idx = parse_id(event_id.as_ref(), &['$'])?;
Ok(EventId { full_id: event_id.into(), colon_idx: Some(colon_idx) })
} else {
validate_id(event_id.as_ref(), &['$'])?;
Ok(EventId { full_id: event_id.into(), colon_idx: None })
}
let colon_idx = ruma_identifiers_validation::event_id::validate(event_id.as_ref())?;
Ok(EventId { full_id: event_id.into(), colon_idx })
}
common_impls!(EventId, try_from, "a Matrix event ID");
@ -112,7 +105,7 @@ mod tests {
use serde_json::{from_str, to_string};
use super::EventId;
use crate::error::Error;
use crate::Error;
#[test]
fn valid_original_event_id() {

View File

@ -9,25 +9,21 @@
)]
#![cfg_attr(docsrs, feature(doc_cfg))]
use std::{convert::TryFrom, num::NonZeroU8};
use std::convert::TryFrom;
#[cfg(feature = "serde")]
use serde::de::{self, Deserialize as _, Deserializer, Unexpected};
#[doc(inline)]
pub use crate::{
device_id::DeviceId,
device_key_id::DeviceKeyId,
device_id::DeviceId, device_key_id::DeviceKeyId, event_id::EventId, room_alias_id::RoomAliasId,
room_id::RoomId, room_id_or_room_alias_id::RoomIdOrAliasId, room_version_id::RoomVersionId,
server_key_id::ServerKeyId, server_name::ServerName, user_id::UserId,
};
#[doc(inline)]
pub use ruma_identifiers_validation::{
error::Error,
event_id::EventId,
key_algorithms::{DeviceKeyAlgorithm, ServerKeyAlgorithm},
room_alias_id::RoomAliasId,
room_id::RoomId,
room_id_or_room_alias_id::RoomIdOrAliasId,
room_version_id::RoomVersionId,
server_key_id::ServerKeyId,
server_name::ServerName,
user_id::UserId,
};
#[macro_use]
@ -37,9 +33,7 @@ pub mod device_id;
pub mod user_id;
mod device_key_id;
mod error;
mod event_id;
mod key_algorithms;
mod room_alias_id;
mod room_id;
mod room_id_or_room_alias_id;
@ -55,14 +49,6 @@ pub fn is_valid_server_name(name: &str) -> bool {
<&ServerName>::try_from(name).is_ok()
}
/// All identifiers must be 255 bytes or less.
const MAX_BYTES: usize = 255;
/// The minimum number of characters an ID can be.
///
/// This is an optimization and not required by the spec. The shortest possible valid ID is a sigil
/// + a single character local ID + a colon + a single character hostname.
const MIN_CHARS: usize = 4;
/// Generates a random identifier localpart.
#[cfg(feature = "rand")]
fn generate_localpart(length: usize) -> Box<str> {
@ -74,38 +60,6 @@ fn generate_localpart(length: usize) -> Box<str> {
.into_boxed_str()
}
/// Checks if an identifier is valid.
fn validate_id(id: &str, valid_sigils: &[char]) -> Result<(), Error> {
if id.len() > MAX_BYTES {
return Err(Error::MaximumLengthExceeded);
}
if id.len() < MIN_CHARS {
return Err(Error::MinimumLengthNotSatisfied);
}
if !valid_sigils.contains(&id.chars().next().unwrap()) {
return Err(Error::MissingSigil);
}
Ok(())
}
/// Checks an identifier that contains a localpart and hostname for validity,
/// and returns the index of the colon that separates the two.
fn parse_id(id: &str, valid_sigils: &[char]) -> Result<NonZeroU8, Error> {
validate_id(id, valid_sigils)?;
let colon_idx = id.find(':').ok_or(Error::MissingDelimiter)?;
if colon_idx < 2 {
return Err(Error::InvalidLocalPart);
}
server_name::validate(&id[colon_idx + 1..])?;
Ok(NonZeroU8::new(colon_idx as u8).unwrap())
}
/// Deserializes any type of id using the provided TryFrom implementation.
///
/// This is a helper function to reduce the boilerplate of the Deserialize implementations.

View File

@ -55,7 +55,7 @@ macro_rules! common_impls {
}
impl ::std::convert::TryFrom<&str> for $id {
type Error = crate::error::Error;
type Error = crate::Error;
fn try_from(s: &str) -> Result<Self, Self::Error> {
$try_from(s)
@ -63,7 +63,7 @@ macro_rules! common_impls {
}
impl ::std::convert::TryFrom<String> for $id {
type Error = crate::error::Error;
type Error = crate::Error;
fn try_from(s: String) -> Result<Self, Self::Error> {
$try_from(s)

View File

@ -2,7 +2,7 @@
use std::{convert::TryFrom, num::NonZeroU8};
use crate::{error::Error, parse_id, server_name::ServerName};
use crate::{server_name::ServerName, Error};
/// A Matrix room alias ID.
///
@ -38,13 +38,12 @@ impl RoomAliasId {
/// Attempts to create a new Matrix room alias ID from a string representation.
///
/// The string must include the leading # sigil, the alias, a literal colon, and a server name.
fn try_from<S>(room_id: S) -> Result<RoomAliasId, Error>
fn try_from<S>(room_alias_id: S) -> Result<RoomAliasId, Error>
where
S: AsRef<str> + Into<Box<str>>,
{
let colon_idx = parse_id(room_id.as_ref(), &['#'])?;
Ok(RoomAliasId { full_id: room_id.into(), colon_idx })
let colon_idx = ruma_identifiers_validation::room_alias_id::validate(room_alias_id.as_ref())?;
Ok(RoomAliasId { full_id: room_alias_id.into(), colon_idx })
}
common_impls!(RoomAliasId, try_from, "a Matrix room alias ID");
@ -57,7 +56,7 @@ mod tests {
use serde_json::{from_str, to_string};
use super::RoomAliasId;
use crate::error::Error;
use crate::Error;
#[test]
fn valid_room_alias_id() {

View File

@ -2,7 +2,7 @@
use std::{convert::TryFrom, num::NonZeroU8};
use crate::{error::Error, parse_id, ServerName};
use crate::{Error, ServerName};
/// A Matrix room ID.
///
@ -58,8 +58,7 @@ fn try_from<S>(room_id: S) -> Result<RoomId, Error>
where
S: AsRef<str> + Into<Box<str>>,
{
let colon_idx = parse_id(room_id.as_ref(), &['!'])?;
let colon_idx = ruma_identifiers_validation::room_id::validate(room_id.as_ref())?;
Ok(RoomId { full_id: room_id.into(), colon_idx })
}
@ -73,7 +72,7 @@ mod tests {
use serde_json::{from_str, to_string};
use super::RoomId;
use crate::error::Error;
use crate::Error;
#[test]
fn valid_room_id() {

View File

@ -2,7 +2,7 @@
use std::{convert::TryFrom, hint::unreachable_unchecked, num::NonZeroU8};
use crate::{error::Error, parse_id, server_name::ServerName, RoomAliasId, RoomId};
use crate::{server_name::ServerName, Error, RoomAliasId, RoomId};
/// A Matrix room ID or a Matrix room alias ID.
///
@ -89,7 +89,8 @@ fn try_from<S>(room_id_or_alias_id: S) -> Result<RoomIdOrAliasId, Error>
where
S: AsRef<str> + Into<Box<str>>,
{
let colon_idx = parse_id(room_id_or_alias_id.as_ref(), &['#', '!'])?;
let colon_idx =
ruma_identifiers_validation::room_id_or_alias_id::validate(room_id_or_alias_id.as_ref())?;
Ok(RoomIdOrAliasId { full_id: room_id_or_alias_id.into(), colon_idx })
}
@ -141,7 +142,7 @@ mod tests {
use serde_json::{from_str, to_string};
use super::RoomIdOrAliasId;
use crate::error::Error;
use crate::Error;
#[test]
fn valid_room_id_or_alias_id_with_a_room_alias_id() {

View File

@ -9,10 +9,7 @@ use std::{
#[cfg(feature = "serde")]
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::error::Error;
/// Room version identifiers cannot be more than 32 code points.
const MAX_CODE_POINTS: usize = 32;
use crate::Error;
/// A Matrix room version ID.
///
@ -231,13 +228,8 @@ where
"4" => RoomVersionId::Version4,
"5" => RoomVersionId::Version5,
custom => {
if custom.is_empty() {
return Err(Error::MinimumLengthNotSatisfied);
} else if custom.chars().count() > MAX_CODE_POINTS {
return Err(Error::MaximumLengthExceeded);
} else {
RoomVersionId::Custom(CustomRoomVersion(room_version_id.into()))
}
ruma_identifiers_validation::room_version_id::validate(custom)?;
RoomVersionId::Custom(CustomRoomVersion(room_version_id.into()))
}
};
@ -245,7 +237,7 @@ where
}
impl TryFrom<&str> for RoomVersionId {
type Error = crate::error::Error;
type Error = crate::Error;
fn try_from(s: &str) -> Result<Self, Error> {
try_from(s)
@ -253,7 +245,7 @@ impl TryFrom<&str> for RoomVersionId {
}
impl TryFrom<String> for RoomVersionId {
type Error = crate::error::Error;
type Error = crate::Error;
fn try_from(s: String) -> Result<Self, Error> {
try_from(s)
@ -314,7 +306,7 @@ mod tests {
use serde_json::{from_str, to_string};
use super::RoomVersionId;
use crate::error::Error;
use crate::Error;
#[test]
fn valid_version_1_room_version_id() {

View File

@ -2,7 +2,7 @@
use std::{num::NonZeroU8, str::FromStr};
use crate::{error::Error, key_algorithms::ServerKeyAlgorithm};
use ruma_identifiers_validation::{key_algorithms::ServerKeyAlgorithm, Error};
/// Key identifiers used for homeserver signing keys.
#[derive(Clone, Debug)]
@ -27,37 +27,12 @@ fn try_from<S>(key_id: S) -> Result<ServerKeyId, Error>
where
S: AsRef<str> + Into<Box<str>>,
{
let key_str = key_id.as_ref();
let colon_idx =
NonZeroU8::new(key_str.find(':').ok_or(Error::MissingServerKeyDelimiter)? as u8)
.ok_or(Error::UnknownKeyAlgorithm)?;
validate_server_key_algorithm(&key_str[..colon_idx.get() as usize])?;
validate_version(&key_str[colon_idx.get() as usize + 1..])?;
let colon_idx = ruma_identifiers_validation::server_key_id::validate(key_id.as_ref())?;
Ok(ServerKeyId { full_id: key_id.into(), colon_idx })
}
common_impls!(ServerKeyId, try_from, "Key ID with algorithm and version");
fn validate_version(version: &str) -> Result<(), Error> {
if version.is_empty() {
return Err(Error::MinimumLengthNotSatisfied);
} else if !version.chars().all(|c| c.is_alphanumeric() || c == '_') {
return Err(Error::InvalidCharacters);
}
Ok(())
}
fn validate_server_key_algorithm(algorithm: &str) -> Result<(), Error> {
match ServerKeyAlgorithm::from_str(algorithm) {
Ok(_) => Ok(()),
Err(_) => Err(Error::UnknownKeyAlgorithm),
}
}
#[cfg(test)]
mod tests {
use std::convert::TryFrom;
@ -65,10 +40,10 @@ mod tests {
#[cfg(feature = "serde")]
use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
use crate::{error::Error, ServerKeyId};
use crate::{Error, ServerKeyId};
#[cfg(feature = "serde")]
use crate::key_algorithms::ServerKeyAlgorithm;
use ruma_identifiers_validation::key_algorithms::ServerKeyAlgorithm;
#[cfg(feature = "serde")]
#[test]

View File

@ -6,7 +6,9 @@ use std::{
mem,
};
use crate::error::Error;
use ruma_identifiers_validation::server_name::validate;
use crate::Error;
/// A Matrix-spec compliant server name.
#[repr(transparent)]
@ -59,51 +61,6 @@ impl From<&ServerName> for Box<ServerName> {
}
}
pub(crate) fn validate(server_name: &str) -> Result<(), Error> {
use std::net::Ipv6Addr;
if server_name.is_empty() {
return Err(Error::InvalidServerName);
}
let end_of_host = if server_name.starts_with('[') {
let end_of_ipv6 = match server_name.find(']') {
Some(idx) => idx,
None => return Err(Error::InvalidServerName),
};
if server_name[1..end_of_ipv6].parse::<Ipv6Addr>().is_err() {
return Err(Error::InvalidServerName);
}
end_of_ipv6 + 1
} else {
let end_of_host = server_name.find(':').unwrap_or_else(|| server_name.len());
if server_name[..end_of_host]
.bytes()
.any(|byte| !(byte.is_ascii_alphanumeric() || byte == b'-' || byte == b'.'))
{
return Err(Error::InvalidServerName);
}
end_of_host
};
if server_name.len() != end_of_host
&& (
// hostname is followed by something other than ":port"
server_name.as_bytes()[end_of_host] != b':'
// the remaining characters after ':' are not a valid port
|| server_name[end_of_host + 1..].parse::<u16>().is_err()
)
{
Err(Error::InvalidServerName)
} else {
Ok(())
}
}
fn try_from<S>(server_name: S) -> Result<Box<ServerName>, Error>
where
S: AsRef<str> + Into<Box<str>>,
@ -140,7 +97,7 @@ impl<'a> TryFrom<&'a str> for &'a ServerName {
}
impl TryFrom<&str> for Box<ServerName> {
type Error = crate::error::Error;
type Error = crate::Error;
fn try_from(s: &str) -> Result<Self, Self::Error> {
try_from(s)
@ -148,7 +105,7 @@ impl TryFrom<&str> for Box<ServerName> {
}
impl TryFrom<String> for Box<ServerName> {
type Error = crate::error::Error;
type Error = crate::Error;
fn try_from(s: String) -> Result<Self, Self::Error> {
try_from(s)

View File

@ -2,7 +2,7 @@
use std::{convert::TryFrom, num::NonZeroU8};
use crate::{error::Error, parse_id, ServerName};
use crate::{Error, ServerName};
/// A Matrix user ID.
///
@ -95,41 +95,14 @@ fn try_from<S>(user_id: S) -> Result<UserId, Error>
where
S: AsRef<str> + Into<Box<str>>,
{
let user_id_str = user_id.as_ref();
let colon_idx = parse_id(user_id_str, &['@'])?;
let localpart = &user_id_str[1..colon_idx.get() as usize];
let is_historical = localpart_is_fully_comforming(localpart)?;
let (colon_idx, is_historical) =
ruma_identifiers_validation::user_id::validate(user_id.as_ref())?;
Ok(UserId { full_id: user_id.into(), colon_idx, is_historical: !is_historical })
}
common_impls!(UserId, try_from, "a Matrix user ID");
/// Check whether the given user id localpart is valid and fully conforming
///
/// Returns an `Err` for invalid user ID localparts, `Ok(false)` for historical user ID localparts
/// and `Ok(true)` for fully conforming user ID localparts.
pub fn localpart_is_fully_comforming(localpart: &str) -> Result<bool, Error> {
if localpart.is_empty() {
return Err(Error::InvalidLocalPart);
}
// See https://matrix.org/docs/spec/appendices#user-identifiers
let is_fully_conforming = localpart
.bytes()
.all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'z' | b'-' | b'.' | b'=' | b'_' | b'/'));
// If it's not fully conforming, check if it contains characters that are also disallowed
// for historical user IDs. If there are, return an error.
// See https://matrix.org/docs/spec/appendices#historical-user-ids
if !is_fully_conforming && localpart.bytes().any(|b| b < 0x21 || b == b':' || b > 0x7E) {
Err(Error::InvalidCharacters)
} else {
Ok(is_fully_conforming)
}
}
pub use ruma_identifiers_validation::user_id::localpart_is_fully_comforming;
#[cfg(test)]
mod tests {
@ -139,7 +112,7 @@ mod tests {
use serde_json::{from_str, to_string};
use super::UserId;
use crate::{error::Error, ServerName};
use crate::{Error, ServerName};
#[test]
fn valid_user_id_from_str() {