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}
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
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/// Shared owned-read dispatcher for native clients and their opaque facades.
273#[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// Keep low-level completion machinery private without leaking its historical
309// operation name through application errors. Preserve status, index, and path.
310#[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    // The fallback cursor owns the unconsumed listing. Track retained snapshots
367    // while exercising the same visitor the Vfsi adapter uses in production.
368    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)] // Mirrors the page-fetch callback contract under test.
380    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}