1use std::io::SeekFrom;
3use std::path::Path;
4use vfsi_core::api::*;
5fn vector_index(error: vfsi_core::api::Error, index: usize) -> vfsi_core::api::Error {
6 if error.index().is_some() {
7 error.with_index(index)
8 } else {
9 error
10 }
11}
12struct VectorBudget {
13 entries: usize,
14 bytes: usize,
15}
16impl VectorBudget {
17 fn new(entries: usize, bytes: usize) -> Self {
18 Self { entries, bytes }
19 }
20 fn charge(&mut self, path: &Path) -> Result<()> {
21 if self.entries == 0 {
22 return Err(vfsi_core::api::Error::client(0, libc::EFBIG as u32)
23 .with_context("vector traversal", path));
24 }
25 self.charge_path(path)?;
26 self.entries -= 1;
27 Ok(())
28 }
29 fn charge_path(&mut self, path: &Path) -> Result<()> {
30 let bytes = path.as_os_str().len();
31 if bytes > self.bytes {
32 return Err(vfsi_core::api::Error::client(0, libc::EFBIG as u32)
33 .with_context("vector traversal", path));
34 }
35 self.bytes -= bytes;
36 Ok(())
37 }
38}
39pub fn visit_directory_pages<P: AsRef<Path>>(
40 roots: &[P],
41 policy: vfsi_core::api::ListDirOptions,
42 limits: ResourceLimits,
43 mut capacity: impl FnMut(&[&Path]) -> Result<usize>,
44 mut validate_root: impl FnMut(&Path) -> Result<()>,
45 mut fetch: impl FnMut(
46 &[&Path],
47 Vec<Option<crate::DirPageCursor>>,
48 usize,
49 usize,
50 ) -> Result<Vec<crate::DirectoryPage>>,
51 mut callback: impl FnMut(usize, DirectoryListing) -> Result<std::ops::ControlFlow<()>>,
52) -> Result<Vec<crate::TraversalCompletion>> {
53 let directory = policy.directory_options(limits);
54 let walk = policy.walk_options(limits);
55 let mut budget = VectorBudget::new(directory.entry_limit(), directory.path_byte_limit());
56 let mut completed = Vec::new();
57 let mut pending = std::collections::VecDeque::new();
58 for (index, root) in roots.iter().enumerate() {
59 pending.push_back((index, root.as_ref().to_path_buf(), 0usize, None));
60 }
61 while !pending.is_empty() {
62 let owner = pending.front().unwrap().0;
63 if policy.is_recursive() && pending.front().unwrap().2 == 0 {
64 validate_root(&pending.front().unwrap().1)
65 .map_err(|error| vector_index(error, owner))?;
66 }
67 let candidate_paths: Vec<_> = pending
69 .iter()
70 .take_while(|state| !policy.is_recursive() || state.0 == owner)
71 .take(32)
72 .map(|state| state.1.as_path())
73 .collect();
74 let capacity = capacity(&candidate_paths)?.clamp(1, 32);
75 let cohort_size = if policy.is_recursive() {
76 pending
77 .iter()
78 .take(capacity)
79 .take_while(|state| state.0 == owner)
80 .count()
81 } else {
82 pending.len().min(capacity)
83 };
84 let mut cohort: Vec<_> = (0..cohort_size)
85 .map(|_| pending.pop_front().unwrap())
86 .collect();
87 let cursors = cohort.iter_mut().map(|state| state.3.take()).collect();
88 let paths: Vec<_> = cohort
89 .iter()
90 .map(|(_, path, _, _)| path.as_path())
91 .collect();
92 let requested = budget.entries.saturating_add(1);
93 let mut deferred_error = None;
94 let pages = match fetch(&paths, cursors, 1, requested) {
95 Ok(pages) => pages,
96 Err(error)
97 if !policy.is_recursive()
98 && !error.is_transport()
99 && error.index().is_some_and(|i| i > 0 && i < cohort.len()) =>
100 {
101 let failed = error.index().unwrap();
105 deferred_error = Some(vector_index(error, cohort[failed].0));
106 let prefix = fetch(
107 &paths[..failed],
108 (0..failed).map(|_| None).collect(),
109 1,
110 requested,
111 )
112 .map_err(|error| error.map_index(|i| cohort.get(i).map_or(i, |state| state.0)))?;
113 cohort.truncate(failed);
114 prefix
115 }
116 Err(error) => return Err(error.map_index(|i| cohort.get(i).map_or(i, |state| state.0))),
117 };
118 if pages.len() != cohort.len() {
119 return Err(vfsi_core::api::Error::transport(
120 None,
121 "invalid directory page result count",
122 ));
123 }
124 let mut children = Vec::new();
125 let mut wave: Vec<_> = cohort
126 .into_iter()
127 .map(|(index, path, depth, _)| (index, path, depth, true))
128 .zip(pages)
129 .collect();
130 while !wave.is_empty() {
131 let mut continuations = Vec::new();
132 for ((index, path, depth, first), (mut page, next, seeds)) in wave {
133 if first && policy.is_recursive() {
134 budget
135 .charge_path(&path)
136 .map_err(|error| vector_index(error, index))?;
137 }
138 if page.path != path || (page.entries.is_empty() && next.is_some()) {
139 return Err(vfsi_core::api::Error::transport(
140 Some(index),
141 "invalid directory page parent or progress",
142 ));
143 }
144 let mut seeds: std::collections::HashMap<_, _> = seeds.into_iter().collect();
145 let mut page_error = None;
146 let mut accepted = 0;
147 for entry in &page.entries {
148 if let Err(error) = budget.charge(entry.path()) {
149 page_error = Some(vector_index(error, index));
150 break;
151 }
152 accepted += 1;
153 if policy.is_recursive() && entry.attrs().is_dir() {
154 if depth >= walk.depth_limit() {
155 if !walk.truncates_at_depth_limit() {
156 page_error = Some(
157 vfsi_core::api::Error::client(index, libc::EFBIG as u32)
158 .with_context("visit_dirs", entry.path()),
159 );
160 break;
161 }
162 } else {
163 children.push((
164 index,
165 entry.path().to_path_buf(),
166 depth + 1,
167 seeds.remove(entry.path()),
168 ));
169 }
170 }
171 }
172 if let Some(error) = &page_error {
173 if accepted == 0 {
174 return Err(error.clone());
175 }
176 page.entries.truncate(accepted);
177 }
178 if !policy.is_recursive() && completed.len() <= index {
179 completed.resize(index + 1, crate::TraversalCompletion::Stopped);
180 }
181 if callback(index, page)
182 .map_err(|error| vector_index(error, index))?
183 .is_break()
184 {
185 if policy.is_recursive() {
186 completed.truncate(index);
187 completed.push(crate::TraversalCompletion::Stopped);
188 } else {
189 completed[index] = crate::TraversalCompletion::Stopped;
190 }
191 return Ok(completed);
192 }
193 if let Some(error) = page_error {
194 return Err(error);
195 }
196 match next {
197 Some(cursor) => continuations.push(((index, path, depth, false), Some(cursor))),
198 None if !policy.is_recursive() => {
199 completed[index] = crate::TraversalCompletion::Complete
200 }
201 None => {}
202 }
203 }
204 if continuations.is_empty() {
205 break;
206 }
207 let cursors = continuations
208 .iter_mut()
209 .map(|state| state.1.take())
210 .collect();
211 let paths: Vec<_> = continuations
212 .iter()
213 .map(|state| state.0.1.as_path())
214 .collect();
215 let requested = budget.entries.saturating_add(1);
216 let pages = fetch(&paths, cursors, requested.min(128), requested).map_err(|error| {
217 error.map_index(|i| continuations.get(i).map_or(i, |state| state.0.0))
218 })?;
219 if pages.len() != continuations.len() {
220 return Err(vfsi_core::api::Error::transport(
221 None,
222 "invalid directory continuation count",
223 ));
224 }
225 wave = continuations
226 .into_iter()
227 .map(|state| state.0)
228 .zip(pages)
229 .collect();
230 }
231 if let Some(error) = deferred_error {
232 return Err(error);
233 }
234 for child in children.into_iter().rev() {
237 pending.push_front(child);
238 }
239 if policy.is_recursive() && pending.front().is_none_or(|state| state.0 != owner) {
240 completed.push(crate::TraversalCompletion::Complete);
241 }
242 }
243 Ok(completed)
244}
245
246macro_rules! file_methods {
247 ($file:ty) => {
248 fn path(&self) -> &Path {
249 <$file>::path(self)
250 }
251 fn attrs(&self) -> Result<Attrs> {
252 <$file>::attrs(self)
253 }
254 fn read_request_at(&self, offset: u64, length: usize) -> Self::ReadRequest<'_> {
255 <$file>::read_request_at(self, offset, length)
256 }
257 fn read_request_at_into<'a>(
258 &'a self,
259 offset: u64,
260 buffer: &'a mut [u8],
261 ) -> Self::ReadIntoRequest<'a> {
262 <$file>::read_request_at_into(self, offset, buffer)
263 }
264 fn read_at(&self, buffer: &mut [u8], offset: u64) -> Result<usize> {
265 <$file>::read_at(self, buffer, offset)
266 }
267 fn write_at(&self, buffer: &[u8], offset: u64) -> Result<usize> {
268 <$file>::write_at(self, buffer, offset)
269 }
270 fn read_native(&mut self, buffer: &mut [u8]) -> Result<usize> {
271 <$file>::read_native(self, buffer)
272 }
273 fn read_to_end_with_limit(&mut self, max_bytes: usize) -> Result<Vec<u8>> {
274 <$file>::read_to_end_with_limit(self, max_bytes)
275 }
276 fn write_native(&mut self, buffer: &[u8]) -> Result<usize> {
277 <$file>::write_native(self, buffer)
278 }
279 fn seek_native(&mut self, position: SeekFrom) -> Result<u64> {
280 <$file>::seek_native(self, position)
281 }
282 fn sync_data(&self) -> Result<()> {
283 <$file>::sync_data(self)
284 }
285 fn sync_all(&self) -> Result<()> {
286 <$file>::sync_all(self)
287 }
288 fn try_close(&mut self) -> Result<()> {
289 <$file>::try_close(self)
290 }
291 fn is_closed(&self) -> bool {
292 <$file>::is_closed(self)
293 }
294 fn close(self) -> Result<()> {
295 <$file>::close(self)
296 }
297 };
298}
299
300impl<F: crate::FileSystem> FileHandle for crate::FsFile<F> {
301 type ReadRequest<'a>
302 = crate::FsRead<'a, F>
303 where
304 Self: 'a;
305 type ReadIntoRequest<'a>
306 = crate::FsReadInto<'a, F>
307 where
308 Self: 'a;
309 file_methods!(crate::FsFile<F>);
310}
311
312pub(crate) trait NativeHooks: Vfsi {
313 fn page_capacity(&self, paths: &[&Path]) -> Result<usize>;
314 fn open_native(&self, request: OpenOp) -> Result<Self::File>;
315 fn stream_native(
316 &self,
317 path: impl AsRef<Path>,
318 options: crate::StreamOptions,
319 callback: impl FnMut(u64, &[u8]) -> Result<bool>,
320 ) -> Result<crate::StreamCompletion>;
321}
322
323macro_rules! client_methods {
324 ($client:ty, $receiver:path) => {
325 client_methods!($client, $receiver, <$client>::vread);
326 };
327 ($client:ty, $receiver:path, $vread_native:expr) => {
328 client_methods!($client, $receiver, $vread_native, $receiver);
329 };
330 ($client:ty, $receiver:path, $vread_native:expr, $read_receiver:path) => {
331 client_methods!(
332 $client,
333 $receiver,
334 $vread_native,
335 $read_receiver,
336 <$client>::write_partial_native,
337 <$client>::write_complete
338 );
339 };
340 ($client:ty, $receiver:path, $vread_native:expr, $read_receiver:path, $vwrite_native:expr, $vwrite_all_native:expr) => {
341 client_methods!(
342 $client,
343 $receiver,
344 $vread_native,
345 $read_receiver,
346 $vwrite_native,
347 $vwrite_all_native,
348 <$client>::vgetattrs
349 );
350 };
351 ($client:ty, $receiver:path, $vread_native:expr, $read_receiver:path, $vwrite_native:expr, $vwrite_all_native:expr, $metadata:expr) => {
352 client_methods!(
353 $client,
354 $receiver,
355 $vread_native,
356 $read_receiver,
357 $vwrite_native,
358 $vwrite_all_native,
359 $metadata,
360 $receiver
361 );
362 };
363 ($client:ty, $receiver:path, $vread_native:expr, $read_receiver:path, $vwrite_native:expr, $vwrite_all_native:expr, $metadata:expr, $write_receiver:path) => {
364 client_methods!(
365 $client,
366 $receiver,
367 $vread_native,
368 $read_receiver,
369 $vwrite_native,
370 $vwrite_all_native,
371 $metadata,
372 $write_receiver,
373 vrename,
374 vcopy,
375 vclose,
376 vopen
377 );
378 };
379 ($client:ty, $receiver:path, $vread_native:expr, $read_receiver:path, $vwrite_native:expr, $vwrite_all_native:expr, $metadata:expr, $write_receiver:path, $rename:ident, $copy:ident, $close:ident, $open_batch:ident) => {
381 fn vrename<P: AsRef<Path>, Q: AsRef<Path>>(
382 &self,
383 pairs: &[(P, Q)],
384 options: vfsi_core::api::RenameOptions,
385 ) -> Result<()> {
386 <$client>::vrename($receiver(self), pairs, options)
387 }
388 fn vlistdirs<P: AsRef<Path>>(
389 &self,
390 paths: &[P],
391 options: vfsi_core::api::ListDirOptions,
392 callback: impl FnMut(usize, DirectoryListing) -> Result<std::ops::ControlFlow<()>>,
393 ) -> Result<Vec<crate::TraversalCompletion>> {
394 visit_directory_pages(
395 paths,
396 options,
397 self.limits(),
398 |paths| <$client as NativeHooks>::page_capacity($receiver(self), paths),
399 |path| {
400 let metadata = self.vgetattrs(
401 &[path],
402 vfsi_core::api::AttrsOptions::new()
403 .fields(vfsi_core::api::Attributes::MODE)
404 .follow_symlinks(false),
405 )?;
406 if metadata.len() != 1 {
407 return Err(vfsi_core::api::Error::transport(
408 None,
409 "invalid directory root metadata count",
410 ));
411 }
412 if !metadata[0].is_dir() {
413 return Err(vfsi_core::api::Error::client(0, libc::ENOTDIR as u32)
414 .with_context("visit_dirs", path));
415 }
416 Ok(())
417 },
418 |paths, cursors, page_size, max_entries| {
419 <$client>::read_dir_pages_with_fields(
420 $receiver(self),
421 paths,
422 options.attributes(),
423 cursors,
424 page_size,
425 max_entries,
426 )
427 },
428 callback,
429 )
430 }
431 fn vstream<P: AsRef<Path>>(
432 &self,
433 paths: &[P],
434 options: crate::StreamOptions,
435 mut callback: impl FnMut(usize, u64, &[u8]) -> Result<bool>,
436 ) -> Result<Vec<crate::StreamCompletion>> {
437 let mut output = Vec::new();
438 for (index, path) in paths.iter().enumerate() {
439 let completion = <$client as NativeHooks>::stream_native(
440 $receiver(self),
441 path,
442 options,
443 |offset, data| callback(index, offset, data),
444 )
445 .map_err(|error| vector_index(error, index))?;
446 output.push(completion);
447 if matches!(completion, crate::StreamCompletion::Stopped { .. }) {
448 break;
449 }
450 }
451 Ok(output)
452 }
453 fn capabilities(&self) -> Result<vfsi_core::Capabilities> {
454 <$client>::capabilities($receiver(self))
455 }
456 fn vsymlink<P: AsRef<Path>, Q: AsRef<Path>>(&self, pairs: &[(P, Q)]) -> Result<()> {
457 <$client>::vsymlink($receiver(self), pairs)
458 }
459 fn vreadlink<P: AsRef<Path>>(&self, paths: &[P]) -> Result<Vec<std::path::PathBuf>> {
460 <$client>::vreadlink($receiver(self), paths)
461 }
462 fn vhardlink<P: AsRef<Path>, Q: AsRef<Path>>(&self, pairs: &[(P, Q)]) -> Result<()> {
463 <$client>::vhardlink($receiver(self), pairs)
464 }
465 fn vstatfs<P: vfsi_core::MetadataOperand<Self::File>>(
466 &self,
467 targets: &[P],
468 ) -> Result<Vec<vfsi_core::FilesystemStats>> {
469 <$client>::vstatfs($receiver(self), targets)
470 }
471 fn vsetattrs<P: vfsi_core::MetadataOperand<Self::File>>(
472 &self,
473 updates: &[(P, vfsi_core::MetadataUpdate)],
474 follow_symlinks: bool,
475 ) -> Result<()> {
476 <$client>::vsetattrs($receiver(self), updates, follow_symlinks)
477 }
478 fn vgetattrs<P: AsRef<Path>>(
479 &self,
480 paths: &[P],
481 options: vfsi_core::api::AttrsOptions,
482 ) -> Result<Vec<Attrs>> {
483 ($metadata)($receiver(self), paths, options)
484 }
485 fn limits(&self) -> ResourceLimits {
486 <$client>::limits($receiver(self))
487 }
488
489 fn vopen(&self, requests: &[OpenOp]) -> Result<Vec<Self::File>> {
490 if requests.len() == 1 {
491 return <$client as NativeHooks>::open_native($receiver(self), requests[0].clone())
493 .map(|file| vec![file]);
494 }
495 <$client>::$open_batch($receiver(self), requests)
496 }
497 fn vread<'a>(
498 &self,
499 ops: impl IntoIterator<Item = vfsi_core::api::ReadOp<'a, Self::File>>,
500 options: vfsi_core::api::ReadOptions,
501 ) -> Result<Vec<vfsi_core::api::ReadResult>> {
502 ($vread_native)($read_receiver(self), ops, options)
503 }
504 fn vwrite<'a>(
505 &self,
506 requests: &[vfsi_core::api::WriteOp<'a, Self::File>],
507 options: vfsi_core::api::WriteOptions,
508 ) -> Result<Vec<WriteResult>> {
509 let result = if options.writes_all() {
510 ($vwrite_all_native)($write_receiver(self), requests)
511 } else {
512 ($vwrite_native)($write_receiver(self), requests)
513 };
514 result.map_err(public_write_error)
515 }
516 fn vclose(&self, files: &mut [Self::File]) -> Result<()> {
517 <$client>::$close($receiver(self), files)
518 }
519 fn vmkdir<P: AsRef<Path>>(&self, paths: &[(P, u32)]) -> Result<()> {
520 <$client>::vmkdir($receiver(self), paths)
521 }
522
523 fn vcopy<P: AsRef<Path>, Q: AsRef<Path>>(
524 &self,
525 pairs: &[(P, Q)],
526 options: vfsi_core::api::CopyOption,
527 ) -> Result<()> {
528 <$client>::$copy($receiver(self), pairs, options)
529 }
530 fn vremove<P: AsRef<Path>>(
531 &self,
532 paths: &[P],
533 mode: vfsi_core::api::RemoveMode,
534 options: crate::RemoveOptions,
535 ) -> Result<()> {
536 match mode {
537 vfsi_core::api::RemoveMode::Entry | vfsi_core::api::RemoveMode::Tree => {
538 <$client>::vremove_with_options_native(
539 $receiver(self),
540 paths,
541 mode == vfsi_core::api::RemoveMode::Tree,
542 options,
543 )
544 }
545 vfsi_core::api::RemoveMode::Contents => {
546 let mut first_error = None;
547 for (index, path) in paths.iter().enumerate() {
548 if let Err(error) = <$client>::remove_dir_contents_with_options(
549 $receiver(self),
550 path,
551 options,
552 ) {
553 let error = vector_index(error, index);
554 if error.is_transport() || !options.continues_on_error() {
555 return Err(error);
556 }
557 first_error.get_or_insert(error);
558 }
559 }
560 first_error.map_or(Ok(()), Err)
561 }
562 }
563 }
564 };
565}
566
567impl<F: crate::Backend + 'static> Vfsi for crate::FsClient<F> {
568 type File = crate::FsFile<F>;
569 client_methods!(
570 crate::FsClient<F>,
571 std::convert::identity,
572 read_backend::<F>,
573 std::convert::identity,
574 write_backend::<F>,
575 write_backend_all::<F>,
576 metadata_backend::<F, _>,
577 std::convert::identity,
578 vrename,
579 vcopy,
580 vclose,
581 vopen
582 );
583}
584
585impl<F: crate::Backend + 'static> NativeHooks for crate::FsClient<F> {
586 fn open_native(&self, request: OpenOp) -> Result<Self::File> {
587 self.open_with_native(request)
588 }
589 fn page_capacity(&self, _paths: &[&Path]) -> Result<usize> {
590 self.directory_page_batch_size()
591 }
592 fn stream_native(
593 &self,
594 path: impl AsRef<Path>,
595 options: crate::StreamOptions,
596 callback: impl FnMut(u64, &[u8]) -> Result<bool>,
597 ) -> Result<crate::StreamCompletion> {
598 self.read_stream_with_options(path, options, callback)
599 }
600}
601
602pub(crate) fn read_backend_owned<F: crate::Backend + 'static>(
603 client: &crate::FsClient<F>,
604 requests: &[ReadRequest<'_, crate::FsRead<'_, F>>],
605 budget: usize,
606) -> Result<Vec<OwnedReadResult>> {
607 if requests.iter().all(|request| request.range_ref().is_some()) {
608 return client.vread_with_limit_projected_native(requests, budget, |request| {
609 request.range_ref().expect("checked ranges")
610 });
611 }
612 read_batch(
613 requests,
614 budget,
615 |ranges, bytes| client.vread_with_limit_projected_native(ranges, bytes, |request| request),
616 |paths, bytes| {
617 client.read_files_native(paths, crate::ReadAllOptions::new().max_total_bytes(bytes))
618 },
619 )
620}
621pub(crate) fn read_backend<'a, F: crate::Backend + 'static>(
622 client: &crate::FsClient<F>,
623 ops: impl IntoIterator<Item = ReadOp<'a, crate::FsFile<F>>>,
624 options: ReadOptions,
625) -> Result<Vec<ReadResult>> {
626 consume_ops(
627 ops,
628 options.limit_or(client.limits().max_read_bytes),
629 |requests, options| read_backend_owned(client, requests, options),
630 |requests, bytes| client.vread_into_with_limit_native(requests, bytes),
631 )
632}
633
634pub(crate) fn public_write_error(error: vfsi_core::api::Error) -> vfsi_core::api::Error {
637 if error.operation() == Some("vwrite_all_native")
638 && let Some(path) = error.path().map(std::path::Path::to_path_buf)
639 {
640 error.with_context("vwrite_native", path)
641 } else {
642 error
643 }
644}
645
646pub(crate) fn write_backend<'a, F>(
647 client: &crate::FsClient<F>,
648 ops: &[WriteOp<'a, crate::FsFile<F>>],
649) -> vfsi_core::api::Result<Vec<vfsi_core::api::WriteResult>>
650where
651 F: crate::Backend + 'static,
652{
653 client.vwrite_mapped_native(ops, |op| op.file().write_request_at(op.offset(), op.data()))
654}
655
656pub(crate) fn write_backend_all<'a, F>(
657 client: &crate::FsClient<F>,
658 ops: &[WriteOp<'a, crate::FsFile<F>>],
659) -> vfsi_core::api::Result<Vec<vfsi_core::api::WriteResult>>
660where
661 F: crate::Backend + 'static,
662{
663 client.vwrite_all_mapped_native(ops, |op| op.file().write_request_at(op.offset(), op.data()))
664}
665
666pub(crate) fn metadata_backend<F, P: AsRef<std::path::Path>>(
667 client: &crate::FsClient<F>,
668 paths: &[P],
669 options: AttrsOptions,
670) -> vfsi_core::api::Result<Vec<crate::Attrs>>
671where
672 F: crate::Backend + 'static,
673{
674 client.vgetattrs_native(
675 paths,
676 options.requested_attributes(),
677 options.follows_symlinks(),
678 )
679}
680
681use vfsi_core::api::internal::{OwnedReadResult, ReadRequest, consume_ops, read_batch};
682#[cfg(test)]
683mod traversal_tests {
684 use super::*;
685 use crate::{AttrMask, DirEntry, DirPageCursor, VfAttrs, VfFile, VfType};
686 use std::sync::{
687 Arc,
688 atomic::{AtomicUsize, Ordering},
689 };
690
691 struct Snapshot {
694 remaining: std::vec::IntoIter<DirEntry>,
695 live: Arc<AtomicUsize>,
696 }
697
698 impl Drop for Snapshot {
699 fn drop(&mut self) {
700 self.live.fetch_sub(1, Ordering::SeqCst);
701 }
702 }
703
704 #[allow(clippy::type_complexity)] fn snapshot_pages(
706 live: Arc<AtomicUsize>,
707 peak: Arc<AtomicUsize>,
708 ) -> impl FnMut(
709 &[&Path],
710 Vec<Option<DirPageCursor>>,
711 usize,
712 usize,
713 ) -> Result<Vec<crate::DirectoryPage>> {
714 move |paths, cursors, page_size, max_entries| {
715 let mut pages = Vec::with_capacity(paths.len());
716 for (path, cursor) in paths.iter().zip(cursors) {
717 let mut snapshot = match cursor {
718 Some(cursor) => cursor.into_state::<Snapshot>()?,
719 None => {
720 let depth = path.components().count();
721 let entries = (0..129)
722 .take(max_entries)
723 .map(|index| {
724 let path = path.join(format!("entry-{index}"));
725 let attrs = VfAttrs {
726 file: VfFile::from_os_path(&path),
727 ftype: if index == 0 && depth < 5 {
728 VfType::Directory
729 } else {
730 VfType::Regular
731 },
732 masks: AttrMask::MODE,
733 ..Default::default()
734 };
735 DirEntry::new(path, vfsi_core::metadata_from_attrs(attrs))
736 })
737 .collect::<Vec<_>>();
738 peak.fetch_max(live.fetch_add(1, Ordering::SeqCst) + 1, Ordering::SeqCst);
739 Snapshot {
740 remaining: entries.into_iter(),
741 live: Arc::clone(&live),
742 }
743 }
744 };
745 let entries = snapshot.remaining.by_ref().take(page_size).collect();
746 let next = if snapshot.remaining.len() == 0 {
747 None
748 } else {
749 Some(DirPageCursor::new(snapshot))
750 };
751 pages.push((
752 crate::DirectoryListing {
753 path: path.to_path_buf(),
754 entries,
755 },
756 next,
757 Vec::new(),
758 ));
759 }
760 Ok(pages)
761 }
762 }
763
764 fn visit(
765 live: Arc<AtomicUsize>,
766 peak: Arc<AtomicUsize>,
767 options: ListDirOptions,
768 callback: impl FnMut(usize, crate::DirectoryListing) -> Result<std::ops::ControlFlow<()>>,
769 ) -> Result<Vec<crate::TraversalCompletion>> {
770 visit_directory_pages(
771 &["/tree"],
772 options,
773 ResourceLimits::default(),
774 |_| Ok(1),
775 |_| Ok(()),
776 snapshot_pages(live, peak),
777 callback,
778 )
779 }
780
781 #[test]
782 fn recursive_visit_releases_fallback_snapshots_before_descending() {
783 let live = Arc::new(AtomicUsize::new(0));
784 let peak = Arc::new(AtomicUsize::new(0));
785 let mut seen = 0;
786 let completions = visit(
787 Arc::clone(&live),
788 Arc::clone(&peak),
789 ListDirOptions::new()
790 .recursive(true)
791 .max_entries(1000)
792 .max_path_bytes(1_000_000),
793 |_, page| {
794 seen += page.entries.len();
795 Ok(std::ops::ControlFlow::Continue(()))
796 },
797 )
798 .unwrap();
799
800 assert_eq!(completions, [crate::TraversalCompletion::Complete]);
801 assert_eq!(seen, 4 * 129);
802 assert_eq!(live.load(Ordering::SeqCst), 0);
803 assert_eq!(peak.load(Ordering::SeqCst), 1);
804 }
805
806 #[test]
807 fn recursive_visit_drops_fallback_snapshot_on_stop_error_and_limit() {
808 for outcome in 0..3 {
809 let live = Arc::new(AtomicUsize::new(0));
810 let peak = Arc::new(AtomicUsize::new(0));
811 let options = ListDirOptions::new()
812 .recursive(true)
813 .max_entries(if outcome == 2 { 2 } else { 1000 })
814 .max_path_bytes(1_000_000);
815 let result = visit(Arc::clone(&live), peak, options, |_, _| match outcome {
816 0 => Ok(std::ops::ControlFlow::Break(())),
817 1 => Err(vfsi_core::api::Error::client(0, libc::EIO as u32)),
818 _ => Ok(std::ops::ControlFlow::Continue(())),
819 });
820 match outcome {
821 0 => assert_eq!(result.unwrap(), [crate::TraversalCompletion::Stopped]),
822 1 => assert_eq!(result.unwrap_err().err_no(), libc::EIO as u32),
823 _ => assert_eq!(result.unwrap_err().err_no(), libc::EFBIG as u32),
824 }
825 assert_eq!(live.load(Ordering::SeqCst), 0);
826 }
827 }
828}