Add 'ruma-identifiers/' from commit 'c0a1d8bd440c7cde0fa4ab5e22898ddb26bb706d'

git-subtree-dir: ruma-identifiers
git-subtree-mainline: 19ce9645b6651e4de42ddf9e81ee50e19c8c0f26
git-subtree-split: c0a1d8bd440c7cde0fa4ab5e22898ddb26bb706d
This commit is contained in:
Jonas Platte 2020-06-07 17:06:45 +02:00
commit 83de77f002
23 changed files with 2788 additions and 0 deletions

View File

@ -0,0 +1,30 @@
image: archlinux
packages:
- rustup
sources:
- https://github.com/ruma/ruma-identifiers
tasks:
- rustup: |
# We specify --profile minimal because we'd otherwise download docs
rustup toolchain install beta --profile minimal -c rustfmt -c clippy
rustup default beta
- test: |
cd ruma-identifiers
# We don't want the build to stop on individual failure of independent
# tools, so capture tool exit codes and set the task exit code manually
set +e
cargo fmt -- --check
fmt_exit=$?
cargo clippy --all-targets --all-features -- -D warnings
clippy_exit=$?
cargo test --no-default-features --verbose
test1_exit=$?
cargo test --all-features --verbose
test2_exit=$?
exit $(( $fmt_exit || $clippy_exit || $test1_exit || $test2_exit ))

View File

@ -0,0 +1,26 @@
image: archlinux
packages:
- rustup
sources:
- https://github.com/ruma/ruma-identifiers
tasks:
- rustup: |
# We specify --profile minimal because we'd otherwise download docs
rustup toolchain install 1.42.0 --profile minimal
rustup default 1.42.0
- test: |
cd ruma-identifiers
# We don't want the build to stop on individual failure of independent
# tools, so capture tool exit codes and set the task exit code manually
set +e
# Only make sure the code builds with the MSRV. Tests can require later
# Rust versions, don't compile or run them.
cargo build --no-default-features --verbose
build1_exit=$?
cargo build --all-features --verbose
build2_exit=$?
exit $(( $build1_exit || $build2_exit ))

View File

@ -0,0 +1,32 @@
image: archlinux
packages:
- rustup
sources:
- https://github.com/ruma/ruma-identifiers
tasks:
- rustup: |
rustup toolchain install nightly --profile minimal
rustup default nightly
# Try installing rustfmt & clippy for nightly, but don't fail the build
# if they are not available
rustup component add rustfmt || true
rustup component add clippy || true
- test: |
cd ruma-identifiers
# We don't want the build to stop on individual failure of independent
# tools, so capture tool exit codes and set the task exit code manually
set +e
if ( rustup component list | grep -q rustfmt ); then
cargo fmt -- --check
fi
fmt_exit=$?
if ( rustup component list | grep -q clippy ); then
cargo clippy --all-targets --all-features -- -D warnings
fi
clippy_exit=$?
exit $(( $fmt_exit || $clippy_exit ))

View File

@ -0,0 +1,32 @@
image: archlinux
packages:
- rustup
sources:
- https://github.com/ruma/ruma-identifiers
tasks:
- rustup: |
# We specify --profile minimal because we'd otherwise download docs
rustup toolchain install stable --profile minimal -c rustfmt -c clippy
rustup default stable
- test: |
cd ruma-identifiers
# We don't want the build to stop on individual failure of independent
# tools, so capture tool exit codes and set the task exit code manually
set +e
cargo fmt -- --check
fmt_exit=$?
cargo clippy --all-targets --all-features -- -D warnings
clippy_exit=$?
cargo test --no-default-features --verbose
test1_exit=$?
cargo test --all-features --verbose
test2_exit=$?
exit $(( $fmt_exit || $clippy_exit || $test1_exit || $test2_exit ))
# TODO: Add audit task once cargo-audit binary releases are available.
# See https://github.com/RustSec/cargo-audit/issues/66

2
ruma-identifiers/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
Cargo.lock
/target

View File

