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