Skip to main content

vfsi_sync/
application.rs

1//! Native execution of the portable VFSI contract.
2use 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        // Recursive traversal completes one input root before starting another.
45        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                // A speculative later root must not hide an earlier callback's
79                // cancellation. Re-fetch only the known successful read-only
80                // prefix; this exceptional path never replays a mutation.
81                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        // Do not hold fallback snapshots while descending; all pages above
215        // were consumed and their cursors released before adding the frontier.
216        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/// Shared owned-read dispatcher for native clients and their opaque facades.
253#[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// Keep low-level completion machinery private without leaking its historical
292// operation name through application errors. Preserve status, index, and path.
293#[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    // The fallback cursor owns the unconsumed listing. Track retained snapshots
346    // while exercising the same visitor the Vfsi adapter uses in production.
347    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)] // Mirrors the page-fetch callback contract under test.
359    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                // Root (5 bytes) and two entry paths (13 bytes each) fit.
469                .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                    // A legal short page leaves a live snapshot when the byte
481                    // limit rejects its second entry, after accepting the first.
482                    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}