Add 'ruma-identifiers/' from commit 'c0a1d8bd440c7cde0fa4ab5e22898ddb26bb706d'
git-subtree-dir: ruma-identifiers git-subtree-mainline: 19ce9645b6651e4de42ddf9e81ee50e19c8c0f26 git-subtree-split: c0a1d8bd440c7cde0fa4ab5e22898ddb26bb706d
This commit is contained in:
commit
83de77f002
30
ruma-identifiers/.builds/beta.yml
Normal file
30
ruma-identifiers/.builds/beta.yml
Normal 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 ))
|
26
ruma-identifiers/.builds/msrv.yml
Normal file
26
ruma-identifiers/.builds/msrv.yml
Normal 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 ))
|
32
ruma-identifiers/.builds/nightly.yml
Normal file
32
ruma-identifiers/.builds/nightly.yml
Normal 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 ))
|
32
ruma-identifiers/.builds/stable.yml
Normal file
32
ruma-identifiers/.builds/stable.yml
Normal 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
2
ruma-identifiers/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
Cargo.lock
|
||||
/target
|
104
ruma-identifiers/CHANGELOG.md
Normal file
104
ruma-identifiers/CHANGELOG.md
Normal 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)
|
29
ruma-identifiers/Cargo.toml
Normal file
29
ruma-identifiers/Cargo.toml
Normal 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
20
ruma-identifiers/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.
|
||||
|
15
ruma-identifiers/README.md
Normal file
15
ruma-identifiers/README.md
Normal file
@ -0,0 +1,15 @@
|
||||
# ruma-identifiers
|
||||
|
||||
[](https://crates.io/crates/ruma-identifiers)
|
||||
[](https://docs.rs/ruma-identifiers/)
|
||||

|
||||
|
||||
**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.
|
27
ruma-identifiers/src/device_id.rs
Normal file
27
ruma-identifiers/src/device_id.rs
Normal 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);
|
||||
}
|
||||
}
|
136
ruma-identifiers/src/device_key_id.rs
Normal file
136
ruma-identifiers/src/device_key_id.rs
Normal 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"));
|
||||
}
|
||||
}
|
54
ruma-identifiers/src/error.rs
Normal file
54
ruma-identifiers/src/error.rs
Normal 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 {}
|
315
ruma-identifiers/src/event_id.rs
Normal file
315
ruma-identifiers/src/event_id.rs
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
33
ruma-identifiers/src/key_algorithms.rs
Normal file
33
ruma-identifiers/src/key_algorithms.rs
Normal 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
171
ruma-identifiers/src/lib.rs
Normal 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))
|
||||
})
|
||||
}
|
115
ruma-identifiers/src/macros.rs
Normal file
115
ruma-identifiers/src/macros.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
170
ruma-identifiers/src/room_alias_id.rs
Normal file
170
ruma-identifiers/src/room_alias_id.rs
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
197
ruma-identifiers/src/room_id.rs
Normal file
197
ruma-identifiers/src/room_id.rs
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
240
ruma-identifiers/src/room_id_or_room_alias_id.rs
Normal file
240
ruma-identifiers/src/room_id_or_room_alias_id.rs
Normal 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.")
|
||||
);
|
||||
}
|
||||
}
|
470
ruma-identifiers/src/room_version_id.rs
Normal file
470
ruma-identifiers/src/room_version_id.rs
Normal 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());
|
||||
}
|
||||
}
|
125
ruma-identifiers/src/server_key_id.rs
Normal file
125
ruma-identifiers/src/server_key_id.rs
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
105
ruma-identifiers/src/server_name.rs
Normal file
105
ruma-identifiers/src/server_name.rs
Normal 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"));
|
||||
}
|
||||
}
|
340
ruma-identifiers/src/user_id.rs
Normal file
340
ruma-identifiers/src/user_id.rs
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user