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