Skip to main content

vfsi_sync/
traits.rs

1use super::*;
2use vfsi_core::internal::ManyResults;
3
4/// Backend-owned continuation state for a paged directory visit.
5///
6/// The state is dropped automatically when the caller stops or an error
7/// occurs, so backends do not need an explicit cursor-close operation.
8#[doc(hidden)]
9pub struct DirPageCursor(Box<dyn std::any::Any + Send>);
10
11impl DirPageCursor {
12    pub fn new<T: std::any::Any + Send>(state: T) -> Self {
13        Self(Box::new(state))
14    }
15
16    pub fn is<T: std::any::Any + Send>(&self) -> bool {
17        self.0.is::<T>()
18    }
19
20    pub fn into_state<T: std::any::Any + Send>(self) -> VfResult<T> {
21        self.0
22            .downcast::<T>()
23            .map(|state| *state)
24            .map_err(|_| VfError::client(0, ERR_INVAL))
25    }
26}
27
28/// Backend page plus optional child seeds aligned with its entries.
29/// A seed starts a child's first page relative to its observed parent.
30#[doc(hidden)]
31pub type BackendDirectoryPage = (
32    Vec<VfAttrs>,
33    Option<DirPageCursor>,
34    Vec<Option<DirPageCursor>>,
35);
36
37/// Owned application page and traversal-scoped anchored child seeds.
38#[doc(hidden)]
39pub type DirectoryPage = (
40    crate::DirectoryListing,
41    Option<DirPageCursor>,
42    Vec<(std::path::PathBuf, DirPageCursor)>,
43);
44
45/// Resource limits for reading multiple complete files into memory.
46#[derive(Clone, Copy, Debug, PartialEq, Eq)]
47pub struct ReadAllOptions {
48    max_total_bytes: usize,
49}
50
51impl ReadAllOptions {
52    pub const fn new() -> Self {
53        Self {
54            max_total_bytes: DEFAULT_READ_ALLV_MAX_TOTAL_BYTES,
55        }
56    }
57
58    /// Set the maximum combined size of all returned buffers.
59    pub const fn max_total_bytes(mut self, bytes: usize) -> Self {
60        self.max_total_bytes = bytes;
61        self
62    }
63
64    pub const fn total_byte_limit(self) -> usize {
65        self.max_total_bytes
66    }
67}
68
69impl Default for ReadAllOptions {
70    fn default() -> Self {
71        Self::new()
72    }
73}
74
75fn contract_error(
76    operation: &str,
77    index: Option<usize>,
78    detail: impl std::fmt::Display,
79) -> VfError {
80    VfError::transport(
81        index,
82        format!("{operation} backend contract violation: {detail}"),
83    )
84}
85
86fn take_single_result<T>(operation: &str, mut results: Vec<T>) -> VfResult<T> {
87    if results.len() != 1 {
88        return Err(contract_error(
89            operation,
90            None,
91            format!("returned {} results for one request", results.len()),
92        ));
93    }
94    Ok(results.pop().expect("validated one result"))
95}
96
97pub(crate) fn validate_read_results(
98    operation: &str,
99    requests: &[ReadOp],
100    results: &[ReadResult],
101) -> VfResult<()> {
102    if results.len() != requests.len() {
103        return Err(contract_error(
104            operation,
105            None,
106            format!(
107                "returned {} results for {} requests",
108                results.len(),
109                requests.len()
110            ),
111        ));
112    }
113    for (index, (request, result)) in requests.iter().zip(results).enumerate() {
114        if result.file != request.file {
115            return Err(contract_error(
116                operation,
117                Some(index),
118                "result file does not match request",
119            ));
120        }
121        if result.data.len() > request.length {
122            return Err(contract_error(
123                operation,
124                Some(index),
125                format!(
126                    "returned {} bytes for a {}-byte read",
127                    result.data.len(),
128                    request.length
129                ),
130            ));
131        }
132        if let VfOffset::At(expected) = request.offset
133            && result.offset != expected
134        {
135            return Err(contract_error(
136                operation,
137                Some(index),
138                format!("result offset {} does not match {expected}", result.offset),
139            ));
140        }
141        if request.length != 0 && result.data.is_empty() && !result.eof {
142            return Err(contract_error(
143                operation,
144                Some(index),
145                "read made no progress without reporting EOF",
146            ));
147        }
148    }
149    Ok(())
150}
151
152pub(crate) fn validate_read_into_results(
153    operation: &str,
154    requests: &[ReadOp],
155    results: &[ReadIntoResult],
156) -> VfResult<()> {
157    if results.len() != requests.len() {
158        return Err(contract_error(
159            operation,
160            None,
161            format!(
162                "returned {} results for {} requests",
163                results.len(),
164                requests.len()
165            ),
166        ));
167    }
168    for (index, (request, result)) in requests.iter().zip(results).enumerate() {
169        if result.file != request.file {
170            return Err(contract_error(
171                operation,
172                Some(index),
173                "result file does not match request",
174            ));
175        }
176        if result.read > request.length {
177            return Err(contract_error(
178                operation,
179                Some(index),
180                format!(
181                    "returned {} bytes for a {}-byte read",
182                    result.read, request.length
183                ),
184            ));
185        }
186        if let VfOffset::At(expected) = request.offset
187            && result.offset != expected
188        {
189            return Err(contract_error(
190                operation,
191                Some(index),
192                format!("result offset {} does not match {expected}", result.offset),
193            ));
194        }
195        if request.length != 0 && result.read == 0 && !result.eof {
196            return Err(contract_error(
197                operation,
198                Some(index),
199                "read made no progress without reporting EOF",
200            ));
201        }
202    }
203    Ok(())
204}
205
206pub(crate) fn validate_write_results(
207    operation: &str,
208    requests: &[WriteOpRef<'_>],
209    results: &[WriteResult],
210) -> VfResult<()> {
211    if results.len() != requests.len() {
212        return Err(contract_error(
213            operation,
214            None,
215            format!(
216                "returned {} results for {} requests",
217                results.len(),
218                requests.len()
219            ),
220        ));
221    }
222    for (index, (request, result)) in requests.iter().zip(results).enumerate() {
223        if result.file != *request.file {
224            return Err(contract_error(
225                operation,
226                Some(index),
227                "result file does not match request",
228            ));
229        }
230        if result.written > request.data.len() {
231            return Err(contract_error(
232                operation,
233                Some(index),
234                format!(
235                    "reported {} bytes written for a {}-byte write",
236                    result.written,
237                    request.data.len()
238                ),
239            ));
240        }
241        if let VfOffset::At(expected) = request.offset
242            && result.offset != expected
243        {
244            return Err(contract_error(
245                operation,
246                Some(index),
247                format!("result offset {} does not match {expected}", result.offset),
248            ));
249        }
250    }
251    Ok(())
252}
253
254/// A vectorized filesystem: many small operations coalesced into as few
255/// round trips as the backend supports.
256///
257/// `VfFile` references files either by descriptor (an open file) or by path
258/// (absolute, or relative to the client's current working directory).
259///
260/// # Partial success
261///
262/// Vector calls are ordered batches, not transactions. On an indexed error,
263/// operations before the failing index may already have completed and later
264/// operations may not have been attempted. In particular, a transport error
265/// after a mutating request was sent can leave its outcome unknown. Backends
266/// must not silently replay such mutations; callers that retry must first
267/// reconcile state or use application-level idempotency.
268pub trait VecFs {
269    /// Finish cleanup after an owned file is dropped. Backends that retain
270    /// failed CLOSE state internally should complete their deferred cleanup.
271    fn close_deferred(&mut self, file: &VfFile) -> VfResult<()> {
272        self.close(file)
273    }
274    /// Deferred operational notifications. Owned clients deliver these after
275    /// unlocking; direct backend users must drain and run them outside any lock.
276    fn take_notifications(&mut self) -> Vec<Box<dyn FnOnce() + Send>> {
277        Vec::new()
278    }
279    /// Negotiated NFS minor version, or `None` for non-NFS backends.
280    #[deprecated(note = "use the NFS backend's NfsExtensions trait")]
281    fn nfs_minorversion(&self) -> Option<u32> {
282        None
283    }
284
285    /// Negotiated SMB dialect revision (for example `0x0311` for SMB 3.1.1),
286    /// or `None` for non-SMB backends.
287    #[deprecated(note = "use the SMB backend's SmbExtensions trait")]
288    fn smb_dialect(&self) -> Option<u16> {
289        None
290    }
291
292    /// Backend capability bits. Capabilities may change after a server
293    /// rejects an optional operation and the client installs a fallback.
294    fn capabilities(&self) -> u64 {
295        0
296    }
297
298    /// Strongly typed capability query used by the Rust-native API.
299    fn typed_capabilities(&self) -> Capabilities {
300        Capabilities::from_bits_retain(self.capabilities())
301    }
302
303    // -- required -----------------------------------------------------------
304
305    /// Return the root-relative form of `path` (resolving it against the
306    /// client's current working directory if it is relative), without a
307    /// leading `/`. "Root" is the backend's application namespace root (the
308    /// export root for NFS, the configured root for the local backend), not a
309    /// server or host path; for NFS the export prefix is deliberately absent.
310    /// [`getcwd`](Self::getcwd) is the display form (with a leading `/`);
311    /// [`vf_path`](Self::vf_path) resolves a [`VfFile`] the same way while
312    /// honoring its [`VfPathBase`].
313    fn abs_path(&self, path: &Path) -> PathBuf;
314
315    /// Open a file by path, similar to `tc_open_by_path(2)`. `base` is
316    /// `VfPathBase::Cwd` or `VfPathBase::Abs`. When `O_CREAT` is set, `mode`
317    /// is applied to the new file.
318    fn open_by_path(
319        &mut self,
320        base: VfPathBase,
321        pathname: &Path,
322        flags: i32,
323        mode: u32,
324    ) -> VfResult<VfFile>;
325
326    /// Close an open file, `tc_close()`.
327    fn close(&mut self, tcf: &VfFile) -> VfResult<()>;
328
329    /// Make prior writes to this descriptor durable. Backends whose writes
330    /// are always stable still validate the handle before returning success.
331    fn sync_data(&mut self, tcf: &VfFile) -> VfResult<()>;
332
333    /// Make file data and metadata durable.
334    fn sync_all(&mut self, tcf: &VfFile) -> VfResult<()> {
335        self.sync_data(tcf)
336    }
337
338    /// Change the client's current directory, `tc_chdir()`.
339    fn chdir(&mut self, path: &Path) -> VfResult<()>;
340
341    /// Current working directory, `tc_getcwd()`.
342    fn getcwd(&self) -> PathBuf;
343
344    /// Read from one or more files, `tc_readv()`. Returns one result per
345    /// request, or fails at the first failing operation.
346    fn readv(&mut self, reads: &[ReadOp]) -> VfResult<Vec<ReadResult>>;
347
348    /// Read into caller-owned buffers without requiring an owned result for
349    /// backends that can decode or read directly into those buffers. The
350    /// default compatibility implementation copies from [`readv`](Self::readv).
351    fn readv_into(
352        &mut self,
353        reads: &[ReadOp],
354        buffers: &mut [&mut [u8]],
355    ) -> VfResult<Vec<ReadIntoResult>> {
356        if reads.len() != buffers.len() {
357            return Err(VfError::client(0, ERR_INVAL));
358        }
359        for (index, (request, buffer)) in reads.iter().zip(buffers.iter()).enumerate() {
360            if request.length != buffer.len() {
361                return Err(VfError::client(index, ERR_INVAL));
362            }
363        }
364        let results = self.readv(reads)?;
365        validate_read_results("readv_into", reads, &results)?;
366        Ok(results
367            .into_iter()
368            .zip(buffers.iter_mut())
369            .map(|(result, buffer)| {
370                buffer[..result.data.len()].copy_from_slice(&result.data);
371                ReadIntoResult {
372                    file: result.file,
373                    offset: result.offset,
374                    read: result.data.len(),
375                    eof: result.eof,
376                }
377            })
378            .collect())
379    }
380
381    /// Read each file in full from offset 0, `tc_read_allv()`. Returns one
382    /// byte buffer per request in input order. The combined result is limited
383    /// to [`DEFAULT_READ_ALLV_MAX_TOTAL_BYTES`].
384    fn read_allv(&mut self, files: &[VfFile]) -> VfResult<Vec<Vec<u8>>> {
385        self.read_allv_with_options(files, ReadAllOptions::default())
386    }
387
388    /// Read complete files with a caller-selected aggregate memory limit.
389    fn read_allv_with_options(
390        &mut self,
391        files: &[VfFile],
392        options: ReadAllOptions,
393    ) -> VfResult<Vec<Vec<u8>>> {
394        let mut out: Vec<Vec<u8>> = files.iter().map(|_| Vec::new()).collect();
395        let mut total = 0usize;
396        let mut limit_error = None;
397        let stream_budget = options
398            .total_byte_limit()
399            .clamp(1, DEFAULT_READ_ALLV_MAX_TOTAL_BYTES);
400        let chunk_size = stream_budget.min(1024 * 1024);
401        self.read_streamv(
402            files,
403            chunk_size,
404            stream_budget,
405            &mut |index, _, data, _| {
406                let Some(next_total) = total.checked_add(data.len()) else {
407                    limit_error = Some(index);
408                    return false;
409                };
410                if next_total > options.total_byte_limit() {
411                    limit_error = Some(index);
412                    return false;
413                }
414                out[index].extend_from_slice(data);
415                total = next_total;
416                true
417            },
418        )?;
419        if let Some(index) = limit_error {
420            return Err(VfError::failure(index, libc::EFBIG as u32));
421        }
422        Ok(out)
423    }
424
425    /// Write to one or more files, `tc_writev()`. Returns one result per
426    /// request, or fails at the first failing operation.
427    fn writev(&mut self, writes: &[WriteOp]) -> VfResult<Vec<WriteResult>>;
428
429    /// Allocation-free input facade. Compatibility backends may copy before
430    /// dispatch; native backends should override this method to retain the
431    /// borrowed buffers through request encoding.
432    fn writev_borrowed(&mut self, writes: &[WriteOpRef<'_>]) -> VfResult<Vec<WriteResult>> {
433        let owned: Vec<WriteOp> = writes
434            .iter()
435            .map(|write| WriteOp {
436                file: write.file.clone(),
437                offset: write.offset,
438                data: write.data.to_vec(),
439                creation: write.creation,
440                truncate: write.truncate,
441            })
442            .collect();
443        self.writev(&owned)
444    }
445
446    /// Reposition the read/write offset of an open file, `tc_fseek()`. The
447    /// offset lives in backend state keyed by the descriptor; `tcf` is not
448    /// modified (hence `&VfFile`). Returns the new offset. Note that
449    /// [`SeekFrom::End`] needs the file size, which is an extra round trip on
450    /// backends that do not cache it.
451    fn fseek(&mut self, tcf: &VfFile, offset: i64, whence: SeekFrom) -> VfResult<i64>;
452
453    /// Get attributes of an array of files, `tc_getattrsv()`. Follows
454    /// symlinks to the target.
455    fn getattrsv(&mut self, attrs: &mut [VfAttrs]) -> VfRes;
456
457    /// Like [`getattrsv`](Self::getattrsv) but does not follow symlinks:
458    /// attributes are for the symlink itself, `tc_lgetattrsv()`.
459    fn lgetattrsv(&mut self, attrs: &mut [VfAttrs]) -> VfRes;
460
461    /// Set attributes on an array of files, `tc_setattrsv()`. Only
462    /// [`AttrMask::MODE`], [`AttrMask::SIZE`], [`AttrMask::ATIME`], and
463    /// [`AttrMask::MTIME`] are part of the portable contract; requesting any
464    /// other bit fails with [`VF_ERR_UNSUPPORTED`] at that index, and an empty
465    /// mask is a no-op. Follows symlinks to the target.
466    fn setattrsv(&mut self, attrs: &[VfAttrs]) -> VfRes;
467
468    /// Like [`setattrsv`](Self::setattrsv) but does not follow symlinks:
469    /// attributes are set on the symlink itself, `tc_lsetattrsv()`. Backends
470    /// without a non-following setter (e.g. no `lchmod` on Linux) must fail
471    /// with [`VF_ERR_UNSUPPORTED`] for symlinks rather than silently follow.
472    fn lsetattrsv(&mut self, attrs: &[VfAttrs]) -> VfRes;
473
474    /// List a directory, `tc_listdir()`. Returns entry paths and attributes.
475    ///
476    /// `max_count` limits the number of entries returned per directory (0
477    /// means no limit). Entry `VfFile`s are absolute (root-relative) paths.
478    /// With `recursive`, entries of nested directories are included depth-
479    /// first (still subject to `max_count`).
480    fn listdir(
481        &mut self,
482        dir: &Path,
483        masks: AttrMask,
484        max_count: usize,
485        recursive: bool,
486    ) -> VfResult<Vec<VfAttrs>>;
487
488    /// Fetch at most `page_size` entries using backend-owned continuation
489    /// state. `max_entries` bounds a generic backend's one-time snapshot;
490    /// zero means the caller explicitly requested an unlimited listing.
491    ///
492    /// Backends with native directory iterators or cookies should override
493    /// this default. Dropping the cursor ends enumeration without retaining
494    /// backend state.
495    fn listdir_page(
496        &mut self,
497        dir: &Path,
498        masks: AttrMask,
499        cursor: Option<DirPageCursor>,
500        page_size: usize,
501        max_entries: usize,
502    ) -> VfResult<(Vec<VfAttrs>, Option<DirPageCursor>)> {
503        if page_size == 0 {
504            return Err(VfError::client(0, ERR_INVAL));
505        }
506        let mut remaining = match cursor {
507            Some(cursor) => cursor.into_state::<std::vec::IntoIter<VfAttrs>>()?,
508            None => self.listdir(dir, masks, max_entries, false)?.into_iter(),
509        };
510        let page: Vec<_> = remaining.by_ref().take(page_size).collect();
511        let next = if remaining.len() == 0 {
512            None
513        } else {
514            Some(DirPageCursor::new(remaining))
515        };
516        Ok((page, next))
517    }
518
519    /// Safe cohort size: fallback snapshots must not accumulate across directories.
520    fn directory_page_batch_size(&self) -> usize {
521        1
522    }
523
524    /// Fetch one bounded page per directory in request order.
525    fn listdir_pages(
526        &mut self,
527        dirs: &[&Path],
528        masks: AttrMask,
529        cursors: Vec<Option<DirPageCursor>>,
530        page_size: usize,
531        max_entries: usize,
532    ) -> VfResult<Vec<BackendDirectoryPage>> {
533        if dirs.len() != cursors.len() || page_size == 0 {
534            return Err(VfError::client(0, ERR_INVAL));
535        }
536        dirs.iter()
537            .zip(cursors)
538            .enumerate()
539            .map(|(index, (dir, cursor))| {
540                self.listdir_page(dir, masks, cursor, page_size, max_entries)
541                    .map(|(entries, next)| {
542                        let children = (0..entries.len()).map(|_| None).collect();
543                        (entries, next, children)
544                    })
545                    .map_err(|error| error.with_index(index))
546            })
547            .collect()
548    }
549
550    /// Recursively enumerate `root`, returning each directory with its entries.
551    ///
552    /// `sort` orders a directory's entries the way the caller's presentation
553    /// layer would (so subdirectories are visited in the same order the caller
554    /// lists them). The default implementation recurses via
555    /// [`listdir`](Self::listdir); a backend may override it to batch many
556    /// directories into few large compounds.
557    fn walk(
558        &mut self,
559        root: &Path,
560        masks: AttrMask,
561        sort: &mut dyn FnMut(&Path, &mut Vec<VfAttrs>),
562    ) -> VfResult<Vec<WalkEntry>> {
563        self.walk_with_options(root, masks, WalkOptions::default(), sort)
564    }
565
566    /// Recursively enumerate `root` with explicit allocation and depth limits.
567    fn walk_with_options(
568        &mut self,
569        root: &Path,
570        masks: AttrMask,
571        options: WalkOptions,
572        sort: &mut dyn FnMut(&Path, &mut Vec<VfAttrs>),
573    ) -> VfResult<Vec<WalkEntry>> {
574        // Explicit stack (pre-order, subdirectories visited in the order the
575        // sort callback produced) so deep trees cannot overflow the call
576        // stack.
577        let mut out = Vec::new();
578        let mut stack = vec![(root.to_path_buf(), 0usize)];
579        let mut entry_count = 0usize;
580        let mut path_bytes = 0usize;
581        while let Some((dir, depth)) = stack.pop() {
582            let remaining = options.entry_limit().saturating_sub(entry_count);
583            let request_count = remaining.saturating_add(1);
584            let mut entries = self.listdir(&dir, masks, request_count, false)?;
585            if entries.len() > remaining {
586                return Err(
587                    VfError::failure(entry_count, libc::EFBIG as u32).with_context("walk", &dir)
588                );
589            }
590            for entry in &entries {
591                let bytes = entry.file.path().map_or(0, |path| path.as_os_str().len());
592                path_bytes = path_bytes.checked_add(bytes).ok_or_else(|| {
593                    VfError::failure(entry_count, libc::EFBIG as u32).with_context("walk", &dir)
594                })?;
595                if path_bytes > options.path_byte_limit() {
596                    return Err(VfError::failure(entry_count, libc::EFBIG as u32)
597                        .with_context("walk", &dir));
598                }
599                entry_count += 1;
600            }
601            sort(dir.as_path(), &mut entries);
602            let subdirs: Vec<PathBuf> = entries
603                .iter()
604                .filter(|e| e.ftype == VfType::Directory)
605                .filter_map(|e| e.file.path().map(|p| p.to_path_buf()))
606                .collect();
607            if !subdirs.is_empty()
608                && depth >= options.depth_limit()
609                && !options.truncates_at_depth_limit()
610            {
611                return Err(
612                    VfError::failure(entry_count, libc::EFBIG as u32).with_context("walk", &dir)
613                );
614            }
615            if depth < options.depth_limit() {
616                for s in subdirs.into_iter().rev() {
617                    stack.push((s, depth + 1));
618                }
619            }
620            out.push(WalkEntry { path: dir, entries });
621        }
622        Ok(out)
623    }
624
625    /// Rename a list of file pairs, `tc_renamev()`.
626    fn renamev(&mut self, pairs: &[(VfFile, VfFile)]) -> VfRes;
627
628    /// Remove a list of files (or empty directories), `tc_removev()`.
629    fn removev(&mut self, files: &[VfFile]) -> VfRes;
630
631    /// Create one or more directories, `tc_mkdirv()`.
632    fn mkdirv(&mut self, dirs: &[VfAttrs]) -> VfRes;
633
634    /// Create a list of symlinks, `tc_symlinkv()`.
635    fn symlinkv(&mut self, oldpaths: &[&Path], newpaths: &[&Path]) -> VfRes;
636
637    /// Read symlink targets, `tc_readlinkv()`.
638    fn readlinkv(&mut self, paths: &[&Path]) -> VfResult<Vec<Vec<u8>>>;
639
640    /// Create hard links, `tc_hardlinkv()`.
641    fn hardlinkv(&mut self, oldpaths: &[&Path], newpaths: &[&Path]) -> VfRes;
642
643    /// Copy extents by reading and writing, `tc_dupv()`. Follows symlinks
644    /// (copies the target's contents).
645    fn dupv(&mut self, pairs: &[ExtentPair]) -> VfRes;
646
647    /// Copy extents without following symlinks, `tc_lcopyv()`: symlinks are
648    /// recreated as symlinks with the same target; other objects are copied
649    /// by data (like [`dupv`](Self::dupv)).
650    fn lcopyv(&mut self, pairs: &[ExtentPair]) -> VfRes;
651
652    /// Write Application Data Blocks, `tc_write_adb()`. Returns the number
653    /// of blocks written for each ADB.
654    fn write_adb(&mut self, patterns: &[Adb]) -> VfResult<Vec<usize>>;
655
656    /// Read files in bounded chunks while retaining vectorized I/O.
657    ///
658    /// At most `memory_limit` bytes are requested in one batch and no single
659    /// request exceeds `chunk_size`. `cb` receives the original file index,
660    /// absolute offset, data, and EOF state. Returning `false` cancels the
661    /// stream successfully, providing backpressure without buffering whole
662    /// files. The callback runs while `self` is borrowed and must not reenter
663    /// this filesystem instance.
664    fn read_streamv(
665        &mut self,
666        files: &[VfFile],
667        chunk_size: usize,
668        memory_limit: usize,
669        cb: &mut ReadStreamCallback<'_>,
670    ) -> VfRes {
671        use std::collections::VecDeque;
672
673        if chunk_size == 0 || memory_limit == 0 {
674            return Err(VfError::failure(0, ERR_INVAL));
675        }
676        let mut pending: VecDeque<usize> = (0..files.len()).collect();
677        let mut offsets = vec![0u64; files.len()];
678        while !pending.is_empty() {
679            let mut budget = memory_limit;
680            let mut batch_indices = Vec::new();
681            let mut reads = Vec::new();
682            while budget > 0 && !pending.is_empty() {
683                let index = pending.pop_front().expect("pending was non-empty");
684                let length = chunk_size.min(budget);
685                reads.push(ReadOp::at(files[index].clone(), offsets[index], length));
686                batch_indices.push(index);
687                budget -= length;
688            }
689            let results = self.readv(&reads).map_err(|error| {
690                error.index().map_or(error.clone(), |local_index| {
691                    batch_indices
692                        .get(local_index)
693                        .copied()
694                        .map_or(error.clone(), |index| error.with_index(index))
695                })
696            })?;
697            validate_read_results("read_streamv", &reads, &results).map_err(|error| {
698                error.index().map_or(error.clone(), |local_index| {
699                    batch_indices
700                        .get(local_index)
701                        .copied()
702                        .map_or(error.clone(), |index| error.with_index(index))
703                })
704            })?;
705            for (batch_index, result) in results.into_iter().enumerate() {
706                let index = batch_indices[batch_index];
707                let offset = offsets[index];
708                let eof = result.eof;
709                offsets[index] = offset
710                    .checked_add(result.data.len() as u64)
711                    .ok_or_else(|| VfError::failure(index, libc::EOVERFLOW as u32))?;
712                if !cb(index, offset, &result.data, eof) {
713                    return Ok(());
714                }
715                if !eof {
716                    pending.push_back(index);
717                }
718            }
719        }
720        Ok(())
721    }
722
723    /// Remove a list of objects, recursively when `recursive`, `tc_rm()`.
724    ///
725    /// This is the fail-fast spelling of
726    /// [`rm_with_options`](Self::rm_with_options); callers wanting best-effort
727    /// semantics should use that.
728    ///
729    /// # Path entry-point race
730    ///
731    /// `objs` are paths. Between the caller naming one and the backend starting
732    /// work, a concurrent actor may replace a path component with a symbolic
733    /// link, causing an unexpected tree to be removed (the classic entry-point
734    /// TOCTOU, RUSTSEC-2023-0018). Prefer the handle-rooted
735    /// [`open_dir`](Self::open_dir) + [`rm_dir_contents`](Self::rm_dir_contents)
736    /// for privileged or attacker-influenced paths.
737    ///
738    /// The returned error index is always the operand index in `objs`.
739    fn rm(&mut self, objs: &[&Path], recursive: bool) -> VfRes {
740        self.rm_with_options(objs, recursive, RemoveOptions::default())
741    }
742
743    /// Recursive removal with explicit [`RemoveOptions`].
744    ///
745    /// The generic implementation is fail-fast and uses `objs` as paths;
746    /// backends with a native remover (NFS) override this to honor the options,
747    /// address directories by handle, and batch protocol operations.
748    /// Generic recursive removal requires no-follow metadata (`LSTAT`); a
749    /// backend that cannot provide it returns `unsupported` rather than risk
750    /// traversing a symbolic link. Non-recursive removal does not require it.
751    fn rm_with_options(
752        &mut self,
753        objs: &[&Path],
754        recursive: bool,
755        options: RemoveOptions,
756    ) -> VfRes {
757        if options != RemoveOptions::default() {
758            return Err(VfError::unsupported(0));
759        }
760        if objs.is_empty() {
761            return Ok(());
762        }
763        if !recursive {
764            let files: Vec<VfFile> = objs.iter().map(|p| VfFile::from_os_path(p)).collect();
765            return self.removev(&files);
766        }
767
768        for (root_index, root) in objs.iter().enumerate() {
769            self.before_remove_type(root_index)?;
770            // Classify the root (no-follow); a non-directory is just removed.
771            let mut root_attrs = VfAttrs {
772                file: VfFile::from_os_path(root),
773                masks: AttrMask::default(),
774                ..VfAttrs::default()
775            };
776            self.lgetattrsv(std::slice::from_mut(&mut root_attrs))
777                .map_err(|error| error.map_index(|_| root_index))?;
778            if root_attrs.ftype != VfType::Directory {
779                self.removev(&[VfFile::from_os_path(root)])
780                    .map_err(|error| error.map_index(|_| root_index))?;
781                continue;
782            }
783
784            // `dir_levels[k]` holds the directories at depth `k`; they are
785            // removed in reverse once deeper levels are gone.
786            let mut dir_levels: Vec<Vec<PathBuf>> = Vec::new();
787            let mut frontier: Vec<PathBuf> = vec![root.to_path_buf()];
788            while !frontier.is_empty() {
789                let refs: Vec<&Path> = frontier.iter().map(PathBuf::as_path).collect();
790                let mut per_dir: Vec<Vec<VfAttrs>> = (0..refs.len()).map(|_| Vec::new()).collect();
791                {
792                    let index_of: std::collections::HashMap<&Path, usize> =
793                        refs.iter().enumerate().map(|(i, p)| (*p, i)).collect();
794                    let mut cb = |attrs: &VfAttrs, dir: &Path| {
795                        if let Some(&slot) = index_of.get(dir) {
796                            per_dir[slot].push(attrs.clone());
797                        }
798                        true
799                    };
800                    self.listdirv(&refs, AttrMask::default(), 0, false, &mut cb)
801                        .map_err(|error| error.map_index(|_| root_index))?;
802                }
803
804                let mut next: Vec<PathBuf> = Vec::new();
805                for entries in per_dir {
806                    let mut files: Vec<VfFile> = Vec::new();
807                    for attrs in entries {
808                        if attrs.ftype == VfType::Directory {
809                            if let Some(path) = attrs.file.path() {
810                                next.push(path.to_path_buf());
811                            }
812                        } else {
813                            files.push(attrs.file);
814                        }
815                    }
816                    if !files.is_empty() {
817                        self.removev(&files)
818                            .map_err(|error| error.map_index(|_| root_index))?;
819                    }
820                }
821                dir_levels.push(frontier);
822                frontier = next;
823            }
824
825            for level in dir_levels.iter().rev() {
826                let dirs: Vec<VfFile> = level
827                    .iter()
828                    .map(|path| VfFile::from_os_path(path))
829                    .collect();
830                self.removev(&dirs)
831                    .map_err(|error| error.map_index(|_| root_index))?;
832            }
833        }
834        Ok(())
835    }
836
837    // -- directory handles --------------------------------------------------
838
839    /// Open a directory and return a handle suitable for
840    /// [`rm_dir_contents`](Self::rm_dir_contents).
841    ///
842    /// The directory is opened without following a final symbolic link, so a
843    /// symlink to a directory is rejected. Backends with native directory
844    /// handles (NFS) return [`VfDir::Descriptor`]; the default verifies the
845    /// type and returns [`VfDir::Path`], which callers must treat as no more
846    /// race-safe than the path API. Release a handle with
847    /// [`close_dir`](Self::close_dir).
848    fn open_dir(&mut self, path: &Path) -> VfResult<VfDir> {
849        let mut attrs = VfAttrs {
850            file: VfFile::from_os_path(path),
851            masks: AttrMask::default(),
852            ..VfAttrs::default()
853        };
854        self.lgetattrsv(std::slice::from_mut(&mut attrs))?;
855        if attrs.ftype != VfType::Directory {
856            return Err(VfError::failure(0, ERR_NOTDIR));
857        }
858        Ok(VfDir::Path(path.to_path_buf()))
859    }
860
861    /// Remove the contents of the directory referenced by `dir`, keeping the
862    /// directory itself. Because `dir` is a handle, the tree root cannot be
863    /// swapped for a symlink between opening and removal.
864    fn rm_dir_contents(&mut self, dir: &VfDir) -> VfRes {
865        self.rm_dir_contents_with_options(dir, RemoveOptions::default())
866    }
867
868    /// [`rm_dir_contents`](Self::rm_dir_contents) with explicit options.
869    fn rm_dir_contents_with_options(&mut self, dir: &VfDir, options: RemoveOptions) -> VfRes {
870        match dir {
871            VfDir::Path(path) => self.rm_contents_with_options(path, options),
872            _ => Err(VfError::unsupported(0)),
873        }
874    }
875
876    /// Release a handle obtained from [`open_dir`](Self::open_dir).
877    fn close_dir(&mut self, _dir: &VfDir) -> VfResult<()> {
878        Ok(())
879    }
880
881    /// Remove everything inside `dir`, keeping `dir` itself.
882    ///
883    /// This takes a path and is therefore subject to the entry-point race
884    /// described on [`rm`](Self::rm); prefer
885    /// [`open_dir`](Self::open_dir) + [`rm_dir_contents`](Self::rm_dir_contents).
886    fn rm_contents(&mut self, dir: &Path) -> VfRes {
887        self.rm_contents_with_options(dir, RemoveOptions::default())
888    }
889
890    /// [`rm_contents`](Self::rm_contents) with explicit options.
891    fn rm_contents_with_options(&mut self, dir: &Path, options: RemoveOptions) -> VfRes {
892        if options != RemoveOptions::default() {
893            return Err(VfError::unsupported(0));
894        }
895        let entries = self
896            .listdir(dir, AttrMask::default(), 0, false)
897            .map_err(|error| error.with_index(0))?;
898        let mut files: Vec<VfFile> = Vec::new();
899        let mut dirs: Vec<PathBuf> = Vec::new();
900        for attrs in entries {
901            if attrs.ftype == VfType::Directory {
902                if let Some(path) = attrs.file.path() {
903                    dirs.push(path.to_path_buf());
904                }
905            } else {
906                files.push(attrs.file);
907            }
908        }
909        if !files.is_empty() {
910            self.removev(&files).map_err(|error| error.with_index(0))?;
911        }
912        for sub in dirs {
913            self.rm(&[sub.as_path()], true)
914                .map_err(|error| error.with_index(0))?;
915        }
916        Ok(())
917    }
918
919    /// Make `dir` an empty directory: create it if missing, otherwise empty it.
920    ///
921    /// Errors if `dir` exists and is not a directory (including a symlink to a
922    /// directory). Subject to the same entry-point race as
923    /// [`rm_contents`](Self::rm_contents).
924    fn ensure_empty_dir(&mut self, dir: &Path) -> VfRes {
925        match self.mkdir(dir, 0o777) {
926            Ok(()) => Ok(()),
927            Err(error) if error.err_no() == ERR_EXIST => {
928                let mut attrs = VfAttrs {
929                    file: VfFile::from_os_path(dir),
930                    masks: AttrMask::default(),
931                    ..VfAttrs::default()
932                };
933                self.lgetattrsv(std::slice::from_mut(&mut attrs))?;
934                if attrs.ftype != VfType::Directory {
935                    return Err(VfError::failure(0, ERR_NOTDIR));
936                }
937                self.rm_contents(dir)
938            }
939            Err(error) => Err(error),
940        }
941    }
942
943    /// Recursively copy a directory tree, `tc_cp_recursive()`.
944    fn cp_recursive(
945        &mut self,
946        src_dir: &Path,
947        dst: &Path,
948        symlinks: bool,
949        use_server_side_copy: bool,
950    ) -> VfRes;
951
952    // -- defaults -----------------------------------------------------------
953
954    /// Resolve a [`VfFile`] to its root-relative path (no leading `/`),
955    /// honoring `VfPathBase::Abs`/`VfPathBase::Cwd` and the client's current
956    /// working directory. Descriptors, `Null`, and `Saved` are not paths and
957    /// fail with [`ERR_INVAL`]. Backends should use this when a method needs
958    /// the file's path, so a `VfFile` resolves identically across all trait
959    /// methods.
960    fn vf_path(&self, file: &VfFile) -> VfResult<PathBuf> {
961        match file {
962            VfFile::Path {
963                base: VfPathBase::Abs,
964                path,
965            } => Ok(self.abs_path(&Path::new("/").join(path))),
966            VfFile::Path {
967                base: VfPathBase::Cwd,
968                path,
969            }
970            | VfFile::CwdPath(path) => Ok(self.abs_path(path)),
971            VfFile::Cwd => Ok(self.abs_path(Path::new(""))),
972            VfFile::Descriptor(_) | VfFile::Saved => Err(VfError::failure(0, ERR_INVAL)),
973            _ => Err(VfError::failure(0, ERR_INVAL)),
974        }
975    }
976
977    /// Open a file by path, `tc_open()`.
978    fn open(&mut self, pathname: &Path, flags: i32, mode: u32) -> VfResult<VfFile> {
979        self.open_by_path(VfPathBase::Cwd, pathname, flags, mode)
980    }
981
982    /// Read from a single file at an absolute offset, `tc_read()`.
983    fn read(&mut self, file: &VfFile, offset: u64, length: usize) -> VfResult<Vec<u8>> {
984        let requests = [ReadOp::at(file.clone(), offset, length)];
985        let mut results = self.readv(&requests)?;
986        validate_read_results("read", &requests, &results)?;
987        Ok(results.pop().expect("validated one result").data)
988    }
989
990    /// Write to a single file at an absolute offset, `tc_write()`.
991    fn write(&mut self, file: &VfFile, offset: u64, data: &[u8]) -> VfResult<usize> {
992        let owned = WriteOp::at(file.clone(), offset, data.to_vec());
993        let requests = [WriteOpRef {
994            file: &owned.file,
995            offset: owned.offset,
996            data: &owned.data,
997            creation: owned.creation,
998            truncate: owned.truncate,
999        }];
1000        let mut results = self.writev(std::slice::from_ref(&owned))?;
1001        validate_write_results("write", &requests, &results)?;
1002        Ok(results.pop().expect("validated one result").written)
1003    }
1004
1005    /// Backend implementation seam for ordered opens.
1006    ///
1007    /// This is not an application-facing partial-outcome API. Entry `n`
1008    /// corresponds to request `n`; an ordered backend may stop after adding
1009    /// the first semantic failure.
1010    #[doc(hidden)]
1011    fn open_many(
1012        &mut self,
1013        paths: &[&Path],
1014        flags: &[i32],
1015        modes: &[u32],
1016    ) -> VfResult<ManyResults<VfFile>> {
1017        if paths.len() != flags.len() || paths.len() != modes.len() {
1018            return Err(VfError::failure(0, ERR_INVAL));
1019        }
1020        let mut results = Vec::with_capacity(paths.len());
1021        for ((path, flag), mode) in paths.iter().zip(flags).zip(modes) {
1022            match self.open(path, *flag, *mode) {
1023                Ok(file) => results.push(Ok(file)),
1024                Err(error) => {
1025                    results.push(Err(error));
1026                    break;
1027                }
1028            }
1029        }
1030        Ok(ManyResults::new(paths.len(), results))
1031    }
1032
1033    /// Test seam invoked immediately before a successful handle is cleaned
1034    /// after a strict `openv` failure.
1035    #[doc(hidden)]
1036    fn before_open_cleanup(&mut self, _index: usize, _file: &VfFile) -> VfResult<()> {
1037        Ok(())
1038    }
1039
1040    /// Test seam invoked before recursive removal classifies each operand.
1041    #[doc(hidden)]
1042    fn before_remove_type(&mut self, _index: usize) -> VfResult<()> {
1043        Ok(())
1044    }
1045
1046    /// Open several files at once, each with its own flags and mode,
1047    /// `tc_openv()`. `flags`, `modes`, and `paths` must have equal lengths;
1048    /// a mismatch fails with [`ERR_INVAL`] at index 0.
1049    fn openv(&mut self, paths: &[&Path], flags: &[i32], modes: &[u32]) -> VfResult<Vec<VfFile>> {
1050        let results = self.open_many(paths, flags, modes)?;
1051        results.try_collect_with_cleanup(paths.len(), |index, file| {
1052            let injected = self.before_open_cleanup(index, file);
1053            let closed = self.close(file);
1054            injected.and(closed)
1055        })
1056    }
1057
1058    /// Open several files at once with a shared flags and mode,
1059    /// `tc_openv_simple()`.
1060    fn openv_simple(&mut self, paths: &[&Path], flags: i32, mode: u32) -> VfResult<Vec<VfFile>> {
1061        let flags_v = vec![flags; paths.len()];
1062        let modes_v = vec![mode; paths.len()];
1063        self.openv(paths, &flags_v, &modes_v)
1064    }
1065
1066    /// Close several files, `tc_closev()`.
1067    fn closev(&mut self, files: &[VfFile]) -> VfRes {
1068        for (i, f) in files.iter().enumerate() {
1069            self.close(f).map_err(|e| e.with_index(i))?;
1070        }
1071        Ok(())
1072    }
1073
1074    /// Stat a path, `tc_stat()`. Follows symlinks to the target.
1075    fn stat(&mut self, path: &Path) -> VfResult<VfAttrs> {
1076        let mut a = VfAttrs {
1077            file: VfFile::from_os_path(path),
1078            masks: AttrMask::stat(),
1079            ..VfAttrs::default()
1080        };
1081        self.getattrsv(std::slice::from_mut(&mut a))?;
1082        Ok(a)
1083    }
1084
1085    /// `tc_lstat()`: like [`stat`](Self::stat) but does not follow symlinks.
1086    fn lstat(&mut self, path: &Path) -> VfResult<VfAttrs> {
1087        let mut a = VfAttrs {
1088            file: VfFile::from_os_path(path),
1089            masks: AttrMask::stat(),
1090            ..VfAttrs::default()
1091        };
1092        self.lgetattrsv(std::slice::from_mut(&mut a))?;
1093        Ok(a)
1094    }
1095
1096    /// `tc_fstat()`.
1097    fn fstat(&mut self, tcf: &VfFile) -> VfResult<VfAttrs> {
1098        let mut a = VfAttrs {
1099            file: tcf.clone(),
1100            masks: AttrMask::stat(),
1101            ..VfAttrs::default()
1102        };
1103        self.getattrsv(std::slice::from_mut(&mut a))?;
1104        Ok(a)
1105    }
1106
1107    /// Whether `path` exists, distinguishing "not found" from other errors
1108    /// (e.g. permission denied) that are returned as `Err`. Uses
1109    /// `lstat` semantics: a dangling symlink exists.
1110    fn exists(&mut self, path: &Path) -> VfResult<bool> {
1111        match self.lstat(path) {
1112            Ok(_) => Ok(true),
1113            Err(e) if e.err_no() == ERR_NOENT => Ok(false),
1114            Err(e) => Err(e),
1115        }
1116    }
1117
1118    /// Return the file type of `path` itself (`lstat` semantics: a symlink
1119    /// reports [`VfType::Symlink`] rather than its target's type).
1120    fn file_type(&mut self, path: &Path) -> VfResult<VfType> {
1121        Ok(self.lstat(path)?.ftype)
1122    }
1123
1124    /// List directories with a callback, `tc_listdirv()`. Returning `false`
1125    /// stops the listing early. Callbacks for different directories may
1126    /// interleave, and an error can leave partial entries from any directory;
1127    /// callers must not treat an indexed failure as proof that earlier
1128    /// directory callbacks were complete.
1129    fn listdirv(
1130        &mut self,
1131        dirs: &[&Path],
1132        masks: AttrMask,
1133        max_entries: usize,
1134        recursive: bool,
1135        cb: &mut dyn FnMut(&VfAttrs, &Path) -> bool,
1136    ) -> VfRes {
1137        let mut count = 0usize;
1138        for (i, d) in dirs.iter().enumerate() {
1139            if max_entries != 0 && count >= max_entries {
1140                break;
1141            }
1142            let remaining = if max_entries == 0 {
1143                0
1144            } else {
1145                max_entries - count
1146            };
1147            if !recursive {
1148                let mut stopped = false;
1149                self.visit_dir(d, masks, remaining, &mut |entry| {
1150                    count += 1;
1151                    if !cb(entry, d) {
1152                        stopped = true;
1153                        return false;
1154                    }
1155                    true
1156                })
1157                .map_err(|e| e.with_index(i))?;
1158                if stopped {
1159                    return Ok(());
1160                }
1161                continue;
1162            }
1163            let entries = self
1164                .listdir(d, masks, remaining, true)
1165                .map_err(|e| e.with_index(i))?;
1166            for e in &entries {
1167                /* A recursive list contains descendants too, so report each
1168                 * entry's actual parent rather than attributing every row to the
1169                 * original root. Backend overrides follow the same contract. */
1170                let entry_dir = if recursive {
1171                    e.file.path().and_then(Path::parent).unwrap_or(d)
1172                } else {
1173                    d
1174                };
1175                if !cb(e, entry_dir) {
1176                    return Ok(());
1177                }
1178                count += 1;
1179            }
1180        }
1181        Ok(())
1182    }
1183
1184    /// Visit one directory without requiring the application to retain its
1185    /// complete listing. Backends may override this to fetch entries page by
1186    /// page; the compatibility fallback materializes a single directory with
1187    /// [`listdir`](Self::listdir). Backends that need a strict peak-memory bound
1188    /// should override this method with a streaming implementation.
1189    /// The callback runs while the backend is borrowed and must not reenter it.
1190    fn visit_dir(
1191        &mut self,
1192        dir: &Path,
1193        masks: AttrMask,
1194        max_entries: usize,
1195        cb: &mut dyn FnMut(&VfAttrs) -> bool,
1196    ) -> VfRes {
1197        let entries = self.listdir(dir, masks, max_entries, false)?;
1198        for entry in &entries {
1199            if !cb(entry) {
1200                break;
1201            }
1202        }
1203        Ok(())
1204    }
1205
1206    /// `tc_unlink()`.
1207    fn unlink(&mut self, pathname: &Path) -> VfResult<()> {
1208        self.removev(&[VfFile::from_os_path(pathname)])
1209    }
1210
1211    /// `tc_unlinkv()`.
1212    fn unlinkv(&mut self, pathnames: &[&Path]) -> VfRes {
1213        let files: Vec<VfFile> = pathnames.iter().map(|p| VfFile::from_os_path(p)).collect();
1214        self.removev(&files)
1215    }
1216
1217    /// Create a directory, `tc_mkdir()`.
1218    fn mkdir(&mut self, path: &Path, mode: u32) -> VfResult<()> {
1219        let a = VfAttrs {
1220            file: VfFile::from_os_path(path),
1221            masks: AttrMask::MODE,
1222            mode,
1223            ..VfAttrs::default()
1224        };
1225        self.mkdirv(std::slice::from_ref(&a))
1226    }
1227
1228    /// Create a symlink, `tc_symlink()`.
1229    fn symlink(&mut self, oldpath: &Path, newpath: &Path) -> VfResult<()> {
1230        self.symlinkv(
1231            std::slice::from_ref(&oldpath),
1232            std::slice::from_ref(&newpath),
1233        )
1234    }
1235
1236    /// Read a symlink target, `tc_readlink()`.
1237    fn readlink(&mut self, path: &Path) -> VfResult<Vec<u8>> {
1238        take_single_result("readlink", self.readlinkv(std::slice::from_ref(&path))?)
1239    }
1240
1241    /// `tc_ldupv()`: same read/write extent copy as
1242    /// [`dupv`](Self::dupv). Retained for C API parity; a backend that
1243    /// distinguishes a "local" copy should override.
1244    fn ldupv(&mut self, pairs: &[ExtentPair]) -> VfRes {
1245        self.dupv(pairs)
1246    }
1247
1248    /// `tc_copyv()`: server-side copy where the backend supports it. The
1249    /// default implementation performs a client-side read/write copy via
1250    /// [`dupv`](Self::dupv); backends with a server-side COPY should
1251    /// override.
1252    fn copyv(&mut self, pairs: &[ExtentPair]) -> VfRes {
1253        self.dupv(pairs)
1254    }
1255
1256    /// Create a directory and all its ancestors, `tc_ensure_dir()`. Uses
1257    /// `mkdir` and accepts an existing directory (`EEXIST`) instead of an
1258    /// exists-then-mkdir check, avoiding the race between the two. `dir` is
1259    /// resolved against the cwd via [`abs_path`](Self::abs_path) and then
1260    /// rebuilt as an absolute (root-relative) path.
1261    fn ensure_dir(&mut self, dir: &Path, mode: u32) -> VfResult<()> {
1262        use std::path::Component;
1263        let mut so_far = PathBuf::new();
1264        for comp in self.abs_path(dir).components() {
1265            if let Component::Normal(part) = comp {
1266                so_far.push(part);
1267                let full = Path::new("/").join(&so_far);
1268                match self.mkdir(&full, mode) {
1269                    Ok(()) => {}
1270                    Err(e) if e.err_no() == ERR_EXIST => {}
1271                    Err(e) => return Err(e),
1272                }
1273            }
1274        }
1275        Ok(())
1276    }
1277}
1278
1279/// `AsRef<Path>` convenience wrappers for [`VecFs`] methods.
1280///
1281/// The object-safe [`VecFs`] trait intentionally takes `&Path`. These
1282/// wrappers accept any type that can be viewed as a path (`&str`, `String`,
1283/// `PathBuf`, `&Path`, ...) and are useful for Rust callers that do not need
1284/// dynamic dispatch.
1285pub trait VecFsExt: VecFs {
1286    fn stat_path<P: AsRef<Path>>(&mut self, path: P) -> VfResult<VfAttrs> {
1287        self.stat(path.as_ref())
1288    }
1289
1290    fn lstat_path<P: AsRef<Path>>(&mut self, path: P) -> VfResult<VfAttrs> {
1291        self.lstat(path.as_ref())
1292    }
1293
1294    fn exists_path<P: AsRef<Path>>(&mut self, path: P) -> VfResult<bool> {
1295        self.exists(path.as_ref())
1296    }
1297
1298    fn file_type_path<P: AsRef<Path>>(&mut self, path: P) -> VfResult<VfType> {
1299        self.file_type(path.as_ref())
1300    }
1301
1302    fn open_path<P: AsRef<Path>>(&mut self, path: P, flags: i32, mode: u32) -> VfResult<VfFile> {
1303        self.open(path.as_ref(), flags, mode)
1304    }
1305
1306    fn mkdir_path<P: AsRef<Path>>(&mut self, path: P, mode: u32) -> VfResult<()> {
1307        self.mkdir(path.as_ref(), mode)
1308    }
1309
1310    fn ensure_dir_path<P: AsRef<Path>>(&mut self, path: P, mode: u32) -> VfResult<()> {
1311        self.ensure_dir(path.as_ref(), mode)
1312    }
1313
1314    fn chdir_path<P: AsRef<Path>>(&mut self, path: P) -> VfResult<()> {
1315        self.chdir(path.as_ref())
1316    }
1317
1318    fn unlink_path<P: AsRef<Path>>(&mut self, path: P) -> VfResult<()> {
1319        self.unlink(path.as_ref())
1320    }
1321
1322    fn readlink_path<P: AsRef<Path>>(&mut self, path: P) -> VfResult<Vec<u8>> {
1323        self.readlink(path.as_ref())
1324    }
1325
1326    fn symlink_path<P, Q>(&mut self, oldpath: P, newpath: Q) -> VfResult<()>
1327    where
1328        P: AsRef<Path>,
1329        Q: AsRef<Path>,
1330    {
1331        self.symlink(oldpath.as_ref(), newpath.as_ref())
1332    }
1333
1334    fn listdir_path<P: AsRef<Path>>(
1335        &mut self,
1336        path: P,
1337        masks: AttrMask,
1338        max_count: usize,
1339        recursive: bool,
1340    ) -> VfResult<Vec<VfAttrs>> {
1341        self.listdir(path.as_ref(), masks, max_count, recursive)
1342    }
1343
1344    fn walk_path<P: AsRef<Path>>(
1345        &mut self,
1346        root: P,
1347        masks: AttrMask,
1348        sort: &mut dyn FnMut(&Path, &mut Vec<VfAttrs>),
1349    ) -> VfResult<Vec<WalkEntry>> {
1350        self.walk(root.as_ref(), masks, sort)
1351    }
1352
1353    fn openv_paths<P: AsRef<Path>>(
1354        &mut self,
1355        paths: &[P],
1356        flags: &[i32],
1357        modes: &[u32],
1358    ) -> VfResult<Vec<VfFile>> {
1359        let refs: Vec<&Path> = paths.iter().map(|p| p.as_ref()).collect();
1360        self.openv(&refs, flags, modes)
1361    }
1362
1363    fn openv_simple_paths<P: AsRef<Path>>(
1364        &mut self,
1365        paths: &[P],
1366        flags: i32,
1367        mode: u32,
1368    ) -> VfResult<Vec<VfFile>> {
1369        let refs: Vec<&Path> = paths.iter().map(|p| p.as_ref()).collect();
1370        self.openv_simple(&refs, flags, mode)
1371    }
1372
1373    fn unlinkv_paths<P: AsRef<Path>>(&mut self, paths: &[P]) -> VfRes {
1374        let refs: Vec<&Path> = paths.iter().map(|p| p.as_ref()).collect();
1375        self.unlinkv(&refs)
1376    }
1377
1378    fn rm_paths<P: AsRef<Path>>(&mut self, paths: &[P], recursive: bool) -> VfRes {
1379        let refs: Vec<&Path> = paths.iter().map(|p| p.as_ref()).collect();
1380        self.rm(&refs, recursive)
1381    }
1382
1383    fn rm_recursive_path<P: AsRef<Path>>(&mut self, path: P) -> VfRes {
1384        self.rm(&[path.as_ref()], true)
1385    }
1386
1387    fn rm_contents_path<P: AsRef<Path>>(&mut self, path: P) -> VfRes {
1388        self.rm_contents(path.as_ref())
1389    }
1390
1391    fn ensure_empty_dir_path<P: AsRef<Path>>(&mut self, path: P) -> VfRes {
1392        self.ensure_empty_dir(path.as_ref())
1393    }
1394}
1395
1396impl<T: VecFs + ?Sized> VecFsExt for T {}
1397
1398/// `tc_rm_recursive()`.
1399pub fn rm_recursive(fs: &mut impl VecFs, dir: &Path) -> VfRes {
1400    fs.rm(&[dir], true)
1401}
1402
1403#[cfg(test)]
1404mod contract_tests {
1405    use super::*;
1406
1407    fn assert_contract_error(error: VfError, index: Option<usize>, detail: &str) {
1408        assert_eq!(error.domain(), vfsi_core::ErrorDomain::Transport);
1409        assert_eq!(error.index(), index);
1410        assert_eq!(error.status(), None);
1411        let message = error.to_string();
1412        assert!(message.contains("backend contract violation"), "{message}");
1413        assert!(message.contains(detail), "{message}");
1414    }
1415
1416    fn request() -> ReadOp {
1417        ReadOp::at(VfFile::from_path("/file"), 0, 1)
1418    }
1419
1420    fn result() -> ReadResult {
1421        ReadResult {
1422            file: VfFile::from_path("/file"),
1423            offset: 0,
1424            data: vec![1],
1425            eof: true,
1426        }
1427    }
1428
1429    #[test]
1430    fn read_result_cardinality_is_checked_before_indexing() {
1431        let requests = [request(), request()];
1432        validate_read_results("test", &requests, &[result(), result()]).unwrap();
1433        validate_read_results("test", &[], &[]).unwrap();
1434        assert_contract_error(
1435            validate_read_results("test", &requests, &[result()]).unwrap_err(),
1436            None,
1437            "returned 1 results for 2 requests",
1438        );
1439        assert_contract_error(
1440            validate_read_results("test", &requests[..1], &[result(), result()]).unwrap_err(),
1441            None,
1442            "returned 2 results for 1 requests",
1443        );
1444    }
1445
1446    #[test]
1447    fn read_result_identity_offset_progress_and_size_are_checked() {
1448        let request = request();
1449        validate_read_results("test", std::slice::from_ref(&request), &[result()]).unwrap();
1450        let larger = ReadOp::at(VfFile::from_path("/file"), 0, 4);
1451        validate_read_results(
1452            "test",
1453            &[larger],
1454            &[ReadResult {
1455                eof: false,
1456                ..result()
1457            }],
1458        )
1459        .unwrap();
1460        validate_read_results(
1461            "test",
1462            std::slice::from_ref(&request),
1463            &[ReadResult {
1464                data: Vec::new(),
1465                eof: true,
1466                ..result()
1467            }],
1468        )
1469        .unwrap();
1470        for malformed in [
1471            ReadResult {
1472                file: VfFile::from_path("/other"),
1473                ..result()
1474            },
1475            ReadResult {
1476                offset: 1,
1477                ..result()
1478            },
1479            ReadResult {
1480                data: vec![1, 2],
1481                ..result()
1482            },
1483            ReadResult {
1484                data: Vec::new(),
1485                eof: false,
1486                ..result()
1487            },
1488        ] {
1489            // Keep a valid prefix so attribution must identify the second
1490            // request rather than always returning request zero.
1491            assert_contract_error(
1492                validate_read_results(
1493                    "test",
1494                    &[request.clone(), request.clone()],
1495                    &[result(), malformed],
1496                )
1497                .unwrap_err(),
1498                Some(1),
1499                "test",
1500            );
1501        }
1502    }
1503
1504    #[test]
1505    fn write_result_identity_offset_size_and_cardinality_are_checked() {
1506        let file = VfFile::from_path("/file");
1507        let request = WriteOpRef::new(&file, VfOffset::At(0), b"x");
1508        let valid = WriteResult {
1509            file: file.clone(),
1510            offset: 0,
1511            written: 1,
1512            stable: true,
1513        };
1514        validate_write_results("test", &[request], std::slice::from_ref(&valid)).unwrap();
1515        // Partial/zero progress is a valid backend result; write_allv owns
1516        // the policy for completing it or rejecting a no-progress loop.
1517        validate_write_results(
1518            "test",
1519            &[request],
1520            &[WriteResult {
1521                written: 0,
1522                ..valid.clone()
1523            }],
1524        )
1525        .unwrap();
1526        validate_write_results("test", &[], &[]).unwrap();
1527        assert_contract_error(
1528            validate_write_results("test", &[request], &[]).unwrap_err(),
1529            None,
1530            "returned 0 results",
1531        );
1532        assert_contract_error(
1533            validate_write_results("test", &[request], &[valid.clone(), valid.clone()])
1534                .unwrap_err(),
1535            None,
1536            "returned 2 results",
1537        );
1538        for malformed in [
1539            WriteResult {
1540                file: VfFile::from_path("/other"),
1541                ..valid.clone()
1542            },
1543            WriteResult {
1544                offset: 1,
1545                ..valid.clone()
1546            },
1547            WriteResult {
1548                written: 2,
1549                ..valid.clone()
1550            },
1551        ] {
1552            assert_contract_error(
1553                validate_write_results("test", &[request, request], &[valid.clone(), malformed])
1554                    .unwrap_err(),
1555                Some(1),
1556                "test",
1557            );
1558        }
1559    }
1560
1561    #[test]
1562    fn scalar_result_cardinality_is_checked_without_panicking() {
1563        assert_contract_error(
1564            take_single_result::<u8>("readlink", Vec::new()).unwrap_err(),
1565            None,
1566            "readlink",
1567        );
1568        assert_contract_error(
1569            take_single_result("readlink", vec![1u8, 2]).unwrap_err(),
1570            None,
1571            "readlink",
1572        );
1573        assert_eq!(take_single_result("readlink", vec![7u8]).unwrap(), 7);
1574    }
1575}
1576
1577#[cfg(test)]
1578mod walk_depth_tests {
1579    use super::{DepthLimit, WalkOptions};
1580    #[test]
1581    fn depth_limit_is_one_byte_and_preserves_optional_inheritance() {
1582        const UNLIMITED: DepthLimit = DepthLimit::new(201);
1583        assert_eq!(std::mem::size_of::<DepthLimit>(), 1);
1584        assert_eq!(std::mem::size_of::<Option<DepthLimit>>(), 1);
1585        assert!(UNLIMITED.is_unlimited());
1586        for depth in 0..=200 {
1587            let limit = DepthLimit::new(depth);
1588            assert!(!limit.is_unlimited());
1589            assert_eq!(limit.get(), depth);
1590        }
1591        for depth in [201, 255, 256, 500, usize::MAX - 1, usize::MAX] {
1592            assert_eq!(DepthLimit::new(depth), DepthLimit::unlimited());
1593        }
1594        assert_ne!(None, Some(DepthLimit::new(0)));
1595        assert_ne!(None, Some(DepthLimit::unlimited()));
1596    }
1597    #[test]
1598    fn depth_limits_normalize_without_wrapping_and_keep_other_options() {
1599        const UNLIMITED: WalkOptions = WalkOptions::new().max_depth(201);
1600        assert_eq!(UNLIMITED.depth_limit(), usize::MAX);
1601        for depth in 0..=200 {
1602            let options = WalkOptions::new()
1603                .max_depth(depth)
1604                .max_entries(7)
1605                .max_path_bytes(11)
1606                .truncate_at_max_depth(true);
1607            assert_eq!(options.depth_limit(), depth);
1608            assert_eq!(options.entry_limit(), 7);
1609            assert_eq!(options.path_byte_limit(), 11);
1610            assert!(options.truncates_at_depth_limit());
1611        }
1612        for depth in [201, 254, 255, 256, 500, usize::MAX - 1, usize::MAX] {
1613            let options = WalkOptions::new().max_depth(depth);
1614            assert_eq!(options.depth_limit(), usize::MAX);
1615            assert_eq!(options.max_depth(0).depth_limit(), 0);
1616            assert_eq!(options.max_depth(200).depth_limit(), 200);
1617        }
1618        assert_eq!(WalkOptions::unlimited().depth_limit(), usize::MAX);
1619    }
1620}