Skip to main content

vfsi_core/api/
traits.rs

1//! Protocol-independent application contracts. Backend implementer traits
2//! live in implementation crates; generic applications need only these traits.
3
4use crate::api::{
5    Attrs, CopyOption, DirectoryListing, OpenOp, ResourceLimits, Result, WriteResult,
6};
7use std::io::{Read, Seek, SeekFrom, Write};
8use std::path::Path;
9
10/// Owned application file. Borrowed requests preserve the file's lifetime;
11/// clients validate connection ownership before dispatching a vector.
12///
13/// External implementations must keep request storage valid for its borrowed
14/// lifetime, validate client ownership before I/O, and report positional progress
15/// without changing the cursor. Request constructors do no I/O. Associated
16/// request types are implementer contracts, not application-facing builders.
17/// A backend must retain ownership of live descriptors through cleanup failures.
18pub trait FileHandle: Read + Write + Seek {
19    /// Borrowed positional read request; constructing it performs no I/O.
20    type ReadRequest<'a>
21    where
22        Self: 'a;
23    /// Borrowed positional request into a caller-owned destination buffer.
24    type ReadIntoRequest<'a>
25    where
26        Self: 'a;
27    /// Diagnostic name captured at open; not current path or object identity.
28    fn path(&self) -> &Path;
29    /// Query the opened object, even if its original pathname was renamed.
30    fn attrs(&self) -> Result<Attrs>;
31    /// Prepare a non-cursor-changing read for this handle's owning client.
32    fn read_request_at(&self, offset: u64, length: usize) -> Self::ReadRequest<'_>;
33    /// Borrow both the handle and destination until the vector call completes.
34    fn read_request_at_into<'a>(
35        &'a self,
36        offset: u64,
37        buffer: &'a mut [u8],
38    ) -> Self::ReadIntoRequest<'a>;
39    /// Read up to the buffer's length without changing the cursor; may be short.
40    fn read_at(&self, buffer: &mut [u8], offset: u64) -> Result<usize>;
41    /// Write up to the input's length without changing the cursor; may be short.
42    fn write_at(&self, buffer: &[u8], offset: u64) -> Result<usize>;
43    /// Cursor-based read retaining structured errors, unlike the `Read` adapter.
44    fn read_native(&mut self, buffer: &mut [u8]) -> Result<usize>;
45    /// Collect from the current cursor with a caller-selected payload budget.
46    /// A failed read may advance the cursor; exceeding the limit is an error,
47    /// not successful truncation. Standard `Read::read_to_end` is not bounded.
48    fn read_to_end_with_limit(&mut self, max_bytes: usize) -> Result<Vec<u8>>;
49    /// Cursor-based possibly short write retaining structured errors.
50    fn write_native(&mut self, buffer: &[u8]) -> Result<usize>;
51    /// Change the cursor, retaining structured errors unlike `Seek`.
52    fn seek_native(&mut self, position: SeekFrom) -> Result<u64>;
53    /// Request durability of file data; errors are not discarded.
54    fn sync_data(&self) -> Result<()>;
55    /// Request durability of file data and metadata supported by the backend.
56    fn sync_all(&self) -> Result<()>;
57    /// Change permission bits on the opened object, not by re-resolving its path.
58    /// The default reports Unsupported; callers must not silently claim preservation.
59    fn set_permissions(&self, _permissions: crate::api::Permissions) -> Result<()> {
60        Err(crate::api::Error::client(0, crate::VF_ERR_UNSUPPORTED))
61    }
62    /// Close without surrendering cleanup ownership on failure. After an
63    /// ambiguous failure, reconcile/close rather than resume ordinary I/O.
64    fn try_close(&mut self) -> Result<()>;
65    /// Local close ownership only, not proof of remote liveness after failure.
66    fn is_closed(&self) -> bool;
67    /// Consuming close: on failure the handle is lost and Drop retries cleanup
68    /// best-effort on a later operation or cleanup drain. Prefer `try_close` when
69    /// cleanup failures need reconciliation.
70    fn close(self) -> Result<()>
71    where
72        Self: Sized;
73}
74
75/// Vectorized filesystem operations for direct and routed application clients.
76/// Construction, builders and protocol-specific diagnostics stay concrete.
77/// Vectors are strict, ordered results on success, not transactions: errors
78/// can follow partially completed requests and never imply rollback.
79/// This uses borrowed request GATs, so it is for static generic dispatch, not
80/// `dyn Vfsi`. It adds no boxing, data copies or serial-loop fallbacks.
81///
82/// # Choosing an operation
83///
84/// | Task | Start with |
85/// | --- | --- |
86/// | Complete small files | [`vread`](Self::vread), [`write_files`](VfsiExt::write_files) |
87/// | Repeated/range I/O on owned handles | [`vopen`](Self::vopen), [`vread`](Self::vread), [`vwrite`](Self::vwrite) |
88/// | Large files without collecting them | [`vstream`](Self::vstream) |
89/// | Directory pages with entry metadata | [`vlistdirs`](Self::vlistdirs) |
90/// | Recursive directory pages | [`vlistdirs`](Self::vlistdirs) with [`ListDirOptions::recursive`](crate::api::ListDirOptions::recursive) |
91///
92/// Generic application code needs a `Vfsi` bound. Import [`VfsiExt`] for
93/// convenience operations such as `read_files`, `write_files`, and scalar open.
94/// [`VfsiExt::read_dirs_with_options`] collects directory pages into vectors;
95/// [`VfsiExt::read_stream_with_options`] adapts streaming to a single path.
96/// Use the core vector operations above to submit multiple targets together.
97/// Extension helpers compose vectors; backend-specific execution stays
98/// here so batching, paging, and recovery do not become scalar-loop fallbacks:
99///
100/// ```no_run
101/// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
102/// fn load_parts(fs: &impl Vfsi) -> vfsi_core::api::Result<Vec<Vec<u8>>> {
103///     fs.read_files(&["/file-1", "/file-2"])
104/// }
105/// ```
106///
107/// # Shared semantics
108///
109/// Paths use the client's namespace, not necessarily the host filesystem.
110/// A leading `/` refers to the configured root; this is not security confinement.
111/// Handles in a vector must belong to this client's connection. Successful
112/// vector results match input order; one vector may span several compounds.
113/// Callback cancellation returns a completed/stopped prefix for vector visitors;
114/// directory-page waves may leave several started roots stopped.
115/// A failed mutation can have partial effects. An error's index identifies an
116/// input when known, **not** a committed-prefix count. Preserve the structured
117/// [`crate::api::Error`] rather than blindly replaying a failed write.
118///
119/// Allocation/traversal defaults come from [`limits`](Self::limits). Explicit
120/// per-call options override them; neither is a process-wide memory cap.
121/// Calls are synchronous. Callback methods invoke user code outside the backend
122/// lock and propagate callback errors, but do not provide a filesystem snapshot.
123/// Writes are not automatically durable: use [`FileHandle::sync_data`] or
124/// [`FileHandle::sync_all`] on an open handle when required. Drop queues handle cleanup
125/// best-effort; explicit close methods let applications observe cleanup errors.
126pub trait Vfsi {
127    /// Query filesystems for paths (following symlinks) and retained open handles.
128    /// Results preserve input order. Unsupported fields are `None`.
129    fn vstatfs<P: crate::MetadataOperand<Self::File>>(
130        &self,
131        targets: &[P],
132    ) -> Result<Vec<crate::FilesystemStats>>;
133
134    /// Query metadata in input order with selected fields and final-symlink behavior.
135    /// Backend execution must preserve vector batching. Ancestor symlinks use
136    /// ordinary namespace resolution; this is not a snapshot or confinement API.
137    ///
138    /// ```no_run
139    /// use vfsi_core::api::{Vfsi, VfsiExt, AttrsOptions, Attributes, WriteOp};
140    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
141    /// let entries = fs.vgetattrs(&["/file-1", "/link"],
142    ///     AttrsOptions::new().fields(Attributes::MODE | Attributes::SIZE)
143    ///         .follow_symlinks(false))?;
144    /// # let _ = entries;
145    /// # Ok(())
146    /// # }
147    /// ```
148    fn vgetattrs<P: AsRef<Path>>(
149        &self,
150        paths: &[P],
151        options: crate::api::AttrsOptions,
152    ) -> Result<Vec<Attrs>>;
153
154    /// Update selected attributes for paths or open handles using native batching.
155    /// Every handle is validated before dispatch, including ownership and closure.
156    /// Unspecified fields are unchanged. `follow_symlinks` controls the final
157    /// component of path targets; it does not change open-handle identity.
158    /// Ancestor symlinks retain ordinary backend resolution.
159    /// Failure can follow partial mutations, including within one request;
160    /// an error index identifies an input, not a committed-prefix count.
161    /// Empty vectors succeed without I/O. Do not replay ambiguous failures.
162    ///
163    /// ```no_run
164    /// use vfsi_core::api::{Vfsi, MetadataUpdate, Permissions};
165    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
166    /// fs.vsetattrs(&[
167    ///     ("/file-1", MetadataUpdate::new().permissions(Permissions::from_mode(0o640)).len(1024)),
168    ///     ("/file-2", MetadataUpdate::new().len(0)),
169    /// ], true)?;
170    /// # Ok(())
171    /// # }
172    /// ```
173    /// Handle targets and paths can share a batch:
174    ///
175    /// ```no_run
176    /// use vfsi_core::api::{Vfsi, MetadataTarget, MetadataUpdate};
177    /// # fn example<F: Vfsi>(fs: &F, file: &F::File) -> vfsi_core::api::Result<()> {
178    /// fs.vsetattrs(&[
179    ///     (MetadataTarget::File(file), MetadataUpdate::new().len(1024)),
180    ///     (MetadataTarget::Path(std::path::Path::new("/other")), MetadataUpdate::new().len(0)),
181    /// ], true)?;
182    /// # Ok(())
183    /// # }
184    /// ```
185    fn vsetattrs<P: crate::api::MetadataOperand<Self::File>>(
186        &self,
187        updates: &[(P, crate::api::MetadataUpdate)],
188        follow_symlinks: bool,
189    ) -> Result<()>;
190
191    /// Capabilities supported by this client. Routed clients report capabilities
192    /// common to their routes; support does not imply authorization for a path.
193    fn capabilities(&self) -> Result<crate::api::Capabilities>;
194
195    /// Create symbolic links from `(target text, link path)` pairs.
196    /// Targets are stored verbatim, including relative or dangling targets.
197    /// Parents must exist. Errors may follow partial creation; no rollback.
198    fn vsymlink<P: AsRef<Path>, Q: AsRef<Path>>(&self, pairs: &[(P, Q)]) -> Result<()>;
199
200    /// Read symbolic link targets in input order without following final links.
201    /// Returns their original path bytes, including non-UTF-8 names on Unix.
202    /// An error identifies the input when known and discards collected results.
203    fn vreadlink<P: AsRef<Path>>(&self, paths: &[P]) -> Result<Vec<std::path::PathBuf>>;
204
205    /// Create hard links from `(existing source, new link path)` pairs.
206    /// Sources are not followed when the final component is a symlink.
207    /// Cross-filesystem links can fail. Errors may follow partial creation.
208    fn vhardlink<P: AsRef<Path>, Q: AsRef<Path>>(&self, pairs: &[(P, Q)]) -> Result<()>;
209
210    /// Owned handle; vectors must contain handles belonging to this client.
211    type File: FileHandle + 'static;
212
213    /// Collection/batch defaults, not a process memory cap or file-reader cap.
214    ///
215    /// This only inspects policy; it performs no filesystem I/O. Configure a
216    /// concrete client's builder or `with_limits` to change its defaults.
217    ///
218    /// ```no_run
219    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
220    /// # fn example(fs: &impl Vfsi) {
221    /// let limits = fs.limits();
222    /// println!("owned-read budget: {} bytes", limits.max_read_bytes);
223    /// println!("walk depth: {}", limits.max_walk_depth);
224    /// # }
225    /// ```
226    fn limits(&self) -> ResourceLimits;
227    /// Strict ordered OPEN results. Failure releases returned handles but cannot
228    /// undo files created/truncated earlier; `index()` is not a progress count.
229    ///
230    /// Different requests can use different flags/modes. All resulting handles
231    /// belong to this client. Prefer this to a scalar loop when opening a cohort.
232    /// Singleton vectors must preserve scalar symlink resolution and independently
233    /// opened handle state: VfsiExt::open_with is built from this primitive.
234    ///
235    /// ```no_run
236    /// use vfsi_core::api::{Vfsi, VfsiExt, OpenFlags, OpenOp, WriteOp};
237    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
238    /// let files = fs.vopen(&[
239    ///     OpenOp::new("/file-1", OpenFlags::READ),
240    ///     OpenOp::new("/file-2", OpenFlags::READ),
241    /// ])?;
242    /// // files[0] corresponds to file-1; files[1] to file-2.
243    /// fs.close_files(files)?;
244    /// # Ok(())
245    /// # }
246    /// ```
247    fn vopen(&self, requests: &[OpenOp]) -> Result<Vec<Self::File>>;
248    /// Consume a read batch with an explicit aggregate logical-byte budget.
249    /// Large files should usually be streamed instead of increasing the budget.
250    /// Explicit options override client defaults.
251    ///
252    /// ```no_run
253    /// use vfsi_core::api::{Vfsi, VfsiExt, ReadOp, ReadOptions, WriteOp};
254    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
255    /// let results = fs.vread(
256    ///     [ReadOp::whole("/config")],
257    ///     ReadOptions::new().max_total_bytes(std::num::NonZeroUsize::new(1024 * 1024)),
258    /// )?;
259    /// println!("{} bytes", results[0].read());
260    /// # Ok(())
261    /// # }
262    /// ```
263    ///
264    /// Consume whole-file, allocating-range, and caller-buffer operations.
265    ///
266    /// Construction does no I/O. Results retain input order and never borrow
267    /// caller storage. Whole files complete or fail; ranges may return short
268    /// progress. Only `buffer[..result.read]` is valid after a buffered read.
269    /// The default aggregate budget comes from `limits().max_read_bytes`.
270    /// All range and buffer lengths must fit before dispatch. Buffer reads run
271    /// first, then allocating ranges, then whole paths, in batched phases.
272    /// Errors retain original indices; buffers may already contain partial
273    /// progress on failure. No snapshot or atomicity is promised.
274    ///
275    /// ```no_run
276    /// use vfsi_core::api::{Vfsi, VfsiExt, FileHandle, ReadOp, WriteOp};
277    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
278    /// let file = fs.open("/large")?;
279    /// let mut buffer = [0_u8; 4096];
280    /// let result = fs.vread([
281    ///     ReadOp::whole("/config"),
282    ///     ReadOp::range(&file, 1024, 4096),
283    ///     ReadOp::into(&file, 8192, &mut buffer),
284    /// ], Default::default());
285    /// let close = file.close();
286    /// let results = result?;
287    /// close?;
288    /// println!("config: {:?}", results[0].data().unwrap());
289    /// println!("buffer: {:?}", &buffer[..results[2].read()]);
290    /// # Ok(())
291    /// # }
292    /// ```
293    fn vread<'a>(
294        &self,
295        ops: impl IntoIterator<Item = crate::api::ReadOp<'a, Self::File>>,
296        options: crate::api::ReadOptions,
297    ) -> Result<Vec<crate::api::ReadResult>>;
298    /// Write positional ranges with explicit short-write policy.
299    /// With default options, report accepted byte counts without completing short writes.
300    /// With `write_all(true)`, finish short writes after whole-batch local preflight.
301    /// Backend failures can still follow mutations; aliasing paths are the
302    /// caller's responsibility and do not imply transactional ordering.
303    ///
304    /// With completion enabled, success means every request's payload was written.
305    /// Short writes are completed at their remaining offsets; this does not
306    /// authorize replay after an ambiguous failure or guarantee durability.
307    ///
308    /// ```no_run
309    /// use vfsi_core::api::{Vfsi, VfsiExt, FileHandle, OpenFlags, OpenOp, WriteOp};
310    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
311    /// let files = fs.vopen(&[
312    ///     OpenOp::new("/file-1", OpenFlags::WRITE),
313    ///     OpenOp::new("/file-2", OpenFlags::WRITE),
314    /// ])?;
315    /// let result = fs.vwrite(&[
316    ///     WriteOp::at(&files[0], 0, b"hello"),
317    ///     WriteOp::at(&files[1], 4096, b"world"),
318    /// ], vfsi_core::api::WriteOptions::new().write_all(true));
319    /// let close = fs.close_files(files);
320    /// result?;
321    /// close?;
322    /// # Ok(())
323    /// # }
324    /// ```
325    fn vwrite<'a>(
326        &self,
327        requests: &[crate::api::WriteOp<'a, Self::File>],
328        options: crate::api::WriteOptions,
329    ) -> Result<Vec<WriteResult>>;
330    /// Retain handles for failed cleanup; completed groups can already be
331    /// closed. A lost reply leaves remote state ambiguous, not safely open.
332    ///
333    /// Unlike [`close_files`](VfsiExt::close_files), this borrows the handles. After an error,
334    /// `is_closed()` reports local cleanup ownership only; reconcile/close the
335    /// retained handles rather than resuming ordinary I/O as if nothing happened.
336    ///
337    /// ```no_run
338    /// use vfsi_core::api::{Vfsi, VfsiExt, FileHandle, OpenFlags, OpenOp, WriteOp};
339    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
340    /// let mut files = fs.vopen(&[OpenOp::new("/file-1", OpenFlags::READ)])?;
341    /// if let Err(error) = fs.vclose(&mut files) {
342    ///     let retained = files.iter().filter(|file| !file.is_closed()).count();
343    ///     eprintln!("{retained} handles retain cleanup ownership: {error}");
344    ///     return Err(error); // Drop performs best-effort cleanup, not reconciliation.
345    /// }
346    /// # Ok(())
347    /// # }
348    /// ```
349    fn vclose(&self, files: &mut [Self::File]) -> Result<()>;
350    /// Strict vector directory creation. Parents must already exist; errors
351    /// can follow completed mutations, and do not imply rollback.
352    ///
353    /// Submit independent siblings together. Do not rely on creating a parent
354    /// and its child in the same vector; create parents first or use
355    /// a dependency-aware tree builder for dependency-aware fresh-tree creation.
356    ///
357    /// ```no_run
358    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
359    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
360    /// fs.create_dir_all("/workspace")?;
361    /// fs.vmkdir(&[("/workspace/input", 0o750), ("/workspace/output", 0o700)])?;
362    /// # Ok(())
363    /// # }
364    /// ```
365    /// Each `(path, mode)` supplies Unix permission bits; backends apply the
366    /// requested mode rather than relying on the process umask. Unsupported
367    /// permission semantics are reported by the backend.
368    fn vmkdir<P: AsRef<Path>>(&self, directories: &[(P, u32)]) -> Result<()>;
369    /// Strict file-copy batches; a failed call can have copied earlier files.
370    ///
371    /// Pairs are `(source, destination)` in this client's namespace. Contents
372    /// are copied, not a recursive tree or a metadata-preserving snapshot.
373    /// `options` controls whether final source symlinks are followed; ancestor
374    /// symlinks retain ordinary path-resolution behavior.
375    /// Avoid aliasing sources/destinations; batching does not make overlapping
376    /// copies transactional or safe to replay after ambiguous failure.
377    ///
378    /// ```no_run
379    /// use vfsi_core::api::{CopyOption, Vfsi, VfsiExt, WriteOp};
380    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
381    /// fs.vcopy(&[
382    ///     ("/input/file-1", "/output/file-1"),
383    ///     ("/input/file-2", "/output/file-2"),
384    /// ], CopyOption::default())?;
385    /// # Ok(())
386    /// # }
387    /// ```
388    fn vcopy<P: AsRef<Path>, Q: AsRef<Path>>(
389        &self,
390        pairs: &[(P, Q)],
391        options: CopyOption,
392    ) -> Result<()>;
393    /// Remove entries, recursive trees, or directory contents with explicit policy.
394    ///
395    /// Contents mode retains each root and uses native anchored removal without
396    /// following final symlinks. Failures may follow partial mutations; no mode
397    /// provides rollback. Options control bounded retries and error handling.
398    ///
399    /// ```no_run
400    /// use vfsi_core::api::{Vfsi, RemoveMode, RemoveOptions};
401    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
402    /// fs.vremove(&["/scratch-1", "/scratch-2"],
403    ///     RemoveMode::Contents, RemoveOptions::new())?;
404    /// # Ok(())
405    /// # }
406    /// ```
407    fn vremove<P: AsRef<Path>>(
408        &self,
409        paths: &[P],
410        mode: crate::api::RemoveMode,
411        options: crate::api::RemoveOptions,
412    ) -> Result<()>;
413    /// Rename independent pairs in input order using the requested atomic
414    /// destination behavior. Each pair is atomic where the backend supports
415    /// the option; the vector is not a transaction, so failures may leave a
416    /// completed prefix. Source and destination are resolved in this filesystem
417    /// namespace. Unsupported guarantees return `Unsupported` rather than
418    /// being emulated with check-then-rename.
419    ///
420    /// ```no_run
421    /// use vfsi_core::api::{RenameOptions, Vfsi};
422    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
423    /// fs.vrename(
424    ///     &[("/old-1", "/new-1"), ("/old-2", "/new-2")],
425    ///     Default::default(), // ordinary replacement semantics
426    /// )?;
427    /// # Ok(())
428    /// # }
429    /// ```
430    fn vrename<P: AsRef<Path>, Q: AsRef<Path>>(
431        &self,
432        pairs: &[(P, Q)],
433        options: crate::api::RenameOptions,
434    ) -> Result<()>;
435
436    /// Visit shallow directories (default) or recursive trees using bounded pages.
437    /// Entry/path-byte limits are shared across roots; recursive visits also
438    /// charge retained directory path bytes. Root index accompanies every owned directory page, including empty directories.
439    /// Callbacks run outside backend locks and may reenter. Entries for one directory may span several pages. Break stops the
440    /// whole vector. The returned prefix covers every root whose pages reached
441    /// the callback; unfinished roots are Stopped. Pages may interleave between roots.
442    ///
443    /// Metadata is selected on directory pages, not through per-entry stat
444    /// calls. Entry symlinks are never followed during recursive descent.
445    /// Each page owns its entries; collection can transfer them without cloning.
446    /// Traversal order is backend-defined; this does not provide a snapshot.
447    ///
448    /// ```no_run
449    /// use vfsi_core::api::{Vfsi, ListDirOptions, ControlFlow};
450    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
451    /// fs.vlistdirs(&["/tree-1", "/tree-2"],
452    ///     ListDirOptions::new().recursive(true).max_depth(8),
453    ///     |index, page| {
454    ///         println!("{index}: {} ({} entries)", page.path.display(), page.entries.len());
455    ///         Ok(ControlFlow::Continue(()))
456    ///     })?;
457    /// # Ok(())
458    /// # }
459    /// ```
460    fn vlistdirs<P: AsRef<Path>>(
461        &self,
462        paths: &[P],
463        options: crate::api::ListDirOptions,
464        callback: impl FnMut(usize, DirectoryListing) -> Result<std::ops::ControlFlow<()>>,
465    ) -> Result<Vec<crate::api::TraversalCompletion>>;
466
467    /// Stream files through bounded chunks without collecting whole contents.
468    /// The callback receives the input index, byte offset, and borrowed chunk.
469    /// False stops the whole vector; results contain its completed/stopped prefix.
470    /// Backends may process streams sequentially; this is not a parallelism promise.
471    ///
472    /// ```no_run
473    /// use vfsi_core::api::{Vfsi, StreamOptions};
474    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
475    /// fs.vstream(&["/large-1", "/large-2"],
476    ///     StreamOptions::new().chunk_size(1024 * 1024), |index, offset, data| {
477    ///         println!("{index}: {} bytes at {offset}", data.len());
478    ///         Ok(true)
479    ///     })?;
480    /// # Ok(())
481    /// # }
482    /// ```
483    fn vstream<P: AsRef<Path>>(
484        &self,
485        paths: &[P],
486        options: crate::api::StreamOptions,
487        callback: impl FnMut(usize, u64, &[u8]) -> Result<bool>,
488    ) -> Result<Vec<crate::api::StreamCompletion>>;
489}
490
491/// Scalar operations and convenience workflows alongside the [`Vfsi`] vectors.
492///
493/// Blanket-implemented for every filesystem implementing Vfsi. Import `VfsiExt` (or
494/// [`crate::api::prelude`]) to use these helpers; generic code needs only an
495/// `Vfsi` bound. Helpers preserve batching, resource limits, and non-atomic
496/// failure semantics. All helpers compose vectorized Vfsi primitives; native execution belongs in Vfsi.
497/// Single-target helpers use conventional names such as `open`, `attrs`,
498/// and `read_dir`. Prefer vector APIs for independent work on many files or
499/// directories so the backend can batch requests.
500///
501/// # Method groups
502///
503/// - Open and close: [`open`](Self::open), [`create`](Self::create).
504/// - Read files: [`read_files`](Self::read_files), [`read_files_with_options`](Self::read_files_with_options).
505/// - Write files: [`write`](Self::write), [`write_files`](Self::write_files).
506/// - Attrs: [`attrs`](Self::attrs), [`attrs_with_options`](Self::attrs_with_options).
507/// - Collect directories: [`read_dir`](Self::read_dir), [`read_dir_with_options`](Self::read_dir_with_options).
508/// - Visit directories: [`visit_dir`](Self::visit_dir), [`visit_walk`](Self::visit_walk).
509/// - Create, move, copy, and remove: [`create_dir`](Self::create_dir), [`create_dir_all`](Self::create_dir_all).
510///
511/// # Boundary
512///
513/// | Contract | Responsibility | Examples |
514/// | --- | --- | --- |
515/// | [`Vfsi`] | Native vector execution, paging, and policy inspection | `vopen`, `vread`, `vlistdirs`, `limits` |
516/// | `VfsiExt` | Scalar adapters and composed workflows | `open`, `read_files`, `create_dir_all` |
517///
518/// Implement only `Vfsi`; this extension is blanket implemented. A helper belongs
519/// here only when it can compose Vfsi primitives without losing native batching,
520/// bounded paging, or recovery semantics. Singular convenience is not a promise
521/// of one RPC, and vector execution is not a promise of atomicity.
522pub trait VfsiExt: Vfsi {
523    /// Query one target through the vector filesystem-statistics engine.
524    fn statfs<P: crate::MetadataOperand<Self::File>>(
525        &self,
526        target: P,
527    ) -> Result<crate::FilesystemStats> {
528        let mut results = self.vstatfs(&[target])?;
529        if results.len() != 1 {
530            return Err(crate::VfError::transport(
531                None,
532                "statfs backend returned an invalid result count",
533            ));
534        }
535        Ok(results.remove(0))
536    }
537
538    // Open and close
539    /// Single-target convenience. For multiple files, prefer [`Vfsi::vopen`] to expose batching opportunities.
540    ///
541    /// Open read-only. Paths are relative to this client's configured namespace.
542    ///
543    /// Use [`open_with`](VfsiExt::open_with) for write/create flags, or
544    /// [`vopen`](Vfsi::vopen) to batch many opens. The returned handle implements
545    /// `std::io::Read`/`Write`/`Seek`; use native methods to retain structured errors.
546    ///
547    /// ```no_run
548    /// use vfsi_core::api::{Vfsi, VfsiExt, FileHandle, WriteOp};
549    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
550    /// let mut file = fs.open("/config")?;
551    /// let mut header = [0_u8; 128];
552    /// let result = file.read_native(&mut header); // May be short.
553    /// let close = file.close();
554    /// let bytes = result?;
555    /// close?;
556    /// println!("read {bytes} bytes");
557    /// # Ok(())
558    /// # }
559    /// ```
560    fn open(&self, path: impl AsRef<Path>) -> Result<Self::File> {
561        self.open_with(OpenOp::new(path.as_ref(), crate::api::OpenFlags::READ))
562    }
563
564    /// Single-target convenience. For multiple creations, prefer [`Vfsi::vopen`] with CREATE/TRUNCATE flags.
565    ///
566    /// Create or truncate a file and open for writing; does not create parents.
567    ///
568    /// Existing contents are discarded immediately. For exclusive creation,
569    /// use [`open_with`](VfsiExt::open_with) with `WRITE | CREATE_NEW` instead.
570    ///
571    /// ```no_run
572    /// use vfsi_core::api::{Vfsi, VfsiExt, FileHandle, WriteOp};
573    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
574    /// let file = fs.create("/output")?;
575    /// let result = fs.vwrite(&[WriteOp::at(&file, 0, b"complete contents")], vfsi_core::api::WriteOptions::new().write_all(true));
576    /// let close = file.close();
577    /// result?;
578    /// close?;
579    /// # Ok(())
580    /// # }
581    /// ```
582    fn create(&self, path: impl AsRef<Path>) -> Result<Self::File> {
583        self.open_with(OpenOp::new(
584            path.as_ref(),
585            crate::api::OpenFlags::WRITE
586                | crate::api::OpenFlags::CREATE
587                | crate::api::OpenFlags::TRUNCATE,
588        ))
589    }
590
591    /// Single-target convenience. For multiple requests, prefer [`Vfsi::vopen`] with per-file flags and modes.
592    ///
593    /// Open with an explicit access/create/truncate request; effects are eager.
594    ///
595    /// Creation/truncation occurs at open, not on the first write. `CREATE_NEW`
596    /// rejects an existing path. This does not create missing parents.
597    /// Delegates to singleton `Vfsi::vopen`. Backends must preserve scalar final-
598    /// symlink resolution and independently opened handles for singleton vectors.
599    ///
600    /// ```no_run
601    /// use vfsi_core::api::{Vfsi, VfsiExt, FileHandle, OpenFlags, OpenOp, WriteOp};
602    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
603    /// let file = fs.open_with(OpenOp::new(
604    ///     "/new-file", OpenFlags::WRITE | OpenFlags::CREATE_NEW,
605    /// ).mode(0o600))?;
606    /// file.close()?;
607    /// # Ok(())
608    /// # }
609    /// ```
610    fn open_with(&self, request: OpenOp) -> Result<Self::File> {
611        let mut files = self.vopen(&[request])?;
612        if files.len() != 1 {
613            return Err(crate::api::Error::transport(
614                None,
615                "vopen returned an invalid result count",
616            ));
617        }
618        Ok(files.remove(0))
619    }
620
621    /// Consume all handles. Errors cannot return cleanup ownership; Drop is
622    /// best-effort. Prefer `vclose` when close errors require reconciliation.
623    ///
624    /// This releases all local handles even on failure; it does not promise
625    /// every remote CLOSE succeeded. Closing alone is not a durability barrier.
626    ///
627    /// ```no_run
628    /// use vfsi_core::api::{Vfsi, VfsiExt, OpenFlags, OpenOp, WriteOp};
629    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
630    /// let files = fs.vopen(&[OpenOp::new("/config", OpenFlags::READ)])?;
631    /// fs.close_files(files)?; // Observe a close error instead of discarding it in Drop.
632    /// # Ok(())
633    /// # }
634    /// ```
635    fn close_files(&self, mut files: Vec<Self::File>) -> Result<()> {
636        self.vclose(&mut files)
637    }
638
639    // Read files
640    /// Read one complete file within the client's aggregate read budget.
641    ///
642    /// For multiple files, prefer [`Self::read_files`]. This is a convenience
643    /// over the vector read operation and retains its allocation limits.
644    fn read(&self, path: impl AsRef<Path>) -> Result<Vec<u8>> {
645        self.read_with_options(path, crate::api::ReadOptions::default())
646    }
647
648    /// Read one complete file with an explicit read policy.
649    fn read_with_options(
650        &self,
651        path: impl AsRef<Path>,
652        options: crate::api::ReadOptions,
653    ) -> Result<Vec<u8>> {
654        let path = path.as_ref();
655        let files = self
656            .read_files_with_options(&[path], options)
657            .map_err(|error| error.with_context("read", path))?;
658        single_completion(files, "read")
659    }
660    /// Read complete files in input order using vectorized whole-file reads.
661    ///
662    /// The aggregate payload is bounded by `limits().max_read_bytes`.
663    /// Use [`Self::read_files_with_options`] to override it, or stream large files.
664    /// A failure returns an error, not a successful partial collection.
665    ///
666    /// ```no_run
667    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
668    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
669    /// let contents = fs.read_files(&["/file-1", "/file-2"])?;
670    /// for bytes in contents { println!("{} bytes", bytes.len()); }
671    /// # Ok(())
672    /// # }
673    /// ```
674    fn read_files<P: AsRef<Path>>(&self, paths: &[P]) -> Result<Vec<Vec<u8>>> {
675        self.read_files_with_options(paths, crate::api::ReadOptions::default())
676    }
677
678    /// Whole-file convenience reads with an explicit aggregate payload budget.
679    ///
680    /// ```no_run
681    /// use vfsi_core::api::{Vfsi, VfsiExt, ReadOptions, WriteOp};
682    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
683    /// let files = fs.read_files_with_options(&["/config"],
684    ///     ReadOptions::new().max_total_bytes(std::num::NonZeroUsize::new(1024 * 1024)))?;
685    /// # let _ = files;
686    /// # Ok(())
687    /// # }
688    /// ```
689    fn read_files_with_options<P: AsRef<Path>>(
690        &self,
691        paths: &[P],
692        options: crate::api::ReadOptions,
693    ) -> Result<Vec<Vec<u8>>> {
694        let results = self.vread(
695            paths
696                .iter()
697                .map(|path| crate::api::ReadOp::whole(path.as_ref())),
698            options,
699        )?;
700        if results.len() != paths.len() {
701            return Err(crate::api::Error::transport(
702                None,
703                "vread_native returned an invalid result count",
704            ));
705        }
706        results
707            .into_iter()
708            .enumerate()
709            .map(|(index, result)| {
710                let data = result.data.ok_or_else(|| {
711                    crate::api::Error::transport(
712                        Some(index),
713                        "whole-file vread_native omitted owned data",
714                    )
715                })?;
716                if data.len() != result.read || result.offset != 0 || !result.eof {
717                    return Err(crate::api::Error::transport(
718                        Some(index),
719                        "whole-file vread_native returned incomplete or invalid data",
720                    ));
721                }
722                Ok(data)
723            })
724            .collect()
725    }
726
727    /// Single-target convenience. For multiple files, prefer [`Vfsi::vread`] with whole-file operations and decode each result as UTF-8.
728    ///
729    /// Read a complete UTF-8 file within this client's aggregate read budget.
730    ///
731    /// Invalid UTF-8 is an invalid-input error; use `read_files` for arbitrary bytes.
732    ///
733    /// ```no_run
734    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
735    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
736    /// let text = fs.read_to_string("/config")?;
737    /// println!("{text}");
738    /// # Ok(())
739    /// # }
740    /// ```
741    fn read_to_string(&self, path: impl AsRef<Path>) -> Result<String> {
742        self.read_to_string_with_options(path, crate::api::ReadOptions::default())
743    }
744
745    /// Single-target convenience. For multiple files, prefer [`Vfsi::vread`] with an aggregate byte budget, then decode UTF-8.
746    ///
747    /// Read a complete UTF-8 file with the same budget policy as `Vfsi::vread`.
748    /// `ReadOptions::default()` inherits the client limit; explicit overrides are nonzero.
749    ///
750    /// ```no_run
751    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
752    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
753    /// let text = fs.read_to_string_with_options("/config", vfsi_core::api::ReadOptions::new()
754    ///     .max_total_bytes(std::num::NonZeroUsize::new(4096)))?;
755    /// println!("{text}");
756    /// # Ok(())
757    /// # }
758    /// ```
759    fn read_to_string_with_options(
760        &self,
761        path: impl AsRef<Path>,
762        options: crate::api::ReadOptions,
763    ) -> Result<String> {
764        let path = path.as_ref();
765        let mut files = self.read_files_with_options(&[path], options)?;
766        String::from_utf8(files.remove(0)).map_err(|_| {
767            crate::api::Error::client(0, crate::ERR_INVAL).with_context("read_to_string", path)
768        })
769    }
770
771    /// Single-target convenience. For multiple files, prefer [`Vfsi::vstream`]. Backends may process streams sequentially.
772    ///
773    /// Stream a file using the client's bounded chunk size instead of collecting it.
774    /// Return `Ok(false)` to stop successfully; the callback runs outside the lock.
775    ///
776    /// ```no_run
777    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
778    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
779    /// fs.read_stream("/large", |offset, bytes| {
780    ///     println!("{} bytes at {offset}", bytes.len());
781    ///     Ok(true)
782    /// })?;
783    /// # Ok(())
784    /// # }
785    /// ```
786    fn read_stream(
787        &self,
788        path: impl AsRef<Path>,
789        callback: impl FnMut(u64, &[u8]) -> Result<bool>,
790    ) -> Result<crate::api::StreamCompletion> {
791        self.read_stream_with_options(
792            path,
793            crate::api::StreamOptions::new().chunk_size(self.limits().stream_chunk_bytes),
794            callback,
795        )
796    }
797
798    /// Single-target convenience. For multiple files, prefer [`Vfsi::vstream`]. Backends may process streams sequentially.
799    ///
800    /// Stream from offset zero outside the backend lock. `false` stops after
801    /// the delivered chunk; completion reports the next offset. Not a snapshot.
802    ///
803    /// The callback borrows each chunk only for its invocation. Consume it
804    /// without collecting chunks to keep memory bounded. `chunk_size` is a
805    /// maximum; negotiated protocol limits and short reads can produce smaller
806    /// chunks. The internally opened file is closed on completion, stopping,
807    /// callback error, or read failure. Stopping successfully does not mean EOF.
808    /// This simple stream does not promise RTT-hiding parallelism; direct NFS
809    /// applications can use `NfsBuilder::connect_read_pool` for pipelined reads.
810    ///
811    /// ```no_run
812    /// use vfsi_core::api::{Vfsi, VfsiExt, StreamOptions, StreamCompletion, WriteOp};
813    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
814    /// let mut processed = 0_u64;
815    /// let completion = fs.read_stream_with_options(
816    ///     "/large.bin", StreamOptions::new().chunk_size(1024 * 1024),
817    ///     |_offset, chunk| {
818    ///         processed += chunk.len() as u64; // Process the borrowed bytes here.
819    ///         Ok(processed < 8 * 1024 * 1024) // Stop after a bounded sample.
820    ///     },
821    /// )?;
822    /// match completion {
823    ///     StreamCompletion::Complete => println!("reached EOF after {processed} bytes"),
824    ///     StreamCompletion::Stopped { next_offset } => println!("stopped at {next_offset}"),
825    /// }
826    /// # Ok(())
827    /// # }
828    /// ```
829    fn read_stream_with_options(
830        &self,
831        path: impl AsRef<Path>,
832        options: crate::api::StreamOptions,
833        callback: impl FnMut(u64, &[u8]) -> Result<bool>,
834    ) -> Result<crate::api::StreamCompletion> {
835        let mut callback = callback;
836        single_completion(
837            self.vstream(&[path], options, |_, offset, data| callback(offset, data))?,
838            "read_streams",
839        )
840    }
841
842    // Write files
843    /// Single-target convenience. For multiple complete files, prefer [`VfsiExt::write_files`]; for opened handles, prefer [`Vfsi::vwrite`].
844    ///
845    /// Replace a file completely, creating/truncating eagerly; not atomic replace.
846    ///
847    /// The parent must exist. Success writes all bytes and closes the internal
848    /// handle; failure may leave a created, truncated, or partially written file.
849    /// For exclusive creation or durability control use an explicit open handle.
850    ///
851    /// ```no_run
852    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
853    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
854    /// fs.write("/output", b"replacement contents")?;
855    /// # Ok(())
856    /// # }
857    /// ```
858    fn write(&self, path: impl AsRef<Path>, data: &[u8]) -> Result<()> {
859        // Scalar open preserves backend-specific final-symlink resolution.
860        // Always attempt close, but retain the write error if both fail.
861        let file = self.create(path)?;
862        let result = self.vwrite(
863            &[crate::api::WriteOp::at(&file, 0, data)],
864            crate::api::WriteOptions::new().write_all(true),
865        );
866        let close_result = file.close();
867        result?;
868        close_result
869    }
870
871    /// Replace files in vector phases. Success completes payloads; errors may
872    /// follow create/truncate/write effects and never authorize blind replay.
873    ///
874    /// Parents must exist. Payloads are completed via vector open/write/close
875    /// phases, not a scalar write loop or necessarily a single RPC. No atomic
876    /// replacement, cross-file transaction, or durability guarantee is provided.
877    ///
878    /// ```no_run
879    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
880    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
881    /// fs.write_files(&[
882    ///     ("/file-1", b"hello".as_slice()),
883    ///     ("/file-2", b"world".as_slice()),
884    /// ])?;
885    /// # Ok(())
886    /// # }
887    /// ```
888    fn write_files<P: AsRef<Path>, B: AsRef<[u8]>>(&self, entries: &[(P, B)]) -> Result<()> {
889        let mut seen = std::collections::HashSet::with_capacity(entries.len());
890        for (index, (path, _)) in entries.iter().enumerate() {
891            if !seen.insert(path.as_ref()) {
892                return Err(crate::api::Error::client(index, crate::ERR_INVAL)
893                    .with_context("write_files", path.as_ref()));
894            }
895        }
896        let requests: Vec<_> = entries
897            .iter()
898            .map(|(path, _)| {
899                OpenOp::new(
900                    path.as_ref(),
901                    crate::api::OpenFlags::WRITE
902                        | crate::api::OpenFlags::CREATE
903                        | crate::api::OpenFlags::TRUNCATE,
904                )
905            })
906            .collect();
907        let files = self.vopen(&requests)?;
908        if files.len() != entries.len() {
909            return Err(crate::api::Error::transport(
910                None,
911                "vopen returned an invalid result count",
912            ));
913        }
914        let writes: Vec<_> = files
915            .iter()
916            .zip(entries)
917            .map(|(file, (_, data))| crate::api::WriteOp::at(file, 0, data.as_ref()))
918            .collect();
919        let result = self.vwrite(&writes, crate::api::WriteOptions::new().write_all(true));
920        drop(writes);
921        let close_result = self.close_files(files);
922        result?;
923        close_result
924    }
925
926    // Attrs
927    /// Single-target convenience. For multiple paths, prefer [`Vfsi::vgetattrs`].
928    ///
929    /// Query a path following its final symlink; unavailable fields remain None.
930    ///
931    /// To inspect the symlink itself use [`symlink_attrs`](VfsiExt::symlink_attrs).
932    /// To identify an already opened object after rename, use [`FileHandle::attrs`].
933    ///
934    /// ```no_run
935    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
936    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
937    /// let attrs = fs.attrs("/file-1")?;
938    /// println!("{} bytes; directory={}", attrs.len(), attrs.is_dir());
939    /// # Ok(())
940    /// # }
941    /// ```
942    fn attrs(&self, path: impl AsRef<Path>) -> Result<Attrs> {
943        attrs_query(self, path, crate::api::AttrsOptions::new(), "attrs")
944    }
945
946    /// Single-target convenience. For multiple paths, prefer [`Vfsi::vgetattrs`] with selected fields and follow_symlinks(false).
947    ///
948    /// Query one path with selected fields and final-symlink behavior; absent fields remain None.
949    ///
950    /// This avoids requesting every optional attribute. Type/size can be
951    /// requested internally even when not selected; missing optional attributes
952    /// must still be handled via their `Option` accessors.
953    ///
954    /// ```no_run
955    /// use vfsi_core::api::{Vfsi, VfsiExt, Attributes, WriteOp};
956    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
957    /// let attrs = fs.attrs_with_options(
958    ///     "/file-1", vfsi_core::api::AttrsOptions::new()
959    ///         .fields(Attributes::MODE | Attributes::BLOCKS).follow_symlinks(false),
960    /// )?;
961    /// if let Some(blocks) = attrs.blocks() {
962    ///     println!("allocated blocks: {blocks}");
963    /// }
964    /// # Ok(())
965    /// # }
966    /// ```
967    fn attrs_with_options(
968        &self,
969        path: impl AsRef<Path>,
970        options: crate::api::AttrsOptions,
971    ) -> Result<Attrs> {
972        attrs_query(self, path, options, "attrs_with_options")
973    }
974
975    /// Single-target convenience. For multiple paths, prefer [`Vfsi::vgetattrs`] with follow_symlinks(false).
976    ///
977    /// Query the final symlink itself instead of following it.
978    ///
979    /// This does not prevent following symlinks in ancestor components and is
980    /// not a race-free namespace confinement primitive.
981    ///
982    /// ```no_run
983    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
984    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
985    /// let attrs = fs.symlink_attrs("/link-or-file")?;
986    /// println!("symlink={}", attrs.is_symlink());
987    /// # Ok(())
988    /// # }
989    /// ```
990    fn symlink_attrs(&self, path: impl AsRef<Path>) -> Result<Attrs> {
991        attrs_query(
992            self,
993            path,
994            crate::api::AttrsOptions::new().follow_symlinks(false),
995            "symlink_attrs",
996        )
997    }
998
999    // Collect directories
1000    /// Single-target convenience. For multiple directories, prefer [`VfsiExt::read_dirs_with_options`] or [`VfsiExt::read_dirs`].
1001    ///
1002    /// Collect one directory using the client's entry and path-byte limits.
1003    ///
1004    /// ```no_run
1005    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
1006    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1007    /// for entry in fs.read_dir("/input")? {
1008    ///     println!("{}", entry.path().display());
1009    /// }
1010    /// # Ok(())
1011    /// # }
1012    /// ```
1013    fn read_dir(&self, path: impl AsRef<Path>) -> Result<Vec<crate::api::DirEntry>> {
1014        self.read_dir_with_options(
1015            path,
1016            crate::api::ListDirOptions::new().fields(crate::api::Attributes::stat()),
1017        )
1018    }
1019
1020    /// Single-target convenience. For multiple directories, prefer [`VfsiExt::read_dirs_with_options`] with one aggregate budget.
1021    ///
1022    /// Collect one shallow directory with selected metadata and entry/path-byte limits.
1023    /// This helper forces `recursive(false)`; use `walk_with_options` for a tree.
1024    ///
1025    /// ```no_run
1026    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
1027    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1028    /// let entries = fs.read_dir_with_options("/input",
1029    ///     vfsi_core::api::ListDirOptions::new().max_entries(100))?;
1030    /// println!("{} entries", entries.len());
1031    /// # Ok(())
1032    /// # }
1033    /// ```
1034    fn read_dir_with_options(
1035        &self,
1036        path: impl AsRef<Path>,
1037        options: crate::api::ListDirOptions,
1038    ) -> Result<Vec<crate::api::DirEntry>> {
1039        let mut trees = self.read_dirs_with_options(&[path], options.recursive(false))?;
1040        let mut listings = single_tree(&mut trees)?;
1041        if listings.len() != 1 {
1042            return Err(crate::api::Error::transport(
1043                None,
1044                "read_dirs returned an invalid result count",
1045            ));
1046        }
1047        Ok(listings.remove(0).entries)
1048    }
1049
1050    /// Collect directory listings under one aggregate entry/path-byte policy.
1051    ///
1052    /// There is one listing per input directory, in input order; entry order is
1053    /// backend-defined. Entries include metadata, avoiding a separate scalar
1054    /// stat per child. The default policy comes from this client's limits.
1055    /// Use [`read_dirs_with_options`](VfsiExt::read_dirs_with_options) to select
1056    /// attributes, or a visitor instead of collecting large listings.
1057    ///
1058    /// ```no_run
1059    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
1060    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1061    /// for listing in fs.read_dirs(&["/input", "/output"])? {
1062    ///     for entry in listing.entries {
1063    ///         println!("{}: {} bytes", entry.path().display(), entry.attrs().len());
1064    ///     }
1065    /// }
1066    /// # Ok(())
1067    /// # }
1068    /// ```
1069    fn read_dirs<P: AsRef<Path>>(&self, paths: &[P]) -> Result<Vec<DirectoryListing>> {
1070        let trees = self.read_dirs_with_options(
1071            paths,
1072            crate::api::ListDirOptions::new().fields(crate::api::Attributes::stat()),
1073        )?;
1074        if trees.len() != paths.len() || trees.iter().any(|tree| tree.len() != 1) {
1075            return Err(crate::api::Error::transport(
1076                None,
1077                "read_dirs returned an invalid result count",
1078            ));
1079        }
1080        Ok(trees.into_iter().flatten().collect())
1081    }
1082
1083    /// Collect shallow directories or recursive trees, grouped by input root.
1084    ///
1085    /// `results[i]` contains the listings for `paths[i]`. Shallow mode
1086    /// returns exactly one listing per root; recursive mode includes the root
1087    /// and descendants. Entries and path-byte budgets are shared across roots;
1088    /// recursive collection also charges retained directory paths.
1089    /// Collection builds on the batched directory-page visitor.
1090    /// Explicit limits override client defaults. Errors discard collected results;
1091    /// this is not a snapshot. Symlink entries are not recursively followed.
1092    ///
1093    /// ```no_run
1094    /// use vfsi_core::api::{Vfsi, VfsiExt, Attributes, ListDirOptions};
1095    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1096    /// let trees = fs.read_dirs_with_options(&["/input", "/output"],
1097    ///     ListDirOptions::new().recursive(true).fields(Attributes::MODE)
1098    ///         .max_entries(10_000).max_path_bytes(1024 * 1024))?;
1099    /// for tree in trees {
1100    ///     for listing in tree {
1101    ///         println!("{}: {} entries", listing.path.display(), listing.entries.len());
1102    ///     }
1103    /// }
1104    /// # Ok(())
1105    /// # }
1106    /// ```
1107    fn read_dirs_with_options<P: AsRef<Path>>(
1108        &self,
1109        paths: &[P],
1110        options: crate::api::ListDirOptions,
1111    ) -> Result<Vec<Vec<DirectoryListing>>> {
1112        let mut trees: Vec<Vec<DirectoryListing>> = (0..paths.len()).map(|_| Vec::new()).collect();
1113        let mut positions: Vec<std::collections::HashMap<std::path::PathBuf, usize>> = (0..paths
1114            .len())
1115            .map(|_| std::collections::HashMap::new())
1116            .collect();
1117        self.vlistdirs(paths, options, |index, page| {
1118            let tree = trees
1119                .get_mut(index)
1120                .ok_or_else(|| crate::api::Error::transport(None, "invalid visitor root index"))?;
1121            let positions = &mut positions[index];
1122            let slot = *positions.entry(page.path.clone()).or_insert_with(|| {
1123                tree.push(DirectoryListing {
1124                    path: page.path.clone(),
1125                    entries: Vec::new(),
1126                });
1127                tree.len() - 1
1128            });
1129            tree[slot].entries.extend(page.entries);
1130            Ok(std::ops::ControlFlow::Continue(()))
1131        })?;
1132        Ok(trees)
1133    }
1134
1135    /// Single-target convenience. For multiple roots, prefer [`VfsiExt::read_dirs_with_options`].
1136    ///
1137    /// Collect a no-follow tree using the client's entry, byte, and depth limits.
1138    ///
1139    /// ```no_run
1140    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
1141    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1142    /// for listing in fs.walk("/project")? {
1143    ///     println!("{}", listing.path.display());
1144    /// }
1145    /// # Ok(())
1146    /// # }
1147    /// ```
1148    fn walk(&self, root: impl AsRef<Path>) -> Result<Vec<DirectoryListing>> {
1149        self.walk_with_options(
1150            root,
1151            crate::api::ListDirOptions::new().fields(crate::api::Attributes::stat()),
1152        )
1153    }
1154
1155    /// Single-target convenience. For multiple roots, prefer [`VfsiExt::read_dirs_with_options`] with one aggregate budget.
1156    ///
1157    /// Collect a bounded tree, without following symlinks; no snapshot promise.
1158    ///
1159    /// Returns directory listings, not one flattened entry vector. Budgets
1160    /// apply across the walk; depth zero is the starting directory. Explicit
1161    /// options override client defaults. This helper forces `recursive(true)`.
1162    /// Use a visitor for incremental delivery
1163    /// or [`walk_events_with_options`](VfsiExt::walk_events_with_options) for pruning.
1164    ///
1165    /// ```no_run
1166    /// use vfsi_core::api::{ListDirOptions, Vfsi, VfsiExt, Attributes, WalkOptions, WriteOp};
1167    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1168    /// let listings = fs.walk_with_options(
1169    ///     "/project", ListDirOptions::new().fields(Attributes::MODE | Attributes::SIZE)
1170    ///         .max_entries(10_000).max_path_bytes(1024 * 1024).max_depth(8),
1171    /// )?;
1172    /// println!("{} directory listings", listings.len());
1173    /// # Ok(())
1174    /// # }
1175    /// ```
1176    fn walk_with_options(
1177        &self,
1178        path: impl AsRef<Path>,
1179        options: crate::api::ListDirOptions,
1180    ) -> Result<Vec<DirectoryListing>> {
1181        let mut trees = self.read_dirs_with_options(&[path], options.recursive(true))?;
1182        if trees.len() != 1 {
1183            return Err(crate::api::Error::transport(
1184                None,
1185                "walks returned an invalid result count",
1186            ));
1187        }
1188        Ok(trees.remove(0))
1189    }
1190
1191    // Visit directories
1192    /// Single-target convenience. For multiple directories, prefer [`Vfsi::vlistdirs`].
1193    ///
1194    /// Visit a directory incrementally using the client's allocation limits.
1195    /// The callback runs outside the backend lock.
1196    ///
1197    /// ```no_run
1198    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
1199    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1200    /// fs.visit_dir("/input", |entry| {
1201    ///     println!("{}", entry.path().display());
1202    ///     Ok(std::ops::ControlFlow::Continue(()))
1203    /// })?;
1204    /// # Ok(())
1205    /// # }
1206    /// ```
1207    fn visit_dir(
1208        &self,
1209        path: impl AsRef<Path>,
1210        callback: impl FnMut(&crate::api::DirEntry) -> Result<std::ops::ControlFlow<()>>,
1211    ) -> Result<crate::api::TraversalCompletion> {
1212        self.visit_dir_with_options(path, crate::api::ListDirOptions::new(), callback)
1213    }
1214
1215    /// Single-target convenience. For multiple roots, prefer [`Vfsi::vlistdirs`].
1216    ///
1217    /// Visit a no-follow tree incrementally using the client's traversal limits.
1218    /// The callback runs outside the backend lock.
1219    ///
1220    /// ```no_run
1221    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
1222    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1223    /// fs.visit_walk("/project", |entry| {
1224    ///     println!("{}", entry.path().display());
1225    ///     Ok(std::ops::ControlFlow::Continue(()))
1226    /// })?;
1227    /// # Ok(())
1228    /// # }
1229    /// ```
1230    fn visit_walk(
1231        &self,
1232        root: impl AsRef<Path>,
1233        callback: impl FnMut(&crate::api::DirEntry) -> Result<std::ops::ControlFlow<()>>,
1234    ) -> Result<crate::api::TraversalCompletion> {
1235        self.visit_dir_with_options(
1236            root,
1237            crate::api::ListDirOptions::new().recursive(true),
1238            callback,
1239        )
1240    }
1241
1242    /// Single-target convenience. For multiple directories, prefer [`Vfsi::vlistdirs`].
1243    ///
1244    /// Visit one directory; `Break(())` returns Stopped, exhaustion returns
1245    /// Complete, and callback errors propagate. The callback may reenter.
1246    ///
1247    /// Defaults to immediate children. Set `recursive(true)` to visit descendants
1248    /// without following entry symlinks. Options are forwarded unchanged, including
1249    /// entry, path-byte, and depth limits. Paging bounds incremental delivery;
1250    /// backends without paging may buffer one bounded listing. Entry order is
1251    /// backend-defined. `Break(())` stops the entire traversal, not one subtree.
1252    /// To prune a subtree, use [`walk_events_with_options`](Self::walk_events_with_options).
1253    ///
1254    /// ```no_run
1255    /// use vfsi_core::api::{ListDirOptions, Vfsi, VfsiExt, ControlFlow, WriteOp};
1256    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1257    /// let completion = fs.visit_dir_with_options(
1258    ///     "/input", ListDirOptions::new().max_entries(10_000), |entry| {
1259    ///         println!("{}", entry.path().display());
1260    ///         Ok(ControlFlow::Continue(()))
1261    ///     },
1262    /// )?;
1263    /// # let _ = completion;
1264    /// # Ok(())
1265    /// # }
1266    /// ```
1267    ///
1268    /// Recursive traversal uses the same entry point:
1269    ///
1270    /// ```no_run
1271    /// use vfsi_core::api::{ControlFlow, VfsiExt, ListDirOptions};
1272    /// # fn example(fs: &impl vfsi_core::Vfsi) -> vfsi_core::api::Result<()> {
1273    /// fs.visit_dir_with_options("/project", ListDirOptions::new().recursive(true), |entry| {
1274    ///     println!("{}", entry.path().display());
1275    ///     Ok(ControlFlow::Continue(()))
1276    /// })?;
1277    /// # Ok(())
1278    /// # }
1279    /// ```
1280    fn visit_dir_with_options(
1281        &self,
1282        path: impl AsRef<Path>,
1283        options: crate::api::ListDirOptions,
1284        callback: impl FnMut(&crate::api::DirEntry) -> Result<std::ops::ControlFlow<()>>,
1285    ) -> Result<crate::api::TraversalCompletion> {
1286        let mut callback = callback;
1287        single_completion(
1288            self.visit_entries_with_options(&[path], options, |_, entry| callback(entry))?,
1289            "visit_dirs",
1290        )
1291    }
1292
1293    /// Visit individual entries using the directory-page primitive.
1294    /// Empty directories produce pages but no entry callbacks.
1295    fn visit_entries_with_options<P: AsRef<Path>>(
1296        &self,
1297        paths: &[P],
1298        options: crate::api::ListDirOptions,
1299        mut callback: impl FnMut(usize, &crate::api::DirEntry) -> Result<std::ops::ControlFlow<()>>,
1300    ) -> Result<Vec<crate::api::TraversalCompletion>> {
1301        self.vlistdirs(paths, options, |index, page| {
1302            for entry in &page.entries {
1303                if callback(index, entry)?.is_break() {
1304                    return Ok(std::ops::ControlFlow::Break(()));
1305                }
1306            }
1307            Ok(std::ops::ControlFlow::Continue(()))
1308        })
1309    }
1310
1311    /// Single-target convenience. For multiple roots without enter/leave events or subtree pruning, prefer [`Vfsi::vlistdirs`]. Keep this helper when those event semantics are required.
1312    ///
1313    /// Incremental no-follow traversal with enter/leave events and pruning.
1314    /// Listings are bounded by the remaining aggregate budget; the callback
1315    /// runs before entering each directory, so pruning avoids its listing.
1316    ///
1317    /// `Enter`/`Leave` surround directories (including pruned ones); symlinks
1318    /// are `Entry` events, never followed. `SkipSubtree` is meaningful on `Enter`;
1319    /// `Stop` stops the entire walk. `sort_by_name` sorts siblings, not the whole
1320    /// tree. Limits charge fetched entries, including the root, even if pruned.
1321    /// Unlike a flat visit, this retains a bounded active frontier/listing.
1322    ///
1323    /// # Example: skip `.git` before listing it
1324    ///
1325    /// ```no_run
1326    /// use vfsi_core::api::{ListDirOptions, Vfsi, VfsiExt, Attributes, WalkControl, WalkEventKind, WalkOptions, WriteOp};
1327    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1328    /// let completion = fs.walk_events_with_options(
1329    ///     "/project", Attributes::MODE, WalkOptions::new(), true,
1330    ///     |event| {
1331    ///         if event.kind == WalkEventKind::Enter
1332    ///             && event.entry.file_name() == Some(std::ffi::OsStr::new(".git")) {
1333    ///             return Ok(WalkControl::SkipSubtree);
1334    ///         }
1335    ///         println!("{:?}: {}", event.kind, event.entry.path().display());
1336    ///         Ok(WalkControl::Continue)
1337    ///     },
1338    /// )?;
1339    /// # let _ = completion;
1340    /// # Ok(())
1341    /// # }
1342    /// ```
1343    fn walk_events_with_options(
1344        &self,
1345        root: impl AsRef<Path>,
1346        fields: crate::api::Attributes,
1347        options: crate::api::WalkOptions,
1348        sort_by_name: bool,
1349        callback: impl FnMut(&crate::api::WalkEvent) -> Result<crate::api::WalkControl>,
1350    ) -> Result<crate::api::TraversalCompletion> {
1351        let root = root.as_ref();
1352        let fields = fields | crate::api::Attributes::MODE;
1353        let metadata = self.attrs_with_options(
1354            root,
1355            crate::api::AttrsOptions::new()
1356                .fields(fields)
1357                .follow_symlinks(false),
1358        )?;
1359        crate::api::walk_events(
1360            crate::api::DirEntry::new(root.to_path_buf(), metadata),
1361            options,
1362            sort_by_name,
1363            |path, limits| {
1364                let mut trees = self.read_dirs_with_options(
1365                    &[path],
1366                    crate::api::ListDirOptions::from(limits).fields(fields),
1367                )?;
1368                let mut listings = single_tree(&mut trees)?;
1369                if listings.len() != 1 {
1370                    return Err(crate::api::Error::transport(
1371                        None,
1372                        "invalid directory result count",
1373                    ));
1374                }
1375                Ok(listings.remove(0).entries)
1376            },
1377            callback,
1378        )
1379    }
1380
1381    /// Visit complete shallow listings in depth-first order with application policy.
1382    ///
1383    /// `order` may reorder siblings (for example, locale-aware `ls` ordering).
1384    /// `descend` selects directories before their contents are read; it does not
1385    /// filter entries delivered in their parent's listing. Symlinks are never
1386    /// traversed. `visitor` runs outside backend locks and may stop immediately
1387    /// or skip all children of the current directory. Errors after delivered
1388    /// listings must not trigger replay through another backend.
1389    ///
1390    /// Storage is bounded by one complete listing and the pending directory
1391    /// frontier. Entry/path budgets charge all fetched children, including those
1392    /// rejected by `descend`. This ordered workflow deliberately does not
1393    /// speculatively list children before admission; use `vlistdirs` to batch
1394    /// already-approved independent directories.
1395    ///
1396    /// ```no_run
1397    /// use vfsi_core::api::{Attributes, Result, Vfsi, VfsiExt, WalkControl};
1398    /// # fn example(fs: &impl Vfsi) -> Result<()> {
1399    /// fs.visit_dirs_ordered("/input", Attributes::MODE | Attributes::SIZE,
1400    ///     fs.limits().walk_options(),
1401    ///     |entries| entries.sort_by(|a, b| a.path().cmp(b.path())),
1402    ///     |entry| entry.file_name() != Some(std::ffi::OsStr::new(".git")),
1403    ///     |listing, depth| {
1404    ///         println!("{}: {} entries at depth {depth}", listing.path.display(), listing.entries.len());
1405    ///         Ok(WalkControl::Continue)
1406    ///     })?;
1407    /// # Ok(())
1408    /// # }
1409    /// ```
1410    fn visit_dirs_ordered(
1411        &self,
1412        root: impl AsRef<Path>,
1413        fields: crate::api::Attributes,
1414        options: crate::api::WalkOptions,
1415        mut order: impl FnMut(&mut [crate::api::DirEntry]),
1416        mut descend: impl FnMut(&crate::api::DirEntry) -> bool,
1417        mut visitor: impl FnMut(DirectoryListing, usize) -> Result<crate::api::WalkControl>,
1418    ) -> Result<crate::api::TraversalCompletion> {
1419        let root = root.as_ref();
1420        let mut count = 1usize;
1421        let mut bytes = root.as_os_str().len();
1422        if count > options.entry_limit() || bytes > options.path_byte_limit() {
1423            return Err(crate::api::Error::client(0, libc::EFBIG as u32));
1424        }
1425        let mut pending = vec![(root.to_path_buf(), 0usize)];
1426        while let Some((path, depth)) = pending.pop() {
1427            if depth >= options.depth_limit() && options.truncates_at_depth_limit() {
1428                continue;
1429            }
1430            let mut entries = self.read_dir_with_options(
1431                &path,
1432                crate::api::ListDirOptions::new()
1433                    .fields(fields | crate::api::Attributes::MODE)
1434                    .max_entries(options.entry_limit().saturating_sub(count))
1435                    .max_path_bytes(options.path_byte_limit().saturating_sub(bytes)),
1436            )?;
1437            for entry in &entries {
1438                count = count
1439                    .checked_add(1)
1440                    .ok_or_else(|| crate::api::Error::client(0, libc::EFBIG as u32))?;
1441                bytes = bytes
1442                    .checked_add(entry.path().as_os_str().len())
1443                    .ok_or_else(|| crate::api::Error::client(0, libc::EFBIG as u32))?;
1444                if count > options.entry_limit()
1445                    || bytes > options.path_byte_limit()
1446                    || depth >= options.depth_limit()
1447                {
1448                    return Err(crate::api::Error::client(0, libc::EFBIG as u32)
1449                        .with_context("visit_dirs_ordered", entry.path()));
1450                }
1451            }
1452            order(&mut entries);
1453            let listing = DirectoryListing { path, entries };
1454            let children: Vec<_> = listing
1455                .entries
1456                .iter()
1457                .filter(|entry| entry.attrs().is_dir() && descend(entry))
1458                .map(|entry| (entry.path().to_path_buf(), depth + 1))
1459                .collect();
1460            match visitor(listing, depth)? {
1461                crate::api::WalkControl::Stop => {
1462                    return Ok(crate::api::TraversalCompletion::Stopped);
1463                }
1464                crate::api::WalkControl::SkipSubtree => continue,
1465                crate::api::WalkControl::Continue => {}
1466            }
1467            pending.extend(children.into_iter().rev());
1468        }
1469        Ok(crate::api::TraversalCompletion::Complete)
1470    }
1471
1472    // Create, move, copy, and remove
1473    /// Single-target convenience. For multiple independent directories, prefer [`Vfsi::vmkdir`].
1474    ///
1475    /// Create one directory; its parent must exist.
1476    ///
1477    /// An existing entry is an error, even if already a directory. Use
1478    /// [`create_dir_all`](VfsiExt::create_dir_all) for missing parents/idempotent
1479    /// directory setup, or [`vmkdir`](Vfsi::vmkdir) for independent siblings.
1480    ///
1481    /// ```no_run
1482    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
1483    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1484    /// fs.create_dir("/fresh-directory")?;
1485    /// # Ok(())
1486    /// # }
1487    /// ```
1488    fn create_dir(&self, path: impl AsRef<Path>) -> Result<()> {
1489        self.vmkdir(&[(path, 0o777)])
1490    }
1491
1492    /// Truncate or extend one path or opened object using [`Vfsi::vsetattrs`].
1493    fn truncate<T: crate::api::MetadataOperand<Self::File>>(
1494        &self,
1495        target: T,
1496        len: u64,
1497    ) -> Result<()> {
1498        self.vsetattrs(
1499            &[(target, crate::api::MetadataUpdate::new().len(len))],
1500            true,
1501        )
1502    }
1503
1504    /// Change permissions on one path or opened object using [`Vfsi::vsetattrs`].
1505    fn chmod<T: crate::api::MetadataOperand<Self::File>>(
1506        &self,
1507        target: T,
1508        permissions: crate::api::Permissions,
1509    ) -> Result<()> {
1510        self.vsetattrs(
1511            &[(
1512                target,
1513                crate::api::MetadataUpdate::new().permissions(permissions),
1514            )],
1515            true,
1516        )
1517    }
1518
1519    /// Change ownership of one path or opened object through [`Vfsi::vsetattrs`].
1520    /// `None` leaves the corresponding owner/group unchanged. Use the vector
1521    /// with `follow_symlinks = false` to change a symlink itself.
1522    fn chown<T: crate::api::MetadataOperand<Self::File>>(
1523        &self,
1524        target: T,
1525        uid: Option<u32>,
1526        gid: Option<u32>,
1527    ) -> Result<()> {
1528        let mut update = crate::api::MetadataUpdate::new();
1529        update.uid = uid;
1530        update.gid = gid;
1531        self.vsetattrs(&[(target, update)], true)
1532    }
1533
1534    /// Create one directory with explicit Unix permission bits.
1535    fn create_dir_with_mode(&self, path: impl AsRef<Path>, mode: u32) -> Result<()> {
1536        self.vmkdir(&[(path, mode)])
1537    }
1538
1539    /// Create one symbolic link; submit multiple pairs with [`Vfsi::vsymlink`].
1540    fn symlink(&self, target: impl AsRef<Path>, link: impl AsRef<Path>) -> Result<()> {
1541        self.vsymlink(&[(target, link)])
1542    }
1543
1544    /// Read one symlink target; submit multiple paths with [`Vfsi::vreadlink`].
1545    fn read_link(&self, path: impl AsRef<Path>) -> Result<std::path::PathBuf> {
1546        let mut targets = self.vreadlink(&[path])?;
1547        if targets.len() != 1 {
1548            return Err(crate::api::Error::transport(
1549                None,
1550                "invalid readlink result count",
1551            ));
1552        }
1553        Ok(targets.pop().expect("validated readlink count"))
1554    }
1555
1556    /// Create one hard link; submit multiple pairs with [`Vfsi::vhardlink`].
1557    fn hard_link(&self, source: impl AsRef<Path>, link: impl AsRef<Path>) -> Result<()> {
1558        self.vhardlink(&[(source, link)])
1559    }
1560
1561    /// Single-target convenience. For multiple directory creations, prefer [`Vfsi::vmkdir`]; plan missing parents before their children.
1562    ///
1563    /// Create missing parents; an error can leave some directories created.
1564    ///
1565    /// Existing directories are accepted; an existing non-directory component
1566    /// is an error. This is not atomic and is not protected against concurrent
1567    /// changes to ancestor paths.
1568    ///
1569    /// ```no_run
1570    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
1571    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1572    /// fs.create_dir_all("/workspace/results/2026")?;
1573    /// # Ok(())
1574    /// # }
1575    /// ```
1576    fn create_dir_all(&self, path: impl AsRef<Path>) -> Result<()> {
1577        let mut current = std::path::PathBuf::new();
1578        for component in path.as_ref().components() {
1579            match component {
1580                std::path::Component::RootDir => {
1581                    current.push(Path::new("/"));
1582                    continue;
1583                }
1584                std::path::Component::Normal(part) => current.push(part),
1585                std::path::Component::CurDir => continue,
1586                std::path::Component::ParentDir => {
1587                    current.push("..");
1588                    continue;
1589                }
1590                std::path::Component::Prefix(_) => {
1591                    return Err(crate::api::Error::client(0, crate::ERR_INVAL));
1592                }
1593            }
1594            match self.vmkdir(&[(&current, 0o777)]) {
1595                Ok(()) => {}
1596                Err(error) if error.err_no() == crate::ERR_EXIST => {
1597                    if !self.attrs(&current)?.is_dir() {
1598                        return Err(crate::api::Error::client(0, crate::ERR_NOTDIR)
1599                            .with_context("create_dir_all", &current));
1600                    }
1601                }
1602                Err(error) => return Err(error),
1603            }
1604        }
1605        Ok(())
1606    }
1607
1608    /// Single-target convenience. For multiple source/destination pairs, prefer [`Vfsi::vrename`].
1609    ///
1610    /// Rename within supported namespaces; cross-filesystem moves can fail.
1611    ///
1612    /// Both paths use this client's namespace. Replacement semantics follow
1613    /// the backend/filesystem; this is not a cross-file vector transaction or
1614    /// a guarantee of durable directory updates. No copy fallback is implied.
1615    ///
1616    /// ```no_run
1617    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
1618    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1619    /// fs.rename("/old-name", "/new-name")?;
1620    /// # Ok(())
1621    /// # }
1622    /// ```
1623    fn rename(&self, source: impl AsRef<Path>, destination: impl AsRef<Path>) -> Result<()> {
1624        self.vrename(&[(source, destination)], crate::api::RenameOptions::Replace)
1625    }
1626
1627    /// Single-target convenience. For multiple file pairs, prefer [`Vfsi::vcopy`].
1628    ///
1629    /// Copy a file's contents; this is not recursive tree copying.
1630    ///
1631    /// Destination creation/replacement and failures follow backend semantics.
1632    /// A failure can leave a partial destination. Do not assume metadata,
1633    /// sparse layout, or durability is preserved. Server-side copy is used
1634    /// only where supported; callers need not build protocol COPY requests.
1635    ///
1636    /// ```no_run
1637    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
1638    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1639    /// fs.copy("/source", "/destination")?;
1640    /// # Ok(())
1641    /// # }
1642    /// ```
1643    fn copy(&self, source: impl AsRef<Path>, destination: impl AsRef<Path>) -> Result<()> {
1644        self.copy_with_options(source, destination, CopyOption::default())
1645    }
1646
1647    /// Single-target copy with explicit operation semantics; prefer [`Vfsi::vcopy`]
1648    /// when submitting multiple pairs.
1649    fn copy_with_options(
1650        &self,
1651        source: impl AsRef<Path>,
1652        destination: impl AsRef<Path>,
1653        options: CopyOption,
1654    ) -> Result<()> {
1655        self.vcopy(&[(source, destination)], options)
1656    }
1657
1658    /// Single-target convenience. For multiple paths, prefer [`Vfsi::vremove`]. That vector API accepts both files and directories.
1659    ///
1660    /// Remove one file or symlink, not the symlink target.
1661    ///
1662    /// Missing paths are errors. To remove a directory use
1663    /// [`remove_dir`](VfsiExt::remove_dir) or [`remove_dir_all`](VfsiExt::remove_dir_all).
1664    ///
1665    /// ```no_run
1666    /// use vfsi_core::api::{Vfsi, VfsiExt, ErrorKind, WriteOp};
1667    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1668    /// match fs.remove_file("/temporary-file") {
1669    ///     Ok(()) => (),
1670    ///     Err(error) if error.kind() == ErrorKind::NotFound => (),
1671    ///     Err(error) => return Err(error),
1672    /// }
1673    /// # Ok(())
1674    /// # }
1675    /// ```
1676    fn remove_file(&self, path: impl AsRef<Path>) -> Result<()> {
1677        let path = path.as_ref();
1678        if self.symlink_attrs(path)?.is_dir() {
1679            return Err(
1680                crate::api::Error::client(0, crate::ERR_ISDIR).with_context("remove_file", path)
1681            );
1682        }
1683        self.vremove(
1684            &[path],
1685            crate::api::RemoveMode::Entry,
1686            crate::api::RemoveOptions::new(),
1687        )
1688    }
1689
1690    /// Single-target convenience. For multiple paths, prefer [`Vfsi::vremove`]. That vector API does not enforce directory-only inputs.
1691    ///
1692    /// Remove one empty directory; not a recursive operation.
1693    ///
1694    /// Nonempty directories fail. Use [`remove_dir_contents`](VfsiExt::remove_dir_contents)
1695    /// to empty a directory while retaining it, or [`remove_dir_all`](VfsiExt::remove_dir_all)
1696    /// to remove its tree. Only operate on paths whose removal you intend.
1697    ///
1698    /// ```no_run
1699    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
1700    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1701    /// fs.remove_dir("/empty-temporary-directory")?;
1702    /// # Ok(())
1703    /// # }
1704    /// ```
1705    fn remove_dir(&self, path: impl AsRef<Path>) -> Result<()> {
1706        let path = path.as_ref();
1707        require_directory(self, path, "remove_dir")?;
1708        self.vremove(
1709            &[path],
1710            crate::api::RemoveMode::Entry,
1711            crate::api::RemoveOptions::new(),
1712        )
1713    }
1714
1715    /// Single-target convenience. For multiple trees, prefer [`Vfsi::vremove`] with `RemoveMode::Tree`.
1716    ///
1717    /// Recursively remove a tree without following directory symlinks.
1718    /// Path-based removal is not a security sandbox or an atomic transaction.
1719    ///
1720    /// A failure can leave a partly removed tree. Use trusted ancestor paths;
1721    /// concurrent namespace substitutions are not a confinement guarantee.
1722    /// This is destructive: do not use it to prepare an arbitrary existing
1723    /// directory for an example or benchmark.
1724    ///
1725    /// ```no_run
1726    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
1727    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1728    /// // Remove only the tree that this application intentionally owns.
1729    /// fs.remove_dir_all("/owned-temporary-tree")?;
1730    /// # Ok(())
1731    /// # }
1732    /// ```
1733    fn remove_dir_all(&self, path: impl AsRef<Path>) -> Result<()> {
1734        let path = path.as_ref();
1735        require_directory(self, path, "remove_dir_all")?;
1736        self.vremove(
1737            &[path],
1738            crate::api::RemoveMode::Tree,
1739            crate::api::RemoveOptions::new(),
1740        )
1741    }
1742
1743    /// Recursively remove one directory using explicit batching and failure
1744    /// policy. The directory is validated before mutation; symlink roots are
1745    /// not followed. Prefer [`Vfsi::vremove`] when removing several trees.
1746    fn remove_dir_all_with_options(
1747        &self,
1748        path: impl AsRef<Path>,
1749        options: crate::api::RemoveOptions,
1750    ) -> Result<()> {
1751        let path = path.as_ref();
1752        require_directory(self, path, "remove_dir_all")?;
1753        self.vremove(&[path], crate::api::RemoveMode::Tree, options)
1754    }
1755
1756    /// Single-target convenience. For multiple directory roots, prefer [`Vfsi::vremove`].
1757    ///
1758    /// Recursively empty a directory while keeping its root.
1759    ///
1760    /// The root must be a directory, not a symlink. Child symlinks are removed
1761    /// without following their targets. Errors can leave partially removed
1762    /// contents; retaining the root does not make the operation transactional.
1763    ///
1764    /// ```no_run
1765    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
1766    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1767    /// fs.remove_dir_contents("/owned-scratch-directory")?;
1768    /// // The scratch directory remains available for subsequent work.
1769    /// # Ok(())
1770    /// # }
1771    /// ```
1772    fn remove_dir_contents(&self, path: impl AsRef<Path>) -> Result<()> {
1773        self.remove_dir_contents_with_options(path, crate::api::RemoveOptions::new())
1774    }
1775
1776    /// Recursively empty one directory while retaining its root, using
1777    /// explicit batching and failure policy. The root is validated without
1778    /// following a symlink. Prefer [`Vfsi::vremove`] for several roots.
1779    fn remove_dir_contents_with_options(
1780        &self,
1781        path: impl AsRef<Path>,
1782        options: crate::api::RemoveOptions,
1783    ) -> Result<()> {
1784        let path = path.as_ref();
1785        require_directory(self, path, "remove_dir_contents")?;
1786        self.vremove(&[path], crate::api::RemoveMode::Contents, options)
1787    }
1788}
1789
1790impl<C: Vfsi + ?Sized> VfsiExt for C {}
1791
1792fn single_completion<T>(mut values: Vec<T>, operation: &'static str) -> Result<T> {
1793    if values.len() != 1 {
1794        return Err(crate::api::Error::transport(
1795            None,
1796            format!("{operation} returned an invalid result count"),
1797        ));
1798    }
1799    Ok(values.remove(0))
1800}
1801fn require_directory<C: Vfsi + ?Sized>(fs: &C, path: &Path, operation: &'static str) -> Result<()> {
1802    if !fs.symlink_attrs(path)?.is_dir() {
1803        return Err(crate::api::Error::client(0, crate::ERR_NOTDIR).with_context(operation, path));
1804    }
1805    Ok(())
1806}
1807fn attrs_query<C: Vfsi + ?Sized>(
1808    client: &C,
1809    path: impl AsRef<Path>,
1810    options: crate::api::AttrsOptions,
1811    operation: &'static str,
1812) -> Result<Attrs> {
1813    let path = path.as_ref();
1814    let mut results = client
1815        .vgetattrs(&[path], options)
1816        .map_err(|error| error.with_context(operation, path))?;
1817    if results.len() != 1 {
1818        return Err(crate::api::Error::transport(
1819            None,
1820            "vgetattrs returned an invalid result count",
1821        )
1822        .with_context(operation, path));
1823    }
1824    Ok(results.remove(0))
1825}
1826
1827fn single_tree(trees: &mut Vec<Vec<DirectoryListing>>) -> Result<Vec<DirectoryListing>> {
1828    if trees.len() != 1 {
1829        return Err(crate::api::Error::transport(
1830            None,
1831            "read_dirs returned an invalid root count",
1832        ));
1833    }
1834    Ok(trees.remove(0))
1835}