1use std::path::Path;
3use vfsi_core::api::*;
4#[doc(hidden)]
5pub fn 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}
12use vfsi_core::internal::TraversalBudget;
13pub fn visit_directory_pages<P: AsRef<Path>>(
14 roots: &[P],
15 policy: vfsi_core::api::ListDirOptions,
16 limits: ResourceLimits,
17 mut capacity: impl FnMut(&[&Path]) -> Result<usize>,
18 mut validate_root: impl FnMut(&Path) -> Result<()>,
19 mut fetch: impl FnMut(
20 &[&Path],
21 Vec<Option<crate::DirPageCursor>>,
22 usize,
23 usize,
24 bool,
25 ) -> Result<Vec<crate::DirectoryPage>>,
26 mut callback: impl FnMut(usize, DirectoryListing) -> Result<std::ops::ControlFlow<()>>,
27) -> Result<Vec<crate::TraversalCompletion>> {
28 let directory = policy.directory_options(limits);
29 let walk = policy.walk_options(limits);
30 let mut budget = TraversalBudget::new(
31 directory.max_entries_or(vfsi_core::api::DEFAULT_DIRECTORY_MAX_ENTRIES),
32 directory.max_path_bytes_or(vfsi_core::api::DEFAULT_DIRECTORY_MAX_PATH_BYTES),
33 );
34 let mut completed = Vec::new();
35 let mut pending = std::collections::VecDeque::new();
36 for (index, root) in roots.iter().enumerate() {
37 pending.push_back((index, root.as_ref().to_path_buf(), 0usize, None));
38 }
39 while !pending.is_empty() {
40 let owner = pending.front().unwrap().0;
41 if policy.recursive() && pending.front().unwrap().2 == 0 {
42 validate_root(&pending.front().unwrap().1)
43 .map_err(|error| vector_index(error, owner))?;
44 }
45 let candidate_paths: Vec<_> = pending
47 .iter()
48 .take_while(|state| !policy.recursive() || state.0 == owner)
49 .take(32)
50 .map(|state| state.1.as_path())
51 .collect();
52 let capacity = capacity(&candidate_paths)?.clamp(1, 32);
53 let cohort_size = if policy.recursive() {
54 pending
55 .iter()
56 .take(capacity)
57 .take_while(|state| state.0 == owner)
58 .count()
59 } else {
60 pending.len().min(capacity)
61 };
62 let mut cohort: Vec<_> = (0..cohort_size)
63 .map(|_| pending.pop_front().unwrap())
64 .collect();
65 let cursors = cohort.iter_mut().map(|state| state.3.take()).collect();
66 let paths: Vec<_> = cohort
67 .iter()
68 .map(|(_, path, _, _)| path.as_path())
69 .collect();
70 let requested = budget.remaining_entries().saturating_add(1);
71 let mut deferred_error = None;
72 let follow_symlinks = policy.allow_symlink_components() && cohort[0].2 == 0;
75 let pages = match fetch(&paths, cursors, 1, requested, follow_symlinks) {
76 Ok(pages) => pages,
77 Err(error)
78 if !policy.recursive()
79 && !error.is_transport()
80 && error.index().is_some_and(|i| i > 0 && i < cohort.len()) =>
81 {
82 let failed = error.index().unwrap();
86 deferred_error = Some(vector_index(error, cohort[failed].0));
87 let prefix = fetch(
88 &paths[..failed],
89 (0..failed).map(|_| None).collect(),
90 1,
91 requested,
92 follow_symlinks,
93 )
94 .map_err(|error| error.map_index(|i| cohort.get(i).map_or(i, |state| state.0)))?;
95 cohort.truncate(failed);
96 prefix
97 }
98 Err(error) => return Err(error.map_index(|i| cohort.get(i).map_or(i, |state| state.0))),
99 };
100 if pages.len() != cohort.len() {
101 return Err(vfsi_core::api::Error::transport(
102 None,
103 "invalid directory page result count",
104 ));
105 }
106 let mut children = Vec::new();
107 let mut wave: Vec<_> = cohort
108 .into_iter()
109 .map(|(index, path, depth, _)| (index, path, depth, true))
110 .zip(pages)
111 .collect();
112 while !wave.is_empty() {
113 let mut continuations = Vec::new();
114 for ((index, path, depth, first), (mut page, next, seeds)) in wave {
115 if first && policy.recursive() {
116 budget.charge_path(&path).map_err(|error| {
117 vector_index(error.with_context("vector traversal", &path), index)
118 })?;
119 }
120 if page.path != path || (page.entries.is_empty() && next.is_some()) {
121 return Err(vfsi_core::api::Error::transport(
122 Some(index),
123 "invalid directory page parent or progress",
124 ));
125 }
126 let mut seeds: std::collections::HashMap<_, _> = seeds.into_iter().collect();
127 let mut page_error = None;
128 let mut accepted = 0;
129 for entry in &page.entries {
130 if let Err(error) = budget.charge(entry.path()) {
131 page_error = Some(vector_index(
132 error.with_context("vector traversal", entry.path()),
133 index,
134 ));
135 break;
136 }
137 accepted += 1;
138 if policy.recursive() && entry.attrs().is_dir() {
139 if depth >= walk.max_depth_or(Default::default()).get() {
140 if !walk.truncate_at_max_depth() {
141 page_error = Some(
142 vfsi_core::api::Error::client(index, libc::EFBIG as u32)
143 .with_context("visit_dirs", entry.path()),
144 );
145 break;
146 }
147 } else {
148 children.push((
149 index,
150 entry.path().to_path_buf(),
151 depth + 1,
152 seeds.remove(entry.path()),
153 ));
154 }
155 }
156 }
157 if let Some(error) = &page_error {
158 if accepted == 0 {
159 return Err(error.clone());
160 }
161 page.entries.truncate(accepted);
162 }
163 if !policy.recursive() && completed.len() <= index {
164 completed.resize(index + 1, crate::TraversalCompletion::Stopped);
165 }
166 if callback(index, page)
167 .map_err(|error| vector_index(error, index))?
168 .is_break()
169 {
170 if policy.recursive() {
171 completed.truncate(index);
172 completed.push(crate::TraversalCompletion::Stopped);
173 } else {
174 completed[index] = crate::TraversalCompletion::Stopped;
175 }
176 return Ok(completed);
177 }
178 if let Some(error) = page_error {
179 return Err(error);
180 }
181 match next {
182 Some(cursor) => continuations.push(((index, path, depth, false), Some(cursor))),
183 None if !policy.recursive() => {
184 completed[index] = crate::TraversalCompletion::Complete
185 }
186 None => {}
187 }
188 }
189 if continuations.is_empty() {
190 break;
191 }
192 let cursors = continuations
193 .iter_mut()
194 .map(|state| state.1.take())
195 .collect();
196 let paths: Vec<_> = continuations
197 .iter()
198 .map(|state| state.0.1.as_path())
199 .collect();
200 let requested = budget.remaining_entries().saturating_add(1);
201 let follow_symlinks = policy.allow_symlink_components() && continuations[0].0.2 == 0;
202 let pages = fetch(
203 &paths,
204 cursors,
205 requested.min(128),
206 requested,
207 follow_symlinks,
208 )
209 .map_err(|error| {
210 error.map_index(|i| continuations.get(i).map_or(i, |state| state.0.0))
211 })?;
212 if pages.len() != continuations.len() {
213 return Err(vfsi_core::api::Error::transport(
214 None,
215 "invalid directory continuation count",
216 ));
217 }
218 wave = continuations
219 .into_iter()
220 .map(|state| state.0)
221 .zip(pages)
222 .collect();
223 }
224 if let Some(error) = deferred_error {
225 return Err(error);
226 }
227 for child in children.into_iter().rev() {
230 pending.push_front(child);
231 }
232 if policy.recursive() && pending.front().is_none_or(|state| state.0 != owner) {
233 completed.push(crate::TraversalCompletion::Complete);
234 }
235 }
236 Ok(completed)
237}
238
239impl<F: crate::backend::HandleBackend> FileHandle for crate::OpenFile<F> {
240 crate::__vfsi_file_methods!(crate::OpenFile<F>);
241}
242
243impl<F: crate::backend::VectorBackend> vfsi_core::api::DirHandle for crate::FsDir<F> {
244 crate::__vfsi_file_methods!(crate::FsDir<F>);
245}
246
247impl<F: crate::backend::VectorBackend + 'static> Vfsi for crate::FsClient<F> {
248 type File = crate::OpenFile<F>;
249 type Dir = crate::FsDir<F>;
250 crate::__vfsi_client_methods!(
251 crate::FsClient<F>,
252 std::convert::identity,
253 read_backend::<F>,
254 std::convert::identity,
255 write_backend::<F>,
256 write_backend_all::<F>,
257 metadata_backend::<F, _>,
258 std::convert::identity,
259 |client: &Self, _: &[&Path]| client.directory_page_batch_size(),
260 Self::open_with_native,
261 Self::read_stream_with_options
262 );
263}
264
265#[doc(hidden)]
267pub fn read_backend_owned<F: crate::backend::VectorBackend + 'static>(
268 client: &crate::FsClient<F>,
269 requests: &[ReadRequest<'_, crate::FsRead<'_, F>>],
270 budget: usize,
271) -> Result<Vec<OwnedReadResult>> {
272 if requests.iter().all(|request| request.range_ref().is_some()) {
273 return client.vread_with_limit_projected_native(requests, budget, |request| {
274 request.range_ref().expect("checked ranges")
275 });
276 }
277 read_batch(
278 requests,
279 budget,
280 |ranges, bytes| client.vread_with_limit_projected_native(ranges, bytes, |request| request),
281 |paths, bytes| {
282 client.read_files_native(
283 paths,
284 crate::ReadAllOptions::new().with_max_total_bytes(bytes),
285 )
286 },
287 )
288}
289pub(crate) fn read_backend<'a, F: crate::backend::VectorBackend + 'static>(
290 client: &crate::FsClient<F>,
291 ops: impl IntoIterator<Item = ReadOp<'a, crate::OpenFile<F>>>,
292 options: ReadOptions,
293) -> Result<Vec<ReadResult>> {
294 consume_ops(
295 ops,
296 options.max_total_bytes_or(client.limits().max_read_bytes()),
297 crate::OpenFile::read_request_at,
298 crate::OpenFile::read_request_at_into,
299 |requests, options| read_backend_owned(client, requests, options),
300 |requests, bytes| client.vread_into_with_limit_native(requests, bytes),
301 )
302}
303
304#[doc(hidden)]
307pub fn public_write_error(error: vfsi_core::api::Error) -> vfsi_core::api::Error {
308 if error.operation() == Some("vwrite_all_native")
309 && let Some(path) = error.path().map(std::path::Path::to_path_buf)
310 {
311 error.with_context("vwrite_native", path)
312 } else {
313 error
314 }
315}
316
317pub(crate) fn write_backend<'a, F>(
318 client: &crate::FsClient<F>,
319 ops: &[WriteOp<'a, crate::OpenFile<F>>],
320) -> vfsi_core::api::Result<Vec<vfsi_core::api::WriteResult>>
321where
322 F: crate::backend::VectorBackend + 'static,
323{
324 client.vwrite_mapped_native(ops, |op| op.file().write_request_at(op.offset(), op.data()))
325}
326
327pub(crate) fn write_backend_all<'a, F>(
328 client: &crate::FsClient<F>,
329 ops: &[WriteOp<'a, crate::OpenFile<F>>],
330) -> vfsi_core::api::Result<Vec<vfsi_core::api::WriteResult>>
331where
332 F: crate::backend::VectorBackend + 'static,
333{
334 client.vwrite_all_mapped_native(ops, |op| op.file().write_request_at(op.offset(), op.data()))
335}
336
337pub(crate) fn metadata_backend<F, P: vfsi_core::AsTarget<crate::OpenFile<F>>>(
338 client: &crate::FsClient<F>,
339 paths: &[P],
340 options: AttrsOptions,
341) -> vfsi_core::api::Result<Vec<crate::Attrs>>
342where
343 F: crate::backend::VectorBackend + 'static,
344{
345 client.vgetattrs_native(paths, options.fields(), options.follow_final_symlink())
346}
347
348use vfsi_core::api::internal::{OwnedReadResult, ReadRequest, consume_ops, read_batch};
349#[cfg(test)]
350mod traversal_tests {
351 use super::*;
352 use crate::{AttrMask, DirEntry, DirPageCursor, FileOperand, VfAttrs, VfType};
353 use std::sync::{
354 Arc,
355 atomic::{AtomicUsize, Ordering},
356 };
357
358 struct Snapshot {
361 remaining: std::vec::IntoIter<DirEntry>,
362 live: Arc<AtomicUsize>,
363 }
364
365 impl Drop for Snapshot {
366 fn drop(&mut self) {
367 self.live.fetch_sub(1, Ordering::SeqCst);
368 }
369 }
370
371 #[allow(clippy::type_complexity)] fn snapshot_pages(
373 live: Arc<AtomicUsize>,
374 peak: Arc<AtomicUsize>,
375 ) -> impl FnMut(
376 &[&Path],
377 Vec<Option<DirPageCursor>>,
378 usize,
379 usize,
380 bool,
381 ) -> Result<Vec<crate::DirectoryPage>> {
382 move |paths, cursors, page_size, max_entries, _follow_symlinks| {
383 let mut pages = Vec::with_capacity(paths.len());
384 for (path, cursor) in paths.iter().zip(cursors) {
385 let mut snapshot = match cursor {
386 Some(cursor) => cursor.into_state::<Snapshot>()?,
387 None => {
388 let depth = path.components().count();
389 let entries = (0..129)
390 .take(max_entries)
391 .map(|index| {
392 let path = path.join(format!("entry-{index}"));
393 let attrs = VfAttrs {
394 file: FileOperand::from_os_path(&path),
395 ftype: if index == 0 && depth < 5 {
396 VfType::Directory
397 } else {
398 VfType::Regular
399 },
400 masks: AttrMask::MODE,
401 ..Default::default()
402 };
403 DirEntry::new(path, vfsi_core::metadata_from_attrs(attrs))
404 })
405 .collect::<Vec<_>>();
406 peak.fetch_max(live.fetch_add(1, Ordering::SeqCst) + 1, Ordering::SeqCst);
407 Snapshot {
408 remaining: entries.into_iter(),
409 live: Arc::clone(&live),
410 }
411 }
412 };
413 let entries = snapshot.remaining.by_ref().take(page_size).collect();
414 let next = if snapshot.remaining.len() == 0 {
415 None
416 } else {
417 Some(DirPageCursor::new(snapshot))
418 };
419 pages.push((
420 crate::DirectoryListing {
421 path: path.to_path_buf(),
422 entries,
423 },
424 next,
425 Vec::new(),
426 ));
427 }
428 Ok(pages)
429 }
430 }
431
432 fn visit(
433 live: Arc<AtomicUsize>,
434 peak: Arc<AtomicUsize>,
435 options: ListDirOptions,
436 callback: impl FnMut(usize, crate::DirectoryListing) -> Result<std::ops::ControlFlow<()>>,
437 ) -> Result<Vec<crate::TraversalCompletion>> {
438 visit_directory_pages(
439 &["/tree"],
440 options,
441 ResourceLimits::default(),
442 |_| Ok(1),
443 |_| Ok(()),
444 snapshot_pages(live, peak),
445 callback,
446 )
447 }
448
449 #[test]
450 fn recursive_visit_releases_fallback_snapshots_before_descending() {
451 let live = Arc::new(AtomicUsize::new(0));
452 let peak = Arc::new(AtomicUsize::new(0));
453 let mut seen = 0;
454 let completions = visit(
455 Arc::clone(&live),
456 Arc::clone(&peak),
457 ListDirOptions::new()
458 .with_recursive(true)
459 .with_max_entries(1000)
460 .with_max_path_bytes(1_000_000),
461 |_, page| {
462 seen += page.entries.len();
463 Ok(std::ops::ControlFlow::Continue(()))
464 },
465 )
466 .unwrap();
467
468 assert_eq!(completions, [crate::TraversalCompletion::Complete]);
469 assert_eq!(seen, 4 * 129);
470 assert_eq!(live.load(Ordering::SeqCst), 0);
471 assert_eq!(peak.load(Ordering::SeqCst), 1);
472 }
473
474 #[test]
475 fn recursive_visit_drops_fallback_snapshot_on_stop_error_and_limit() {
476 for outcome in 0..4 {
477 let live = Arc::new(AtomicUsize::new(0));
478 let peak = Arc::new(AtomicUsize::new(0));
479 let options = ListDirOptions::new()
480 .with_recursive(true)
481 .with_max_entries(if outcome == 2 { 2 } else { 1000 })
482 .with_max_path_bytes(if outcome == 3 { 31 } else { 1_000_000 });
484 let mut fetch = snapshot_pages(Arc::clone(&live), peak);
485 let mut callbacks = 0;
486 let mut live_budget_page = false;
487 let result = visit_directory_pages(
488 &["/tree"],
489 options,
490 ResourceLimits::default(),
491 |_| Ok(1),
492 |_| Ok(()),
493 |paths, cursors, page_size, max_entries, follow_symlinks| {
494 let pages = fetch(
497 paths,
498 cursors,
499 page_size.min(2),
500 max_entries,
501 follow_symlinks,
502 )?;
503 if outcome == 3 && pages[0].0.entries.len() == 2 {
504 assert!(pages[0].1.is_some());
505 live_budget_page = true;
506 }
507 Ok(pages)
508 },
509 |_, page| {
510 if outcome != 2 {
511 assert_eq!(live.load(Ordering::SeqCst), 1);
512 }
513 assert_eq!(page.entries.len(), 1);
514 assert_eq!(
515 page.entries[0].path(),
516 Path::new(&format!("/tree/entry-{callbacks}"))
517 );
518 callbacks += 1;
519 match outcome {
520 0 => Ok(std::ops::ControlFlow::Break(())),
521 1 => Err(vfsi_core::api::Error::client(0, libc::EIO as u32)),
522 _ => Ok(std::ops::ControlFlow::Continue(())),
523 }
524 },
525 );
526 match outcome {
527 0 => assert_eq!(result.unwrap(), [crate::TraversalCompletion::Stopped]),
528 1 => assert_eq!(result.unwrap_err().raw_code(), libc::EIO as u32),
529 _ => {
530 let error = result.unwrap_err();
531 assert_eq!(error.raw_code(), libc::EFBIG as u32);
532 assert_eq!(error.path(), Some(Path::new("/tree/entry-2")));
533 if outcome == 3 {
534 assert!(live_budget_page);
535 }
536 }
537 }
538 assert_eq!(callbacks, if outcome >= 2 { 2 } else { 1 });
539 assert_eq!(live.load(Ordering::SeqCst), 0);
540 }
541 }
542 #[test]
543 fn recursive_pages_keep_root_policy_and_reject_descendant_symlinks() {
544 for recursive in [false, true] {
545 for allow in [false, true] {
546 let live = Arc::new(AtomicUsize::new(0));
547 let peak = Arc::new(AtomicUsize::new(0));
548 let mut fetch = snapshot_pages(live, peak);
549 let mut observed = Vec::new();
550 visit_directory_pages(
551 &["/tree"],
552 ListDirOptions::new()
553 .with_recursive(recursive)
554 .with_allow_symlink_components(allow),
555 ResourceLimits::default(),
556 |_| Ok(1),
557 |_| Ok(()),
558 |paths, cursors, page_size, max_entries, follow| {
559 for path in paths {
560 observed.push((path.to_path_buf(), follow));
561 }
562 fetch(paths, cursors, page_size, max_entries, follow)
563 },
564 |_, _| Ok(std::ops::ControlFlow::Continue(())),
565 )
566 .unwrap();
567 assert!(observed.len() > 1, "exercise continuation fetches");
568 assert_eq!(
569 observed.iter().any(|(path, _)| path != Path::new("/tree")),
570 recursive
571 );
572 for (path, follow) in observed {
573 assert_eq!(follow, allow && path == Path::new("/tree"));
574 }
575 }
576 }
577 }
578}