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