@ -0,0 +1,104 @@
# [unreleased]
Breaking changes:
* Removed diesel integration. If you were using it, please comment on the corresponding issue:
https://github.com/ruma/ruma-identifiers/issues/22
* Remove `TryFrom<Cow<'_, str>>` implementations for identifier types
* Update `parse_with_server_name`s signature (instead of `Into<String>` it now requires
`Into<Box<str>>` of the id type). This is technically a breaking change, but extremely unlikely
to affect any existing code.
Improvements:
* Add `DeviceKeyId`, `KeyAlgorithm`, and `ServerKeyId`
# 0.16.2
Improvements:
* Update the internal representation of identifiers to be more compact
* Add `RoomVersionId::version_6` and `RoomVersionId::is_version_6`
* Add `PartialOrd` and `Ord` implementations for `RoomVersionId`
# 0.16.1
Bug fixes:
* Change `PartialEq` implementations to compare IDs with string literals from `str` to `&str`
* This is technically a breaking change, but the previous implementations were extremely
unlikely to actually be used
# 0.16.0
Breaking changes:
* Update `RoomId::parse_with_server_name`s bounds from `Into<Cow<'_, str>>` to
`AsRef<str> + Into<String>`. While this is a breaking change, it is not expected to actually
require code changes.
Improvements:
* Add conversion functions for `RoomIdOrAliasId`
* `impl From<RoomId> for RoomIdOrAliasId`
* `impl From<RoomAliasId> for RoomIdOrAliasId`
* `impl TryFrom<RoomIdOrAliasId> for RoomId`
* `impl TryFrom<RoomIdOrAliasId> for RoomAliasId`
* `RoomIdOrAliasId::into_either` (if the optional dependency `either` is activated with the
identically named feature)
# 0.15.1
Bug fixes:
* Fix docs.rs build
# 0.15.0
Breaking changes:
* All identifiers now allocate at maximum one string (localpart and host are no longer stored
separately)
* Because of this, these traits are now implemented for them and only allocate in the obvious
case:
* `impl From<…Id> for String`
* `impl AsRef<str> for …Id`
* `impl TryFrom<Cow<'_, str>> for …Id`
* `impl TryFrom<String> for …Id`
* `PartialEq` for `String`s and string slices
* Additionally, the `Hash` implementations will now yield the same hashes as hashing the string
representation
* Note that hashes are generally only guaranteed consistent in the lifetime of the program
though, so do not persist them!
* The `hostname` methods have been rename to `server_name` and updated to return string slices
instead of `&url::Host`
* `Error::InvalidHost` has been renamed to `Error::InvalidServerName`, because it also covers errors
in the port, not just the host part section of the server name
* The random identifier generation functions (`Id::new`) are now only available if the `rand`
feature of this crate is enabled
Improvements:
* Add support for historical uppercase MXIDs
* Made all dependencies optional
* `serde` is the only one that is enabled by default
* The `user_id` module is now public and contains `fn localpart_is_fully_conforming`
* This function can be used to determine whether a user name (the localpart of a user ID) is valid
without actually constructing a full user ID first
* Add `UserId::parse_with_server_name`
# 0.14.1
Breaking changes:
* Our Minimum Supported Rust Version is now 1.36.0
* This is done in a patch version because it is only a documentation change. Practially, a new
project using even ruma-identifiers 0.14 won't build out of the box on older versions of Rust
because of an MSRV bump in a minor release of an indirect dependency. Using ruma-identifiers
with older versions of Rust will potentially continue to work with some crates pinned to older
versions, but won't be tested in CI.
Improvements:
* Remove the dependency on `lazy_static` and `regex`
* We now support [historical user IDs](https://matrix.org/docs/spec/appendices#historical-user-ids)

View File

@ -0,0 +1,29 @@
[package]
authors = ["Jimmy Cuadra <jimmy@jimmycuadra.com>"]
categories = ["api-bindings"]
description = "Resource identifiers for Matrix."
documentation = "https://docs.rs/ruma-identifiers"
homepage = "https://github.com/ruma/ruma-identifiers"
keywords = ["matrix", "chat", "messaging", "ruma"]
license = "MIT"
name = "ruma-identifiers"
readme = "README.md"
repository = "https://github.com/ruma/ruma-identifiers"
version = "0.16.1"
edition = "2018"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[features]
default = ["serde"]
[dependencies]
either = { version = "1.5.3", optional = true }
rand = { version = "0.7.3", optional = true }
serde = { version = "1.0.106", optional = true, features = ["derive"] }
strum = { version = "0.18.0", features = ["derive"] }
[dev-dependencies]
serde_json = "1.0.51"

20
ruma-identifiers/LICENSE Normal file
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,15 @@
# ruma-identifiers
[![crates.io page](https://img.shields.io/crates/v/ruma-identifiers.svg)](https://crates.io/crates/ruma-identifiers)
[![docs.rs page](https://docs.rs/ruma-identifiers/badge.svg)](https://docs.rs/ruma-identifiers/)
![license: MIT](https://img.shields.io/crates/l/ruma-identifiers.svg)
**ruma-identifiers** contains types for [Matrix](https://matrix.org/) identifiers for events, rooms, room aliases, and users.
## Minimum Rust version
ruma-identifiers requires Rust 1.36.0 or later.
## Documentation
ruma-identifiers has [comprehensive documentation](https://docs.rs/ruma-identifiers) available on docs.rs.

View File

@ -0,0 +1,27 @@
//! Matrix device identifiers.
#[cfg(feature = "rand")]
use crate::generate_localpart;
/// A Matrix device ID.
///
/// Device identifiers in Matrix are completely opaque character sequences. This type alias is
/// provided simply for its semantic value.
pub type DeviceId = String;
/// Generates a random `DeviceId`, suitable for assignment to a new device.
#[cfg(feature = "rand")]
#[cfg_attr(docsrs, doc(cfg(feature = "rand")))]
pub fn generate() -> DeviceId {
generate_localpart(8)
}
#[cfg(all(test, feature = "rand"))]
mod tests {
use super::generate;
#[test]
fn generate_device_id() {
assert_eq!(generate().len(), 8);
}
}

View File

@ -0,0 +1,136 @@
//! Identifiers for device keys for end-to-end encryption.
use crate::{device_id::DeviceId, error::Error, key_algorithms::DeviceKeyAlgorithm};
use std::num::NonZeroU8;
use std::str::FromStr;
/// A key algorithm and a device id, combined with a ':'
#[derive(Clone, Debug)]
pub struct DeviceKeyId<T> {
full_id: T,
colon_idx: NonZeroU8,
}
impl<T> DeviceKeyId<T> {
/// Returns key algorithm of the device key ID.
pub fn algorithm(&self) -> DeviceKeyAlgorithm
where
T: AsRef<str>,
{
DeviceKeyAlgorithm::from_str(&self.full_id.as_ref()[..self.colon_idx.get() as usize])
.unwrap()
}
/// Returns device ID of the device key ID.
pub fn device_id(&self) -> DeviceId
where
T: AsRef<str>,
{
DeviceId::from(&self.full_id.as_ref()[self.colon_idx.get() as usize + 1..])
}
}
fn try_from<S, T>(key_id: S) -> Result<DeviceKeyId<T>, Error>
where
S: AsRef<str> + Into<T>,
{
let key_str = key_id.as_ref();
let colon_idx =
NonZeroU8::new(key_str.find(':').ok_or(Error::MissingDeviceKeyDelimiter)? as u8)
.ok_or(Error::UnknownKeyAlgorithm)?;
DeviceKeyAlgorithm::from_str(&key_str[0..colon_idx.get() as usize])
.map_err(|_| Error::UnknownKeyAlgorithm)?;
Ok(DeviceKeyId {
full_id: key_id.into(),
colon_idx,
})
}
common_impls!(
DeviceKeyId,
try_from,
"Device key ID with algorithm and device ID"
);
#[cfg(test)]
mod test {
use std::convert::TryFrom;
#[cfg(feature = "serde")]
use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
use super::DeviceKeyId;
use crate::{device_id::DeviceId, error::Error, key_algorithms::DeviceKeyAlgorithm};
#[test]
fn convert_device_key_id() {
assert_eq!(
DeviceKeyId::<&str>::try_from("ed25519:JLAFKJWSCS")
.expect("Failed to create device key ID.")
.as_ref(),
"ed25519:JLAFKJWSCS"
);
}
#[cfg(feature = "serde")]
#[test]
fn serialize_device_key_id() {
let device_key_id = DeviceKeyId::<&str>::try_from("ed25519:JLAFKJWSCS").unwrap();
let serialized = to_json_value(device_key_id).unwrap();
let expected = json!("ed25519:JLAFKJWSCS");
assert_eq!(serialized, expected);
}
#[cfg(feature = "serde")]
#[test]
fn deserialize_device_key_id() {
let deserialized: DeviceKeyId<_> = from_json_value(json!("ed25519:JLAFKJWSCS")).unwrap();
let expected = DeviceKeyId::try_from("ed25519:JLAFKJWSCS").unwrap();
assert_eq!(deserialized, expected);
}
#[test]
fn missing_key_algorithm() {
assert_eq!(
DeviceKeyId::<&str>::try_from(":JLAFKJWSCS").unwrap_err(),
Error::UnknownKeyAlgorithm
);
}
#[test]
fn missing_delimiter() {
assert_eq!(
DeviceKeyId::<&str>::try_from("ed25519|JLAFKJWSCS").unwrap_err(),
Error::MissingDeviceKeyDelimiter,
);
}
#[test]
fn unknown_key_algorithm() {
assert_eq!(
DeviceKeyId::<&str>::try_from("signed_curve25510:JLAFKJWSCS").unwrap_err(),
Error::UnknownKeyAlgorithm,
);
}
#[test]
fn empty_device_id_ok() {
assert!(DeviceKeyId::<&str>::try_from("ed25519:").is_ok());
}
#[test]
fn valid_key_algorithm() {
let device_key_id = DeviceKeyId::<&str>::try_from("ed25519:JLAFKJWSCS").unwrap();
assert_eq!(device_key_id.algorithm(), DeviceKeyAlgorithm::Ed25519);
}
#[test]
fn valid_device_id() {
let device_key_id = DeviceKeyId::<&str>::try_from("ed25519:JLAFKJWSCS").unwrap();
assert_eq!(device_key_id.device_id(), DeviceId::from("JLAFKJWSCS"));
}
}

View File

@ -0,0 +1,54 @@
//! Error conditions.
use std::fmt::{self, Display, Formatter};
/// An error encountered when trying to parse an invalid ID string.
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
pub enum Error {
/// The ID's localpart contains invalid characters.
///
/// Only relevant for user IDs.
InvalidCharacters,
/// The key version contains outside of [a-zA-Z0-9_].
InvalidKeyVersion,
/// The localpart of the ID string is not valid (because it is empty).
InvalidLocalPart,
/// The server name part of the the ID string is not a valid server name.
InvalidServerName,
/// The ID exceeds 255 bytes (or 32 codepoints for a room version ID.)
MaximumLengthExceeded,
/// The ID is less than 4 characters (or is an empty room version ID.)
MinimumLengthNotSatisfied,
/// The ID is missing the colon delimiter between localpart and server name.
MissingDelimiter,
/// The ID is missing the colon delimiter between key algorithm and device ID.
MissingDeviceKeyDelimiter,
/// The ID is missing the colon delimiter between key algorithm and version.
MissingServerKeyDelimiter,
/// The ID is missing the leading sigil.
MissingSigil,
/// The key algorithm is not recognized.
UnknownKeyAlgorithm,
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let message = match self {
Error::InvalidCharacters => "localpart contains invalid characters",
Error::InvalidKeyVersion => "key id version contains invalid characters",
Error::InvalidLocalPart => "localpart is empty",
Error::InvalidServerName => "server name is not a valid IP address or domain name",
Error::MaximumLengthExceeded => "ID exceeds 255 bytes",
Error::MinimumLengthNotSatisfied => "ID must be at least 4 characters",
Error::MissingDelimiter => "colon is required between localpart and server name",
Error::MissingDeviceKeyDelimiter => "colon is required between algorithm and device ID",
Error::MissingServerKeyDelimiter => "colon is required between algorithm and version",
Error::MissingSigil => "leading sigil is missing",
Error::UnknownKeyAlgorithm => "unknown key algorithm specified",
};
write!(f, "{}", message)
}
}
impl std::error::Error for Error {}

View File

@ -0,0 +1,315 @@
//! Matrix event identifiers.
use std::num::NonZeroU8;
use crate::{error::Error, parse_id, validate_id};
/// A Matrix event ID.
///
/// An `EventId` is generated randomly or converted from a string slice, and can be converted back
/// into a string as needed.
///
/// It is discouraged to use this type directly instead use one of the aliases (`EventId` and
/// `EventIdRef`) in the crate root.
///
/// # Room versions
///
/// Matrix specifies multiple [room versions](https://matrix.org/docs/spec/#room-versions) and the
/// format of event identifiers differ between them. The original format used by room versions 1
/// and 2 uses a short pseudorandom "localpart" followed by the hostname and port of the
/// originating homeserver. Later room versions change event identifiers to be a hash of the event
/// encoded with Base64. Some of the methods provided by `EventId` are only relevant to the
/// original event format.
///
/// ```
/// # use std::convert::TryFrom;
/// # use ruma_identifiers::EventId;
/// // Original format
/// assert_eq!(
/// EventId::try_from("$h29iv0s8:example.com").unwrap().as_ref(),
/// "$h29iv0s8:example.com"
/// );
/// // Room version 3 format
/// assert_eq!(
/// EventId::try_from("$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk").unwrap().as_ref(),
/// "$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk"
/// );
/// // Room version 4 format
/// assert_eq!(
/// EventId::try_from("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg").unwrap().as_ref(),
/// "$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg"
/// );
/// ```
#[derive(Clone, Copy, Debug)]
pub struct EventId<T> {
full_id: T,
colon_idx: Option<NonZeroU8>,
}
impl<T> EventId<T> {
/// Attempts to generate an `EventId` for the given origin server with a localpart consisting
/// of 18 random ASCII characters. This should only be used for events in the original format
/// as used by Matrix room versions 1 and 2.
///
/// Does not currently ever fail, but may fail in the future if the homeserver cannot be parsed
/// parsed as a valid host.
#[cfg(feature = "rand")]
#[cfg_attr(docsrs, doc(cfg(feature = "rand")))]
pub fn new(server_name: &str) -> Result<Self, Error>
where
String: Into<T>,
{
use crate::{generate_localpart, is_valid_server_name};
if !is_valid_server_name(server_name) {
return Err(Error::InvalidServerName);
}
let full_id = format!("${}:{}", generate_localpart(18), server_name).into();
Ok(Self {
full_id,
colon_idx: NonZeroU8::new(19),
})
}
/// Returns the event's unique ID. For the original event format as used by Matrix room
/// versions 1 and 2, this is the "localpart" that precedes the homeserver. For later formats,
/// this is the entire ID without the leading $ sigil.
pub fn localpart(&self) -> &str
where
T: AsRef<str>,
{
let idx = match self.colon_idx {
Some(idx) => idx.get() as usize,
None => self.full_id.as_ref().len(),
};
&self.full_id.as_ref()[1..idx]
}
/// Returns the server name of the event ID.
///
/// Only applicable to events in the original format as used by Matrix room versions 1 and 2.
pub fn server_name(&self) -> Option<&str>
where
T: AsRef<str>,
{
self.colon_idx
.map(|idx| &self.full_id.as_ref()[idx.get() as usize + 1..])
}
}
/// Attempts to create a new Matrix event ID from a string representation.
///
/// If using the original event format as used by Matrix room versions 1 and 2, the string must
/// include the leading $ sigil, the localpart, a literal colon, and a valid homeserver hostname.
fn try_from<S, T>(event_id: S) -> Result<EventId<T>, Error>
where
S: AsRef<str> + Into<T>,
{
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,
})
}
}
common_impls!(EventId, try_from, "a Matrix event ID");
#[cfg(test)]
mod tests {
use std::convert::TryFrom;
#[cfg(feature = "serde")]
use serde_json::{from_str, to_string};
use crate::error::Error;
type EventId = super::EventId<Box<str>>;
#[test]
fn valid_original_event_id() {
assert_eq!(
EventId::try_from("$39hvsi03hlne:example.com")
.expect("Failed to create EventId.")
.as_ref(),
"$39hvsi03hlne:example.com"
);
}
#[test]
fn valid_base64_event_id() {
assert_eq!(
EventId::try_from("$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk")
.expect("Failed to create EventId.")
.as_ref(),
"$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk"
)
}
#[test]
fn valid_url_safe_base64_event_id() {
assert_eq!(
EventId::try_from("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg")
.expect("Failed to create EventId.")
.as_ref(),
"$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg"
)
}
#[cfg(feature = "rand")]
#[test]
fn generate_random_valid_event_id() {
let event_id = EventId::new("example.com").expect("Failed to generate EventId.");
let id_str: &str = event_id.as_ref();
assert!(id_str.starts_with('$'));
assert_eq!(id_str.len(), 31);
}
#[cfg(feature = "rand")]
#[test]
fn generate_random_invalid_event_id() {
assert!(EventId::new("").is_err());
}
#[cfg(feature = "serde")]
#[test]
fn serialize_valid_original_event_id() {
assert_eq!(
to_string(
&EventId::try_from("$39hvsi03hlne:example.com").expect("Failed to create EventId.")
)
.expect("Failed to convert EventId to JSON."),
r#""$39hvsi03hlne:example.com""#
);
}
#[cfg(feature = "serde")]
#[test]
fn serialize_valid_base64_event_id() {
assert_eq!(
to_string(
&EventId::try_from("$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk")
.expect("Failed to create EventId.")
)
.expect("Failed to convert EventId to JSON."),
r#""$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk""#
);
}
#[cfg(feature = "serde")]
#[test]
fn serialize_valid_url_safe_base64_event_id() {
assert_eq!(
to_string(
&EventId::try_from("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg")
.expect("Failed to create EventId.")
)
.expect("Failed to convert EventId to JSON."),
r#""$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg""#
);
}
#[cfg(feature = "serde")]
#[test]
fn deserialize_valid_original_event_id() {
assert_eq!(
from_str::<EventId>(r#""$39hvsi03hlne:example.com""#)
.expect("Failed to convert JSON to EventId"),
EventId::try_from("$39hvsi03hlne:example.com").expect("Failed to create EventId.")
);
}
#[cfg(feature = "serde")]
#[test]
fn deserialize_valid_base64_event_id() {
assert_eq!(
from_str::<EventId>(r#""$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk""#)
.expect("Failed to convert JSON to EventId"),
EventId::try_from("$acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk")
.expect("Failed to create EventId.")
);
}
#[cfg(feature = "serde")]
#[test]
fn deserialize_valid_url_safe_base64_event_id() {
assert_eq!(
from_str::<EventId>(r#""$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg""#)
.expect("Failed to convert JSON to EventId"),
EventId::try_from("$Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg")
.expect("Failed to create EventId.")
);
}
#[test]
fn valid_original_event_id_with_explicit_standard_port() {
assert_eq!(
EventId::try_from("$39hvsi03hlne:example.com:443")
.expect("Failed to create EventId.")
.as_ref(),
"$39hvsi03hlne:example.com:443"
);
}
#[test]
fn valid_original_event_id_with_non_standard_port() {
assert_eq!(
EventId::try_from("$39hvsi03hlne:example.com:5000")
.expect("Failed to create EventId.")
.as_ref(),
"$39hvsi03hlne:example.com:5000"
);
}
#[test]
fn missing_original_event_id_sigil() {
assert_eq!(
EventId::try_from("39hvsi03hlne:example.com").unwrap_err(),
Error::MissingSigil
);
}
#[test]
fn missing_base64_event_id_sigil() {
assert_eq!(
EventId::try_from("acR1l0raoZnm60CBwAVgqbZqoO/mYU81xysh1u7XcJk").unwrap_err(),
Error::MissingSigil
);
}
#[test]
fn missing_url_safe_base64_event_id_sigil() {
assert_eq!(
EventId::try_from("Rqnc-F-dvnEYJTyHq_iKxU2bZ1CI92-kuZq3a5lr5Zg").unwrap_err(),
Error::MissingSigil
);
}
#[test]
fn invalid_event_id_host() {
assert_eq!(
EventId::try_from("$39hvsi03hlne:/").unwrap_err(),
Error::InvalidServerName
);
}
#[test]
fn invalid_event_id_port() {
assert_eq!(
EventId::try_from("$39hvsi03hlne:example.com:notaport").unwrap_err(),
Error::InvalidServerName
);
}
}

View File

@ -0,0 +1,33 @@
//! Key algorithms used in Matrix spec.
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use strum::{AsRefStr, Display, EnumString};
/// The basic key algorithms in the specification
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, AsRefStr, Display, EnumString)]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
#[non_exhaustive]
pub enum DeviceKeyAlgorithm {
/// The Ed25519 signature algorithm.
#[strum(to_string = "ed25519")]
Ed25519,
/// The Curve25519 ECDH algorithm.
#[strum(to_string = "curve25519")]
Curve25519,
/// The Curve25519 ECDH algorithm, but the key also contains signatures
#[strum(to_string = "signed_curve25519")]
SignedCurve25519,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, AsRefStr, Display, EnumString)]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
#[non_exhaustive]
pub enum ServerKeyAlgorithm {
/// The Ed25519 signature algorithm.
#[strum(to_string = "ed25519")]
Ed25519,
}

171
ruma-identifiers/src/lib.rs Normal file
View File

@ -0,0 +1,171 @@
//! Crate **ruma_identifiers** contains types for [Matrix](https://matrix.org/) identifiers
//! for events, rooms, room aliases, room versions, and users.
#![warn(rust_2018_idioms)]
#![deny(
missing_copy_implementations,
missing_debug_implementations,
//missing_docs
)]
// Since we support Rust 1.36.0, we can't apply this suggestion yet
#![allow(clippy::use_self)]
#![cfg_attr(docsrs, feature(doc_cfg))]
use std::num::NonZeroU8;
#[cfg(feature = "serde")]
use serde::de::{self, Deserialize as _, Deserializer, Unexpected};
#[doc(inline)]
pub use crate::{error::Error, server_name::is_valid_server_name};
#[macro_use]
mod macros;
mod error;
mod server_name;
pub mod device_id;
pub mod device_key_id;
pub mod event_id;
pub mod key_algorithms;
pub mod room_alias_id;
pub mod room_id;
pub mod room_id_or_room_alias_id;
pub mod room_version_id;
pub mod server_key_id;
pub mod user_id;
/// An owned event ID.
///
/// Can be created via `new` (if the `rand` feature is enabled) and `TryFrom<String>` +
/// `TryFrom<&str>`, implements `Serialize` and `Deserialize` if the `serde` feature is enabled.
pub type EventId = event_id::EventId<Box<str>>;
/// A reference to an event ID.
///
/// Can be created via `TryFrom<&str>`, implements `Serialize` if the `serde` feature is enabled.
pub type EventIdRef<'a> = event_id::EventId<&'a str>;
/// An owned room alias ID.
///
/// Can be created via `TryFrom<String>` and `TryFrom<&str>`, implements `Serialize` and
/// `Deserialize` if the `serde` feature is enabled.
pub type RoomAliasId = room_alias_id::RoomAliasId<Box<str>>;
/// A reference to a room alias ID.
///
/// Can be created via `TryFrom<&str>`, implements `Serialize` if the `serde` feature is enabled.
pub type RoomAliasIdRef<'a> = room_alias_id::RoomAliasId<&'a str>;
/// An owned room ID.
///
/// Can be created via `new` (if the `rand` feature is enabled) and `TryFrom<String>` +
/// `TryFrom<&str>`, implements `Serialize` and `Deserialize` if the `serde` feature is enabled.
pub type RoomId = room_id::RoomId<Box<str>>;
/// A reference to a room ID.
///
/// Can be created via `TryFrom<&str>`, implements `Serialize` if the `serde` feature is enabled.
pub type RoomIdRef<'a> = room_id::RoomId<&'a str>;
/// An owned room alias ID or room ID.
///
/// Can be created via `TryFrom<String>`, `TryFrom<&str>`, `From<RoomId>` and `From<RoomAliasId>`;
/// implements `Serialize` and `Deserialize` if the `serde` feature is enabled.
pub type RoomIdOrAliasId = room_id_or_room_alias_id::RoomIdOrAliasId<Box<str>>;
/// A reference to a room alias ID or room ID.
///
/// Can be created via `TryFrom<&str>`, `From<RoomIdRef>` and `From<RoomAliasIdRef>`; implements
/// `Serialize` if the `serde` feature is enabled.
pub type RoomIdOrAliasIdRef<'a> = room_id_or_room_alias_id::RoomIdOrAliasId<&'a str>;
/// An owned room version ID.
///
/// Can be created using the `version_N` constructor functions, `TryFrom<String>` and
/// `TryFrom<&str>`; implements `Serialize` and `Deserialize` if the `serde` feature is enabled.
pub type RoomVersionId = room_version_id::RoomVersionId<Box<str>>;
/// A reference to a room version ID.
///
/// Can be created using the `version_N` constructor functions and via `TryFrom<&str>`, implements
/// `Serialize` if the `serde` feature is enabled.
pub type RoomVersionIdRef<'a> = room_version_id::RoomVersionId<&'a str>;
/// An owned user ID.
///
/// Can be created via `new` (if the `rand` feature is enabled) and `TryFrom<String>` +
/// `TryFrom<&str>`, implements `Serialize` and `Deserialize` if the `serde` feature is enabled.
pub type UserId = user_id::UserId<Box<str>>;
/// A reference to a user ID.
///
/// Can be created via `TryFrom<&str>`, implements `Serialize` if the `serde` feature is enabled.
pub type UserIdRef<'a> = user_id::UserId<&'a str>;
/// All identifiers must be 255 bytes or less.
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) -> String {
use rand::Rng as _;
rand::thread_rng()
.sample_iter(&rand::distributions::Alphanumeric)
.take(length)
.collect()
}
/// 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);
}
if !is_valid_server_name(&id[colon_idx + 1..]) {
return Err(Error::InvalidServerName);
}
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.
#[cfg(feature = "serde")]
fn deserialize_id<'de, D, T>(deserializer: D, expected_str: &str) -> Result<T, D::Error>
where
D: Deserializer<'de>,
T: for<'a> std::convert::TryFrom<&'a str>,
{
std::borrow::Cow::<'_, str>::deserialize(deserializer).and_then(|v| {
T::try_from(&v).map_err(|_| de::Error::invalid_value(Unexpected::Str(&v), &expected_str))
})
}

