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