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