View File

@ -0,0 +1,115 @@
macro_rules! common_impls {
($id:ident, $try_from:ident, $desc:literal) => {
impl ::std::convert::From<$id<Box<str>>> for ::std::string::String {
fn from(id: $id<Box<str>>) -> Self {
id.full_id.into()
}
}
impl<'a> ::std::convert::TryFrom<&'a str> for $id<&'a str> {
type Error = crate::error::Error;
fn try_from(s: &'a str) -> Result<Self, Self::Error> {
$try_from(s)
}
}
impl ::std::convert::TryFrom<&str> for $id<Box<str>> {
type Error = crate::error::Error;
fn try_from(s: &str) -> Result<Self, Self::Error> {
$try_from(s)
}
}
impl ::std::convert::TryFrom<String> for $id<Box<str>> {
type Error = crate::error::Error;
fn try_from(s: String) -> Result<Self, Self::Error> {
$try_from(s)
}
}
impl<T: ::std::convert::AsRef<str>> ::std::convert::AsRef<str> for $id<T> {
fn as_ref(&self) -> &str {
self.full_id.as_ref()
}
}
impl<T: ::std::fmt::Display> ::std::fmt::Display for $id<T> {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
write!(f, "{}", self.full_id)
}
}
impl<T: ::std::cmp::PartialEq> ::std::cmp::PartialEq for $id<T> {
fn eq(&self, other: &Self) -> bool {
self.full_id == other.full_id
}
}
impl<T: ::std::cmp::Eq> ::std::cmp::Eq for $id<T> {}
impl<T: ::std::cmp::PartialOrd> ::std::cmp::PartialOrd for $id<T> {
fn partial_cmp(&self, other: &Self) -> Option<::std::cmp::Ordering> {
::std::cmp::PartialOrd::partial_cmp(&self.full_id, &other.full_id)
}
}
impl<T: ::std::cmp::Ord> ::std::cmp::Ord for $id<T> {
fn cmp(&self, other: &Self) -> ::std::cmp::Ordering {
::std::cmp::Ord::cmp(&self.full_id, &other.full_id)
}
}
impl<T: ::std::hash::Hash> ::std::hash::Hash for $id<T> {
fn hash<H: ::std::hash::Hasher>(&self, state: &mut H) {
self.full_id.hash(state);
}
}
#[cfg(feature = "serde")]
impl<T: AsRef<str>> ::serde::Serialize for $id<T> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: ::serde::Serializer,
{
serializer.serialize_str(self.full_id.as_ref())
}
}
#[cfg(feature = "serde")]
impl<'de> ::serde::Deserialize<'de> for $id<Box<str>> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: ::serde::Deserializer<'de>,
{
crate::deserialize_id(deserializer, $desc)
}
}
impl<T: AsRef<str>> ::std::cmp::PartialEq<&str> for $id<T> {
fn eq(&self, other: &&str) -> bool {
self.full_id.as_ref() == *other
}
}
impl<T: AsRef<str>> ::std::cmp::PartialEq<$id<T>> for &str {
fn eq(&self, other: &$id<T>) -> bool {
*self == other.full_id.as_ref()
}
}
impl<T: AsRef<str>> ::std::cmp::PartialEq<::std::string::String> for $id<T> {
fn eq(&self, other: &::std::string::String) -> bool {
self.full_id.as_ref() == &other[..]
}
}
impl<T: AsRef<str>> ::std::cmp::PartialEq<$id<T>> for ::std::string::String {
fn eq(&self, other: &$id<T>) -> bool {
&self[..] == other.full_id.as_ref()
}
}
};
}

