aster_forge_cloud_files_macos_bridge/
enumeration.rs1use std::collections::HashSet;
4
5use aster_forge_cloud_files_core::{CloudItemKey, CloudItemPage, PageCursor};
6
7use crate::{
8 MacosBridgeError, MacosFileProviderIdentifier, MacosFileProviderItem,
9 MacosFileProviderSystemContainer, Result,
10};
11
12pub const MAX_ENUMERATION_PAGE_ITEMS: usize = 4_096;
14pub const MAX_ENUMERATION_ITEMS: usize = 100_000;
16pub const MAX_ENUMERATION_STATE_BYTES: usize = 16 * 1024 * 1024;
18
19#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct MacosEnumerationRequest {
22 container: MacosFileProviderIdentifier,
23 page: Option<PageCursor>,
24}
25
26impl MacosEnumerationRequest {
27 #[must_use]
29 pub const fn new(container: MacosFileProviderIdentifier, page: Option<PageCursor>) -> Self {
30 Self { container, page }
31 }
32
33 #[must_use]
35 pub const fn container(&self) -> &MacosFileProviderIdentifier {
36 &self.container
37 }
38
39 #[must_use]
41 pub const fn page(&self) -> Option<&PageCursor> {
42 self.page.as_ref()
43 }
44
45 pub fn backend_parent<'a>(&'a self, root_key: &'a CloudItemKey) -> Result<&'a CloudItemKey> {
51 match &self.container {
52 MacosFileProviderIdentifier::Item { key, .. } => Ok(key),
53 MacosFileProviderIdentifier::System(MacosFileProviderSystemContainer::Root) => {
54 Ok(root_key)
55 }
56 MacosFileProviderIdentifier::System(
57 MacosFileProviderSystemContainer::WorkingSet
58 | MacosFileProviderSystemContainer::Trash,
59 ) => Err(MacosBridgeError::UnsupportedSystemContainer),
60 }
61 }
62}
63
64#[derive(Debug, Clone, PartialEq, Eq)]
66pub struct MacosEnumerationPage {
67 items: Vec<MacosFileProviderItem>,
68 next_page: Option<PageCursor>,
69}
70
71#[derive(Debug, Clone)]
74pub struct MacosEnumerationState {
75 container: MacosFileProviderIdentifier,
76 next_page: Option<PageCursor>,
77 seen_identifiers: HashSet<String>,
78 seen_filenames: HashSet<String>,
79 seen_cursors: HashSet<PageCursor>,
80 retained_bytes: usize,
81 finished: bool,
82}
83
84impl MacosEnumerationState {
85 #[must_use]
87 pub fn new(container: MacosFileProviderIdentifier) -> Self {
88 Self {
89 container,
90 next_page: None,
91 seen_identifiers: HashSet::new(),
92 seen_filenames: HashSet::new(),
93 seen_cursors: HashSet::new(),
94 retained_bytes: 0,
95 finished: false,
96 }
97 }
98
99 pub fn request(&self) -> Result<MacosEnumerationRequest> {
105 if self.finished {
106 return Err(MacosBridgeError::InvalidBackendResponse {
107 reason: "enumeration requested another page after terminal completion",
108 });
109 }
110 Ok(MacosEnumerationRequest::new(
111 self.container.clone(),
112 self.next_page.clone(),
113 ))
114 }
115
116 pub fn accept_page(&mut self, page: MacosEnumerationPage) -> Result<MacosEnumerationPage> {
122 if self.finished {
123 return Err(MacosBridgeError::InvalidBackendResponse {
124 reason: "enumeration returned a page after terminal completion",
125 });
126 }
127 let new_item_count = self
128 .seen_identifiers
129 .len()
130 .checked_add(page.items().len())
131 .ok_or(MacosBridgeError::InvalidBackendResponse {
132 reason: "enumeration item count overflowed",
133 })?;
134 if new_item_count > MAX_ENUMERATION_ITEMS {
135 return Err(MacosBridgeError::InvalidBackendResponse {
136 reason: "enumeration exceeded the retained item limit",
137 });
138 }
139 let mut added_bytes = 0usize;
140 for item in page.items() {
141 if self.seen_identifiers.contains(item.identifier().as_str()) {
142 return Err(MacosBridgeError::InvalidBackendResponse {
143 reason: "enumeration repeated a persistent identifier across pages",
144 });
145 }
146 if self.seen_filenames.contains(item.filename()) {
147 return Err(MacosBridgeError::InvalidBackendResponse {
148 reason: "enumeration repeated a filename across pages",
149 });
150 }
151 added_bytes = added_bytes
152 .checked_add(item.identifier().as_str().len())
153 .and_then(|value| value.checked_add(item.filename().len()))
154 .ok_or(MacosBridgeError::InvalidBackendResponse {
155 reason: "enumeration retained byte count overflowed",
156 })?;
157 }
158 let next_page = match page.next_page() {
159 Some(cursor) => {
160 if self.seen_cursors.contains(cursor) {
161 return Err(MacosBridgeError::InvalidBackendResponse {
162 reason: "enumeration repeated a continuation cursor",
163 });
164 }
165 added_bytes = added_bytes.checked_add(cursor.as_bytes().len()).ok_or(
166 MacosBridgeError::InvalidBackendResponse {
167 reason: "enumeration retained byte count overflowed",
168 },
169 )?;
170 Some(cursor.clone())
171 }
172 None => None,
173 };
174 let retained_bytes = self.retained_bytes.checked_add(added_bytes).ok_or(
175 MacosBridgeError::InvalidBackendResponse {
176 reason: "enumeration retained byte count overflowed",
177 },
178 )?;
179 if retained_bytes > MAX_ENUMERATION_STATE_BYTES {
180 return Err(MacosBridgeError::InvalidBackendResponse {
181 reason: "enumeration exceeded the retained byte limit",
182 });
183 }
184 self.seen_identifiers.extend(
185 page.items()
186 .iter()
187 .map(|item| item.identifier().as_str().to_owned()),
188 );
189 self.seen_filenames
190 .extend(page.items().iter().map(|item| item.filename().to_owned()));
191 if let Some(cursor) = &next_page {
192 self.seen_cursors.insert(cursor.clone());
193 } else {
194 self.finished = true;
195 }
196 self.next_page = next_page;
197 self.retained_bytes = retained_bytes;
198 Ok(page)
199 }
200
201 #[must_use]
203 pub const fn is_finished(&self) -> bool {
204 self.finished
205 }
206}
207
208impl MacosEnumerationPage {
209 pub fn from_backend(
216 root_key: &CloudItemKey,
217 parent: &CloudItemKey,
218 page: CloudItemPage,
219 ) -> Result<Self> {
220 let (items, next_page) = page.into_parts();
221 if items.len() > MAX_ENUMERATION_PAGE_ITEMS {
222 return Err(MacosBridgeError::InvalidBackendResponse {
223 reason: "enumeration page exceeded the item limit",
224 });
225 }
226 let mut identifiers = HashSet::new();
227 let mut filenames = HashSet::new();
228 let mut converted = Vec::with_capacity(items.len());
229 for item in items {
230 if item.key().scope() != parent.scope() || item.parent_id() != Some(parent.item_id()) {
231 return Err(MacosBridgeError::InvalidBackendResponse {
232 reason: "enumerated item escaped the requested parent scope",
233 });
234 }
235 let converted_item = MacosFileProviderItem::from_cloud_item(&item, root_key)?;
236 if !identifiers.insert(converted_item.identifier().as_str().to_owned()) {
237 return Err(MacosBridgeError::InvalidBackendResponse {
238 reason: "enumeration returned duplicate persistent identifiers",
239 });
240 }
241 if !filenames.insert(converted_item.filename().to_owned()) {
242 return Err(MacosBridgeError::InvalidBackendResponse {
243 reason: "enumeration returned duplicate filenames",
244 });
245 }
246 converted.push(converted_item);
247 }
248 Ok(Self {
249 items: converted,
250 next_page,
251 })
252 }
253
254 #[must_use]
256 pub fn items(&self) -> &[MacosFileProviderItem] {
257 &self.items
258 }
259
260 #[must_use]
262 pub const fn next_page(&self) -> Option<&PageCursor> {
263 self.next_page.as_ref()
264 }
265
266 #[must_use]
268 pub fn into_parts(self) -> (Vec<MacosFileProviderItem>, Option<PageCursor>) {
269 (self.items, self.next_page)
270 }
271}