aster_forge_cloud_files_macos_bridge/
identifier.rs1use 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
12pub const FILE_PROVIDER_ROOT_CONTAINER_IDENTIFIER: &str =
14 "NSFileProviderRootContainerItemIdentifier";
15pub const FILE_PROVIDER_CURRENT_WORKING_SET_IDENTIFIER: &str =
17 "NSFileProviderWorkingSetContainerItemIdentifier";
18pub const FILE_PROVIDER_TRASH_CONTAINER_IDENTIFIER: &str =
20 "NSFileProviderTrashContainerItemIdentifier";
21
22const PREFIX: &str = "afcf1.";
23const FIELD_COUNT: usize = 3;
24pub const MAX_IDENTITY_FIELD_BYTES: usize = 1_024;
26pub const MAX_FILE_PROVIDER_IDENTIFIER_BYTES: usize = 4_128;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
31pub enum MacosFileProviderSystemContainer {
32 Root,
34 WorkingSet,
36 Trash,
38}
39
40#[derive(Clone, PartialEq, Eq, Hash)]
42pub enum MacosFileProviderIdentifier {
43 Item { encoded: String, key: CloudItemKey },
45 System(MacosFileProviderSystemContainer),
47}
48
49impl MacosFileProviderIdentifier {
50 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 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 #[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 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 #[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 #[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}