Skip to main content

mas_data_model/compat/
device.rs

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