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]
|
||||
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"
|
||||
|
@ -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() {
|
||||
|
@ -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() {
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
@ -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() {
|
||||
|
@ -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() {
|
||||
|
@ -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() {
|
||||
|
@ -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() {
|
||||
|
@ -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]
|
||||
|
@ -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)
|
||||
|
@ -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() {
|
||||
|
Loading…
x
Reference in New Issue
Block a user