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