Skip to main content

vfsi_core/api/
extensions.rs

1use crate::api::{Attrs, CopyOptions, DirectoryListing, MkDirOp, OpenOp, Result, SetAttrsOp};
2use std::path::Path;
3
4use super::{FileHandle, Vfsi};
5
6/// Scalar operations and convenience workflows alongside the [`Vfsi`] vectors.
7///
8/// Blanket-implemented for every filesystem implementing Vfsi. Import `VfsiExt` (or
9/// [`crate::api::prelude`]) to use these helpers; generic code needs only an
10/// `Vfsi` bound. Helpers preserve batching, resource limits, and non-atomic
11/// failure semantics. All helpers compose vectorized Vfsi primitives; native execution belongs in Vfsi.
12/// Single-target helpers use conventional names such as `open`, `attrs`,
13/// and `read_dir`. Prefer vector APIs for independent work on many files or
14/// directories so the backend can batch requests.
15///
16/// # Method groups
17///
18/// - Open and close: [`open`](Self::open), [`create`](Self::create).
19/// - Read files: [`read_files`](Self::read_files), [`read_files_with_options`](Self::read_files_with_options).
20/// - Write files: [`write`](Self::write), [`write_files`](Self::write_files).
21/// - Attrs: [`attrs`](Self::attrs), [`attrs_with_options`](Self::attrs_with_options).
22/// - Collect directories: [`read_dir`](Self::read_dir), [`read_dir_with_options`](Self::read_dir_with_options).
23/// - Visit directories: [`listdir`](Self::listdir), [`visit_dirs_ordered`](Self::visit_dirs_ordered).
24/// - Create, move, copy, and remove: [`create_dir`](Self::create_dir), [`create_dir_all`](Self::create_dir_all).
25///
26/// # Boundary
27///
28/// | Contract | Responsibility | Examples |
29/// | --- | --- | --- |
30/// | [`Vfsi`] | Native vector execution, paging, and policy inspection | `vopen`, `vread`, `vlistdirs`, `limits` |
31/// | `VfsiExt` | Scalar adapters and composed workflows | `open`, `read_files`, `create_dir_all` |
32///
33/// Implement only `Vfsi`; this extension is blanket implemented. A helper belongs
34/// here only when it can compose Vfsi primitives without losing native batching,
35/// bounded paging, or recovery semantics. Singular convenience is not a promise
36/// of one RPC, and vector execution is not a promise of atomicity.
37pub trait VfsiExt: Vfsi {
38    /// Build reusable open options for this client.
39    fn open_options(&self) -> super::OpenOptions<'_, Self> {
40        super::OpenOptions::new(self)
41    }
42
43    /// Query one target through the vector filesystem-statistics engine.
44    fn statfs<P: crate::AsTarget<Self::File>>(&self, target: P) -> Result<crate::FilesystemStats> {
45        single_completion(self.vstatfs(&[target])?, "statfs backend")
46    }
47
48    // Open and close
49    /// Single-target convenience. For multiple files, prefer [`Vfsi::vopen`] to expose batching opportunities.
50    ///
51    /// Open read-only. Paths are relative to this client's configured namespace.
52    ///
53    /// Use [`open_with`](VfsiExt::open_with) for write/create flags, or
54    /// [`vopen`](Vfsi::vopen) to batch many opens. The returned handle owns
55    /// lifecycle only; use vectors or [`std_io`](VfsiExt::std_io) for I/O.
56    ///
57    /// ```no_run
58    /// use vfsi_core::api::{Vfsi, VfsiExt, FileHandle, WriteOp};
59    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
60    /// let file = fs.open("/config")?;
61    /// let mut header = [0_u8; 128];
62    /// let result = fs.vread([vfsi_core::api::ReadOp::into(&file, 0, &mut header)], Default::default());
63    /// let close = file.close();
64    /// let bytes = result?[0].bytes_read();
65    /// close?;
66    /// println!("read {bytes} bytes");
67    /// # Ok(())
68    /// # }
69    /// ```
70    fn open(&self, path: impl AsRef<Path>) -> Result<Self::File> {
71        self.open_with(OpenOp::new(path.as_ref(), crate::api::OpenFlags::READ))
72    }
73
74    /// Single-target convenience. For multiple creations, prefer [`Vfsi::vopen`] with CREATE/TRUNCATE flags.
75    ///
76    /// Create or truncate a file and open for writing; does not create parents.
77    ///
78    /// Existing contents are discarded immediately. For exclusive creation,
79    /// use [`open_with`](VfsiExt::open_with) with `WRITE | CREATE_NEW` instead.
80    ///
81    /// ```no_run
82    /// use vfsi_core::api::{Vfsi, VfsiExt, FileHandle, WriteOp};
83    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
84    /// let file = fs.create("/output")?;
85    /// let result = fs.vwrite(&[WriteOp::at(&file, 0, b"complete contents")], vfsi_core::api::WriteOptions::new().with_write_all(true));
86    /// let close = file.close();
87    /// result?;
88    /// close?;
89    /// # Ok(())
90    /// # }
91    /// ```
92    fn create(&self, path: impl AsRef<Path>) -> Result<Self::File> {
93        self.open_with(OpenOp::new(
94            path.as_ref(),
95            crate::api::OpenFlags::WRITE
96                | crate::api::OpenFlags::CREATE
97                | crate::api::OpenFlags::TRUNCATE,
98        ))
99    }
100
101    /// Single-target convenience. For multiple requests, prefer [`Vfsi::vopen`] with per-file flags and modes.
102    ///
103    /// Open with an explicit access/create/truncate request; effects are eager.
104    ///
105    /// Creation/truncation occurs at open, not on the first write. `CREATE_NEW`
106    /// rejects an existing path. This does not create missing parents.
107    /// Delegates to singleton `Vfsi::vopen`. Backends must preserve scalar final-
108    /// symlink resolution and independently opened handles for singleton vectors.
109    ///
110    /// ```no_run
111    /// use vfsi_core::api::{Vfsi, VfsiExt, FileHandle, OpenFlags, OpenOp, WriteOp};
112    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
113    /// let file = fs.open_with(OpenOp::new(
114    ///     "/new-file", OpenFlags::WRITE | OpenFlags::CREATE_NEW,
115    /// ).mode(0o600))?;
116    /// file.close()?;
117    /// # Ok(())
118    /// # }
119    /// ```
120    fn open_with(&self, request: OpenOp) -> Result<Self::File> {
121        single_completion(self.vopen(&[request])?, "vopen")
122    }
123
124    /// Consume all handles. Errors cannot return cleanup ownership; Drop is
125    /// best-effort. Prefer `vclose` when close errors require reconciliation.
126    ///
127    /// This releases all local handles even on failure; it does not promise
128    /// every remote CLOSE succeeded. Closing alone is not a durability barrier.
129    ///
130    /// ```no_run
131    /// use vfsi_core::api::{Vfsi, VfsiExt, OpenFlags, OpenOp, WriteOp};
132    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
133    /// let files = fs.vopen(&[OpenOp::new("/config", OpenFlags::READ)])?;
134    /// fs.close_files(files)?; // Observe a close error instead of discarding it in Drop.
135    /// # Ok(())
136    /// # }
137    /// ```
138    fn close_files(&self, mut files: Vec<Self::File>) -> Result<()> {
139        self.vclose(&mut files)
140    }
141
142    // Read files
143    /// Read one complete file within the client's aggregate read budget.
144    ///
145    /// For multiple files, prefer [`Self::read_files`]. This is a convenience
146    /// over the vector read operation and retains its allocation limits.
147    fn read(&self, path: impl AsRef<Path>) -> Result<Vec<u8>> {
148        self.read_with_options(path, crate::api::ReadOptions::default())
149    }
150
151    /// Read one complete file with an explicit read policy.
152    fn read_with_options(
153        &self,
154        path: impl AsRef<Path>,
155        options: crate::api::ReadOptions,
156    ) -> Result<Vec<u8>> {
157        let path = path.as_ref();
158        let files = self
159            .read_files_with_options(&[path], options)
160            .map_err(|error| error.with_context("read", path))?;
161        single_completion(files, "read")
162    }
163    /// Read complete files in input order using vectorized whole-file reads.
164    ///
165    /// The aggregate payload is bounded by `limits().max_read_bytes()`.
166    /// Use [`Self::read_files_with_options`] to override it, or stream large files.
167    /// A failure returns an error, not a successful partial collection.
168    ///
169    /// ```no_run
170    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
171    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
172    /// let contents = fs.read_files(&["/file-1", "/file-2"])?;
173    /// for bytes in contents { println!("{} bytes", bytes.len()); }
174    /// # Ok(())
175    /// # }
176    /// ```
177    fn read_files<P: AsRef<Path>>(&self, paths: &[P]) -> Result<Vec<Vec<u8>>> {
178        self.read_files_with_options(paths, crate::api::ReadOptions::default())
179    }
180
181    /// Whole-file convenience reads with an explicit aggregate payload budget.
182    ///
183    /// ```no_run
184    /// use vfsi_core::api::{Vfsi, VfsiExt, ReadOptions, WriteOp};
185    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
186    /// let files = fs.read_files_with_options(&["/config"],
187    ///     ReadOptions::new().with_max_total_bytes(std::num::NonZeroUsize::new(1024 * 1024).unwrap()))?;
188    /// # let _ = files;
189    /// # Ok(())
190    /// # }
191    /// ```
192    fn read_files_with_options<P: AsRef<Path>>(
193        &self,
194        paths: &[P],
195        options: crate::api::ReadOptions,
196    ) -> Result<Vec<Vec<u8>>> {
197        let results = self.vread(
198            paths
199                .iter()
200                .map(|path| crate::api::ReadOp::whole(path.as_ref())),
201            options,
202        )?;
203        if results.len() != paths.len() {
204            return Err(crate::api::Error::transport(
205                None,
206                "vread_native returned an invalid result count",
207            ));
208        }
209        results
210            .into_iter()
211            .enumerate()
212            .map(|(index, result)| {
213                let complete = result.offset() == 0 && result.eof();
214                let data = result.into_data().ok_or_else(|| {
215                    crate::api::Error::transport(
216                        Some(index),
217                        "whole-file vread_native omitted owned data",
218                    )
219                })?;
220                if !complete {
221                    return Err(crate::api::Error::transport(
222                        Some(index),
223                        "whole-file vread_native returned incomplete or invalid data",
224                    ));
225                }
226                Ok(data)
227            })
228            .collect()
229    }
230
231    /// Single-target convenience. For multiple files, prefer [`Vfsi::vread`] with whole-file operations and decode each result as UTF-8.
232    ///
233    /// Read a complete UTF-8 file within this client's aggregate read budget.
234    ///
235    /// Invalid UTF-8 is an invalid-input error; use `read_files` for arbitrary bytes.
236    ///
237    /// ```no_run
238    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
239    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
240    /// let text = fs.read_to_string("/config")?;
241    /// println!("{text}");
242    /// # Ok(())
243    /// # }
244    /// ```
245    fn read_to_string(&self, path: impl AsRef<Path>) -> Result<String> {
246        self.read_to_string_with_options(path, crate::api::ReadOptions::default())
247    }
248
249    /// Single-target convenience. For multiple files, prefer [`Vfsi::vread`] with an aggregate byte budget, then decode UTF-8.
250    ///
251    /// Read a complete UTF-8 file with the same budget policy as `Vfsi::vread`.
252    /// `ReadOptions::default()` inherits the client limit; explicit overrides are nonzero.
253    ///
254    /// ```no_run
255    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
256    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
257    /// let text = fs.read_to_string_with_options("/config", vfsi_core::api::ReadOptions::new()
258    ///     .with_max_total_bytes(std::num::NonZeroUsize::new(4096).unwrap()))?;
259    /// println!("{text}");
260    /// # Ok(())
261    /// # }
262    /// ```
263    fn read_to_string_with_options(
264        &self,
265        path: impl AsRef<Path>,
266        options: crate::api::ReadOptions,
267    ) -> Result<String> {
268        let path = path.as_ref();
269        let mut files = self.read_files_with_options(&[path], options)?;
270        String::from_utf8(files.remove(0)).map_err(|_| {
271            crate::api::Error::client(0, crate::ERR_INVAL).with_context("read_to_string", path)
272        })
273    }
274
275    /// Single-target convenience. For multiple files, prefer [`Vfsi::vstream`]. Backends may process streams sequentially.
276    ///
277    /// Stream a file using the client's bounded chunk size instead of collecting it.
278    /// Return `Ok(ControlFlow::Break(()))` to stop successfully; the callback runs outside the lock.
279    ///
280    /// ```no_run
281    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
282    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
283    /// fs.read_stream("/large", |offset, bytes| {
284    ///     println!("{} bytes at {offset}", bytes.len());
285    ///     Ok(std::ops::ControlFlow::Continue(()))
286    /// })?;
287    /// # Ok(())
288    /// # }
289    /// ```
290    fn read_stream(
291        &self,
292        path: impl AsRef<Path>,
293        callback: impl FnMut(u64, &[u8]) -> Result<std::ops::ControlFlow<()>>,
294    ) -> Result<crate::api::StreamCompletion> {
295        self.read_stream_with_options(
296            path,
297            crate::api::StreamOptions::new().with_chunk_size(
298                std::num::NonZeroUsize::new(self.limits().stream_chunk_size()).unwrap(),
299            ),
300            callback,
301        )
302    }
303
304    /// Single-target convenience. For multiple files, prefer [`Vfsi::vstream`]. Backends may process streams sequentially.
305    ///
306    /// Stream from offset zero outside the backend lock. `ControlFlow::Break(())` stops after
307    /// the delivered chunk; completion reports the next offset. Not a snapshot.
308    ///
309    /// The callback borrows each chunk only for its invocation. Consume it
310    /// without collecting chunks to keep memory bounded. `chunk_size` is a
311    /// maximum; negotiated protocol limits and short reads can produce smaller
312    /// chunks. The internally opened file is closed on completion, stopping,
313    /// callback error, or read failure. Stopping successfully does not mean EOF.
314    /// This simple stream does not promise RTT-hiding parallelism; direct NFS
315    /// applications can use `NfsBuilder::connect_read_pool` for pipelined reads.
316    ///
317    /// ```no_run
318    /// use vfsi_core::api::{Vfsi, VfsiExt, StreamOptions, StreamCompletion, WriteOp};
319    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
320    /// let mut processed = 0_u64;
321    /// let completion = fs.read_stream_with_options(
322    ///     "/large.bin", StreamOptions::new().with_chunk_size(std::num::NonZeroUsize::new(1024 * 1024).unwrap()),
323    ///     |_offset, chunk| {
324    ///         processed += chunk.len() as u64; // Process the borrowed bytes here.
325    ///         Ok(if processed < 8 * 1024 * 1024 {
326    ///             std::ops::ControlFlow::Continue(())
327    ///         } else {
328    ///             std::ops::ControlFlow::Break(())
329    ///         }) // Stop after a bounded sample.
330    ///     },
331    /// )?;
332    /// match completion {
333    ///     StreamCompletion::Complete => println!("reached EOF after {processed} bytes"),
334    ///     StreamCompletion::Stopped { next_offset } => println!("stopped at {next_offset}"),
335    /// }
336    /// # Ok(())
337    /// # }
338    /// ```
339    fn read_stream_with_options(
340        &self,
341        path: impl AsRef<Path>,
342        options: crate::api::StreamOptions,
343        callback: impl FnMut(u64, &[u8]) -> Result<std::ops::ControlFlow<()>>,
344    ) -> Result<crate::api::StreamCompletion> {
345        let mut callback = callback;
346        single_completion(
347            self.vstream(&[path], options, |_, offset, data| callback(offset, data))?,
348            "read_streams",
349        )
350    }
351
352    // Write files
353    /// Single-target convenience. For multiple complete files, prefer [`VfsiExt::write_files`]; for opened handles, prefer [`Vfsi::vwrite`].
354    ///
355    /// Replace a file completely, creating/truncating eagerly; not atomic replace.
356    ///
357    /// The parent must exist. Success writes all bytes and closes the internal
358    /// handle; failure may leave a created, truncated, or partially written file.
359    /// For exclusive creation or durability control use an explicit open handle.
360    ///
361    /// ```no_run
362    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
363    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
364    /// fs.write("/output", b"replacement contents")?;
365    /// # Ok(())
366    /// # }
367    /// ```
368    fn write(&self, path: impl AsRef<Path>, data: &[u8]) -> Result<()> {
369        // Scalar open preserves backend-specific final-symlink resolution.
370        // Always attempt close, but retain the write error if both fail.
371        let file = self.create(path)?;
372        let result = self.vwrite(
373            &[crate::api::WriteOp::at(&file, 0, data)],
374            crate::api::WriteOptions::new().with_write_all(true),
375        );
376        let close_result = file.close();
377        result?;
378        close_result
379    }
380
381    /// Replace files in vector phases. Success completes payloads; errors may
382    /// follow create/truncate/write effects and never authorize blind replay.
383    ///
384    /// Parents must exist. Payloads are completed via vector open/write/close
385    /// phases, not a scalar write loop or necessarily a single RPC. No atomic
386    /// replacement, cross-file transaction, or durability guarantee is provided.
387    ///
388    /// ```no_run
389    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
390    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
391    /// fs.write_files(&[
392    ///     ("/file-1", b"hello".as_slice()),
393    ///     ("/file-2", b"world".as_slice()),
394    /// ])?;
395    /// # Ok(())
396    /// # }
397    /// ```
398    fn write_files<P: AsRef<Path>, B: AsRef<[u8]>>(&self, entries: &[(P, B)]) -> Result<()> {
399        let mut seen = std::collections::HashSet::with_capacity(entries.len());
400        for (index, (path, _)) in entries.iter().enumerate() {
401            if !seen.insert(path.as_ref()) {
402                return Err(crate::api::Error::client(index, crate::ERR_INVAL)
403                    .with_context("write_files", path.as_ref()));
404            }
405        }
406        let requests: Vec<_> = entries
407            .iter()
408            .map(|(path, _)| {
409                OpenOp::new(
410                    path.as_ref(),
411                    crate::api::OpenFlags::WRITE
412                        | crate::api::OpenFlags::CREATE
413                        | crate::api::OpenFlags::TRUNCATE,
414                )
415            })
416            .collect();
417        let files = self.vopen(&requests)?;
418        if files.len() != entries.len() {
419            return Err(crate::api::Error::transport(
420                None,
421                "vopen returned an invalid result count",
422            ));
423        }
424        let writes: Vec<_> = files
425            .iter()
426            .zip(entries)
427            .map(|(file, (_, data))| crate::api::WriteOp::at(file, 0, data.as_ref()))
428            .collect();
429        let result = self.vwrite(
430            &writes,
431            crate::api::WriteOptions::new().with_write_all(true),
432        );
433        drop(writes);
434        let close_result = self.close_files(files);
435        result?;
436        close_result
437    }
438
439    /// Borrow a standard-I/O adapter with an independent cursor, initially zero.
440    /// Reads/writes use this client's vectors; collecting reads enforce its
441    /// payload budget. The adapter does not own or close the file.
442    fn std_io<'a>(
443        &'a self,
444        file: &'a Self::File,
445    ) -> impl std::io::Read + std::io::Write + std::io::Seek + 'a {
446        super::io::StdIo::new(self, file)
447    }
448
449    /// Synchronize one retained handle through [`Vfsi::vfsync`].
450    fn sync_data(&self, file: &Self::File) -> Result<()> {
451        self.vfsync(&[file], crate::api::SyncMode::Data)
452    }
453
454    /// Synchronize one retained handle's data and metadata through [`Vfsi::vfsync`].
455    fn sync_all(&self, file: &Self::File) -> Result<()> {
456        self.vfsync(&[file], crate::api::SyncMode::All)
457    }
458
459    // Attrs
460    /// Single-target convenience. For multiple paths, prefer [`Vfsi::vgetattrs`].
461    ///
462    /// Query a path following its final symlink; unavailable fields remain None.
463    ///
464    /// To inspect the symlink itself use [`symlink_attrs`](VfsiExt::symlink_attrs).
465    /// To query an opened object after rename, pass [`crate::api::Target::file`].
466    ///
467    /// ```no_run
468    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
469    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
470    /// let attrs = fs.attrs("/file-1")?;
471    /// println!("{:?} bytes; directory={}", attrs.len(), attrs.is_dir());
472    /// # Ok(())
473    /// # }
474    /// ```
475    fn attrs<T: crate::AsTarget<Self::File>>(&self, path: T) -> Result<Attrs> {
476        attrs_query(self, path, crate::api::AttrsOptions::new(), "attrs")
477    }
478
479    /// Single-target convenience. For multiple paths, prefer [`Vfsi::vgetattrs`] with selected fields and follow_final_symlink(false).
480    ///
481    /// Query one path with selected fields and final-symlink behavior; absent fields remain None.
482    ///
483    /// This avoids requesting every optional attribute. Type/size can be
484    /// requested internally even when not selected; missing optional attributes
485    /// must still be handled via their `Option` accessors.
486    ///
487    /// ```no_run
488    /// use vfsi_core::api::{Vfsi, VfsiExt, Attributes, WriteOp};
489    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
490    /// let attrs = fs.attrs_with_options(
491    ///     "/file-1", vfsi_core::api::AttrsOptions::new()
492    ///         .with_fields(Attributes::MODE | Attributes::BLOCKS).with_follow_final_symlink(false),
493    /// )?;
494    /// if let Some(blocks) = attrs.blocks() {
495    ///     println!("allocated blocks: {blocks}");
496    /// }
497    /// # Ok(())
498    /// # }
499    /// ```
500    fn attrs_with_options<T: crate::AsTarget<Self::File>>(
501        &self,
502        path: T,
503        options: crate::api::AttrsOptions,
504    ) -> Result<Attrs> {
505        attrs_query(self, path, options, "attrs_with_options")
506    }
507
508    /// Single-target convenience. For multiple paths, prefer [`Vfsi::vgetattrs`] with follow_final_symlink(false).
509    ///
510    /// Query the final symlink itself instead of following it.
511    ///
512    /// This does not prevent following symlinks in ancestor components and is
513    /// not a race-free namespace confinement primitive.
514    ///
515    /// ```no_run
516    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
517    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
518    /// let attrs = fs.symlink_attrs("/link-or-file")?;
519    /// println!("symlink={}", attrs.is_symlink());
520    /// # Ok(())
521    /// # }
522    /// ```
523    fn symlink_attrs(&self, path: impl AsRef<Path>) -> Result<Attrs> {
524        attrs_query(
525            self,
526            path,
527            crate::api::AttrsOptions::new().with_follow_final_symlink(false),
528            "symlink_attrs",
529        )
530    }
531
532    // Collect directories
533    /// Single-target convenience. For multiple directories, prefer [`VfsiExt::read_dirs_with_options`] or [`VfsiExt::read_dirs`].
534    ///
535    /// Collect one directory using the client's entry and path-byte limits.
536    ///
537    /// ```no_run
538    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
539    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
540    /// for entry in fs.read_dir("/input")? {
541    ///     println!("{}", entry.path().display());
542    /// }
543    /// # Ok(())
544    /// # }
545    /// ```
546    fn read_dir(&self, path: impl AsRef<Path>) -> Result<Vec<crate::api::DirEntry>> {
547        self.read_dir_with_options(path, crate::api::ListDirOptions::new())
548    }
549
550    /// Single-target convenience. For multiple directories, prefer [`VfsiExt::read_dirs_with_options`] with one aggregate budget.
551    ///
552    /// Collect one shallow directory with selected metadata and entry/path-byte limits.
553    /// Recursive options are rejected; use `read_dirs_with_options` with
554    /// `ListDirOptions::with_recursive(true)` to collect a tree.
555    ///
556    /// ```no_run
557    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
558    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
559    /// let entries = fs.read_dir_with_options("/input",
560    ///     vfsi_core::api::ListDirOptions::new().with_max_entries(100))?;
561    /// println!("{} entries", entries.len());
562    /// # Ok(())
563    /// # }
564    /// ```
565    fn read_dir_with_options(
566        &self,
567        path: impl AsRef<Path>,
568        options: crate::api::ListDirOptions,
569    ) -> Result<Vec<crate::api::DirEntry>> {
570        if options.recursive() {
571            return Err(
572                crate::api::Error::client(0, libc::EINVAL as u32).with_context(
573                    "read_dir_with_options requires shallow options",
574                    path.as_ref(),
575                ),
576            );
577        }
578        let mut trees = self.read_dirs_with_options(&[path], options)?;
579        Ok(single_completion(single_tree(&mut trees)?, "read_dirs")?.entries)
580    }
581
582    /// Collect directory listings under one aggregate entry/path-byte policy.
583    ///
584    /// There is one listing per input directory, in input order; entry order is
585    /// backend-defined. Entries include metadata, avoiding a separate scalar
586    /// stat per child. The default policy comes from this client's limits.
587    /// Use [`read_dirs_with_options`](VfsiExt::read_dirs_with_options) to select
588    /// attributes, or a visitor instead of collecting large listings.
589    ///
590    /// ```no_run
591    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
592    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
593    /// for listing in fs.read_dirs(&["/input", "/output"])? {
594    ///     for entry in listing.entries {
595    ///         println!("{}: {:?} bytes", entry.path().display(), entry.attrs().len());
596    ///     }
597    /// }
598    /// # Ok(())
599    /// # }
600    /// ```
601    fn read_dirs<P: AsRef<Path>>(&self, paths: &[P]) -> Result<Vec<DirectoryListing>> {
602        let trees = self.read_dirs_with_options(paths, crate::api::ListDirOptions::new())?;
603        if trees.len() != paths.len() || trees.iter().any(|tree| tree.len() != 1) {
604            return Err(crate::api::Error::transport(
605                None,
606                "read_dirs returned an invalid result count",
607            ));
608        }
609        Ok(trees.into_iter().flatten().collect())
610    }
611
612    /// Collect shallow directories or recursive trees, grouped by input root.
613    ///
614    /// `results[i]` contains the listings for `paths[i]`. Shallow mode
615    /// returns exactly one listing per root; recursive mode includes the root
616    /// and descendants. Entries and path-byte budgets are shared across roots;
617    /// recursive collection also charges retained directory paths.
618    /// Collection builds on the batched directory-page visitor.
619    /// Explicit limits override client defaults. Errors discard collected results;
620    /// this is not a snapshot. Symlink entries are not recursively followed.
621    ///
622    /// ```no_run
623    /// use vfsi_core::api::{Vfsi, VfsiExt, Attributes, ListDirOptions};
624    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
625    /// let trees = fs.read_dirs_with_options(&["/input", "/output"],
626    ///     ListDirOptions::new().with_recursive(true).with_fields(Attributes::MODE)
627    ///         .with_max_entries(10_000).with_max_path_bytes(1024 * 1024))?;
628    /// for tree in trees {
629    ///     for listing in tree {
630    ///         println!("{}: {} entries", listing.path.display(), listing.entries.len());
631    ///     }
632    /// }
633    /// # Ok(())
634    /// # }
635    /// ```
636    fn read_dirs_with_options<P: AsRef<Path>>(
637        &self,
638        paths: &[P],
639        options: crate::api::ListDirOptions,
640    ) -> Result<Vec<Vec<DirectoryListing>>> {
641        let mut trees: Vec<Vec<DirectoryListing>> = (0..paths.len()).map(|_| Vec::new()).collect();
642        let mut positions: Vec<std::collections::HashMap<std::path::PathBuf, usize>> = (0..paths
643            .len())
644            .map(|_| std::collections::HashMap::new())
645            .collect();
646        self.vlistdirs(paths, options, |index, page| {
647            let tree = trees
648                .get_mut(index)
649                .ok_or_else(|| crate::api::Error::transport(None, "invalid visitor root index"))?;
650            let positions = &mut positions[index];
651            if let Some(&slot) = positions.get(&page.path) {
652                tree[slot].entries.extend(page.entries);
653            } else {
654                positions.insert(page.path.clone(), tree.len());
655                // Keep the first page's path and entry storage; later pages
656                // need only a borrowed lookup to find this directory.
657                tree.push(page);
658            }
659            Ok(std::ops::ControlFlow::Continue(()))
660        })?;
661        Ok(trees)
662    }
663
664    // Visit directories
665    /// List one directory through callbacks, optionally recursively.
666    /// For independent roots and owned pages, prefer [`Vfsi::vlistdirs`].
667    ///
668    /// By default, only child `Entry` events are delivered, with no extra root
669    /// attribute request or full-listing allocation on paging backends. Set
670    /// `recursive(true)` to descend without following entry symlinks. Entry-only
671    /// traversal retains native anchored child cursors and sibling batching;
672    /// its order is backend-defined.
673    ///
674    /// `enter_leave(true)` includes the root and emits depth-first `Enter` and
675    /// `Leave` events for traversed directories, including pruned directories.
676    /// In shallow lifecycle mode, children are `Entry` events. Symlinks are
677    /// always `Entry` events. A non-directory lifecycle root produces one Entry.
678    /// Lifecycle traversal currently retains bounded directory buffers because
679    /// the vector page primitive does not expose resumable cursors.
680    /// `sort_by_name(true)` also buffers a bounded directory before sorting.
681    /// Unsorted entry-only visiting remains paged. This is not a snapshot.
682    /// Buffered recursive/lifecycle visiting rejects symlinks in reopened
683    /// directory paths, including ancestors. Backends unable to guarantee that
684    /// return Unsupported instead of checking and then following the path.
685    /// Ordinary shallow listing retains the backend's path resolution; use
686    /// `allow_symlink_components(false)` to require no-follow directory opens explicitly.
687    ///
688    /// The root is depth zero and its children have depth one. Directory depth
689    /// limits control descent; shallow mode ignores them. Lifecycle mode charges
690    /// the root against the entry/path budgets; recursive entry-only mode charges
691    /// retained directory paths. All unspecified budgets inherit client limits.
692    ///
693    /// `SkipSubtree` on Enter prevents reading that directory's children;
694    /// it does nothing on files or Leave. Pruning a recursive directory Entry
695    /// without lifecycle events is rejected: enable `enter_leave(true)`.
696    /// `Stop` returns Stopped immediately,
697    /// without synthesizing pending Leave events. Callback errors propagate
698    /// without replay. Callbacks run outside backend locks and may reenter.
699    ///
700    /// ```no_run
701    /// use vfsi_core::api::{ListDirOptions, Vfsi, VfsiExt, WalkControl};
702    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
703    /// fs.listdir("/input", ListDirOptions::new(), |event| {
704    ///     println!("{}", event.entry.path().display());
705    ///     Ok(WalkControl::Continue)
706    /// })?;
707    /// # Ok(())
708    /// # }
709    /// ```
710    ///
711    /// Recursive lifecycle events allow pruning before listing children:
712    ///
713    /// ```no_run
714    /// use vfsi_core::api::{ListDirOptions, Vfsi, VfsiExt, WalkControl, WalkEventKind};
715    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
716    /// fs.listdir("/project",
717    ///     vfsi_core::api::WalkOptions::new(ListDirOptions::new().with_recursive(true))
718    ///         .with_enter_leave(true).with_sort_by_name(true),
719    ///     |event| {
720    ///         if event.kind == WalkEventKind::Enter
721    ///             && event.entry.file_name() == Some(std::ffi::OsStr::new(".git")) {
722    ///             return Ok(WalkControl::SkipSubtree);
723    ///         }
724    ///         println!("{:?}: {}", event.kind, event.entry.path().display());
725    ///         Ok(WalkControl::Continue)
726    ///     })?;
727    /// # Ok(())
728    /// # }
729    /// ```
730    fn listdir(
731        &self,
732        root: impl AsRef<Path>,
733        options: impl Into<crate::api::WalkOptions>,
734        visitor: impl FnMut(&crate::api::WalkEvent) -> Result<crate::api::WalkControl>,
735    ) -> Result<crate::api::TraversalCompletion> {
736        super::listdir::listdir(self, root.as_ref(), options.into(), visitor)
737    }
738
739    /// Visit individual entries using the directory-page primitive.
740    /// Empty directories produce pages but no entry callbacks.
741    fn visit_entries_with_options<P: AsRef<Path>>(
742        &self,
743        paths: &[P],
744        options: crate::api::ListDirOptions,
745        mut callback: impl FnMut(usize, &crate::api::DirEntry) -> Result<std::ops::ControlFlow<()>>,
746    ) -> Result<Vec<crate::api::TraversalCompletion>> {
747        self.vlistdirs(paths, options, |index, page| {
748            for entry in &page.entries {
749                if callback(index, entry)?.is_break() {
750                    return Ok(std::ops::ControlFlow::Break(()));
751                }
752            }
753            Ok(std::ops::ControlFlow::Continue(()))
754        })
755    }
756
757    /// Visit complete shallow listings in depth-first order with application policy.
758    ///
759    /// `order` compares immutable siblings (for example, locale-aware `ls` ordering).
760    /// The shared workflow sorts them; callbacks cannot replace validated entries.
761    /// `descend` selects directories before their contents are read; it does not
762    /// filter entries delivered in their parent's listing. Symlinks are never
763    /// traversed. Descendant opens reject symlinks in every component; backends
764    /// unable to guarantee that return Unsupported. The root uses the supplied
765    /// path-resolution policy. `visitor` runs outside backend locks and may stop immediately
766    /// or skip all children of the current directory. Errors after delivered
767    /// listings must not trigger replay through another backend.
768    ///
769    /// Storage is bounded by one complete listing and the pending directory
770    /// frontier. Entry/path budgets charge all fetched children, including those
771    /// rejected by `descend`. This ordered workflow deliberately does not
772    /// speculatively list children before admission; use `vlistdirs` to batch
773    /// already-approved independent directories.
774    ///
775    /// ```no_run
776    /// use vfsi_core::api::{Attributes, Result, Vfsi, VfsiExt, WalkControl};
777    /// # fn example(fs: &impl Vfsi) -> Result<()> {
778    /// fs.visit_dirs_ordered("/input",
779    ///     fs.limits().walk_options().with_fields(Attributes::MODE | Attributes::SIZE),
780    ///     |a, b| a.path().cmp(b.path()),
781    ///     |entry| entry.file_name() != Some(std::ffi::OsStr::new(".git")),
782    ///     |listing, depth| {
783    ///         println!("{}: {} entries at depth {depth}", listing.path.display(), listing.entries.len());
784    ///         Ok(WalkControl::Continue)
785    ///     })?;
786    /// # Ok(())
787    /// # }
788    /// ```
789    fn visit_dirs_ordered(
790        &self,
791        root: impl AsRef<Path>,
792        options: crate::api::ListDirOptions,
793        mut order: impl FnMut(&crate::api::DirEntry, &crate::api::DirEntry) -> std::cmp::Ordering,
794        mut descend: impl FnMut(&crate::api::DirEntry) -> bool,
795        mut visitor: impl FnMut(DirectoryListing, usize) -> Result<crate::api::WalkControl>,
796    ) -> Result<crate::api::TraversalCompletion> {
797        let options = options.walk_options(self.limits());
798        let root = root.as_ref();
799        let mut budget = crate::internal::TraversalBudget::new(
800            options.max_entries_or(crate::api::DEFAULT_DIRECTORY_MAX_ENTRIES),
801            options.max_path_bytes_or(crate::api::DEFAULT_DIRECTORY_MAX_PATH_BYTES),
802        );
803        budget.charge(root)?;
804        let mut pending = vec![(root.to_path_buf(), 0usize)];
805        while let Some((path, depth)) = pending.pop() {
806            let mut entries = self.read_dir_with_options(
807                &path,
808                budget.remaining_options(
809                    crate::api::ListDirOptions::new()
810                        .with_fields(options.fields())
811                        // Descendants were observed as real directories in a prior
812                        // listing. Reopening must not follow swapped ancestors.
813                        .with_allow_symlink_components(
814                            depth == 0 && options.allow_symlink_components(),
815                        ),
816                ),
817            )?;
818            for entry in &entries {
819                budget
820                    .charge(entry.path())
821                    .map_err(|error| error.with_context("visit_dirs_ordered", entry.path()))?;
822            }
823            entries.sort_by(&mut order);
824            let listing = DirectoryListing { path, entries };
825            let children: Vec<_> = listing
826                .entries
827                .iter()
828                .filter(|entry| entry.attrs().is_dir() && descend(entry))
829                .map(|entry| entry.path().to_path_buf())
830                .collect();
831            match visitor(listing, depth)? {
832                crate::api::WalkControl::Stop => {
833                    return Ok(crate::api::TraversalCompletion::Stopped);
834                }
835                crate::api::WalkControl::SkipSubtree => continue,
836                crate::api::WalkControl::Continue => {}
837            }
838            if depth >= options.max_depth_or(Default::default()).get() && !children.is_empty() {
839                if !options.truncate_at_max_depth() {
840                    return Err(crate::api::Error::client(0, libc::EFBIG as u32)
841                        .with_context("visit_dirs_ordered", &children[0]));
842                }
843            } else {
844                pending.extend(children.into_iter().rev().map(|child| (child, depth + 1)));
845            }
846        }
847        Ok(crate::api::TraversalCompletion::Complete)
848    }
849
850    // Create, move, copy, and remove
851    /// Single-target convenience. For multiple independent directories, prefer [`Vfsi::vmkdir`].
852    ///
853    /// Create one directory; its parent must exist.
854    ///
855    /// An existing entry is an error, even if already a directory. Use
856    /// [`create_dir_all`](VfsiExt::create_dir_all) for missing parents/idempotent
857    /// directory setup, or [`vmkdir`](Vfsi::vmkdir) for independent siblings.
858    ///
859    /// ```no_run
860    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
861    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
862    /// fs.create_dir("/fresh-directory")?;
863    /// # Ok(())
864    /// # }
865    /// ```
866    fn create_dir(&self, path: impl AsRef<Path>) -> Result<()> {
867        self.vmkdir(&[MkDirOp::new(path, 0o777)])
868    }
869
870    /// Truncate or extend one path or opened object using [`Vfsi::vsetattrs`].
871    fn truncate<T: crate::api::AsTarget<Self::File>>(&self, target: T, len: u64) -> Result<()> {
872        self.vsetattrs(&[SetAttrsOp::new(target).len(len)])
873    }
874
875    /// Change permissions on one path or opened object using [`Vfsi::vsetattrs`].
876    fn chmod<T: crate::api::AsTarget<Self::File>>(
877        &self,
878        target: T,
879        permissions: crate::api::Permissions,
880    ) -> Result<()> {
881        self.vsetattrs(&[SetAttrsOp::new(target).permissions(permissions)])
882    }
883
884    /// Change ownership of one path or opened object through [`Vfsi::vsetattrs`].
885    /// `None` leaves the corresponding owner/group unchanged. Use the vector
886    /// with [`SetAttrsOp::with_follow_final_symlink(false)`](SetAttrsOp::with_follow_final_symlink)
887    /// to change a symlink itself.
888    fn chown<T: crate::api::AsTarget<Self::File>>(
889        &self,
890        target: T,
891        uid: Option<u32>,
892        gid: Option<u32>,
893    ) -> Result<()> {
894        let mut op = SetAttrsOp::new(target);
895        if let Some(uid) = uid {
896            op = op.uid(uid);
897        }
898        if let Some(gid) = gid {
899            op = op.gid(gid);
900        }
901        self.vsetattrs(&[op])
902    }
903
904    /// Create one directory with explicit Unix permission bits.
905    fn create_dir_with_mode(&self, path: impl AsRef<Path>, mode: u32) -> Result<()> {
906        self.vmkdir(&[MkDirOp::new(path, mode)])
907    }
908
909    /// Create one symbolic link; submit multiple pairs with [`Vfsi::vsymlink`].
910    fn symlink(&self, target: impl AsRef<Path>, link: impl AsRef<Path>) -> Result<()> {
911        self.vsymlink(&[(target, link)])
912    }
913
914    /// Read one symlink target; submit multiple paths with [`Vfsi::vreadlink`].
915    fn read_link(&self, path: impl AsRef<Path>) -> Result<std::path::PathBuf> {
916        let mut targets = self.vreadlink(&[path])?;
917        if targets.len() != 1 {
918            return Err(crate::api::Error::transport(
919                None,
920                "invalid readlink result count",
921            ));
922        }
923        Ok(targets.pop().expect("validated readlink count"))
924    }
925
926    /// Create one hard link; submit multiple pairs with [`Vfsi::vhardlink`].
927    fn hard_link(&self, source: impl AsRef<Path>, link: impl AsRef<Path>) -> Result<()> {
928        self.vhardlink(&[(source, link)])
929    }
930
931    /// Single-target convenience. For multiple directory creations, prefer [`Vfsi::vmkdir`]; plan missing parents before their children.
932    ///
933    /// Create missing parents; an error can leave some directories created.
934    ///
935    /// Existing directories are accepted; an existing non-directory component
936    /// is an error. This is not atomic and is not protected against concurrent
937    /// changes to ancestor paths.
938    ///
939    /// ```no_run
940    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
941    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
942    /// fs.create_dir_all("/workspace/results/2026")?;
943    /// # Ok(())
944    /// # }
945    /// ```
946    fn create_dir_all(&self, path: impl AsRef<Path>) -> Result<()> {
947        let mut current = std::path::PathBuf::new();
948        for component in path.as_ref().components() {
949            match component {
950                std::path::Component::RootDir => {
951                    current.push(Path::new("/"));
952                    continue;
953                }
954                std::path::Component::Normal(part) => current.push(part),
955                std::path::Component::CurDir => continue,
956                std::path::Component::ParentDir => {
957                    current.push("..");
958                    continue;
959                }
960                std::path::Component::Prefix(_) => {
961                    return Err(crate::api::Error::client(0, crate::ERR_INVAL));
962                }
963            }
964            match self.vmkdir(&[MkDirOp::new(&current, 0o777)]) {
965                Ok(()) => {}
966                Err(error) if error.raw_code() == crate::ERR_EXIST => {
967                    if !self.attrs(&current)?.is_dir() {
968                        return Err(crate::api::Error::client(0, crate::ERR_NOTDIR)
969                            .with_context("create_dir_all", &current));
970                    }
971                }
972                Err(error) => return Err(error),
973            }
974        }
975        Ok(())
976    }
977
978    /// Single-target convenience. For multiple source/destination pairs, prefer [`Vfsi::vrename`].
979    ///
980    /// Rename within supported namespaces; cross-filesystem moves can fail.
981    ///
982    /// Both paths use this client's namespace. Replacement semantics follow
983    /// the backend/filesystem; this is not a cross-file vector transaction or
984    /// a guarantee of durable directory updates. No copy fallback is implied.
985    ///
986    /// ```no_run
987    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
988    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
989    /// fs.rename("/old-name", "/new-name")?;
990    /// # Ok(())
991    /// # }
992    /// ```
993    fn rename(&self, source: impl AsRef<Path>, destination: impl AsRef<Path>) -> Result<()> {
994        self.vrename(&[(source, destination)], crate::api::RenameMode::Replace)
995    }
996
997    /// Single-target convenience. For multiple file pairs, prefer [`Vfsi::vcopy`].
998    ///
999    /// Copy a file's contents; this is not recursive tree copying.
1000    ///
1001    /// Destination creation/replacement and failures follow backend semantics.
1002    /// A failure can leave a partial destination. Do not assume metadata,
1003    /// sparse layout, or durability is preserved. Server-side copy is used
1004    /// only where supported; callers need not build protocol COPY requests.
1005    ///
1006    /// ```no_run
1007    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
1008    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1009    /// fs.copy("/source", "/destination")?;
1010    /// # Ok(())
1011    /// # }
1012    /// ```
1013    fn copy(&self, source: impl AsRef<Path>, destination: impl AsRef<Path>) -> Result<()> {
1014        self.copy_with_options(source, destination, CopyOptions::default())
1015    }
1016
1017    /// Single-target copy with explicit operation semantics; prefer [`Vfsi::vcopy`]
1018    /// when submitting multiple pairs.
1019    fn copy_with_options(
1020        &self,
1021        source: impl AsRef<Path>,
1022        destination: impl AsRef<Path>,
1023        options: CopyOptions,
1024    ) -> Result<()> {
1025        self.vcopy(&[(source, destination)], options)
1026    }
1027
1028    /// Single-target convenience. For multiple paths, prefer [`Vfsi::vremove`]. That vector API accepts both files and directories.
1029    ///
1030    /// Remove one file or symlink, not the symlink target.
1031    ///
1032    /// Missing paths are errors. To remove a directory use
1033    /// [`remove_dir`](VfsiExt::remove_dir) or [`remove_dir_all`](VfsiExt::remove_dir_all).
1034    ///
1035    /// ```no_run
1036    /// use vfsi_core::api::{Vfsi, VfsiExt, ErrorKind, WriteOp};
1037    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1038    /// match fs.remove_file("/temporary-file") {
1039    ///     Ok(()) => (),
1040    ///     Err(error) if error.kind() == ErrorKind::NotFound => (),
1041    ///     Err(error) => return Err(error),
1042    /// }
1043    /// # Ok(())
1044    /// # }
1045    /// ```
1046    fn remove_file(&self, path: impl AsRef<Path>) -> Result<()> {
1047        let path = path.as_ref();
1048        if self.symlink_attrs(path)?.is_dir() {
1049            return Err(
1050                crate::api::Error::client(0, crate::ERR_ISDIR).with_context("remove_file", path)
1051            );
1052        }
1053        self.vremove(
1054            &[path],
1055            crate::api::RemoveMode::Entry,
1056            crate::api::RemoveOptions::new(),
1057        )
1058    }
1059
1060    /// Single-target convenience. For multiple paths, prefer [`Vfsi::vremove`]. That vector API does not enforce directory-only inputs.
1061    ///
1062    /// Remove one empty directory; not a recursive operation.
1063    ///
1064    /// Nonempty directories fail. Use [`remove_dir_contents`](VfsiExt::remove_dir_contents)
1065    /// to empty a directory while retaining it, or [`remove_dir_all`](VfsiExt::remove_dir_all)
1066    /// to remove its tree. Only operate on paths whose removal you intend.
1067    ///
1068    /// ```no_run
1069    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
1070    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1071    /// fs.remove_dir("/empty-temporary-directory")?;
1072    /// # Ok(())
1073    /// # }
1074    /// ```
1075    fn remove_dir(&self, path: impl AsRef<Path>) -> Result<()> {
1076        let path = path.as_ref();
1077        require_directory(self, path, "remove_dir")?;
1078        self.vremove(
1079            &[path],
1080            crate::api::RemoveMode::Entry,
1081            crate::api::RemoveOptions::new(),
1082        )
1083    }
1084
1085    /// Single-target convenience. For multiple trees, prefer [`Vfsi::vremove`] with `RemoveMode::Tree`.
1086    ///
1087    /// Recursively remove a tree without following directory symlinks.
1088    /// Path-based removal is not a security sandbox or an atomic transaction.
1089    ///
1090    /// A failure can leave a partly removed tree. Use trusted ancestor paths;
1091    /// concurrent namespace substitutions are not a confinement guarantee.
1092    /// This is destructive: do not use it to prepare an arbitrary existing
1093    /// directory for an example or benchmark.
1094    ///
1095    /// ```no_run
1096    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
1097    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1098    /// // Remove only the tree that this application intentionally owns.
1099    /// fs.remove_dir_all("/owned-temporary-tree")?;
1100    /// # Ok(())
1101    /// # }
1102    /// ```
1103    fn remove_dir_all(&self, path: impl AsRef<Path>) -> Result<()> {
1104        let path = path.as_ref();
1105        require_directory(self, path, "remove_dir_all")?;
1106        self.vremove(
1107            &[path],
1108            crate::api::RemoveMode::Tree,
1109            crate::api::RemoveOptions::new(),
1110        )
1111    }
1112
1113    /// Recursively remove one directory using explicit batching and failure
1114    /// policy. The directory is validated before mutation; symlink roots are
1115    /// not followed. Prefer [`Vfsi::vremove`] when removing several trees.
1116    fn remove_dir_all_with_options(
1117        &self,
1118        path: impl AsRef<Path>,
1119        options: crate::api::RemoveOptions,
1120    ) -> Result<()> {
1121        let path = path.as_ref();
1122        require_directory(self, path, "remove_dir_all")?;
1123        self.vremove(&[path], crate::api::RemoveMode::Tree, options)
1124    }
1125
1126    /// Single-target convenience. For multiple directory roots, prefer [`Vfsi::vremove`].
1127    ///
1128    /// Recursively empty a directory while keeping its root.
1129    ///
1130    /// The root must be a directory, not a symlink. Child symlinks are removed
1131    /// without following their targets. Errors can leave partially removed
1132    /// contents; retaining the root does not make the operation transactional.
1133    /// The path is resolved for this call, not pinned across calls or renames.
1134    /// Use trusted ancestors; this is not a path-confinement guarantee.
1135    ///
1136    /// ```no_run
1137    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
1138    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1139    /// fs.remove_dir_contents("/owned-scratch-directory")?;
1140    /// // The scratch directory remains available for subsequent work.
1141    /// # Ok(())
1142    /// # }
1143    /// ```
1144    fn remove_dir_contents(&self, path: impl AsRef<Path>) -> Result<()> {
1145        self.remove_dir_contents_with_options(path, crate::api::RemoveOptions::new())
1146    }
1147
1148    /// Recursively empty one directory while retaining its root, using
1149    /// explicit batching and failure policy. The root is validated without
1150    /// following a symlink. Prefer [`Vfsi::vremove`] for several roots.
1151    /// Like [`Self::remove_dir_contents`], this resolves the current pathname;
1152    /// it does not retain a previously opened directory's identity.
1153    fn remove_dir_contents_with_options(
1154        &self,
1155        path: impl AsRef<Path>,
1156        options: crate::api::RemoveOptions,
1157    ) -> Result<()> {
1158        let path = path.as_ref();
1159        require_directory(self, path, "remove_dir_contents")?;
1160        self.vremove(&[path], crate::api::RemoveMode::Contents, options)
1161    }
1162}
1163
1164impl<C: Vfsi + ?Sized> VfsiExt for C {}
1165
1166fn single_completion<T>(mut values: Vec<T>, operation: &'static str) -> Result<T> {
1167    if values.len() != 1 {
1168        return Err(crate::api::Error::transport(
1169            None,
1170            format!("{operation} returned an invalid result count"),
1171        ));
1172    }
1173    Ok(values.remove(0))
1174}
1175fn require_directory<C: Vfsi + ?Sized>(fs: &C, path: &Path, operation: &'static str) -> Result<()> {
1176    if !fs.symlink_attrs(path)?.is_dir() {
1177        return Err(crate::api::Error::client(0, crate::ERR_NOTDIR).with_context(operation, path));
1178    }
1179    Ok(())
1180}
1181fn attrs_query<C: Vfsi + ?Sized>(
1182    client: &C,
1183    target: impl crate::AsTarget<C::File>,
1184    options: crate::api::AttrsOptions,
1185    operation: &'static str,
1186) -> Result<Attrs> {
1187    let target = target.as_target();
1188    let path = match target {
1189        crate::Target::Path(path) => path,
1190        crate::Target::File(file) => file.path(),
1191    };
1192    let results = client
1193        .vgetattrs(&[target], options)
1194        .map_err(|error| error.with_context(operation, path))?;
1195    single_completion(results, "vgetattrs").map_err(|error| error.with_context(operation, path))
1196}
1197
1198fn single_tree(trees: &mut Vec<Vec<DirectoryListing>>) -> Result<Vec<DirectoryListing>> {
1199    if trees.len() != 1 {
1200        return Err(crate::api::Error::transport(
1201            None,
1202            "read_dirs returned an invalid root count",
1203        ));
1204    }
1205    Ok(trees.remove(0))
1206}