mas_data_model/compat/
device.rs

1// Copyright 2024 New Vector Ltd.
2// Copyright 2023, 2024 The Matrix.org Foundation C.I.C.
3//
4// SPDX-License-Identifier: AGPL-3.0-only
5// Please see LICENSE in the repository root for full details.
6
7use oauth2_types::scope::ScopeToken;
8use rand::{
9    RngCore,
10    distributions::{Alphanumeric, DistString},
11};
12use serde::{Deserialize, Serialize};
13use thiserror::Error;
14
15static GENERATED_DEVICE_ID_LENGTH: usize = 10;
16static UNSTABLE_DEVICE_SCOPE_PREFIX: &str = "urn:matrix:org.matrix.msc2967.client:device:";
17static STABLE_DEVICE_SCOPE_PREFIX: &str = "urn:matrix:client:device:";
18
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(transparent)]
21pub struct Device {
22    id: String,
23}
24
25#[derive(Debug, Error)]
26pub enum ToScopeTokenError {
27    #[error("Device ID contains characters that can't be encoded in a scope")]
28    InvalidCharacters,
29}
30
31impl Device {
32    /// Get the corresponding stable and unstable [`ScopeToken`] for that device
33    ///
34    /// # Errors
35    ///
36    /// Returns an error if the device ID contains characters that can't be
37    /// encoded in a scope
38    pub fn to_scope_token(&self) -> Result<[ScopeToken; 2], ToScopeTokenError> {
39        Ok([
40            format!("{STABLE_DEVICE_SCOPE_PREFIX}{}", self.id)
41                .parse()
42                .map_err(|_| ToScopeTokenError::InvalidCharacters)?,
43            format!("{UNSTABLE_DEVICE_SCOPE_PREFIX}{}", self.id)
44                .parse()
45                .map_err(|_| ToScopeTokenError::InvalidCharacters)?,
46        ])
47    }
48
49    /// Get the corresponding [`Device`] from a [`ScopeToken`]
50    ///
51    /// Returns `None` if the [`ScopeToken`] is not a device scope
52    #[must_use]
53    pub fn from_scope_token(token: &ScopeToken) -> Option<Self> {
54        let stable = token.as_str().strip_prefix(STABLE_DEVICE_SCOPE_PREFIX);
55        let unstable = token.as_str().strip_prefix(UNSTABLE_DEVICE_SCOPE_PREFIX);
56        let id = stable.or(unstable)?;
57        Some(Device::from(id.to_owned()))
58    }
59
60    /// Generate a random device ID
61    pub fn generate<R: RngCore + ?Sized>(rng: &mut R) -> Self {
62        let id: String = Alphanumeric.sample_string(rng, GENERATED_DEVICE_ID_LENGTH);
63        Self { id }
64    }
65
66    /// Get the inner device ID as [`&str`]
67    #[must_use]
68    pub fn as_str(&self) -> &str {
69        &self.id
70    }
71}
72
73impl From<String> for Device {
74    fn from(id: String) -> Self {
75        Self { id }
76    }
77}
78
79impl From<Device> for String {
80    fn from(device: Device) -> Self {
81        device.id
82    }
83}
84
85impl std::fmt::Display for Device {
86    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87        f.write_str(&self.id)
88    }
89}
90
91#[cfg(test)]
92mod test {
93    use oauth2_types::scope::OPENID;
94
95    use crate::Device;
96
97    #[test]
98    fn test_device_id_to_from_scope_token() {
99        let device = Device::from("AABBCCDDEE".to_owned());
100        let [stable_scope_token, unstable_scope_token] = device.to_scope_token().unwrap();
101        assert_eq!(
102            stable_scope_token.as_str(),
103            "urn:matrix:client:device:AABBCCDDEE"
104        );
105        assert_eq!(
106            unstable_scope_token.as_str(),
107            "urn:matrix:org.matrix.msc2967.client:device:AABBCCDDEE"
108        );
109        assert_eq!(
110            Device::from_scope_token(&unstable_scope_token).as_ref(),
111            Some(&device)
112        );
113        assert_eq!(
114            Device::from_scope_token(&stable_scope_token).as_ref(),
115            Some(&device)
116        );
117        assert_eq!(Device::from_scope_token(&OPENID), None);
118    }
119}