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}
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
246impl<F: crate::backend::HandleBackend> FileHandle for crate::FsFile<F> {
247 crate::__vfsi_file_methods!(crate::FsFile<F>);
248}
249
250impl<F: crate::backend::VectorBackend> vfsi_core::api::DirHandle for crate::FsDir<F> {
251 crate::__vfsi_file_methods!(crate::FsDir<F>);
252}
253
254impl<F: crate::backend::VectorBackend + 'static> Vfsi for crate::FsClient<F> {
255 type File = crate::FsFile<F>;
256 type Dir = crate::FsDir<F>;
257 crate::__vfsi_client_methods!(
258 crate::FsClient<F>,
259 std::convert::identity,
260 read_backend::<F>,
261 std::convert::identity,
262 write_backend::<F>,
263 write_backend_all::<F>,
264 metadata_backend::<F, _>,
265 std::convert::identity,
266 |client: &Self, _: &[&Path]| client.directory_page_batch_size(),
267 Self::open_with_native,
268 Self::read_stream_with_options
269 );
270}
271
272#[doc(hidden)]
274pub fn read_backend_owned<F: crate::backend::VectorBackend + 'static>(
275 client: &crate::FsClient<F>,
276 requests: &[ReadRequest<'_, crate::FsRead<'_, F>>],
277 budget: usize,
278) -> Result<Vec<OwnedReadResult>> {
279 if requests.iter().all(|request| request.range_ref().is_some()) {
280 return client.vread_with_limit_projected_native(requests, budget, |request| {
281 request.range_ref().expect("checked ranges")
282 });
283 }
284 read_batch(
285 requests,
286 budget,
287 |ranges, bytes| client.vread_with_limit_projected_native(ranges, bytes, |request| request),
288 |paths, bytes| {
289 client.read_files_native(paths, crate::ReadAllOptions::new().max_total_bytes(bytes))
290 },
291 )
292}
293pub(crate) fn read_backend<'a, F: crate::backend::VectorBackend + 'static>(
294 client: &crate::FsClient<F>,
295 ops: impl IntoIterator<Item = ReadOp<'a, crate::FsFile<F>>>,
296 options: ReadOptions,
297) -> Result<Vec<ReadResult>> {
298 consume_ops(
299 ops,
300 options.limit_or(client.limits().read_byte_limit()),
301 crate::FsFile::read_request_at,
302 crate::FsFile::read_request_at_into,
303 |requests, options| read_backend_owned(client, requests, options),
304 |requests, bytes| client.vread_into_with_limit_native(requests, bytes),
305 )
306}
307
308#[doc(hidden)]
311pub fn public_write_error(error: vfsi_core::api::Error) -> vfsi_core::api::Error {
312 if error.operation() == Some("vwrite_all_native")
313 && let Some(path) = error.path().map(std::path::Path::to_path_buf)
314 {
315 error.with_context("vwrite_native", path)
316 } else {
317 error
318 }
319}
320
321pub(crate) fn write_backend<'a, F>(
322 client: &crate::FsClient<F>,
323 ops: &[WriteOp<'a, crate::FsFile<F>>],
324) -> vfsi_core::api::Result<Vec<vfsi_core::api::WriteResult>>
325where
326 F: crate::backend::VectorBackend + 'static,
327{
328 client.vwrite_mapped_native(ops, |op| op.file().write_request_at(op.offset(), op.data()))
329}
330
331pub(crate) fn write_backend_all<'a, F>(
332 client: &crate::FsClient<F>,
333 ops: &[WriteOp<'a, crate::FsFile<F>>],
334) -> vfsi_core::api::Result<Vec<vfsi_core::api::WriteResult>>
335where
336 F: crate::backend::VectorBackend + 'static,
337{
338 client.vwrite_all_mapped_native(ops, |op| op.file().write_request_at(op.offset(), op.data()))
339}
340
341pub(crate) fn metadata_backend<F, P: vfsi_core::AsTarget<crate::FsFile<F>>>(
342 client: &crate::FsClient<F>,
343 paths: &[P],
344 options: AttrsOptions,
345) -> vfsi_core::api::Result<Vec<crate::Attrs>>
346where
347 F: crate::backend::VectorBackend + 'static,
348{
349 client.vgetattrs_native(
350 paths,
351 options.requested_attributes(),
352 options.follows_symlinks(),
353 )
354}
355
356use vfsi_core::api::internal::{OwnedReadResult, ReadRequest, consume_ops, read_batch};
357#[cfg(test)]
358mod traversal_tests {
359 use super::*;
360 use crate::{AttrMask, DirEntry, DirPageCursor, VfAttrs, VfFile, VfType};
361 use std::sync::{
362 Arc,
363 atomic::{AtomicUsize, Ordering},
364 };
365
366 struct Snapshot {
369 remaining: std::vec::IntoIter<DirEntry>,
370 live: Arc<AtomicUsize>,
371 }
372
373 impl Drop for Snapshot {
374 fn drop(&mut self) {
375 self.live.fetch_sub(1, Ordering::SeqCst);
376 }
377 }
378
379 #[allow(clippy::type_complexity)] fn snapshot_pages(
381 live: Arc<AtomicUsize>,
382 peak: Arc<AtomicUsize>,
383 ) -> impl FnMut(
384 &[&Path],
385 Vec<Option<DirPageCursor>>,
386 usize,
387 usize,
388 ) -> Result<Vec<crate::DirectoryPage>> {
389 move |paths, cursors, page_size, max_entries| {
390 let mut pages = Vec::with_capacity(paths.len());
391 for (path, cursor) in paths.iter().zip(cursors) {
392 let mut snapshot = match cursor {
393 Some(cursor) => cursor.into_state::<Snapshot>()?,
394 None => {
395 let depth = path.components().count();
396 let entries = (0..129)
397 .take(max_entries)
398 .map(|index| {
399 let path = path.join(format!("entry-{index}"));
400 let attrs = VfAttrs {
401 file: VfFile::from_os_path(&path),
402 ftype: if index == 0 && depth < 5 {
403 VfType::Directory
404 } else {
405 VfType::Regular
406 },
407 masks: AttrMask::MODE,
408 ..Default::default()
409 };
410 DirEntry::new(path, vfsi_core::metadata_from_attrs(attrs))
411 })
412 .collect::<Vec<_>>();
413 peak.fetch_max(live.fetch_add(1, Ordering::SeqCst) + 1, Ordering::SeqCst);
414 Snapshot {
415 remaining: entries.into_iter(),
416 live: Arc::clone(&live),
417 }
418 }
419 };
420 let entries = snapshot.remaining.by_ref().take(page_size).collect();
421 let next = if snapshot.remaining.len() == 0 {
422 None
423 } else {
424 Some(DirPageCursor::new(snapshot))
425 };
426 pages.push((
427 crate::DirectoryListing {
428 path: path.to_path_buf(),
429 entries,
430 },
431 next,
432 Vec::new(),
433 ));
434 }
435 Ok(pages)
436 }
437 }
438
439 fn visit(
440 live: Arc<AtomicUsize>,
441 peak: Arc<AtomicUsize>,
442 options: ListDirOptions,
443 callback: impl FnMut(usize, crate::DirectoryListing) -> Result<std::ops::ControlFlow<()>>,
444 ) -> Result<Vec<crate::TraversalCompletion>> {
445 visit_directory_pages(
446 &["/tree"],
447 options,
448 ResourceLimits::default(),
449 |_| Ok(1),
450 |_| Ok(()),
451 snapshot_pages(live, peak),
452 callback,
453 )
454 }
455
456 #[test]
457 fn recursive_visit_releases_fallback_snapshots_before_descending() {
458 let live = Arc::new(AtomicUsize::new(0));
459 let peak = Arc::new(AtomicUsize::new(0));
460 let mut seen = 0;
461 let completions = visit(
462 Arc::clone(&live),
463 Arc::clone(&peak),
464 ListDirOptions::new()
465 .recursive(true)
466 .max_entries(1000)
467 .max_path_bytes(1_000_000),
468 |_, page| {
469 seen += page.entries.len();
470 Ok(std::ops::ControlFlow::Continue(()))
471 },
472 )
473 .unwrap();
474
475 assert_eq!(completions, [crate::TraversalCompletion::Complete]);
476 assert_eq!(seen, 4 * 129);
477 assert_eq!(live.load(Ordering::SeqCst), 0);
478 assert_eq!(peak.load(Ordering::SeqCst), 1);
479 }
480
481 #[test]
482 fn recursive_visit_drops_fallback_snapshot_on_stop_error_and_limit() {
483 for outcome in 0..3 {
484 let live = Arc::new(AtomicUsize::new(0));
485 let peak = Arc::new(AtomicUsize::new(0));
486 let options = ListDirOptions::new()
487 .recursive(true)
488 .max_entries(if outcome == 2 { 2 } else { 1000 })
489 .max_path_bytes(1_000_000);
490 let result = visit(Arc::clone(&live), peak, options, |_, _| match outcome {
491 0 => Ok(std::ops::ControlFlow::Break(())),
492 1 => Err(vfsi_core::api::Error::client(0, libc::EIO as u32)),
493 _ => Ok(std::ops::ControlFlow::Continue(())),
494 });
495 match outcome {
496 0 => assert_eq!(result.unwrap(), [crate::TraversalCompletion::Stopped]),
497 1 => assert_eq!(result.unwrap_err().err_no(), libc::EIO as u32),
498 _ => assert_eq!(result.unwrap_err().err_no(), libc::EFBIG as u32),
499 }
500 assert_eq!(live.load(Ordering::SeqCst), 0);
501 }
502 }
503}