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 DEVICE_SCOPE_PREFIX: &str = "urn:matrix:org.matrix.msc2967.client:device:";
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(transparent)]
20pub struct Device {
21    id: String,
22}
23
24#[derive(Debug, Error)]
25pub enum ToScopeTokenError {
26    #[error("Device ID contains characters that can't be encoded in a scope")]
27    InvalidCharacters,
28}
29
30impl Device {
31    /// Get the corresponding [`ScopeToken`] for that device
32    ///
33    /// # Errors
34    ///
35    /// Returns an error if the device ID contains characters that can't be
36    /// encoded in a scope
37    pub fn to_scope_token(&self) -> Result<ScopeToken, ToScopeTokenError> {
38        format!("{DEVICE_SCOPE_PREFIX}{}", self.id)
39            .parse()
40            .map_err(|_| ToScopeTokenError::InvalidCharacters)
41    }
42
43    /// Get the corresponding [`Device`] from a [`ScopeToken`]
44    ///
45    /// Returns `None` if the [`ScopeToken`] is not a device scope
46    #[must_use]
47    pub fn from_scope_token(token: &ScopeToken) -> Option<Self> {
48        let id = token.as_str().strip_prefix(DEVICE_SCOPE_PREFIX)?;
49        Some(Device::from(id.to_owned()))
50    }
51
52    /// Generate a random device ID
53    pub fn generate<R: RngCore + ?Sized>(rng: &mut R) -> Self {
54        let id: String = Alphanumeric.sample_string(rng, GENERATED_DEVICE_ID_LENGTH);
55        Self { id }
56    }
57
58    /// Get the inner device ID as [`&str`]
59    #[must_use]
60    pub fn as_str(&self) -> &str {
61        &self.id
62    }
63}
64
65impl From<String> for Device {
66    fn from(id: String) -> Self {
67        Self { id }
68    }
69}
70
71impl From<Device> for String {
72    fn from(device: Device) -> Self {
73        device.id
74    }
75}
76
77impl std::fmt::Display for Device {
78    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79        f.write_str(&self.id)
80    }
81}
82
83#[cfg(test)]
84mod test {
85    use oauth2_types::scope::OPENID;
86
87    use crate::Device;
88
89    #[test]
90    fn test_device_id_to_from_scope_token() {
91        let device = Device::from("AABBCCDDEE".to_owned());
92        let scope_token = device.to_scope_token().unwrap();
93        assert_eq!(
94            scope_token.as_str(),
95            "urn:matrix:org.matrix.msc2967.client:device:AABBCCDDEE"
96        );
97        assert_eq!(Device::from_scope_token(&scope_token), Some(device));
98        assert_eq!(Device::from_scope_token(&OPENID), None);
99    }
100}