Skip to main content

vfsi_sync/
application.rs

1//! Native execution of the portable VFSI contract.
2use 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        // Recursive traversal completes one input root before starting another.
68        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                // A speculative later root must not hide an earlier callback's
102                // cancellation. Re-fetch only the known successful read-only
103                // prefix; this exceptional path never replays a mutation.
104                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        // Do not hold fallback snapshots while descending; all pages above
235        // were consumed and their cursors released before adding the frontier.
236        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    // Application clients and backend clients use different native method names.
380    ($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                // Preserve native symlink resolution and independent-handle state.
492                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
634// Keep low-level completion machinery private without leaking its historical
635// operation name through application errors. Preserve status, index, and path.
636pub(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    // The fallback cursor owns the unconsumed listing. Track retained snapshots
692    // while exercising the same visitor the Vfsi adapter uses in production.
693    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)] // Mirrors the page-fetch callback contract under test.
705    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}