aster_forge_cloud_files_macos_bridge/
engine.rs

1//! Runtime-neutral read-only File Provider engine used by Swift bridge implementations.
2
3use std::{num::NonZeroU64, sync::Arc};
4
5use aster_forge_cloud_files_core::{
6    ByteRange, CloudFilesBackend, CloudItemKey, CloudItemKind, ContentReadRequest, ContentRevision,
7};
8use bytes::Bytes;
9
10use crate::{
11    MacosBridgeError, MacosEnumerationPage, MacosEnumerationRequest, MacosFileProviderIdentifier,
12    MacosFileProviderItem, Result,
13};
14
15/// Validated metadata required to stream one exact file revision into native staging storage.
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct MacosContentFetchPlan {
18    key: CloudItemKey,
19    revision: ContentRevision,
20    size: u64,
21}
22
23impl MacosContentFetchPlan {
24    /// Returns the exact scoped item being fetched.
25    #[must_use]
26    pub const fn key(&self) -> &CloudItemKey {
27        &self.key
28    }
29
30    /// Returns the exact revision being fetched.
31    #[must_use]
32    pub const fn revision(&self) -> &ContentRevision {
33        &self.revision
34    }
35
36    /// Returns the complete logical file size.
37    #[must_use]
38    pub const fn size(&self) -> u64 {
39        self.size
40    }
41}
42
43/// One validated bounded chunk from a [`MacosContentFetchPlan`].
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct MacosFetchedContentChunk {
46    offset: u64,
47    bytes: Bytes,
48}
49
50impl MacosFetchedContentChunk {
51    /// Returns the chunk's logical file offset.
52    pub const fn offset(&self) -> u64 {
53        self.offset
54    }
55
56    /// Returns the bounded chunk bytes.
57    pub const fn bytes(&self) -> &Bytes {
58        &self.bytes
59    }
60
61    /// Consumes the chunk into its offset and bytes.
62    pub fn into_parts(self) -> (u64, Bytes) {
63        (self.offset, self.bytes)
64    }
65}
66
67/// Product-neutral read-only engine for a single File Provider domain/root.
68pub struct MacosReadOnlyEngine<B> {
69    backend: Arc<B>,
70    root_key: CloudItemKey,
71}
72
73impl<B> Clone for MacosReadOnlyEngine<B> {
74    fn clone(&self) -> Self {
75        Self {
76            backend: self.backend.clone(),
77            root_key: self.root_key.clone(),
78        }
79    }
80}
81
82impl<B> MacosReadOnlyEngine<B>
83where
84    B: CloudFilesBackend + 'static,
85{
86    /// Creates an engine scoped to one exact product-neutral root item.
87    pub const fn new(backend: Arc<B>, root_key: CloudItemKey) -> Self {
88        Self { backend, root_key }
89    }
90
91    /// Returns the stable root item key mapped to Apple's root system container.
92    #[must_use]
93    pub const fn root_key(&self) -> &CloudItemKey {
94        &self.root_key
95    }
96
97    /// Loads an item by persistent identifier and validates its identity/root role.
98    /// # Errors
99    ///
100    /// Returns an error when validation fails or an underlying backend, store, or platform
101    /// operation fails.
102    pub async fn item(
103        &self,
104        identifier: &MacosFileProviderIdentifier,
105    ) -> Result<MacosFileProviderItem> {
106        let key = match identifier.system_container() {
107            Some(crate::MacosFileProviderSystemContainer::Root) => &self.root_key,
108            Some(
109                crate::MacosFileProviderSystemContainer::WorkingSet
110                | crate::MacosFileProviderSystemContainer::Trash,
111            ) => return Err(MacosBridgeError::UnsupportedSystemContainer),
112            None => identifier.item_key()?,
113        };
114        let item = self.backend.get_item(key).await?;
115        if item.key() != key {
116            return Err(MacosBridgeError::InvalidBackendResponse {
117                reason: "get_item returned a different scoped stable identity",
118            });
119        }
120        if item.is_root() != (key == &self.root_key) {
121            return Err(MacosBridgeError::InvalidBackendResponse {
122                reason: "get_item root shape did not match the File Provider root role",
123            });
124        }
125        MacosFileProviderItem::from_cloud_item(&item, &self.root_key)
126    }
127
128    /// Loads and validates one backend page for a File Provider enumerator.
129    /// # Errors
130    ///
131    /// Returns an error when validation fails or an underlying backend, store, or platform
132    /// operation fails.
133    pub async fn enumerate(
134        &self,
135        request: &MacosEnumerationRequest,
136    ) -> Result<MacosEnumerationPage> {
137        let parent = request.backend_parent(&self.root_key)?;
138        let parent_item = self.backend.get_item(parent).await?;
139        if parent_item.key() != parent {
140            return Err(MacosBridgeError::InvalidBackendResponse {
141                reason: "enumeration parent lookup returned a different stable identity",
142            });
143        }
144        if parent_item.kind() != CloudItemKind::Directory {
145            return Err(MacosBridgeError::InvalidBackendResponse {
146                reason: "enumeration parent is not a directory",
147            });
148        }
149        if parent_item.is_root() != (parent == &self.root_key) {
150            return Err(MacosBridgeError::InvalidBackendResponse {
151                reason: "enumeration parent root shape did not match the requested container",
152            });
153        }
154        let page = self.backend.list_children(parent, request.page()).await?;
155        MacosEnumerationPage::from_backend(&self.root_key, parent, page)
156    }
157
158    /// Validates metadata and revision before a caller creates native staging storage.
159    /// # Errors
160    ///
161    /// Returns an error when validation fails or an underlying backend, store, or platform
162    /// operation fails.
163    pub async fn prepare_content_fetch(
164        &self,
165        identifier: &MacosFileProviderIdentifier,
166        requested_revision: &ContentRevision,
167    ) -> Result<MacosContentFetchPlan> {
168        let key = identifier.item_key()?;
169        let item = self.backend.get_item(key).await?;
170        if item.key() != key {
171            return Err(MacosBridgeError::InvalidBackendResponse {
172                reason: "content metadata lookup returned a different stable identity",
173            });
174        }
175        if item.is_root() || item.kind() != CloudItemKind::File {
176            return Err(MacosBridgeError::InvalidBackendResponse {
177                reason: "content fetch target is not a regular file",
178            });
179        }
180        let content = item
181            .content()
182            .ok_or(MacosBridgeError::InvalidBackendResponse {
183                reason: "regular file omitted content metadata",
184            })?;
185        if content.revision() != requested_revision {
186            return Err(MacosBridgeError::Backend(
187                aster_forge_cloud_files_core::CloudBackendError::new(
188                    aster_forge_cloud_files_core::CloudBackendErrorKind::PreconditionFailed,
189                ),
190            ));
191        }
192        Ok(MacosContentFetchPlan {
193            key: key.clone(),
194            revision: requested_revision.clone(),
195            size: content.size(),
196        })
197    }
198
199    /// Reads one bounded chunk for a validated fetch plan.
200    /// # Errors
201    ///
202    /// Returns an error when validation fails or an underlying backend, store, or platform
203    /// operation fails.
204    pub async fn read_content_chunk(
205        &self,
206        plan: &MacosContentFetchPlan,
207        offset: u64,
208        maximum_length: NonZeroU64,
209    ) -> Result<MacosFetchedContentChunk> {
210        if offset >= plan.size {
211            return Err(MacosBridgeError::InvalidBackendResponse {
212                reason: "content chunk offset is outside the planned file",
213            });
214        }
215        let length = maximum_length.get().min(plan.size - offset);
216        let range = ByteRange::new(offset, length).map_err(|_| {
217            MacosBridgeError::InvalidBackendResponse {
218                reason: "content chunk range exceeded the planned file",
219            }
220        })?;
221        let request =
222            ContentReadRequest::range(plan.key.clone(), plan.revision.clone(), plan.size, range);
223        let response = self.backend.read_content(&request).await?;
224        request.validate_response(&response).map_err(|_| {
225            MacosBridgeError::InvalidBackendResponse {
226                reason: "content response violated the requested revision or chunk extent",
227            }
228        })?;
229        let (_, response_offset, bytes, _) = response.into_parts();
230        Ok(MacosFetchedContentChunk {
231            offset: response_offset,
232            bytes,
233        })
234    }
235}