Move ruma-identifiers validation logic into a new crate
This commit is contained in:
parent
6c4589d642
commit
1881e45eee
16
ruma-identifiers-validation/Cargo.toml
Normal file
16
ruma-identifiers-validation/Cargo.toml
Normal 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"] }
|
20
ruma-identifiers-validation/LICENSE
Normal file
20
ruma-identifiers-validation/LICENSE
Normal 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.
|
||||||
|
|
13
ruma-identifiers-validation/src/device_key_id.rs
Normal file
13
ruma-identifiers-validation/src/device_key_id.rs
Normal 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)
|
||||||
|
}
|
13
ruma-identifiers-validation/src/event_id.rs
Normal file
13
ruma-identifiers-validation/src/event_id.rs
Normal 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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
56
ruma-identifiers-validation/src/lib.rs
Normal file
56
ruma-identifiers-validation/src/lib.rs
Normal 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())
|
||||||
|
}
|
7
ruma-identifiers-validation/src/room_alias_id.rs
Normal file
7
ruma-identifiers-validation/src/room_alias_id.rs
Normal 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, &['#'])
|
||||||
|
}
|
7
ruma-identifiers-validation/src/room_id.rs
Normal file
7
ruma-identifiers-validation/src/room_id.rs
Normal 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, &['!'])
|
||||||
|
}
|
7
ruma-identifiers-validation/src/room_id_or_alias_id.rs
Normal file
7
ruma-identifiers-validation/src/room_id_or_alias_id.rs
Normal 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, &['#', '!'])
|
||||||
|
}
|
14
ruma-identifiers-validation/src/room_version_id.rs
Normal file
14
ruma-identifiers-validation/src/room_version_id.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
30
ruma-identifiers-validation/src/server_key_id.rs
Normal file
30
ruma-identifiers-validation/src/server_key_id.rs
Normal 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),
|
||||||
|
}
|
||||||
|
}
|
46
ruma-identifiers-validation/src/server_name.rs
Normal file
46
ruma-identifiers-validation/src/server_name.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
35
ruma-identifiers-validation/src/user_id.rs
Normal file
35
ruma-identifiers-validation/src/user_id.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,8 @@
|
|||||||
[package]
|
[package]
|
||||||
authors = ["Jimmy Cuadra <jimmy@jimmycuadra.com>"]
|
authors = [
|
||||||
|
"Jimmy Cuadra <jimmy@jimmycuadra.com>",
|
||||||
|
"Jonas Platte <jplatte@posteo.de>",
|
||||||
|
]
|
||||||
categories = ["api-bindings"]
|
categories = ["api-bindings"]
|
||||||
description = "Resource identifiers for Matrix."
|
description = "Resource identifiers for Matrix."
|
||||||
documentation = "https://docs.rs/ruma-identifiers"
|
documentation = "https://docs.rs/ruma-identifiers"
|
||||||
@ -22,9 +25,10 @@ default = ["serde"]
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
either = { version = "1.5.3", optional = true }
|
either = { version = "1.5.3", optional = true }
|
||||||
rand = { version = "0.7.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"] }
|
serde = { version = "1.0.114", optional = true, features = ["derive"] }
|
||||||
strum = { version = "0.18.0", features = ["derive"] }
|
strum = { version = "0.18.0", features = ["derive"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
matches = "0.1.8"
|
matches = "0.1.8"
|
||||||
serde_json = "1.0.56"
|
serde_json = "1.0.57"
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
//! Identifiers for device keys for end-to-end encryption.
|
//! Identifiers for device keys for end-to-end encryption.
|
||||||
|
|
||||||
use crate::{error::Error, key_algorithms::DeviceKeyAlgorithm, DeviceId};
|
|
||||||
use std::{num::NonZeroU8, str::FromStr};
|
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 ':'
|
/// A key algorithm and a device id, combined with a ':'
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct DeviceKeyId {
|
pub struct DeviceKeyId {
|
||||||
@ -26,14 +29,7 @@ fn try_from<S>(key_id: S) -> Result<DeviceKeyId, Error>
|
|||||||
where
|
where
|
||||||
S: AsRef<str> + Into<Box<str>>,
|
S: AsRef<str> + Into<Box<str>>,
|
||||||
{
|
{
|
||||||
let key_str = key_id.as_ref();
|
let colon_idx = ruma_identifiers_validation::device_key_id::validate(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 })
|
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 {
|
mod test {
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
use ruma_identifiers_validation::{key_algorithms::DeviceKeyAlgorithm, Error};
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
|
use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
|
||||||
|
|
||||||
use super::DeviceKeyId;
|
use super::DeviceKeyId;
|
||||||
use crate::{error::Error, key_algorithms::DeviceKeyAlgorithm};
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn convert_device_key_id() {
|
fn convert_device_key_id() {
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
use std::{convert::TryFrom, num::NonZeroU8};
|
use std::{convert::TryFrom, num::NonZeroU8};
|
||||||
|
|
||||||
use crate::{error::Error, parse_id, validate_id, ServerName};
|
use crate::{Error, ServerName};
|
||||||
|
|
||||||
/// A Matrix event ID.
|
/// A Matrix event ID.
|
||||||
///
|
///
|
||||||
@ -91,15 +91,8 @@ fn try_from<S>(event_id: S) -> Result<EventId, Error>
|
|||||||
where
|
where
|
||||||
S: AsRef<str> + Into<Box<str>>,
|
S: AsRef<str> + Into<Box<str>>,
|
||||||
{
|
{
|
||||||
if event_id.as_ref().contains(':') {
|
let colon_idx = ruma_identifiers_validation::event_id::validate(event_id.as_ref())?;
|
||||||
let colon_idx = parse_id(event_id.as_ref(), &['$'])?;
|
Ok(EventId { full_id: event_id.into(), colon_idx })
|
||||||
|
|
||||||
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 })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
common_impls!(EventId, try_from, "a Matrix event ID");
|
common_impls!(EventId, try_from, "a Matrix event ID");
|
||||||
@ -112,7 +105,7 @@ mod tests {
|
|||||||
use serde_json::{from_str, to_string};
|
use serde_json::{from_str, to_string};
|
||||||
|
|
||||||
use super::EventId;
|
use super::EventId;
|
||||||
use crate::error::Error;
|
use crate::Error;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn valid_original_event_id() {
|
fn valid_original_event_id() {
|
||||||
|
@ -9,25 +9,21 @@
|
|||||||
)]
|
)]
|
||||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||||
|
|
||||||
use std::{convert::TryFrom, num::NonZeroU8};
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
use serde::de::{self, Deserialize as _, Deserializer, Unexpected};
|
use serde::de::{self, Deserialize as _, Deserializer, Unexpected};
|
||||||
|
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
pub use crate::{
|
pub use crate::{
|
||||||
device_id::DeviceId,
|
device_id::DeviceId, device_key_id::DeviceKeyId, event_id::EventId, room_alias_id::RoomAliasId,
|
||||||
device_key_id::DeviceKeyId,
|
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,
|
error::Error,
|
||||||
event_id::EventId,
|
|
||||||
key_algorithms::{DeviceKeyAlgorithm, ServerKeyAlgorithm},
|
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]
|
#[macro_use]
|
||||||
@ -37,9 +33,7 @@ pub mod device_id;
|
|||||||
pub mod user_id;
|
pub mod user_id;
|
||||||
|
|
||||||
mod device_key_id;
|
mod device_key_id;
|
||||||
mod error;
|
|
||||||
mod event_id;
|
mod event_id;
|
||||||
mod key_algorithms;
|
|
||||||
mod room_alias_id;
|
mod room_alias_id;
|
||||||
mod room_id;
|
mod room_id;
|
||||||
mod room_id_or_room_alias_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()
|
<&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.
|
/// Generates a random identifier localpart.
|
||||||
#[cfg(feature = "rand")]
|
#[cfg(feature = "rand")]
|
||||||
fn generate_localpart(length: usize) -> Box<str> {
|
fn generate_localpart(length: usize) -> Box<str> {
|
||||||
@ -74,38 +60,6 @@ fn generate_localpart(length: usize) -> Box<str> {
|
|||||||
.into_boxed_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.
|
/// Deserializes any type of id using the provided TryFrom implementation.
|
||||||
///
|
///
|
||||||
/// This is a helper function to reduce the boilerplate of the Deserialize implementations.
|
/// This is a helper function to reduce the boilerplate of the Deserialize implementations.
|
||||||
|
@ -55,7 +55,7 @@ macro_rules! common_impls {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ::std::convert::TryFrom<&str> for $id {
|
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> {
|
fn try_from(s: &str) -> Result<Self, Self::Error> {
|
||||||
$try_from(s)
|
$try_from(s)
|
||||||
@ -63,7 +63,7 @@ macro_rules! common_impls {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ::std::convert::TryFrom<String> for $id {
|
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> {
|
fn try_from(s: String) -> Result<Self, Self::Error> {
|
||||||
$try_from(s)
|
$try_from(s)
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
use std::{convert::TryFrom, num::NonZeroU8};
|
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.
|
/// A Matrix room alias ID.
|
||||||
///
|
///
|
||||||
@ -38,13 +38,12 @@ impl RoomAliasId {
|
|||||||
/// Attempts to create a new Matrix room alias ID from a string representation.
|
/// 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.
|
/// 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
|
where
|
||||||
S: AsRef<str> + Into<Box<str>>,
|
S: AsRef<str> + Into<Box<str>>,
|
||||||
{
|
{
|
||||||
let colon_idx = parse_id(room_id.as_ref(), &['#'])?;
|
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 })
|
||||||
Ok(RoomAliasId { full_id: room_id.into(), colon_idx })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
common_impls!(RoomAliasId, try_from, "a Matrix room alias ID");
|
common_impls!(RoomAliasId, try_from, "a Matrix room alias ID");
|
||||||
@ -57,7 +56,7 @@ mod tests {
|
|||||||
use serde_json::{from_str, to_string};
|
use serde_json::{from_str, to_string};
|
||||||
|
|
||||||
use super::RoomAliasId;
|
use super::RoomAliasId;
|
||||||
use crate::error::Error;
|
use crate::Error;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn valid_room_alias_id() {
|
fn valid_room_alias_id() {
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
use std::{convert::TryFrom, num::NonZeroU8};
|
use std::{convert::TryFrom, num::NonZeroU8};
|
||||||
|
|
||||||
use crate::{error::Error, parse_id, ServerName};
|
use crate::{Error, ServerName};
|
||||||
|
|
||||||
/// A Matrix room ID.
|
/// A Matrix room ID.
|
||||||
///
|
///
|
||||||
@ -58,8 +58,7 @@ fn try_from<S>(room_id: S) -> Result<RoomId, Error>
|
|||||||
where
|
where
|
||||||
S: AsRef<str> + Into<Box<str>>,
|
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 })
|
Ok(RoomId { full_id: room_id.into(), colon_idx })
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,7 +72,7 @@ mod tests {
|
|||||||
use serde_json::{from_str, to_string};
|
use serde_json::{from_str, to_string};
|
||||||
|
|
||||||
use super::RoomId;
|
use super::RoomId;
|
||||||
use crate::error::Error;
|
use crate::Error;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn valid_room_id() {
|
fn valid_room_id() {
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
use std::{convert::TryFrom, hint::unreachable_unchecked, num::NonZeroU8};
|
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.
|
/// 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
|
where
|
||||||
S: AsRef<str> + Into<Box<str>>,
|
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 })
|
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 serde_json::{from_str, to_string};
|
||||||
|
|
||||||
use super::RoomIdOrAliasId;
|
use super::RoomIdOrAliasId;
|
||||||
use crate::error::Error;
|
use crate::Error;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn valid_room_id_or_alias_id_with_a_room_alias_id() {
|
fn valid_room_id_or_alias_id_with_a_room_alias_id() {
|
||||||
|
@ -9,10 +9,7 @@ use std::{
|
|||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||||
|
|
||||||
use crate::error::Error;
|
use crate::Error;
|
||||||
|
|
||||||
/// Room version identifiers cannot be more than 32 code points.
|
|
||||||
const MAX_CODE_POINTS: usize = 32;
|
|
||||||
|
|
||||||
/// A Matrix room version ID.
|
/// A Matrix room version ID.
|
||||||
///
|
///
|
||||||
@ -231,21 +228,16 @@ where
|
|||||||
"4" => RoomVersionId::Version4,
|
"4" => RoomVersionId::Version4,
|
||||||
"5" => RoomVersionId::Version5,
|
"5" => RoomVersionId::Version5,
|
||||||
custom => {
|
custom => {
|
||||||
if custom.is_empty() {
|
ruma_identifiers_validation::room_version_id::validate(custom)?;
|
||||||
return Err(Error::MinimumLengthNotSatisfied);
|
|
||||||
} else if custom.chars().count() > MAX_CODE_POINTS {
|
|
||||||
return Err(Error::MaximumLengthExceeded);
|
|
||||||
} else {
|
|
||||||
RoomVersionId::Custom(CustomRoomVersion(room_version_id.into()))
|
RoomVersionId::Custom(CustomRoomVersion(room_version_id.into()))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(version)
|
Ok(version)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&str> for RoomVersionId {
|
impl TryFrom<&str> for RoomVersionId {
|
||||||
type Error = crate::error::Error;
|
type Error = crate::Error;
|
||||||
|
|
||||||
fn try_from(s: &str) -> Result<Self, Error> {
|
fn try_from(s: &str) -> Result<Self, Error> {
|
||||||
try_from(s)
|
try_from(s)
|
||||||
@ -253,7 +245,7 @@ impl TryFrom<&str> for RoomVersionId {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<String> for RoomVersionId {
|
impl TryFrom<String> for RoomVersionId {
|
||||||
type Error = crate::error::Error;
|
type Error = crate::Error;
|
||||||
|
|
||||||
fn try_from(s: String) -> Result<Self, Error> {
|
fn try_from(s: String) -> Result<Self, Error> {
|
||||||
try_from(s)
|
try_from(s)
|
||||||
@ -314,7 +306,7 @@ mod tests {
|
|||||||
use serde_json::{from_str, to_string};
|
use serde_json::{from_str, to_string};
|
||||||
|
|
||||||
use super::RoomVersionId;
|
use super::RoomVersionId;
|
||||||
use crate::error::Error;
|
use crate::Error;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn valid_version_1_room_version_id() {
|
fn valid_version_1_room_version_id() {
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
use std::{num::NonZeroU8, str::FromStr};
|
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.
|
/// Key identifiers used for homeserver signing keys.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@ -27,37 +27,12 @@ fn try_from<S>(key_id: S) -> Result<ServerKeyId, Error>
|
|||||||
where
|
where
|
||||||
S: AsRef<str> + Into<Box<str>>,
|
S: AsRef<str> + Into<Box<str>>,
|
||||||
{
|
{
|
||||||
let key_str = key_id.as_ref();
|
let colon_idx = ruma_identifiers_validation::server_key_id::validate(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 })
|
Ok(ServerKeyId { full_id: key_id.into(), colon_idx })
|
||||||
}
|
}
|
||||||
|
|
||||||
common_impls!(ServerKeyId, try_from, "Key ID with algorithm and version");
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
@ -65,10 +40,10 @@ mod tests {
|
|||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
|
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")]
|
#[cfg(feature = "serde")]
|
||||||
use crate::key_algorithms::ServerKeyAlgorithm;
|
use ruma_identifiers_validation::key_algorithms::ServerKeyAlgorithm;
|
||||||
|
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -6,7 +6,9 @@ use std::{
|
|||||||
mem,
|
mem,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::error::Error;
|
use ruma_identifiers_validation::server_name::validate;
|
||||||
|
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
/// A Matrix-spec compliant server name.
|
/// A Matrix-spec compliant server name.
|
||||||
#[repr(transparent)]
|
#[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>
|
fn try_from<S>(server_name: S) -> Result<Box<ServerName>, Error>
|
||||||
where
|
where
|
||||||
S: AsRef<str> + Into<Box<str>>,
|
S: AsRef<str> + Into<Box<str>>,
|
||||||
@ -140,7 +97,7 @@ impl<'a> TryFrom<&'a str> for &'a ServerName {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&str> for Box<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> {
|
fn try_from(s: &str) -> Result<Self, Self::Error> {
|
||||||
try_from(s)
|
try_from(s)
|
||||||
@ -148,7 +105,7 @@ impl TryFrom<&str> for Box<ServerName> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<String> 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> {
|
fn try_from(s: String) -> Result<Self, Self::Error> {
|
||||||
try_from(s)
|
try_from(s)
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
use std::{convert::TryFrom, num::NonZeroU8};
|
use std::{convert::TryFrom, num::NonZeroU8};
|
||||||
|
|
||||||
use crate::{error::Error, parse_id, ServerName};
|
use crate::{Error, ServerName};
|
||||||
|
|
||||||
/// A Matrix user ID.
|
/// A Matrix user ID.
|
||||||
///
|
///
|
||||||
@ -95,41 +95,14 @@ fn try_from<S>(user_id: S) -> Result<UserId, Error>
|
|||||||
where
|
where
|
||||||
S: AsRef<str> + Into<Box<str>>,
|
S: AsRef<str> + Into<Box<str>>,
|
||||||
{
|
{
|
||||||
let user_id_str = user_id.as_ref();
|
let (colon_idx, is_historical) =
|
||||||
|
ruma_identifiers_validation::user_id::validate(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)?;
|
|
||||||
|
|
||||||
Ok(UserId { full_id: user_id.into(), colon_idx, is_historical: !is_historical })
|
Ok(UserId { full_id: user_id.into(), colon_idx, is_historical: !is_historical })
|
||||||
}
|
}
|
||||||
|
|
||||||
common_impls!(UserId, try_from, "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
|
pub use ruma_identifiers_validation::user_id::localpart_is_fully_comforming;
|
||||||
///
|
|
||||||
/// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
@ -139,7 +112,7 @@ mod tests {
|
|||||||
use serde_json::{from_str, to_string};
|
use serde_json::{from_str, to_string};
|
||||||
|
|
||||||
use super::UserId;
|
use super::UserId;
|
||||||
use crate::{error::Error, ServerName};
|
use crate::{Error, ServerName};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn valid_user_id_from_str() {
|
fn valid_user_id_from_str() {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user