View File

@ -0,0 +1,170 @@
//! Matrix room alias identifiers.
use std::num::NonZeroU8;
use crate::{error::Error, parse_id};
/// A Matrix room alias ID.
///
/// It is discouraged to use this type directly instead use one of the aliases (`RoomAliasId` and
/// `RoomAliasIdRef`) in the crate root.
///
/// A `RoomAliasId` is converted from a string slice, and can be converted back into a string as
/// needed.
///
/// ```
/// # use std::convert::TryFrom;
/// # use ruma_identifiers::RoomAliasId;
/// assert_eq!(
/// RoomAliasId::try_from("#ruma:example.com").unwrap().as_ref(),
/// "#ruma:example.com"
/// );
/// ```
#[derive(Clone, Copy, Debug)]
pub struct RoomAliasId<T> {
pub(crate) full_id: T,
pub(crate) colon_idx: NonZeroU8,
}
impl<T: AsRef<str>> RoomAliasId<T> {
/// Returns the room's alias.
pub fn alias(&self) -> &str {
&self.full_id.as_ref()[1..self.colon_idx.get() as usize]
}
/// Returns the server name of the room alias ID.
pub fn server_name(&self) -> &str {
&self.full_id.as_ref()[self.colon_idx.get() as usize + 1..]
}
}
/// Attempts to create a new Matrix room alias ID from a string representation.
///
/// The string must include the leading # sigil, the alias, a literal colon, and a server name.
fn try_from<S, T>(room_id: S) -> Result<RoomAliasId<T>, Error>
where
S: AsRef<str> + Into<T>,
{
let colon_idx = parse_id(room_id.as_ref(), &['#'])?;
Ok(RoomAliasId {
full_id: room_id.into(),
colon_idx,
})
}
common_impls!(RoomAliasId, try_from, "a Matrix room alias ID");
#[cfg(test)]
mod tests {
use std::convert::TryFrom;
#[cfg(feature = "serde")]
use serde_json::{from_str, to_string};
use crate::error::Error;
type RoomAliasId = super::RoomAliasId<Box<str>>;
#[test]
fn valid_room_alias_id() {
assert_eq!(
RoomAliasId::try_from("#ruma:example.com")
.expect("Failed to create RoomAliasId.")
.as_ref(),
"#ruma:example.com"
);
}
#[cfg(feature = "serde")]
#[test]
fn serialize_valid_room_alias_id() {
assert_eq!(
to_string(
&RoomAliasId::try_from("#ruma:example.com").expect("Failed to create RoomAliasId.")
)
.expect("Failed to convert RoomAliasId to JSON."),
r##""#ruma:example.com""##
);
}
#[cfg(feature = "serde")]
#[test]
fn deserialize_valid_room_alias_id() {
assert_eq!(
from_str::<RoomAliasId>(r##""#ruma:example.com""##)
.expect("Failed to convert JSON to RoomAliasId"),
RoomAliasId::try_from("#ruma:example.com").expect("Failed to create RoomAliasId.")
);
}
#[test]
fn valid_room_alias_id_with_explicit_standard_port() {
assert_eq!(
RoomAliasId::try_from("#ruma:example.com:443")
.expect("Failed to create RoomAliasId.")
.as_ref(),
"#ruma:example.com:443"
);
}
#[test]
fn valid_room_alias_id_with_non_standard_port() {
assert_eq!(
RoomAliasId::try_from("#ruma:example.com:5000")
.expect("Failed to create RoomAliasId.")
.as_ref(),
"#ruma:example.com:5000"
);
}
#[test]
fn valid_room_alias_id_unicode() {
assert_eq!(
RoomAliasId::try_from("#老虎£я:example.com")
.expect("Failed to create RoomAliasId.")
.as_ref(),
"#老虎£я:example.com"
);
}
#[test]
fn missing_room_alias_id_sigil() {
assert_eq!(
RoomAliasId::try_from("39hvsi03hlne:example.com").unwrap_err(),
Error::MissingSigil
);
}
#[test]
fn missing_localpart() {
assert_eq!(
RoomAliasId::try_from("#:example.com").unwrap_err(),
Error::InvalidLocalPart
);
}
#[test]
fn missing_room_alias_id_delimiter() {
assert_eq!(
RoomAliasId::try_from("#ruma").unwrap_err(),
Error::MissingDelimiter
);
}
#[test]
fn invalid_room_alias_id_host() {
assert_eq!(
RoomAliasId::try_from("#ruma:/").unwrap_err(),
Error::InvalidServerName
);
}
#[test]
fn invalid_room_alias_id_port() {
assert_eq!(
RoomAliasId::try_from("#ruma:example.com:notaport").unwrap_err(),
Error::InvalidServerName
);
}
}

View File

@ -0,0 +1,197 @@
//! Matrix room identifiers.
use std::num::NonZeroU8;
use crate::{error::Error, parse_id};
/// A Matrix room ID.
///
/// A `RoomId` is generated randomly or converted from a string slice, and can be converted back
/// into a string as needed.
///
/// It is discouraged to use this type directly instead use one of the aliases (`RoomId` and
/// `RoomIdRef`) in the crate root.
///
/// ```
/// # use std::convert::TryFrom;
/// # use ruma_identifiers::RoomId;
/// assert_eq!(
/// RoomId::try_from("!n8f893n9:example.com").unwrap().as_ref(),
/// "!n8f893n9:example.com"
/// );
/// ```
#[derive(Clone, Copy, Debug)]
pub struct RoomId<T> {
pub(crate) full_id: T,
pub(crate) colon_idx: NonZeroU8,
}
impl<T> RoomId<T> {
/// Attempts to generate a `RoomId` for the given origin server with a localpart consisting of
/// 18 random ASCII characters.
///
/// Fails if the given homeserver cannot be parsed as a valid host.
#[cfg(feature = "rand")]
#[cfg_attr(docsrs, doc(cfg(feature = "rand")))]
pub fn new(server_name: &str) -> Result<Self, Error>
where
String: Into<T>,
{
use crate::{generate_localpart, is_valid_server_name};
if !is_valid_server_name(server_name) {
return Err(Error::InvalidServerName);
}
let full_id = format!("!{}:{}", generate_localpart(18), server_name).into();
Ok(Self {
full_id,
colon_idx: NonZeroU8::new(19).unwrap(),
})
}
/// Returns the rooms's unique ID.
pub fn localpart(&self) -> &str
where
T: AsRef<str>,
{
&self.full_id.as_ref()[1..self.colon_idx.get() as usize]
}
/// Returns the server name of the room ID.
pub fn server_name(&self) -> &str
where
T: AsRef<str>,
{
&self.full_id.as_ref()[self.colon_idx.get() as usize + 1..]
}
}
/// Attempts to create a new Matrix room ID from a string representation.
///
/// The string must include the leading ! sigil, the localpart, a literal colon, and a server name.
fn try_from<S, T>(room_id: S) -> Result<RoomId<T>, Error>
where
S: AsRef<str> + Into<T>,
{
let colon_idx = parse_id(room_id.as_ref(), &['!'])?;
Ok(RoomId {
full_id: room_id.into(),
colon_idx,
})
}
common_impls!(RoomId, try_from, "a Matrix room ID");
#[cfg(test)]
mod tests {
use std::convert::TryFrom;
#[cfg(feature = "serde")]
use serde_json::{from_str, to_string};
use crate::error::Error;
type RoomId = super::RoomId<Box<str>>;
#[test]
fn valid_room_id() {
assert_eq!(
RoomId::try_from("!29fhd83h92h0:example.com")
.expect("Failed to create RoomId.")
.as_ref(),
"!29fhd83h92h0:example.com"
);
}
#[cfg(feature = "rand")]
#[test]
fn generate_random_valid_room_id() {
let room_id = RoomId::new("example.com").expect("Failed to generate RoomId.");
let id_str: &str = room_id.as_ref();
assert!(id_str.starts_with('!'));
assert_eq!(id_str.len(), 31);
}
#[cfg(feature = "rand")]
#[test]
fn generate_random_invalid_room_id() {
assert!(RoomId::new("").is_err());
}
#[cfg(feature = "serde")]
#[test]
fn serialize_valid_room_id() {
assert_eq!(
to_string(
&RoomId::try_from("!29fhd83h92h0:example.com").expect("Failed to create RoomId.")
)
.expect("Failed to convert RoomId to JSON."),
r#""!29fhd83h92h0:example.com""#
);
}
#[cfg(feature = "serde")]
#[test]
fn deserialize_valid_room_id() {
assert_eq!(
from_str::<RoomId>(r#""!29fhd83h92h0:example.com""#)
.expect("Failed to convert JSON to RoomId"),
RoomId::try_from("!29fhd83h92h0:example.com").expect("Failed to create RoomId.")
);
}
#[test]
fn valid_room_id_with_explicit_standard_port() {
assert_eq!(
RoomId::try_from("!29fhd83h92h0:example.com:443")
.expect("Failed to create RoomId.")
.as_ref(),
"!29fhd83h92h0:example.com:443"
);
}
#[test]
fn valid_room_id_with_non_standard_port() {
assert_eq!(
RoomId::try_from("!29fhd83h92h0:example.com:5000")
.expect("Failed to create RoomId.")
.as_ref(),
"!29fhd83h92h0:example.com:5000"
);
}
#[test]
fn missing_room_id_sigil() {
assert_eq!(
RoomId::try_from("carl:example.com").unwrap_err(),
Error::MissingSigil
);
}
#[test]
fn missing_room_id_delimiter() {
assert_eq!(
RoomId::try_from("!29fhd83h92h0").unwrap_err(),
Error::MissingDelimiter
);
}
#[test]
fn invalid_room_id_host() {
assert_eq!(
RoomId::try_from("!29fhd83h92h0:/").unwrap_err(),
Error::InvalidServerName
);
}
#[test]
fn invalid_room_id_port() {
assert_eq!(
RoomId::try_from("!29fhd83h92h0:example.com:notaport").unwrap_err(),
Error::InvalidServerName
);
}
}

View File

@ -0,0 +1,240 @@
//! Matrix identifiers for places where a room ID or room alias ID are used interchangeably.
use std::{convert::TryFrom, hint::unreachable_unchecked, num::NonZeroU8};
use crate::{error::Error, parse_id, room_alias_id::RoomAliasId, room_id::RoomId};
/// A Matrix room ID or a Matrix room alias ID.
///
/// `RoomIdOrAliasId` is useful for APIs that accept either kind of room identifier. It is converted
/// from a string slice, and can be converted back into a string as needed. When converted from a
/// string slice, the variant is determined by the leading sigil character.
///
/// It is discouraged to use this type directly instead use one of the aliases
/// (`RoomIdOrRoomAliasId` and `RoomIdOrRoomAliasIdRef`) in the crate root.
///
/// ```
/// # use std::convert::TryFrom;
/// # use ruma_identifiers::RoomIdOrAliasId;
/// assert_eq!(
/// RoomIdOrAliasId::try_from("#ruma:example.com").unwrap().as_ref(),
/// "#ruma:example.com"
/// );
///
/// assert_eq!(
/// RoomIdOrAliasId::try_from("!n8f893n9:example.com").unwrap().as_ref(),
/// "!n8f893n9:example.com"
/// );
/// ```
#[derive(Clone, Copy, Debug)]
pub struct RoomIdOrAliasId<T> {
full_id: T,
colon_idx: NonZeroU8,
}
impl<T: AsRef<str>> RoomIdOrAliasId<T> {
/// Returns the local part (everything after the `!` or `#` and before the first colon).
pub fn localpart(&self) -> &str {
&self.full_id.as_ref()[1..self.colon_idx.get() as usize]
}
/// Returns the server name of the room (alias) ID.
pub fn server_name(&self) -> &str {
&self.full_id.as_ref()[self.colon_idx.get() as usize + 1..]
}
/// Whether this is a room id (starts with `'!'`)
pub fn is_room_id(&self) -> bool {
self.variant() == Variant::RoomId
}
/// Whether this is a room alias id (starts with `'#'`)
pub fn is_room_alias_id(&self) -> bool {
self.variant() == Variant::RoomAliasId
}
/// Turn this `RoomIdOrAliasId` into `Either<RoomId, RoomAliasId>`
#[cfg(feature = "either")]
#[cfg_attr(docsrs, doc(cfg(feature = "either")))]
pub fn into_either(self) -> either::Either<RoomId<T>, RoomAliasId<T>> {
match self.variant() {
Variant::RoomId => either::Either::Left(RoomId {
full_id: self.full_id,
colon_idx: self.colon_idx,
}),
Variant::RoomAliasId => either::Either::Right(RoomAliasId {
full_id: self.full_id,
colon_idx: self.colon_idx,
}),
}
}
fn variant(&self) -> Variant {
match self.full_id.as_ref().bytes().next() {
Some(b'!') => Variant::RoomId,
Some(b'#') => Variant::RoomAliasId,
_ => unsafe { unreachable_unchecked() },
}
}
}
#[derive(PartialEq)]
enum Variant {
RoomId,
RoomAliasId,
}
/// Attempts to create a new Matrix room ID or a room alias ID from a string representation.
///
/// The string must either include the leading ! sigil, the localpart, a literal colon, and a
/// valid homeserver host or include the leading # sigil, the alias, a literal colon, and a
/// valid homeserver host.
fn try_from<S, T>(room_id_or_alias_id: S) -> Result<RoomIdOrAliasId<T>, Error>
where
S: AsRef<str> + Into<T>,
{
let colon_idx = parse_id(room_id_or_alias_id.as_ref(), &['#', '!'])?;
Ok(RoomIdOrAliasId {
full_id: room_id_or_alias_id.into(),
colon_idx,
})
}
common_impls!(
RoomIdOrAliasId,
try_from,
"a Matrix room ID or room alias ID"
);
impl<T> From<RoomId<T>> for RoomIdOrAliasId<T> {
fn from(RoomId { full_id, colon_idx }: RoomId<T>) -> Self {
Self { full_id, colon_idx }
}
}
impl<T> From<RoomAliasId<T>> for RoomIdOrAliasId<T> {
fn from(RoomAliasId { full_id, colon_idx }: RoomAliasId<T>) -> Self {
Self { full_id, colon_idx }
}
}
impl<T: AsRef<str>> TryFrom<RoomIdOrAliasId<T>> for RoomId<T> {
type Error = RoomAliasId<T>;
fn try_from(id: RoomIdOrAliasId<T>) -> Result<RoomId<T>, RoomAliasId<T>> {
match id.variant() {
Variant::RoomId => Ok(RoomId {
full_id: id.full_id,
colon_idx: id.colon_idx,
}),
Variant::RoomAliasId => Err(RoomAliasId {
full_id: id.full_id,
colon_idx: id.colon_idx,
}),
}
}
}
impl<T: AsRef<str>> TryFrom<RoomIdOrAliasId<T>> for RoomAliasId<T> {
type Error = RoomId<T>;
fn try_from(id: RoomIdOrAliasId<T>) -> Result<RoomAliasId<T>, RoomId<T>> {
match id.variant() {
Variant::RoomAliasId => Ok(RoomAliasId {
full_id: id.full_id,
colon_idx: id.colon_idx,
}),
Variant::RoomId => Err(RoomId {
full_id: id.full_id,
colon_idx: id.colon_idx,
}),
}
}
}
#[cfg(test)]
mod tests {
use std::convert::TryFrom;
#[cfg(feature = "serde")]
use serde_json::{from_str, to_string};
use crate::error::Error;
type RoomIdOrAliasId = super::RoomIdOrAliasId<Box<str>>;
#[test]
fn valid_room_id_or_alias_id_with_a_room_alias_id() {
assert_eq!(
RoomIdOrAliasId::try_from("#ruma:example.com")
.expect("Failed to create RoomAliasId.")
.as_ref(),
"#ruma:example.com"
);
}
#[test]
fn valid_room_id_or_alias_id_with_a_room_id() {
assert_eq!(
RoomIdOrAliasId::try_from("!29fhd83h92h0:example.com")
.expect("Failed to create RoomId.")
.as_ref(),
"!29fhd83h92h0:example.com"
);
}
#[test]
fn missing_sigil_for_room_id_or_alias_id() {
assert_eq!(
RoomIdOrAliasId::try_from("ruma:example.com").unwrap_err(),
Error::MissingSigil
);
}
#[cfg(feature = "serde")]
#[test]
fn serialize_valid_room_id_or_alias_id_with_a_room_alias_id() {
assert_eq!(
to_string(
&RoomIdOrAliasId::try_from("#ruma:example.com")
.expect("Failed to create RoomAliasId.")
)
.expect("Failed to convert RoomAliasId to JSON."),
r##""#ruma:example.com""##
);
}
#[cfg(feature = "serde")]
#[test]
fn serialize_valid_room_id_or_alias_id_with_a_room_id() {
assert_eq!(
to_string(
&RoomIdOrAliasId::try_from("!29fhd83h92h0:example.com")
.expect("Failed to create RoomId.")
)
.expect("Failed to convert RoomId to JSON."),
r#""!29fhd83h92h0:example.com""#
);
}
#[cfg(feature = "serde")]
#[test]
fn deserialize_valid_room_id_or_alias_id_with_a_room_alias_id() {
assert_eq!(
from_str::<RoomIdOrAliasId>(r##""#ruma:example.com""##)
.expect("Failed to convert JSON to RoomAliasId"),
RoomIdOrAliasId::try_from("#ruma:example.com").expect("Failed to create RoomAliasId.")
);
}
#[cfg(feature = "serde")]
#[test]
fn deserialize_valid_room_id_or_alias_id_with_a_room_id() {
assert_eq!(
from_str::<RoomIdOrAliasId>(r##""!29fhd83h92h0:example.com""##)
.expect("Failed to convert JSON to RoomId"),
RoomIdOrAliasId::try_from("!29fhd83h92h0:example.com")
.expect("Failed to create RoomAliasId.")
);
}
}

View File

@ -0,0 +1,470 @@
//! Matrix room version identifiers.
use std::{
cmp::Ordering,
convert::TryFrom,
fmt::{self, Display, Formatter},
};
#[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;
/// A Matrix room version ID.
///
/// A `RoomVersionId` can be or converted or deserialized from a string slice, and can be converted
/// or serialized back into a string as needed.
///
/// It is discouraged to use this type directly instead use one of the aliases (`RoomVersionId`
/// and `RoomVersionIdRef`) in the crate root.
///
/// ```
/// # use std::convert::TryFrom;
/// # use ruma_identifiers::RoomVersionId;
/// assert_eq!(RoomVersionId::try_from("1").unwrap().as_ref(), "1");
/// ```
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct RoomVersionId<T>(InnerRoomVersionId<T>);
/// Possibile values for room version, distinguishing between official Matrix versions and custom
/// versions.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum InnerRoomVersionId<T> {
/// A version 1 room.
Version1,
/// A version 2 room.
Version2,
/// A version 3 room.
Version3,
/// A version 4 room.
Version4,
/// A version 5 room.
Version5,
/// A version 6 room.
Version6,
/// A custom room version.
Custom(T),
}
impl<T> RoomVersionId<T> {
/// Creates a version 1 room ID.
pub fn version_1() -> Self {
Self(InnerRoomVersionId::Version1)
}
/// Creates a version 2 room ID.
pub fn version_2() -> Self {
Self(InnerRoomVersionId::Version2)
}
/// Creates a version 3 room ID.
pub fn version_3() -> Self {
Self(InnerRoomVersionId::Version3)
}
/// Creates a version 4 room ID.
pub fn version_4() -> Self {
Self(InnerRoomVersionId::Version4)
}
/// Creates a version 5 room ID.
pub fn version_5() -> Self {
Self(InnerRoomVersionId::Version5)
}
/// Creates a version 6 room ID.
pub fn version_6() -> Self {
Self(InnerRoomVersionId::Version6)
}
/// Creates a custom room version ID from the given string slice.
pub fn custom(id: String) -> Self
where
String: Into<T>,
{
Self(InnerRoomVersionId::Custom(id.into()))
}
/// Whether or not this room version is an official one specified by the Matrix protocol.
pub fn is_official(&self) -> bool {
!self.is_custom()
}
/// Whether or not this is a custom room version.
pub fn is_custom(&self) -> bool {
match self.0 {
InnerRoomVersionId::Custom(_) => true,
_ => false,
}
}
/// Whether or not this is a version 1 room.
pub fn is_version_1(&self) -> bool {
matches!(self.0, InnerRoomVersionId::Version1)
}
/// Whether or not this is a version 2 room.
pub fn is_version_2(&self) -> bool {
matches!(self.0, InnerRoomVersionId::Version2)
}
/// Whether or not this is a version 3 room.
pub fn is_version_3(&self) -> bool {
matches!(self.0, InnerRoomVersionId::Version3)
}
/// Whether or not this is a version 4 room.
pub fn is_version_4(&self) -> bool {
matches!(self.0, InnerRoomVersionId::Version4)
}
/// Whether or not this is a version 5 room.
pub fn is_version_5(&self) -> bool {
matches!(self.0, InnerRoomVersionId::Version5)
}
/// Whether or not this is a version 6 room.
pub fn is_version_6(&self) -> bool {
matches!(self.0, InnerRoomVersionId::Version5)
}
}
impl From<RoomVersionId<Box<str>>> for String {
fn from(id: RoomVersionId<Box<str>>) -> Self {
match id.0 {
InnerRoomVersionId::Version1 => "1".to_owned(),
InnerRoomVersionId::Version2 => "2".to_owned(),
InnerRoomVersionId::Version3 => "3".to_owned(),
InnerRoomVersionId::Version4 => "4".to_owned(),
InnerRoomVersionId::Version5 => "5".to_owned(),
InnerRoomVersionId::Version6 => "6".to_owned(),
InnerRoomVersionId::Custom(version) => version.into(),
}
}
}
impl<T: AsRef<str>> AsRef<str> for RoomVersionId<T> {
fn as_ref(&self) -> &str {
match &self.0 {
InnerRoomVersionId::Version1 => "1",
InnerRoomVersionId::Version2 => "2",
InnerRoomVersionId::Version3 => "3",
InnerRoomVersionId::Version4 => "4",
InnerRoomVersionId::Version5 => "5",
InnerRoomVersionId::Version6 => "6",
InnerRoomVersionId::Custom(version) => version.as_ref(),
}
}
}
impl<T: AsRef<str>> Display for RoomVersionId<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_ref())
}
}
impl<T: PartialEq + AsRef<str>> PartialOrd for RoomVersionId<T> {
fn partial_cmp(&self, other: &RoomVersionId<T>) -> Option<Ordering> {
self.as_ref().partial_cmp(other.as_ref())
}
}
impl<T: Eq + AsRef<str>> Ord for RoomVersionId<T> {
fn cmp(&self, other: &Self) -> Ordering {
self.as_ref().cmp(other.as_ref())
}
}
#[cfg(feature = "serde")]
impl<T: AsRef<str>> Serialize for RoomVersionId<T> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.as_ref())
}
}
#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for RoomVersionId<Box<str>> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
crate::deserialize_id(deserializer, "a Matrix room version ID as a string")
}
}
/// Attempts to create a new Matrix room version ID from a string representation.
fn try_from<S, T>(room_version_id: S) -> Result<RoomVersionId<T>, Error>
where
S: AsRef<str> + Into<T>,
{
let version = match room_version_id.as_ref() {
"1" => RoomVersionId(InnerRoomVersionId::Version1),
"2" => RoomVersionId(InnerRoomVersionId::Version2),
"3" => RoomVersionId(InnerRoomVersionId::Version3),
"4" => RoomVersionId(InnerRoomVersionId::Version4),
"5" => RoomVersionId(InnerRoomVersionId::Version5),
custom => {
if custom.is_empty() {
return Err(Error::MinimumLengthNotSatisfied);
} else if custom.chars().count() > MAX_CODE_POINTS {
return Err(Error::MaximumLengthExceeded);
} else {
RoomVersionId(InnerRoomVersionId::Custom(room_version_id.into()))
}
}
};
Ok(version)
}
impl<'a> TryFrom<&'a str> for RoomVersionId<&'a str> {
type Error = crate::error::Error;
fn try_from(s: &'a str) -> Result<Self, Error> {
try_from(s)
}
}
impl TryFrom<&str> for RoomVersionId<Box<str>> {
type Error = crate::error::Error;
fn try_from(s: &str) -> Result<Self, Error> {
try_from(s)
}
}
impl TryFrom<String> for RoomVersionId<Box<str>> {
type Error = crate::error::Error;
fn try_from(s: String) -> Result<Self, Error> {
try_from(s)
}
}
impl<T: AsRef<str>> PartialEq<&str> for RoomVersionId<T> {
fn eq(&self, other: &&str) -> bool {
self.as_ref() == *other
}
}
impl<T: AsRef<str>> PartialEq<RoomVersionId<T>> for &str {
fn eq(&self, other: &RoomVersionId<T>) -> bool {
*self == other.as_ref()
}
}
impl<T: AsRef<str>> PartialEq<String> for RoomVersionId<T> {
fn eq(&self, other: &String) -> bool {
self.as_ref() == other
}
}
impl<T: AsRef<str>> PartialEq<RoomVersionId<T>> for String {
fn eq(&self, other: &RoomVersionId<T>) -> bool {
self == other.as_ref()
}
}
#[cfg(test)]
mod tests {
use std::convert::TryFrom;
#[cfg(feature = "serde")]
use serde_json::{from_str, to_string};
use crate::error::Error;
type RoomVersionId = super::RoomVersionId<Box<str>>;
#[test]
fn valid_version_1_room_version_id() {
assert_eq!(
RoomVersionId::try_from("1")
.expect("Failed to create RoomVersionId.")
.as_ref(),
"1"
);
}
#[test]
fn valid_version_2_room_version_id() {
assert_eq!(
RoomVersionId::try_from("2")
.expect("Failed to create RoomVersionId.")
.as_ref(),
"2"
);
}
#[test]
fn valid_version_3_room_version_id() {
assert_eq!(
RoomVersionId::try_from("3")
.expect("Failed to create RoomVersionId.")
.as_ref(),
"3"
);
}
#[test]
fn valid_version_4_room_version_id() {
assert_eq!(
RoomVersionId::try_from("4")
.expect("Failed to create RoomVersionId.")
.as_ref(),
"4"
);
}
#[test]
fn valid_version_5_room_version_id() {
assert_eq!(
RoomVersionId::try_from("5")
.expect("Failed to create RoomVersionId.")
.as_ref(),
"5"
);
}
#[test]
fn valid_custom_room_version_id() {
assert_eq!(
RoomVersionId::try_from("io.ruma.1")
.expect("Failed to create RoomVersionId.")
.as_ref(),
"io.ruma.1"
);
}
#[test]
fn empty_room_version_id() {
assert_eq!(
RoomVersionId::try_from(""),
Err(Error::MinimumLengthNotSatisfied)
);
}
#[test]
fn over_max_code_point_room_version_id() {
assert_eq!(
RoomVersionId::try_from("0123456789012345678901234567890123456789"),
Err(Error::MaximumLengthExceeded)
);
}
#[cfg(feature = "serde")]
#[test]
fn serialize_official_room_id() {
assert_eq!(
to_string(&RoomVersionId::try_from("1").expect("Failed to create RoomVersionId."))
.expect("Failed to convert RoomVersionId to JSON."),
r#""1""#
);
}
#[cfg(feature = "serde")]
#[test]
fn deserialize_official_room_id() {
let deserialized =
from_str::<RoomVersionId>(r#""1""#).expect("Failed to convert RoomVersionId to JSON.");
assert!(deserialized.is_version_1());
assert!(deserialized.is_official());
assert_eq!(
deserialized,
RoomVersionId::try_from("1").expect("Failed to create RoomVersionId.")
);
}
#[cfg(feature = "serde")]
#[test]
fn serialize_custom_room_id() {
assert_eq!(
to_string(
&RoomVersionId::try_from("io.ruma.1").expect("Failed to create RoomVersionId.")
)
.expect("Failed to convert RoomVersionId to JSON."),
r#""io.ruma.1""#
);
}
#[cfg(feature = "serde")]
#[test]
fn deserialize_custom_room_id() {
let deserialized = from_str::<RoomVersionId>(r#""io.ruma.1""#)
.expect("Failed to convert RoomVersionId to JSON.");
assert!(deserialized.is_custom());
assert_eq!(
deserialized,
RoomVersionId::try_from("io.ruma.1").expect("Failed to create RoomVersionId.")
);
}
#[test]
fn constructors() {
assert!(RoomVersionId::version_1().is_version_1());
assert!(RoomVersionId::version_2().is_version_2());
assert!(RoomVersionId::version_3().is_version_3());
assert!(RoomVersionId::version_4().is_version_4());
assert!(RoomVersionId::version_5().is_version_5());
assert!(RoomVersionId::custom("foo".into()).is_custom());
}
#[test]
#[allow(clippy::cognitive_complexity)]
fn predicate_methods() {
let version_1 = RoomVersionId::try_from("1").expect("Failed to create RoomVersionId.");
let version_2 = RoomVersionId::try_from("2").expect("Failed to create RoomVersionId.");
let version_3 = RoomVersionId::try_from("3").expect("Failed to create RoomVersionId.");
let version_4 = RoomVersionId::try_from("4").expect("Failed to create RoomVersionId.");
let version_5 = RoomVersionId::try_from("5").expect("Failed to create RoomVersionId.");
let custom = RoomVersionId::try_from("io.ruma.1").expect("Failed to create RoomVersionId.");
assert!(version_1.is_version_1());
assert!(version_2.is_version_2());
assert!(version_3.is_version_3());
assert!(version_4.is_version_4());
assert!(version_5.is_version_5());
assert!(!version_1.is_version_2());
assert!(!version_1.is_version_3());
assert!(!version_1.is_version_4());
assert!(!version_1.is_version_5());
assert!(version_1.is_official());
assert!(version_2.is_official());
assert!(version_3.is_official());
assert!(version_4.is_official());
assert!(version_5.is_official());
assert!(!version_1.is_custom());
assert!(!version_2.is_custom());
assert!(!version_3.is_custom());
assert!(!version_4.is_custom());
assert!(!version_5.is_custom());
assert!(custom.is_custom());
assert!(!custom.is_official());
assert!(!custom.is_version_1());
assert!(!custom.is_version_2());
assert!(!custom.is_version_3());
assert!(!custom.is_version_4());
assert!(!custom.is_version_5());
}
}

View File

@ -0,0 +1,125 @@
//! Identifiers for homeserver signing keys used for federation.
use std::{num::NonZeroU8, str::FromStr};
use crate::{error::Error, key_algorithms::ServerKeyAlgorithm};
/// Key identifiers used for homeserver signing keys.
#[derive(Clone, Debug)]
pub struct ServerKeyId<T> {
full_id: T,
colon_idx: NonZeroU8,
}
impl<T> ServerKeyId<T> {
/// Returns key algorithm of the server key ID.
pub fn algorithm(&self) -> ServerKeyAlgorithm
where
T: AsRef<str>,
{
ServerKeyAlgorithm::from_str(&self.full_id.as_ref()[..self.colon_idx.get() as usize])
.unwrap()
}
/// Returns the version of the server key ID.
pub fn version(&self) -> &str
where
T: AsRef<str>,
{
&self.full_id.as_ref()[self.colon_idx.get() as usize + 1..]
}
}
fn try_from<S, T>(key_id: S) -> Result<ServerKeyId<T>, Error>
where
S: AsRef<str> + Into<T>,
{
let key_str = key_id.as_ref();
let colon_idx =
NonZeroU8::new(key_str.find(':').ok_or(Error::MissingServerKeyDelimiter)? as u8)
.ok_or(Error::UnknownKeyAlgorithm)?;
validate_server_key_algorithm(&key_str[..colon_idx.get() as usize])?;
validate_version(&key_str[colon_idx.get() as usize + 1..])?;
Ok(ServerKeyId {
full_id: key_id.into(),
colon_idx,
})
}
common_impls!(ServerKeyId, try_from, "Key ID with algorithm and version");
fn validate_version(version: &str) -> Result<(), Error> {
if version.is_empty() {
return Err(Error::MinimumLengthNotSatisfied);
} else if !version.chars().all(|c| c.is_alphanumeric() || c == '_') {
return Err(Error::InvalidCharacters);
}
Ok(())
}
fn validate_server_key_algorithm(algorithm: &str) -> Result<(), Error> {
match ServerKeyAlgorithm::from_str(algorithm) {
Ok(_) => Ok(()),
Err(_) => Err(Error::UnknownKeyAlgorithm),
}
}
#[cfg(test)]
mod tests {
use std::convert::TryFrom;
#[cfg(feature = "serde")]
use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
use super::ServerKeyId;
use crate::error::Error;
#[cfg(feature = "serde")]
use crate::key_algorithms::ServerKeyAlgorithm;
#[cfg(feature = "serde")]
#[test]
fn deserialize_id() {
let server_key_id: ServerKeyId<_> = from_json_value(json!("ed25519:Abc_1")).unwrap();
assert_eq!(server_key_id.algorithm(), ServerKeyAlgorithm::Ed25519);
assert_eq!(server_key_id.version(), "Abc_1");
}
#[cfg(feature = "serde")]
#[test]
fn serialize_id() {
let server_key_id: ServerKeyId<&str> = ServerKeyId::try_from("ed25519:abc123").unwrap();
assert_eq!(
to_json_value(&server_key_id).unwrap(),
json!("ed25519:abc123")
);
}
#[test]
fn invalid_version_characters() {
assert_eq!(
ServerKeyId::<&str>::try_from("ed25519:Abc-1").unwrap_err(),
Error::InvalidCharacters,
);
}
#[test]
fn invalid_key_algorithm() {
assert_eq!(
ServerKeyId::<&str>::try_from("signed_curve25519:Abc-1").unwrap_err(),
Error::UnknownKeyAlgorithm,
);
}
#[test]
fn missing_delimiter() {
assert_eq!(
ServerKeyId::<&str>::try_from("ed25519|Abc_1").unwrap_err(),
Error::MissingServerKeyDelimiter,
);
}
}

View File

@ -0,0 +1,105 @@
/// Check whether a given string is a valid server name according to [the specification][].
///
/// [the specification]: https://matrix.org/docs/spec/appendices#server-name
pub fn is_valid_server_name(name: &str) -> bool {
use std::net::Ipv6Addr;
if name.is_empty() {
return false;
}
let end_of_host = if name.starts_with('[') {
let end_of_ipv6 = match name.find(']') {
Some(idx) => idx,
None => return false,
};
if name[1..end_of_ipv6].parse::<Ipv6Addr>().is_err() {
return false;
}
end_of_ipv6 + 1
} else {
let end_of_host = name.find(':').unwrap_or_else(|| name.len());
if name[..end_of_host]
.bytes()
.any(|byte| !(byte.is_ascii_alphanumeric() || byte == b'-' || byte == b'.'))
{
return false;
}
end_of_host
};
if name.len() == end_of_host {
true
} else if name.as_bytes()[end_of_host] != b':' {
// hostname is followed by something other than ":port"
false
} else {
// are the remaining characters after ':' a valid port?
name[end_of_host + 1..].parse::<u16>().is_ok()
}
}
#[cfg(test)]
mod tests {
use super::is_valid_server_name;
#[test]
fn ipv4_host() {
assert!(is_valid_server_name("127.0.0.1"));
}
#[test]
fn ipv4_host_and_port() {
assert!(is_valid_server_name("1.1.1.1:12000"));
}
#[test]
fn ipv6() {
assert!(is_valid_server_name("[::1]"));
}
#[test]
fn ipv6_with_port() {
assert!(is_valid_server_name("[1234:5678::abcd]:5678"));
}
#[test]
fn dns_name() {
assert!(is_valid_server_name("example.com"));
}
#[test]
fn dns_name_with_port() {
assert!(is_valid_server_name("ruma.io:8080"));
}
#[test]
fn empty_string() {
assert!(!is_valid_server_name(""));
}
#[test]
fn invalid_ipv6() {
assert!(!is_valid_server_name("[test::1]"));
}
#[test]
fn ipv4_with_invalid_port() {
assert!(!is_valid_server_name("127.0.0.1:"));
}
#[test]
fn ipv6_with_invalid_port() {
assert!(!is_valid_server_name("[fe80::1]:100000"));
assert!(!is_valid_server_name("[fe80::1]!"));
}
#[test]
fn dns_name_with_invalid_port() {
assert!(!is_valid_server_name("matrix.org:hello"));
}
}

View File

@ -0,0 +1,340 @@
//! Matrix user identifiers.
use std::num::NonZeroU8;
use crate::{error::Error, is_valid_server_name, parse_id};
/// A Matrix user ID.
///
/// A `UserId` is generated randomly or converted from a string slice, and can be converted back
/// into a string as needed.
///
/// It is discouraged to use this type directly instead use one of the aliases (`UserId` and
/// `UserIdRef`) in the crate root.
///
/// ```
/// # use std::convert::TryFrom;
/// # use ruma_identifiers::UserId;
/// assert_eq!(
/// UserId::try_from("@carl:example.com").unwrap().as_ref(),
/// "@carl:example.com"
/// );
/// ```
#[derive(Clone, Copy, Debug)]
pub struct UserId<T> {
full_id: T,
colon_idx: NonZeroU8,
/// Whether this user id is a historical one.
///
/// A historical user id is one that is not legal per the regular user id rules, but was
/// accepted by previous versions of the spec and thus has to be supported because users with
/// these kinds of ids still exist.
is_historical: bool,
}
impl<T> UserId<T> {
/// Attempts to generate a `UserId` for the given origin server with a localpart consisting of
/// 12 random ASCII characters.
///
/// Fails if the given homeserver cannot be parsed as a valid host.
#[cfg(feature = "rand")]
#[cfg_attr(docsrs, doc(cfg(feature = "rand")))]
pub fn new(server_name: &str) -> Result<Self, Error>
where
String: Into<T>,
{
use crate::generate_localpart;
if !is_valid_server_name(server_name) {
return Err(Error::InvalidServerName);
}
let full_id = format!("@{}:{}", generate_localpart(12).to_lowercase(), server_name).into();
Ok(Self {
full_id,
colon_idx: NonZeroU8::new(13).unwrap(),
is_historical: false,
})
}
/// Attempts to complete a user ID, by adding the colon + server name and `@` prefix, if not
/// present already.
///
/// This is a convenience function for the login API, where a user can supply either their full
/// user ID or just the localpart. It only supports a valid user ID or a valid user ID
/// localpart, not the localpart plus the `@` prefix, or the localpart plus server name without
/// the `@` prefix.
pub fn parse_with_server_name(
id: impl AsRef<str> + Into<T>,
server_name: &str,
) -> Result<Self, Error>
where
String: Into<T>,
{
let id_str = id.as_ref();
if id_str.starts_with('@') {
try_from(id)
} else {
let is_fully_conforming = localpart_is_fully_comforming(id_str)?;
if !is_valid_server_name(server_name) {
return Err(Error::InvalidServerName);
}
Ok(Self {
full_id: format!("@{}:{}", id_str, server_name).into(),
colon_idx: NonZeroU8::new(id_str.len() as u8 + 1).unwrap(),
is_historical: !is_fully_conforming,
})
}
}
/// Returns the user's localpart.
pub fn localpart(&self) -> &str
where
T: AsRef<str>,
{
&self.full_id.as_ref()[1..self.colon_idx.get() as usize]
}
/// Returns the server name of the user ID.
pub fn server_name(&self) -> &str
where
T: AsRef<str>,
{
&self.full_id.as_ref()[self.colon_idx.get() as usize + 1..]
}
/// Whether this user ID is a historical one, i.e. one that doesn't conform to the latest
/// specification of the user ID grammar but is still accepted because it was previously
/// allowed.
pub fn is_historical(&self) -> bool {
self.is_historical
}
}
/// Attempts to create a new Matrix user ID from a string representation.
///
/// The string must include the leading @ sigil, the localpart, a literal colon, and a server name.
fn try_from<S, T>(user_id: S) -> Result<UserId<T>, Error>
where
S: AsRef<str> + Into<T>,
{
let colon_idx = parse_id(user_id.as_ref(), &['@'])?;
let localpart = &user_id.as_ref()[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,
})
}
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| match b {
b'0'..=b'9' | b'a'..=b'z' | b'-' | b'.' | b'=' | b'_' | b'/' => true,
_ => false,
});
// If it's not fully conforming, check if it contains characters that are also disallowed
// for historical user IDs. If there are, return an error.
// See https://matrix.org/docs/spec/appendices#historical-user-ids
if !is_fully_conforming && localpart.bytes().any(|b| b < 0x21 || b == b':' || b > 0x7E) {
Err(Error::InvalidCharacters)
} else {
Ok(is_fully_conforming)
}
}
#[cfg(test)]
mod tests {
use std::convert::TryFrom;
#[cfg(feature = "serde")]
use serde_json::{from_str, to_string};
use crate::error::Error;
type UserId = super::UserId<Box<str>>;
#[test]
fn valid_user_id_from_str() {
let user_id = UserId::try_from("@carl:example.com").expect("Failed to create UserId.");
assert_eq!(user_id.as_ref(), "@carl:example.com");
assert_eq!(user_id.localpart(), "carl");
assert_eq!(user_id.server_name(), "example.com");
assert!(!user_id.is_historical());
}
#[test]
fn parse_valid_user_id() {
let user_id = UserId::parse_with_server_name("@carl:example.com", "example.com")
.expect("Failed to create UserId.");
assert_eq!(user_id.as_ref(), "@carl:example.com");
assert_eq!(user_id.localpart(), "carl");
assert_eq!(user_id.server_name(), "example.com");
assert!(!user_id.is_historical());
}
#[test]
fn parse_valid_user_id_parts() {
let user_id = UserId::parse_with_server_name("carl", "example.com")
.expect("Failed to create UserId.");
assert_eq!(user_id.as_ref(), "@carl:example.com");
assert_eq!(user_id.localpart(), "carl");
assert_eq!(user_id.server_name(), "example.com");
assert!(!user_id.is_historical());
}
#[test]
fn valid_historical_user_id() {
let user_id = UserId::try_from("@a%b[irc]:example.com").expect("Failed to create UserId.");
assert_eq!(user_id.as_ref(), "@a%b[irc]:example.com");
assert_eq!(user_id.localpart(), "a%b[irc]");
assert_eq!(user_id.server_name(), "example.com");
assert!(user_id.is_historical());
}
#[test]
fn parse_valid_historical_user_id() {
let user_id = UserId::parse_with_server_name("@a%b[irc]:example.com", "example.com")
.expect("Failed to create UserId.");
assert_eq!(user_id.as_ref(), "@a%b[irc]:example.com");
assert_eq!(user_id.localpart(), "a%b[irc]");
assert_eq!(user_id.server_name(), "example.com");
assert!(user_id.is_historical());
}
#[test]
fn parse_valid_historical_user_id_parts() {
let user_id = UserId::parse_with_server_name("a%b[irc]", "example.com")
.expect("Failed to create UserId.");
assert_eq!(user_id.as_ref(), "@a%b[irc]:example.com");
assert_eq!(user_id.localpart(), "a%b[irc]");
assert_eq!(user_id.server_name(), "example.com");
assert!(user_id.is_historical());
}
#[test]
fn uppercase_user_id() {
let user_id = UserId::try_from("@CARL:example.com").expect("Failed to create UserId.");
assert_eq!(user_id.as_ref(), "@CARL:example.com");
assert!(user_id.is_historical());
}
#[cfg(feature = "rand")]
#[test]
fn generate_random_valid_user_id() {
let user_id = UserId::new("example.com").expect("Failed to generate UserId.");
assert_eq!(user_id.localpart().len(), 12);
assert_eq!(user_id.server_name(), "example.com");
let id_str: &str = user_id.as_ref();
assert!(id_str.starts_with('@'));
assert_eq!(id_str.len(), 25);
}
#[cfg(feature = "rand")]
#[test]
fn generate_random_invalid_user_id() {
assert!(UserId::new("").is_err());
}
#[cfg(feature = "serde")]
#[test]
fn serialize_valid_user_id() {
assert_eq!(
to_string(&UserId::try_from("@carl:example.com").expect("Failed to create UserId."))
.expect("Failed to convert UserId to JSON."),
r#""@carl:example.com""#
);
}
#[cfg(feature = "serde")]
#[test]
fn deserialize_valid_user_id() {
assert_eq!(
from_str::<UserId>(r#""@carl:example.com""#).expect("Failed to convert JSON to UserId"),
UserId::try_from("@carl:example.com").expect("Failed to create UserId.")
);
}
#[test]
fn valid_user_id_with_explicit_standard_port() {
assert_eq!(
UserId::try_from("@carl:example.com:443")
.expect("Failed to create UserId.")
.as_ref(),
"@carl:example.com:443"
);
}
#[test]
fn valid_user_id_with_non_standard_port() {
let user_id = UserId::try_from("@carl:example.com:5000").expect("Failed to create UserId.");
assert_eq!(user_id.as_ref(), "@carl:example.com:5000");
assert!(!user_id.is_historical());
}
#[test]
fn invalid_characters_in_user_id_localpart() {
assert_eq!(
UserId::try_from("@te\nst:example.com").unwrap_err(),
Error::InvalidCharacters
);
}
#[test]
fn missing_user_id_sigil() {
assert_eq!(
UserId::try_from("carl:example.com").unwrap_err(),
Error::MissingSigil
);
}
#[test]
fn missing_localpart() {
assert_eq!(
UserId::try_from("@:example.com").unwrap_err(),
Error::InvalidLocalPart
);
}
#[test]
fn missing_user_id_delimiter() {
assert_eq!(
UserId::try_from("@carl").unwrap_err(),
Error::MissingDelimiter
);
}
#[test]
fn invalid_user_id_host() {
assert_eq!(
UserId::try_from("@carl:/").unwrap_err(),
Error::InvalidServerName
);
}
#[test]
fn invalid_user_id_port() {
assert_eq!(
UserId::try_from("@carl:example.com:notaport").unwrap_err(),
Error::InvalidServerName
);
}
}