aster_forge_cloud_files_macos_bridge/
identifier.rs

1//! Versioned persistent identifiers for Apple File Provider items and system containers.
2
3use std::fmt;
4
5use aster_forge_cloud_files_core::{
6    CloudItemId, CloudItemKey, CloudNamespaceId, CloudRootId, CloudScope,
7};
8use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
9
10use crate::{MacosBridgeError, Result};
11
12/// Apple root-container identifier used by File Provider extensions.
13pub const FILE_PROVIDER_ROOT_CONTAINER_IDENTIFIER: &str =
14    "NSFileProviderRootContainerItemIdentifier";
15/// Apple working-set pseudo-container identifier.
16pub const FILE_PROVIDER_CURRENT_WORKING_SET_IDENTIFIER: &str =
17    "NSFileProviderWorkingSetContainerItemIdentifier";
18/// Apple trash pseudo-container identifier.
19pub const FILE_PROVIDER_TRASH_CONTAINER_IDENTIFIER: &str =
20    "NSFileProviderTrashContainerItemIdentifier";
21
22const PREFIX: &str = "afcf1.";
23const FIELD_COUNT: usize = 3;
24/// Maximum UTF-8 byte length accepted for one opaque identity field.
25pub const MAX_IDENTITY_FIELD_BYTES: usize = 1_024;
26/// Maximum UTF-8 byte length accepted for one encoded persistent identifier.
27pub const MAX_FILE_PROVIDER_IDENTIFIER_BYTES: usize = 4_128;
28
29/// File Provider system container that does not represent a product item.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
31pub enum MacosFileProviderSystemContainer {
32    /// Root container for the active domain.
33    Root,
34    /// Working-set pseudo-container maintained by the extension/system contract.
35    WorkingSet,
36    /// Trash pseudo-container.
37    Trash,
38}
39
40/// Owned, validated persistent File Provider identifier.
41#[derive(Clone, PartialEq, Eq, Hash)]
42pub enum MacosFileProviderIdentifier {
43    /// One exact scoped product item.
44    Item { encoded: String, key: CloudItemKey },
45    /// A File Provider system container.
46    System(MacosFileProviderSystemContainer),
47}
48
49impl MacosFileProviderIdentifier {
50    /// Encodes one stable path-independent item key as a File Provider identifier.
51    /// # Errors
52    ///
53    /// Returns an error when validation fails or an underlying backend, store, or platform
54    /// operation fails.
55    pub fn encode(key: &CloudItemKey) -> Result<Self> {
56        for field in [
57            key.scope().namespace_id().as_str(),
58            key.scope().root_id().as_str(),
59            key.item_id().as_str(),
60        ] {
61            if field.len() > MAX_IDENTITY_FIELD_BYTES {
62                return Err(invalid("identifier field exceeds the accepted byte length"));
63            }
64        }
65        let encoded = [
66            key.scope().namespace_id().as_str(),
67            key.scope().root_id().as_str(),
68            key.item_id().as_str(),
69        ]
70        .map(|value| URL_SAFE_NO_PAD.encode(value.as_bytes()))
71        .join(".");
72        let encoded = format!("{PREFIX}{encoded}");
73        if encoded.len() > MAX_FILE_PROVIDER_IDENTIFIER_BYTES {
74            return Err(invalid("identifier exceeds the accepted byte length"));
75        }
76        Ok(Self::Item {
77            encoded,
78            key: key.clone(),
79        })
80    }
81
82    /// Parses an identifier received from Swift/File Provider.
83    /// # Errors
84    ///
85    /// Returns an error when validation fails or an underlying backend, store, or platform
86    /// operation fails.
87    pub fn parse(value: impl Into<String>) -> Result<Self> {
88        let value = value.into();
89        if value.len() > MAX_FILE_PROVIDER_IDENTIFIER_BYTES {
90            return Err(invalid("identifier exceeds the accepted byte length"));
91        }
92        match value.as_str() {
93            FILE_PROVIDER_ROOT_CONTAINER_IDENTIFIER => {
94                return Ok(Self::System(MacosFileProviderSystemContainer::Root));
95            }
96            FILE_PROVIDER_CURRENT_WORKING_SET_IDENTIFIER => {
97                return Ok(Self::System(MacosFileProviderSystemContainer::WorkingSet));
98            }
99            FILE_PROVIDER_TRASH_CONTAINER_IDENTIFIER => {
100                return Ok(Self::System(MacosFileProviderSystemContainer::Trash));
101            }
102            _ => {}
103        }
104        let payload = value
105            .strip_prefix(PREFIX)
106            .ok_or_else(|| invalid("identifier prefix or version is unsupported"))?;
107        let fields = payload.split('.').collect::<Vec<_>>();
108        if fields.len() != FIELD_COUNT {
109            return Err(invalid(
110                "identifier must contain exactly three encoded fields",
111            ));
112        }
113        let mut decoded = Vec::with_capacity(FIELD_COUNT);
114        for field in fields {
115            if field.is_empty() {
116                return Err(invalid("identifier field must not be empty"));
117            }
118            let bytes = URL_SAFE_NO_PAD
119                .decode(field)
120                .map_err(|_| invalid("identifier field is not canonical base64url"))?;
121            if bytes.len() > MAX_IDENTITY_FIELD_BYTES {
122                return Err(invalid("identifier field exceeds the accepted byte length"));
123            }
124            let text = String::from_utf8(bytes)
125                .map_err(|_| invalid("identifier field is not valid UTF-8"))?;
126            if URL_SAFE_NO_PAD.encode(text.as_bytes()) != field {
127                return Err(invalid("identifier field is not canonical base64url"));
128            }
129            decoded.push(text);
130        }
131        let mut decoded = decoded.into_iter();
132        let namespace = decoded
133            .next()
134            .ok_or_else(|| invalid("identifier namespace is missing"))?;
135        let root = decoded
136            .next()
137            .ok_or_else(|| invalid("identifier root is missing"))?;
138        let item = decoded
139            .next()
140            .ok_or_else(|| invalid("identifier item is missing"))?;
141        let namespace = CloudNamespaceId::new(namespace)
142            .map_err(|_| invalid("identifier namespace must not be empty"))?;
143        let root =
144            CloudRootId::new(root).map_err(|_| invalid("identifier root must not be empty"))?;
145        let item =
146            CloudItemId::new(item).map_err(|_| invalid("identifier item must not be empty"))?;
147        let key = CloudItemKey::new(CloudScope::new(namespace, root), item);
148        Ok(Self::Item {
149            encoded: value,
150            key,
151        })
152    }
153
154    /// Returns the exact identifier string supplied to File Provider.
155    #[must_use]
156    pub fn as_str(&self) -> &str {
157        match self {
158            Self::Item { encoded, .. } => encoded,
159            Self::System(MacosFileProviderSystemContainer::Root) => {
160                FILE_PROVIDER_ROOT_CONTAINER_IDENTIFIER
161            }
162            Self::System(MacosFileProviderSystemContainer::WorkingSet) => {
163                FILE_PROVIDER_CURRENT_WORKING_SET_IDENTIFIER
164            }
165            Self::System(MacosFileProviderSystemContainer::Trash) => {
166                FILE_PROVIDER_TRASH_CONTAINER_IDENTIFIER
167            }
168        }
169    }
170
171    /// Returns the decoded product item key, or a classified system-container error.
172    /// # Errors
173    ///
174    /// Returns an error when validation fails or an underlying backend, store, or platform
175    /// operation fails.
176    pub fn item_key(&self) -> Result<&CloudItemKey> {
177        match self {
178            Self::Item { key, .. } => Ok(key),
179            Self::System(_) => Err(MacosBridgeError::SystemContainerIsNotItem),
180        }
181    }
182
183    /// Returns the system-container class, when this is not a product item.
184    #[must_use]
185    pub const fn system_container(&self) -> Option<MacosFileProviderSystemContainer> {
186        match self {
187            Self::Item { .. } => None,
188            Self::System(container) => Some(*container),
189        }
190    }
191
192    /// Consumes the identifier into its exact string representation.
193    #[must_use]
194    pub fn into_string(self) -> String {
195        match self {
196            Self::Item { encoded, .. } => encoded,
197            Self::System(container) => match container {
198                MacosFileProviderSystemContainer::Root => {
199                    FILE_PROVIDER_ROOT_CONTAINER_IDENTIFIER.to_owned()
200                }
201                MacosFileProviderSystemContainer::WorkingSet => {
202                    FILE_PROVIDER_CURRENT_WORKING_SET_IDENTIFIER.to_owned()
203                }
204                MacosFileProviderSystemContainer::Trash => {
205                    FILE_PROVIDER_TRASH_CONTAINER_IDENTIFIER.to_owned()
206                }
207            },
208        }
209    }
210}
211
212impl fmt::Debug for MacosFileProviderIdentifier {
213    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
214        match self {
215            Self::Item { encoded, .. } => formatter
216                .debug_struct("MacosFileProviderIdentifier")
217                .field("kind", &"item")
218                .field("byte_len", &encoded.len())
219                .finish(),
220            Self::System(container) => formatter
221                .debug_tuple("MacosFileProviderIdentifier")
222                .field(container)
223                .finish(),
224        }
225    }
226}
227
228const fn invalid(reason: &'static str) -> MacosBridgeError {
229    MacosBridgeError::InvalidIdentifier { reason }
230}