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 aggregate payload limit for [`VecFs::read_allv`].
8pub const DEFAULT_READ_ALLV_MAX_TOTAL_BYTES: usize = DEFAULT_READ_MAX_BYTES;
9
10/// Default maximum number of entries returned by allocating directory APIs.
11pub const DEFAULT_DIRECTORY_MAX_ENTRIES: usize = 100_000;
12
13/// Default combined path-storage budget for allocating directory APIs.
14pub const DEFAULT_DIRECTORY_MAX_PATH_BYTES: usize = 16 * 1024 * 1024;
15
16/// Default recursion depth for [`VecFs::walk`].
17pub const DEFAULT_WALK_MAX_DEPTH: usize = 128;
18
19/// Resource limits for one allocating directory listing.
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
21pub struct ReadDirOptions {
22    max_entries: usize,
23    max_path_bytes: usize,
24}
25
26impl ReadDirOptions {
27    pub const fn new() -> Self {
28        Self {
29            max_entries: DEFAULT_DIRECTORY_MAX_ENTRIES,
30            max_path_bytes: DEFAULT_DIRECTORY_MAX_PATH_BYTES,
31        }
32    }
33
34    /// Explicitly opt out of the default allocation limits.
35    pub const fn unlimited() -> Self {
36        Self {
37            max_entries: usize::MAX,
38            max_path_bytes: usize::MAX,
39        }
40    }
41
42    pub const fn max_entries(mut self, entries: usize) -> Self {
43        self.max_entries = entries;
44        self
45    }
46
47    pub const fn max_path_bytes(mut self, bytes: usize) -> Self {
48        self.max_path_bytes = bytes;
49        self
50    }
51
52    pub const fn entry_limit(self) -> usize {
53        self.max_entries
54    }
55
56    pub const fn path_byte_limit(self) -> usize {
57        self.max_path_bytes
58    }
59}
60
61impl Default for ReadDirOptions {
62    fn default() -> Self {
63        Self::new()
64    }
65}
66
67/// Resource limits for an allocating recursive directory walk.
68#[derive(Clone, Copy, Debug, PartialEq, Eq)]
69pub struct WalkOptions {
70    directory: ReadDirOptions,
71    max_depth: usize,
72}
73
74impl WalkOptions {
75    pub const fn new() -> Self {
76        Self {
77            directory: ReadDirOptions::new(),
78            max_depth: DEFAULT_WALK_MAX_DEPTH,
79        }
80    }
81
82    /// Explicitly opt out of the default allocation and recursion limits.
83    pub const fn unlimited() -> Self {
84        Self {
85            directory: ReadDirOptions::unlimited(),
86            max_depth: usize::MAX,
87        }
88    }
89
90    pub const fn max_entries(mut self, entries: usize) -> Self {
91        self.directory = self.directory.max_entries(entries);
92        self
93    }
94
95    pub const fn max_path_bytes(mut self, bytes: usize) -> Self {
96        self.directory = self.directory.max_path_bytes(bytes);
97        self
98    }
99
100    pub const fn max_depth(mut self, depth: usize) -> Self {
101        self.max_depth = depth;
102        self
103    }
104
105    pub const fn entry_limit(self) -> usize {
106        self.directory.entry_limit()
107    }
108
109    pub const fn path_byte_limit(self) -> usize {
110        self.directory.path_byte_limit()
111    }
112
113    pub const fn depth_limit(self) -> usize {
114        self.max_depth
115    }
116}
117
118impl Default for WalkOptions {
119    fn default() -> Self {
120        Self::new()
121    }
122}
123
124/// Resource limits for reading multiple complete files into memory.
125#[derive(Clone, Copy, Debug, PartialEq, Eq)]
126pub struct ReadAllOptions {
127    max_total_bytes: usize,
128}
129
130impl ReadAllOptions {
131    pub const fn new() -> Self {
132        Self {
133            max_total_bytes: DEFAULT_READ_ALLV_MAX_TOTAL_BYTES,
134        }
135    }
136
137    /// Set the maximum combined size of all returned buffers.
138    pub const fn max_total_bytes(mut self, bytes: usize) -> Self {
139        self.max_total_bytes = bytes;
140        self
141    }
142
143    pub const fn total_byte_limit(self) -> usize {
144        self.max_total_bytes
145    }
146}
147
148impl Default for ReadAllOptions {
149    fn default() -> Self {
150        Self::new()
151    }
152}
153
154fn contract_error(
155    operation: &str,
156    index: Option<usize>,
157    detail: impl std::fmt::Display,
158) -> VfError {
159    VfError::transport(
160        index,
161        format!("{operation} backend contract violation: {detail}"),
162    )
163}
164
165fn take_single_result<T>(operation: &str, mut results: Vec<T>) -> VfResult<T> {
166    if results.len() != 1 {
167        return Err(contract_error(
168            operation,
169            None,
170            format!("returned {} results for one request", results.len()),
171        ));
172    }
173    Ok(results.pop().expect("validated one result"))
174}
175
176pub(crate) fn validate_read_results(
177    operation: &str,
178    requests: &[ReadOp],
179    results: &[ReadResult],
180) -> VfResult<()> {
181    if results.len() != requests.len() {
182        return Err(contract_error(
183            operation,
184            None,
185            format!(
186                "returned {} results for {} requests",
187                results.len(),
188                requests.len()
189            ),
190        ));
191    }
192    for (index, (request, result)) in requests.iter().zip(results).enumerate() {
193        if result.file != request.file {
194            return Err(contract_error(
195                operation,
196                Some(index),
197                "result file does not match request",
198            ));
199        }
200        if result.data.len() > request.length {
201            return Err(contract_error(
202                operation,
203                Some(index),
204                format!(
205                    "returned {} bytes for a {}-byte read",
206                    result.data.len(),
207                    request.length
208                ),
209            ));
210        }
211        if let VfOffset::At(expected) = request.offset
212            && result.offset != expected
213        {
214            return Err(contract_error(
215                operation,
216                Some(index),
217                format!("result offset {} does not match {expected}", result.offset),
218            ));
219        }
220        if request.length != 0 && result.data.is_empty() && !result.eof {
221            return Err(contract_error(
222                operation,
223                Some(index),
224                "read made no progress without reporting EOF",
225            ));
226        }
227    }
228    Ok(())
229}
230
231pub(crate) fn validate_write_results(
232    operation: &str,
233    requests: &[WriteOpRef<'_>],
234    results: &[WriteResult],
235) -> VfResult<()> {
236    if results.len() != requests.len() {
237        return Err(contract_error(
238            operation,
239            None,
240            format!(
241                "returned {} results for {} requests",
242                results.len(),
243                requests.len()
244            ),
245        ));
246    }
247    for (index, (request, result)) in requests.iter().zip(results).enumerate() {
248        if result.file != *request.file {
249            return Err(contract_error(
250                operation,
251                Some(index),
252                "result file does not match request",
253            ));
254        }
255        if result.written > request.data.len() {
256            return Err(contract_error(
257                operation,
258                Some(index),
259                format!(
260                    "reported {} bytes written for a {}-byte write",
261                    result.written,
262                    request.data.len()
263                ),
264            ));
265        }
266        if let VfOffset::At(expected) = request.offset
267            && result.offset != expected
268        {
269            return Err(contract_error(
270                operation,
271                Some(index),
272                format!("result offset {} does not match {expected}", result.offset),
273            ));
274        }
275    }
276    Ok(())
277}
278
279/// A vectorized filesystem: many small operations coalesced into as few
280/// round trips as the backend supports.
281///
282/// `VfFile` references files either by descriptor (an open file) or by path
283/// (absolute, or relative to the client's current working directory).
284///
285/// # Partial success
286///
287/// Vector calls are ordered batches, not transactions. On an indexed error,
288/// operations before the failing index may already have completed and later
289/// operations may not have been attempted. In particular, a transport error
290/// after a mutating request was sent can leave its outcome unknown. Backends
291/// must not silently replay such mutations; callers that retry must first
292/// reconcile state or use application-level idempotency.
293pub trait VecFs {
294    /// Negotiated NFS minor version, or `None` for non-NFS backends.
295    #[deprecated(note = "use the NFS backend's NfsExtensions trait")]
296    fn nfs_minorversion(&self) -> Option<u32> {
297        None
298    }
299
300    /// Negotiated SMB dialect revision (for example `0x0311` for SMB 3.1.1),
301    /// or `None` for non-SMB backends.
302    #[deprecated(note = "use the SMB backend's SmbExtensions trait")]
303    fn smb_dialect(&self) -> Option<u16> {
304        None
305    }
306
307    /// Backend capability bits. Capabilities may change after a server
308    /// rejects an optional operation and the client installs a fallback.
309    fn capabilities(&self) -> u64 {
310        0
311    }
312
313    /// Strongly typed capability query used by the Rust-native API.
314    fn typed_capabilities(&self) -> Capabilities {
315        Capabilities::from_bits_retain(self.capabilities())
316    }
317
318    // -- required -----------------------------------------------------------
319
320    /// Return the root-relative form of `path` (resolving it against the
321    /// client's current working directory if it is relative), without a
322    /// leading `/`. [`getcwd`](Self::getcwd) is the display form (with a
323    /// leading `/`); [`vf_path`](Self::vf_path) resolves a [`VfFile`] the same
324    /// way while honoring its [`VfPathBase`].
325    fn abs_path(&self, path: &Path) -> PathBuf;
326
327    /// Open a file by path, similar to `tc_open_by_path(2)`. `base` is
328    /// `VfPathBase::Cwd` or `VfPathBase::Abs`. When `O_CREAT` is set, `mode`
329    /// is applied to the new file.
330    fn open_by_path(
331        &mut self,
332        base: VfPathBase,
333        pathname: &Path,
334        flags: i32,
335        mode: u32,
336    ) -> VfResult<VfFile>;
337
338    /// Close an open file, `tc_close()`.
339    fn close(&mut self, tcf: &VfFile) -> VfResult<()>;
340
341    /// Make prior writes to this descriptor durable. Backends whose writes
342    /// are always stable still validate the handle before returning success.
343    fn sync_data(&mut self, tcf: &VfFile) -> VfResult<()>;
344
345    /// Make file data and metadata durable.
346    fn sync_all(&mut self, tcf: &VfFile) -> VfResult<()> {
347        self.sync_data(tcf)
348    }
349
350    /// Change the client's current directory, `tc_chdir()`.
351    fn chdir(&mut self, path: &Path) -> VfResult<()>;
352
353    /// Current working directory, `tc_getcwd()`.
354    fn getcwd(&self) -> PathBuf;
355
356    /// Read from one or more files, `tc_readv()`. Returns one result per
357    /// request, or fails at the first failing operation.
358    fn readv(&mut self, reads: &[ReadOp]) -> VfResult<Vec<ReadResult>>;
359
360    /// Read each file in full from offset 0, `tc_read_allv()`. Returns one
361    /// byte buffer per request in input order. The combined result is limited
362    /// to [`DEFAULT_READ_ALLV_MAX_TOTAL_BYTES`].
363    fn read_allv(&mut self, files: &[VfFile]) -> VfResult<Vec<Vec<u8>>> {
364        self.read_allv_with_options(files, ReadAllOptions::default())
365    }
366
367    /// Read complete files with a caller-selected aggregate memory limit.
368    fn read_allv_with_options(
369        &mut self,
370        files: &[VfFile],
371        options: ReadAllOptions,
372    ) -> VfResult<Vec<Vec<u8>>> {
373        let mut out: Vec<Vec<u8>> = files.iter().map(|_| Vec::new()).collect();
374        let mut total = 0usize;
375        let mut limit_error = None;
376        let stream_budget = options
377            .total_byte_limit()
378            .clamp(1, DEFAULT_READ_ALLV_MAX_TOTAL_BYTES);
379        let chunk_size = stream_budget.min(1024 * 1024);
380        self.read_streamv(
381            files,
382            chunk_size,
383            stream_budget,
384            &mut |index, _, data, _| {
385                let Some(next_total) = total.checked_add(data.len()) else {
386                    limit_error = Some(index);
387                    return false;
388                };
389                if next_total > options.total_byte_limit() {
390                    limit_error = Some(index);
391                    return false;
392                }
393                out[index].extend_from_slice(data);
394                total = next_total;
395                true
396            },
397        )?;
398        if let Some(index) = limit_error {
399            return Err(VfError::failure(index, libc::EFBIG as u32));
400        }
401        Ok(out)
402    }
403
404    /// Write to one or more files, `tc_writev()`. Returns one result per
405    /// request, or fails at the first failing operation.
406    fn writev(&mut self, writes: &[WriteOp]) -> VfResult<Vec<WriteResult>>;
407
408    /// Allocation-free input facade. Compatibility backends may copy before
409    /// dispatch; native backends should override this method to retain the
410    /// borrowed buffers through request encoding.
411    fn writev_borrowed(&mut self, writes: &[WriteOpRef<'_>]) -> VfResult<Vec<WriteResult>> {
412        let owned: Vec<WriteOp> = writes
413            .iter()
414            .map(|write| WriteOp {
415                file: write.file.clone(),
416                offset: write.offset,
417                data: write.data.to_vec(),
418                creation: write.creation,
419                truncate: write.truncate,
420            })
421            .collect();
422        self.writev(&owned)
423    }
424
425    /// Reposition the read/write offset of an open file, `tc_fseek()`. The
426    /// offset lives in backend state keyed by the descriptor; `tcf` is not
427    /// modified (hence `&VfFile`). Returns the new offset. Note that
428    /// [`SeekFrom::End`] needs the file size, which is an extra round trip on
429    /// backends that do not cache it.
430    fn fseek(&mut self, tcf: &VfFile, offset: i64, whence: SeekFrom) -> VfResult<i64>;
431
432    /// Get attributes of an array of files, `tc_getattrsv()`. Follows
433    /// symlinks to the target.
434    fn getattrsv(&mut self, attrs: &mut [VfAttrs]) -> VfRes;
435
436    /// Like [`getattrsv`](Self::getattrsv) but does not follow symlinks:
437    /// attributes are for the symlink itself, `tc_lgetattrsv()`.
438    fn lgetattrsv(&mut self, attrs: &mut [VfAttrs]) -> VfRes;
439
440    /// Set attributes on an array of files, `tc_setattrsv()`. Only
441    /// [`AttrMask::MODE`], [`AttrMask::SIZE`], [`AttrMask::ATIME`], and
442    /// [`AttrMask::MTIME`] are part of the portable contract; requesting any
443    /// other bit fails with [`VF_ERR_UNSUPPORTED`] at that index, and an empty
444    /// mask is a no-op. Follows symlinks to the target.
445    fn setattrsv(&mut self, attrs: &[VfAttrs]) -> VfRes;
446
447    /// Like [`setattrsv`](Self::setattrsv) but does not follow symlinks:
448    /// attributes are set on the symlink itself, `tc_lsetattrsv()`. Backends
449    /// without a non-following setter (e.g. no `lchmod` on Linux) must fail
450    /// with [`VF_ERR_UNSUPPORTED`] for symlinks rather than silently follow.
451    fn lsetattrsv(&mut self, attrs: &[VfAttrs]) -> VfRes;
452
453    /// List a directory, `tc_listdir()`. Returns entry paths and attributes.
454    ///
455    /// `max_count` limits the number of entries returned per directory (0
456    /// means no limit). Entry `VfFile`s are absolute (root-relative) paths.
457    /// With `recursive`, entries of nested directories are included depth-
458    /// first (still subject to `max_count`).
459    fn listdir(
460        &mut self,
461        dir: &Path,
462        masks: AttrMask,
463        max_count: usize,
464        recursive: bool,
465    ) -> VfResult<Vec<VfAttrs>>;
466
467    /// Recursively enumerate `root`, returning each directory with its entries.
468    ///
469    /// `sort` orders a directory's entries the way the caller's presentation
470    /// layer would (so subdirectories are visited in the same order the caller
471    /// lists them). The default implementation recurses via
472    /// [`listdir`](Self::listdir); a backend may override it to batch many
473    /// directories into few large compounds.
474    fn walk(
475        &mut self,
476        root: &Path,
477        masks: AttrMask,
478        sort: &mut dyn FnMut(&Path, &mut Vec<VfAttrs>),
479    ) -> VfResult<Vec<WalkEntry>> {
480        self.walk_with_options(root, masks, WalkOptions::default(), sort)
481    }
482
483    /// Recursively enumerate `root` with explicit allocation and depth limits.
484    fn walk_with_options(
485        &mut self,
486        root: &Path,
487        masks: AttrMask,
488        options: WalkOptions,
489        sort: &mut dyn FnMut(&Path, &mut Vec<VfAttrs>),
490    ) -> VfResult<Vec<WalkEntry>> {
491        // Explicit stack (pre-order, subdirectories visited in the order the
492        // sort callback produced) so deep trees cannot overflow the call
493        // stack.
494        let mut out = Vec::new();
495        let mut stack = vec![(root.to_path_buf(), 0usize)];
496        let mut entry_count = 0usize;
497        let mut path_bytes = 0usize;
498        while let Some((dir, depth)) = stack.pop() {
499            let remaining = options.entry_limit().saturating_sub(entry_count);
500            let request_count = remaining.saturating_add(1);
501            let mut entries = self.listdir(&dir, masks, request_count, false)?;
502            if entries.len() > remaining {
503                return Err(
504                    VfError::failure(entry_count, libc::EFBIG as u32).with_context("walk", &dir)
505                );
506            }
507            for entry in &entries {
508                let bytes = entry.file.path().map_or(0, |path| path.as_os_str().len());
509                path_bytes = path_bytes.checked_add(bytes).ok_or_else(|| {
510                    VfError::failure(entry_count, libc::EFBIG as u32).with_context("walk", &dir)
511                })?;
512                if path_bytes > options.path_byte_limit() {
513                    return Err(VfError::failure(entry_count, libc::EFBIG as u32)
514                        .with_context("walk", &dir));
515                }
516                entry_count += 1;
517            }
518            sort(dir.as_path(), &mut entries);
519            let subdirs: Vec<PathBuf> = entries
520                .iter()
521                .filter(|e| e.ftype == VfType::Directory)
522                .filter_map(|e| e.file.path().map(|p| p.to_path_buf()))
523                .collect();
524            if !subdirs.is_empty() && depth >= options.depth_limit() {
525                return Err(
526                    VfError::failure(entry_count, libc::EFBIG as u32).with_context("walk", &dir)
527                );
528            }
529            for s in subdirs.into_iter().rev() {
530                stack.push((s, depth + 1));
531            }
532            out.push(WalkEntry { path: dir, entries });
533        }
534        Ok(out)
535    }
536
537    /// Rename a list of file pairs, `tc_renamev()`.
538    fn renamev(&mut self, pairs: &[(VfFile, VfFile)]) -> VfRes;
539
540    /// Remove a list of files (or empty directories), `tc_removev()`.
541    fn removev(&mut self, files: &[VfFile]) -> VfRes;
542
543    /// Create one or more directories, `tc_mkdirv()`.
544    fn mkdirv(&mut self, dirs: &[VfAttrs]) -> VfRes;
545
546    /// Create a list of symlinks, `tc_symlinkv()`.
547    fn symlinkv(&mut self, oldpaths: &[&Path], newpaths: &[&Path]) -> VfRes;
548
549    /// Read symlink targets, `tc_readlinkv()`.
550    fn readlinkv(&mut self, paths: &[&Path]) -> VfResult<Vec<Vec<u8>>>;
551
552    /// Create hard links, `tc_hardlinkv()`.
553    fn hardlinkv(&mut self, oldpaths: &[&Path], newpaths: &[&Path]) -> VfRes;
554
555    /// Copy extents by reading and writing, `tc_dupv()`. Follows symlinks
556    /// (copies the target's contents).
557    fn dupv(&mut self, pairs: &[ExtentPair]) -> VfRes;
558
559    /// Copy extents without following symlinks, `tc_lcopyv()`: symlinks are
560    /// recreated as symlinks with the same target; other objects are copied
561    /// by data (like [`dupv`](Self::dupv)).
562    fn lcopyv(&mut self, pairs: &[ExtentPair]) -> VfRes;
563
564    /// Write Application Data Blocks, `tc_write_adb()`. Returns the number
565    /// of blocks written for each ADB.
566    fn write_adb(&mut self, patterns: &[Adb]) -> VfResult<Vec<usize>>;
567
568    /// Read files in bounded chunks while retaining vectorized I/O.
569    ///
570    /// At most `memory_limit` bytes are requested in one batch and no single
571    /// request exceeds `chunk_size`. `cb` receives the original file index,
572    /// absolute offset, data, and EOF state. Returning `false` cancels the
573    /// stream successfully, providing backpressure without buffering whole
574    /// files. The callback runs while `self` is borrowed and must not reenter
575    /// this filesystem instance.
576    fn read_streamv(
577        &mut self,
578        files: &[VfFile],
579        chunk_size: usize,
580        memory_limit: usize,
581        cb: &mut ReadStreamCallback<'_>,
582    ) -> VfRes {
583        use std::collections::VecDeque;
584
585        if chunk_size == 0 || memory_limit == 0 {
586            return Err(VfError::failure(0, ERR_INVAL));
587        }
588        let mut pending: VecDeque<usize> = (0..files.len()).collect();
589        let mut offsets = vec![0u64; files.len()];
590        while !pending.is_empty() {
591            let mut budget = memory_limit;
592            let mut batch_indices = Vec::new();
593            let mut reads = Vec::new();
594            while budget > 0 && !pending.is_empty() {
595                let index = pending.pop_front().expect("pending was non-empty");
596                let length = chunk_size.min(budget);
597                reads.push(ReadOp::at(files[index].clone(), offsets[index], length));
598                batch_indices.push(index);
599                budget -= length;
600            }
601            let results = self.readv(&reads).map_err(|error| {
602                error.index_opt().map_or(error.clone(), |local_index| {
603                    batch_indices
604                        .get(local_index)
605                        .copied()
606                        .map_or(error.clone(), |index| error.with_index(index))
607                })
608            })?;
609            validate_read_results("read_streamv", &reads, &results).map_err(|error| {
610                error.index_opt().map_or(error.clone(), |local_index| {
611                    batch_indices
612                        .get(local_index)
613                        .copied()
614                        .map_or(error.clone(), |index| error.with_index(index))
615                })
616            })?;
617            for (batch_index, result) in results.into_iter().enumerate() {
618                let index = batch_indices[batch_index];
619                let offset = offsets[index];
620                let eof = result.eof;
621                offsets[index] = offset
622                    .checked_add(result.data.len() as u64)
623                    .ok_or_else(|| VfError::failure(index, libc::EOVERFLOW as u32))?;
624                if !cb(index, offset, &result.data, eof) {
625                    return Ok(());
626                }
627                if !eof {
628                    pending.push_back(index);
629                }
630            }
631        }
632        Ok(())
633    }
634
635    /// Remove a list of objects, recursively when `recursive`, `tc_rm()`.
636    fn rm(&mut self, objs: &[&Path], recursive: bool) -> VfRes;
637
638    /// Recursively copy a directory tree, `tc_cp_recursive()`.
639    fn cp_recursive(
640        &mut self,
641        src_dir: &Path,
642        dst: &Path,
643        symlinks: bool,
644        use_server_side_copy: bool,
645    ) -> VfRes;
646
647    // -- defaults -----------------------------------------------------------
648
649    /// Resolve a [`VfFile`] to its root-relative path (no leading `/`),
650    /// honoring `VfPathBase::Abs`/`VfPathBase::Cwd` and the client's current
651    /// working directory. Descriptors, `Null`, and `Saved` are not paths and
652    /// fail with [`ERR_INVAL`]. Backends should use this when a method needs
653    /// the file's path, so a `VfFile` resolves identically across all trait
654    /// methods.
655    fn vf_path(&self, file: &VfFile) -> VfResult<PathBuf> {
656        match file {
657            VfFile::Path {
658                base: VfPathBase::Abs,
659                path,
660            } => Ok(self.abs_path(&Path::new("/").join(path))),
661            VfFile::Path {
662                base: VfPathBase::Cwd,
663                path,
664            }
665            | VfFile::CwdPath(path) => Ok(self.abs_path(path)),
666            VfFile::Cwd => Ok(self.abs_path(Path::new(""))),
667            VfFile::Descriptor(_) | VfFile::Saved => Err(VfError::failure(0, ERR_INVAL)),
668            _ => Err(VfError::failure(0, ERR_INVAL)),
669        }
670    }
671
672    /// Open a file by path, `tc_open()`.
673    fn open(&mut self, pathname: &Path, flags: i32, mode: u32) -> VfResult<VfFile> {
674        self.open_by_path(VfPathBase::Cwd, pathname, flags, mode)
675    }
676
677    /// Read from a single file at an absolute offset, `tc_read()`.
678    fn read(&mut self, file: &VfFile, offset: u64, length: usize) -> VfResult<Vec<u8>> {
679        let requests = [ReadOp::at(file.clone(), offset, length)];
680        let mut results = self.readv(&requests)?;
681        validate_read_results("read", &requests, &results)?;
682        Ok(results.pop().expect("validated one result").data)
683    }
684
685    /// Write to a single file at an absolute offset, `tc_write()`.
686    fn write(&mut self, file: &VfFile, offset: u64, data: &[u8]) -> VfResult<usize> {
687        let owned = WriteOp::at(file.clone(), offset, data.to_vec());
688        let requests = [WriteOpRef {
689            file: &owned.file,
690            offset: owned.offset,
691            data: &owned.data,
692            creation: owned.creation,
693            truncate: owned.truncate,
694        }];
695        let mut results = self.writev(std::slice::from_ref(&owned))?;
696        validate_write_results("write", &requests, &results)?;
697        Ok(results.pop().expect("validated one result").written)
698    }
699
700    /// Backend implementation seam for ordered opens.
701    ///
702    /// This is not an application-facing partial-outcome API. Entry `n`
703    /// corresponds to request `n`; an ordered backend may stop after adding
704    /// the first semantic failure.
705    #[doc(hidden)]
706    fn open_many(
707        &mut self,
708        paths: &[&Path],
709        flags: &[i32],
710        modes: &[u32],
711    ) -> VfResult<ManyResults<VfFile>> {
712        if paths.len() != flags.len() || paths.len() != modes.len() {
713            return Err(VfError::failure(0, ERR_INVAL));
714        }
715        let mut results = Vec::with_capacity(paths.len());
716        for ((path, flag), mode) in paths.iter().zip(flags).zip(modes) {
717            match self.open(path, *flag, *mode) {
718                Ok(file) => results.push(Ok(file)),
719                Err(error) => {
720                    results.push(Err(error));
721                    break;
722                }
723            }
724        }
725        Ok(ManyResults::new(paths.len(), results))
726    }
727
728    /// Test seam invoked immediately before a successful handle is cleaned
729    /// after a strict `openv` failure.
730    #[doc(hidden)]
731    fn before_open_cleanup(&mut self, _index: usize, _file: &VfFile) -> VfResult<()> {
732        Ok(())
733    }
734
735    /// Open several files at once, each with its own flags and mode,
736    /// `tc_openv()`. `flags`, `modes`, and `paths` must have equal lengths;
737    /// a mismatch fails with [`ERR_INVAL`] at index 0.
738    fn openv(&mut self, paths: &[&Path], flags: &[i32], modes: &[u32]) -> VfResult<Vec<VfFile>> {
739        let results = self.open_many(paths, flags, modes)?;
740        results.try_collect_with_cleanup(paths.len(), |index, file| {
741            let injected = self.before_open_cleanup(index, file);
742            let closed = self.close(file);
743            injected.and(closed)
744        })
745    }
746
747    /// Open several files at once with a shared flags and mode,
748    /// `tc_openv_simple()`.
749    fn openv_simple(&mut self, paths: &[&Path], flags: i32, mode: u32) -> VfResult<Vec<VfFile>> {
750        let flags_v = vec![flags; paths.len()];
751        let modes_v = vec![mode; paths.len()];
752        self.openv(paths, &flags_v, &modes_v)
753    }
754
755    /// Close several files, `tc_closev()`.
756    fn closev(&mut self, files: &[VfFile]) -> VfRes {
757        for (i, f) in files.iter().enumerate() {
758            self.close(f).map_err(|e| e.with_index(i))?;
759        }
760        Ok(())
761    }
762
763    /// Stat a path, `tc_stat()`. Follows symlinks to the target.
764    fn stat(&mut self, path: &Path) -> VfResult<VfAttrs> {
765        let mut a = VfAttrs {
766            file: VfFile::from_os_path(path),
767            masks: AttrMask::stat(),
768            ..VfAttrs::default()
769        };
770        self.getattrsv(std::slice::from_mut(&mut a))?;
771        Ok(a)
772    }
773
774    /// `tc_lstat()`: like [`stat`](Self::stat) but does not follow symlinks.
775    fn lstat(&mut self, path: &Path) -> VfResult<VfAttrs> {
776        let mut a = VfAttrs {
777            file: VfFile::from_os_path(path),
778            masks: AttrMask::stat(),
779            ..VfAttrs::default()
780        };
781        self.lgetattrsv(std::slice::from_mut(&mut a))?;
782        Ok(a)
783    }
784
785    /// `tc_fstat()`.
786    fn fstat(&mut self, tcf: &VfFile) -> VfResult<VfAttrs> {
787        let mut a = VfAttrs {
788            file: tcf.clone(),
789            masks: AttrMask::stat(),
790            ..VfAttrs::default()
791        };
792        self.getattrsv(std::slice::from_mut(&mut a))?;
793        Ok(a)
794    }
795
796    /// Whether `path` exists, distinguishing "not found" from other errors
797    /// (e.g. permission denied) that are returned as `Err`. Uses
798    /// `lstat` semantics: a dangling symlink exists.
799    fn exists(&mut self, path: &Path) -> VfResult<bool> {
800        match self.lstat(path) {
801            Ok(_) => Ok(true),
802            Err(e) if e.err_no() == ERR_NOENT => Ok(false),
803            Err(e) => Err(e),
804        }
805    }
806
807    /// Return the file type of `path` itself (`lstat` semantics: a symlink
808    /// reports [`VfType::Symlink`] rather than its target's type).
809    fn file_type(&mut self, path: &Path) -> VfResult<VfType> {
810        Ok(self.lstat(path)?.ftype)
811    }
812
813    /// List directories with a callback, `tc_listdirv()`. Returning `false`
814    /// stops the listing early.
815    fn listdirv(
816        &mut self,
817        dirs: &[&Path],
818        masks: AttrMask,
819        max_entries: usize,
820        recursive: bool,
821        cb: &mut dyn FnMut(&VfAttrs, &Path) -> bool,
822    ) -> VfRes {
823        for (i, d) in dirs.iter().enumerate() {
824            let entries = self
825                .listdir(d, masks, max_entries, recursive)
826                .map_err(|e| e.with_index(i))?;
827            for e in &entries {
828                /* A recursive list contains descendants too, so report each
829                 * entry's actual parent rather than attributing every row to the
830                 * original root. Backend overrides follow the same contract. */
831                let entry_dir = if recursive {
832                    e.file.path().and_then(Path::parent).unwrap_or(d)
833                } else {
834                    d
835                };
836                if !cb(e, entry_dir) {
837                    return Ok(());
838                }
839            }
840        }
841        Ok(())
842    }
843
844    /// `tc_unlink()`.
845    fn unlink(&mut self, pathname: &Path) -> VfResult<()> {
846        self.removev(&[VfFile::from_os_path(pathname)])
847    }
848
849    /// `tc_unlinkv()`.
850    fn unlinkv(&mut self, pathnames: &[&Path]) -> VfRes {
851        let files: Vec<VfFile> = pathnames.iter().map(|p| VfFile::from_os_path(p)).collect();
852        self.removev(&files)
853    }
854
855    /// Create a directory, `tc_mkdir()`.
856    fn mkdir(&mut self, path: &Path, mode: u32) -> VfResult<()> {
857        let a = VfAttrs {
858            file: VfFile::from_os_path(path),
859            masks: AttrMask::MODE,
860            mode,
861            ..VfAttrs::default()
862        };
863        self.mkdirv(std::slice::from_ref(&a))
864    }
865
866    /// Create a symlink, `tc_symlink()`.
867    fn symlink(&mut self, oldpath: &Path, newpath: &Path) -> VfResult<()> {
868        self.symlinkv(
869            std::slice::from_ref(&oldpath),
870            std::slice::from_ref(&newpath),
871        )
872    }
873
874    /// Read a symlink target, `tc_readlink()`.
875    fn readlink(&mut self, path: &Path) -> VfResult<Vec<u8>> {
876        take_single_result("readlink", self.readlinkv(std::slice::from_ref(&path))?)
877    }
878
879    /// `tc_ldupv()`: same read/write extent copy as
880    /// [`dupv`](Self::dupv). Retained for C API parity; a backend that
881    /// distinguishes a "local" copy should override.
882    fn ldupv(&mut self, pairs: &[ExtentPair]) -> VfRes {
883        self.dupv(pairs)
884    }
885
886    /// `tc_copyv()`: server-side copy where the backend supports it. The
887    /// default implementation performs a client-side read/write copy via
888    /// [`dupv`](Self::dupv); backends with a server-side COPY should
889    /// override.
890    fn copyv(&mut self, pairs: &[ExtentPair]) -> VfRes {
891        self.dupv(pairs)
892    }
893
894    /// Create a directory and all its ancestors, `tc_ensure_dir()`. Uses
895    /// `mkdir` and accepts an existing directory (`EEXIST`) instead of an
896    /// exists-then-mkdir check, avoiding the race between the two. `dir` is
897    /// resolved against the cwd via [`abs_path`](Self::abs_path) and then
898    /// rebuilt as an absolute (root-relative) path.
899    fn ensure_dir(&mut self, dir: &Path, mode: u32) -> VfResult<()> {
900        use std::path::Component;
901        let mut so_far = PathBuf::new();
902        for comp in self.abs_path(dir).components() {
903            if let Component::Normal(part) = comp {
904                so_far.push(part);
905                let full = Path::new("/").join(&so_far);
906                match self.mkdir(&full, mode) {
907                    Ok(()) => {}
908                    Err(e) if e.err_no() == ERR_EXIST => {}
909                    Err(e) => return Err(e),
910                }
911            }
912        }
913        Ok(())
914    }
915}
916
917/// `AsRef<Path>` convenience wrappers for [`VecFs`] methods.
918///
919/// The object-safe [`VecFs`] trait intentionally takes `&Path`. These
920/// wrappers accept any type that can be viewed as a path (`&str`, `String`,
921/// `PathBuf`, `&Path`, ...) and are useful for Rust callers that do not need
922/// dynamic dispatch.
923pub trait VecFsExt: VecFs {
924    fn stat_path<P: AsRef<Path>>(&mut self, path: P) -> VfResult<VfAttrs> {
925        self.stat(path.as_ref())
926    }
927
928    fn lstat_path<P: AsRef<Path>>(&mut self, path: P) -> VfResult<VfAttrs> {
929        self.lstat(path.as_ref())
930    }
931
932    fn exists_path<P: AsRef<Path>>(&mut self, path: P) -> VfResult<bool> {
933        self.exists(path.as_ref())
934    }
935
936    fn file_type_path<P: AsRef<Path>>(&mut self, path: P) -> VfResult<VfType> {
937        self.file_type(path.as_ref())
938    }
939
940    fn open_path<P: AsRef<Path>>(&mut self, path: P, flags: i32, mode: u32) -> VfResult<VfFile> {
941        self.open(path.as_ref(), flags, mode)
942    }
943
944    fn mkdir_path<P: AsRef<Path>>(&mut self, path: P, mode: u32) -> VfResult<()> {
945        self.mkdir(path.as_ref(), mode)
946    }
947
948    fn ensure_dir_path<P: AsRef<Path>>(&mut self, path: P, mode: u32) -> VfResult<()> {
949        self.ensure_dir(path.as_ref(), mode)
950    }
951
952    fn chdir_path<P: AsRef<Path>>(&mut self, path: P) -> VfResult<()> {
953        self.chdir(path.as_ref())
954    }
955
956    fn unlink_path<P: AsRef<Path>>(&mut self, path: P) -> VfResult<()> {
957        self.unlink(path.as_ref())
958    }
959
960    fn readlink_path<P: AsRef<Path>>(&mut self, path: P) -> VfResult<Vec<u8>> {
961        self.readlink(path.as_ref())
962    }
963
964    fn symlink_path<P, Q>(&mut self, oldpath: P, newpath: Q) -> VfResult<()>
965    where
966        P: AsRef<Path>,
967        Q: AsRef<Path>,
968    {
969        self.symlink(oldpath.as_ref(), newpath.as_ref())
970    }
971
972    fn listdir_path<P: AsRef<Path>>(
973        &mut self,
974        path: P,
975        masks: AttrMask,
976        max_count: usize,
977        recursive: bool,
978    ) -> VfResult<Vec<VfAttrs>> {
979        self.listdir(path.as_ref(), masks, max_count, recursive)
980    }
981
982    fn walk_path<P: AsRef<Path>>(
983        &mut self,
984        root: P,
985        masks: AttrMask,
986        sort: &mut dyn FnMut(&Path, &mut Vec<VfAttrs>),
987    ) -> VfResult<Vec<WalkEntry>> {
988        self.walk(root.as_ref(), masks, sort)
989    }
990
991    fn openv_paths<P: AsRef<Path>>(
992        &mut self,
993        paths: &[P],
994        flags: &[i32],
995        modes: &[u32],
996    ) -> VfResult<Vec<VfFile>> {
997        let refs: Vec<&Path> = paths.iter().map(|p| p.as_ref()).collect();
998        self.openv(&refs, flags, modes)
999    }
1000
1001    fn openv_simple_paths<P: AsRef<Path>>(
1002        &mut self,
1003        paths: &[P],
1004        flags: i32,
1005        mode: u32,
1006    ) -> VfResult<Vec<VfFile>> {
1007        let refs: Vec<&Path> = paths.iter().map(|p| p.as_ref()).collect();
1008        self.openv_simple(&refs, flags, mode)
1009    }
1010
1011    fn unlinkv_paths<P: AsRef<Path>>(&mut self, paths: &[P]) -> VfRes {
1012        let refs: Vec<&Path> = paths.iter().map(|p| p.as_ref()).collect();
1013        self.unlinkv(&refs)
1014    }
1015
1016    fn rm_paths<P: AsRef<Path>>(&mut self, paths: &[P], recursive: bool) -> VfRes {
1017        let refs: Vec<&Path> = paths.iter().map(|p| p.as_ref()).collect();
1018        self.rm(&refs, recursive)
1019    }
1020
1021    fn rm_recursive_path<P: AsRef<Path>>(&mut self, path: P) -> VfRes {
1022        self.rm(&[path.as_ref()], true)
1023    }
1024}
1025
1026impl<T: VecFs + ?Sized> VecFsExt for T {}
1027
1028/// `tc_rm_recursive()`.
1029pub fn rm_recursive(fs: &mut impl VecFs, dir: &Path) -> VfRes {
1030    fs.rm(&[dir], true)
1031}
1032
1033#[cfg(test)]
1034mod contract_tests {
1035    use super::*;
1036
1037    fn request() -> ReadOp {
1038        ReadOp::at(VfFile::from_path("/file"), 0, 1)
1039    }
1040
1041    fn result() -> ReadResult {
1042        ReadResult {
1043            file: VfFile::from_path("/file"),
1044            offset: 0,
1045            data: vec![1],
1046            eof: true,
1047        }
1048    }
1049
1050    #[test]
1051    fn read_result_cardinality_is_checked_before_indexing() {
1052        let requests = [request(), request()];
1053        assert!(validate_read_results("test", &requests, &[result()]).is_err());
1054        assert!(validate_read_results("test", &requests[..1], &[result(), result()]).is_err());
1055    }
1056
1057    #[test]
1058    fn read_result_identity_offset_progress_and_size_are_checked() {
1059        let request = request();
1060        for malformed in [
1061            ReadResult {
1062                file: VfFile::from_path("/other"),
1063                ..result()
1064            },
1065            ReadResult {
1066                offset: 1,
1067                ..result()
1068            },
1069            ReadResult {
1070                data: vec![1, 2],
1071                ..result()
1072            },
1073            ReadResult {
1074                data: Vec::new(),
1075                eof: false,
1076                ..result()
1077            },
1078        ] {
1079            assert!(
1080                validate_read_results("test", std::slice::from_ref(&request), &[malformed])
1081                    .is_err()
1082            );
1083        }
1084    }
1085
1086    #[test]
1087    fn write_result_identity_offset_size_and_cardinality_are_checked() {
1088        let file = VfFile::from_path("/file");
1089        let request = WriteOpRef::new(&file, VfOffset::At(0), b"x");
1090        let valid = WriteResult {
1091            file: file.clone(),
1092            offset: 0,
1093            written: 1,
1094            stable: true,
1095        };
1096        assert!(validate_write_results("test", &[request], &[]).is_err());
1097        for malformed in [
1098            WriteResult {
1099                file: VfFile::from_path("/other"),
1100                ..valid.clone()
1101            },
1102            WriteResult {
1103                offset: 1,
1104                ..valid.clone()
1105            },
1106            WriteResult {
1107                written: 2,
1108                ..valid.clone()
1109            },
1110        ] {
1111            assert!(validate_write_results("test", &[request], &[malformed]).is_err());
1112        }
1113    }
1114
1115    #[test]
1116    fn scalar_result_cardinality_is_checked_without_panicking() {
1117        assert!(take_single_result::<u8>("readlink", Vec::new()).is_err());
1118        assert!(take_single_result("readlink", vec![1u8, 2]).is_err());
1119        assert_eq!(take_single_result("readlink", vec![7u8]).unwrap(), 7);
1120    }
1121}