Skip to main content

vfsi_sync/
traits.rs

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