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        require_page_options(options)?;
642        let mut trees: Vec<Vec<DirectoryListing>> = (0..paths.len()).map(|_| Vec::new()).collect();
643        let mut positions: Vec<std::collections::HashMap<std::path::PathBuf, usize>> = (0..paths
644            .len())
645            .map(|_| std::collections::HashMap::new())
646            .collect();
647        self.vlistdirs(paths, options, |index, page| {
648            let tree = trees
649                .get_mut(index)
650                .ok_or_else(|| crate::api::Error::transport(None, "invalid visitor root index"))?;
651            let positions = &mut positions[index];
652            let slot = *positions.entry(page.path.clone()).or_insert_with(|| {
653                tree.push(DirectoryListing {
654                    path: page.path.clone(),
655                    entries: Vec::new(),
656                });
657                tree.len() - 1
658            });
659            tree[slot].entries.extend(page.entries);
660            Ok(std::ops::ControlFlow::Continue(()))
661        })?;
662        Ok(trees)
663    }
664
665    // Visit directories
666    /// List one directory through callbacks, optionally recursively.
667    /// For independent roots and owned pages, prefer [`Vfsi::vlistdirs`].
668    ///
669    /// By default, only child `Entry` events are delivered, with no extra root
670    /// attribute request or full-listing allocation on paging backends. Set
671    /// `recursive(true)` to descend without following entry symlinks. Entry-only
672    /// traversal retains native anchored child cursors and sibling batching;
673    /// its order is backend-defined.
674    ///
675    /// `enter_leave(true)` includes the root and emits depth-first `Enter` and
676    /// `Leave` events for traversed directories, including pruned directories.
677    /// In shallow lifecycle mode, children are `Entry` events. Symlinks are
678    /// always `Entry` events. A non-directory lifecycle root produces one Entry.
679    /// Lifecycle traversal currently retains bounded directory buffers because
680    /// the vector page primitive does not expose resumable cursors.
681    /// `sort_by_name(true)` also buffers a bounded directory before sorting.
682    /// Unsorted entry-only visiting remains paged. This is not a snapshot.
683    /// Buffered recursive/lifecycle visiting rejects symlinks in reopened
684    /// directory paths, including ancestors. Backends unable to guarantee that
685    /// return Unsupported instead of checking and then following the path.
686    /// Ordinary shallow listing retains the backend's path resolution; use
687    /// `allow_symlink_components(false)` to require no-follow directory opens explicitly.
688    ///
689    /// The root is depth zero and its children have depth one. Directory depth
690    /// limits control descent; shallow mode ignores them. Lifecycle mode charges
691    /// the root against the entry/path budgets; recursive entry-only mode charges
692    /// retained directory paths. All unspecified budgets inherit client limits.
693    ///
694    /// `SkipSubtree` on Enter prevents reading that directory's children;
695    /// it does nothing on files or Leave. Pruning a recursive directory Entry
696    /// without lifecycle events is rejected: enable `enter_leave(true)`.
697    /// `Stop` returns Stopped immediately,
698    /// without synthesizing pending Leave events. Callback errors propagate
699    /// without replay. Callbacks run outside backend locks and may reenter.
700    ///
701    /// ```no_run
702    /// use vfsi_core::api::{ListDirOptions, Vfsi, VfsiExt, WalkControl};
703    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
704    /// fs.listdir("/input", ListDirOptions::new(), |event| {
705    ///     println!("{}", event.entry.path().display());
706    ///     Ok(WalkControl::Continue)
707    /// })?;
708    /// # Ok(())
709    /// # }
710    /// ```
711    ///
712    /// Recursive lifecycle events allow pruning before listing children:
713    ///
714    /// ```no_run
715    /// use vfsi_core::api::{ListDirOptions, Vfsi, VfsiExt, WalkControl, WalkEventKind};
716    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
717    /// fs.listdir("/project",
718    ///     ListDirOptions::new().with_recursive(true).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: crate::api::ListDirOptions,
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, 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        require_page_options(options)?;
748        self.vlistdirs(paths, options, |index, page| {
749            for entry in &page.entries {
750                if callback(index, entry)?.is_break() {
751                    return Ok(std::ops::ControlFlow::Break(()));
752                }
753            }
754            Ok(std::ops::ControlFlow::Continue(()))
755        })
756    }
757
758    /// Visit complete shallow listings in depth-first order with application policy.
759    ///
760    /// `order` compares immutable siblings (for example, locale-aware `ls` ordering).
761    /// The shared workflow sorts them; callbacks cannot replace validated entries.
762    /// `descend` selects directories before their contents are read; it does not
763    /// filter entries delivered in their parent's listing. Symlinks are never
764    /// traversed. `visitor` runs outside backend locks and may stop immediately
765    /// or skip all children of the current directory. Errors after delivered
766    /// listings must not trigger replay through another backend.
767    ///
768    /// Storage is bounded by one complete listing and the pending directory
769    /// frontier. Entry/path budgets charge all fetched children, including those
770    /// rejected by `descend`. This ordered workflow deliberately does not
771    /// speculatively list children before admission; use `vlistdirs` to batch
772    /// already-approved independent directories.
773    ///
774    /// ```no_run
775    /// use vfsi_core::api::{Attributes, Result, Vfsi, VfsiExt, WalkControl};
776    /// # fn example(fs: &impl Vfsi) -> Result<()> {
777    /// fs.visit_dirs_ordered("/input",
778    ///     fs.limits().walk_options().with_fields(Attributes::MODE | Attributes::SIZE),
779    ///     |a, b| a.path().cmp(b.path()),
780    ///     |entry| entry.file_name() != Some(std::ffi::OsStr::new(".git")),
781    ///     |listing, depth| {
782    ///         println!("{}: {} entries at depth {depth}", listing.path.display(), listing.entries.len());
783    ///         Ok(WalkControl::Continue)
784    ///     })?;
785    /// # Ok(())
786    /// # }
787    /// ```
788    fn visit_dirs_ordered(
789        &self,
790        root: impl AsRef<Path>,
791        options: crate::api::ListDirOptions,
792        mut order: impl FnMut(&crate::api::DirEntry, &crate::api::DirEntry) -> std::cmp::Ordering,
793        mut descend: impl FnMut(&crate::api::DirEntry) -> bool,
794        mut visitor: impl FnMut(DirectoryListing, usize) -> Result<crate::api::WalkControl>,
795    ) -> Result<crate::api::TraversalCompletion> {
796        require_page_options(options)?;
797        let options = options.walk_options(self.limits());
798        let root = root.as_ref();
799        let mut budget =
800            crate::internal::TraversalBudget::new(options.max_entries(), options.max_path_bytes());
801        budget.charge(root)?;
802        let mut pending = vec![(root.to_path_buf(), 0usize)];
803        while let Some((path, depth)) = pending.pop() {
804            let mut entries = self.read_dir_with_options(
805                &path,
806                budget.remaining_options(
807                    crate::api::ListDirOptions::new()
808                        .with_fields(options.fields())
809                        .with_allow_symlink_components(options.allow_symlink_components()),
810                ),
811            )?;
812            for entry in &entries {
813                budget
814                    .charge(entry.path())
815                    .map_err(|error| error.with_context("visit_dirs_ordered", entry.path()))?;
816            }
817            entries.sort_by(&mut order);
818            let listing = DirectoryListing { path, entries };
819            let children: Vec<_> = listing
820                .entries
821                .iter()
822                .filter(|entry| entry.attrs().is_dir() && descend(entry))
823                .map(|entry| entry.path().to_path_buf())
824                .collect();
825            match visitor(listing, depth)? {
826                crate::api::WalkControl::Stop => {
827                    return Ok(crate::api::TraversalCompletion::Stopped);
828                }
829                crate::api::WalkControl::SkipSubtree => continue,
830                crate::api::WalkControl::Continue => {}
831            }
832            if depth >= options.max_depth().get() && !children.is_empty() {
833                if !options.truncate_at_max_depth() {
834                    return Err(crate::api::Error::client(0, libc::EFBIG as u32)
835                        .with_context("visit_dirs_ordered", &children[0]));
836                }
837            } else {
838                pending.extend(children.into_iter().rev().map(|child| (child, depth + 1)));
839            }
840        }
841        Ok(crate::api::TraversalCompletion::Complete)
842    }
843
844    // Create, move, copy, and remove
845    /// Single-target convenience. For multiple independent directories, prefer [`Vfsi::vmkdir`].
846    ///
847    /// Create one directory; its parent must exist.
848    ///
849    /// An existing entry is an error, even if already a directory. Use
850    /// [`create_dir_all`](VfsiExt::create_dir_all) for missing parents/idempotent
851    /// directory setup, or [`vmkdir`](Vfsi::vmkdir) for independent siblings.
852    ///
853    /// ```no_run
854    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
855    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
856    /// fs.create_dir("/fresh-directory")?;
857    /// # Ok(())
858    /// # }
859    /// ```
860    fn create_dir(&self, path: impl AsRef<Path>) -> Result<()> {
861        self.vmkdir(&[MkDirOp::new(path, 0o777)])
862    }
863
864    /// Truncate or extend one path or opened object using [`Vfsi::vsetattrs`].
865    fn truncate<T: crate::api::AsTarget<Self::File>>(&self, target: T, len: u64) -> Result<()> {
866        self.vsetattrs(&[SetAttrsOp::new(target).len(len)])
867    }
868
869    /// Change permissions on one path or opened object using [`Vfsi::vsetattrs`].
870    fn chmod<T: crate::api::AsTarget<Self::File>>(
871        &self,
872        target: T,
873        permissions: crate::api::Permissions,
874    ) -> Result<()> {
875        self.vsetattrs(&[SetAttrsOp::new(target).permissions(permissions)])
876    }
877
878    /// Change ownership of one path or opened object through [`Vfsi::vsetattrs`].
879    /// `None` leaves the corresponding owner/group unchanged. Use the vector
880    /// with [`SetAttrsOp::with_follow_final_symlink(false)`](SetAttrsOp::with_follow_final_symlink)
881    /// to change a symlink itself.
882    fn chown<T: crate::api::AsTarget<Self::File>>(
883        &self,
884        target: T,
885        uid: Option<u32>,
886        gid: Option<u32>,
887    ) -> Result<()> {
888        let mut op = SetAttrsOp::new(target);
889        if let Some(uid) = uid {
890            op = op.uid(uid);
891        }
892        if let Some(gid) = gid {
893            op = op.gid(gid);
894        }
895        self.vsetattrs(&[op])
896    }
897
898    /// Create one directory with explicit Unix permission bits.
899    fn create_dir_with_mode(&self, path: impl AsRef<Path>, mode: u32) -> Result<()> {
900        self.vmkdir(&[MkDirOp::new(path, mode)])
901    }
902
903    /// Create one symbolic link; submit multiple pairs with [`Vfsi::vsymlink`].
904    fn symlink(&self, target: impl AsRef<Path>, link: impl AsRef<Path>) -> Result<()> {
905        self.vsymlink(&[(target, link)])
906    }
907
908    /// Read one symlink target; submit multiple paths with [`Vfsi::vreadlink`].
909    fn read_link(&self, path: impl AsRef<Path>) -> Result<std::path::PathBuf> {
910        let mut targets = self.vreadlink(&[path])?;
911        if targets.len() != 1 {
912            return Err(crate::api::Error::transport(
913                None,
914                "invalid readlink result count",
915            ));
916        }
917        Ok(targets.pop().expect("validated readlink count"))
918    }
919
920    /// Create one hard link; submit multiple pairs with [`Vfsi::vhardlink`].
921    fn hard_link(&self, source: impl AsRef<Path>, link: impl AsRef<Path>) -> Result<()> {
922        self.vhardlink(&[(source, link)])
923    }
924
925    /// Single-target convenience. For multiple directory creations, prefer [`Vfsi::vmkdir`]; plan missing parents before their children.
926    ///
927    /// Create missing parents; an error can leave some directories created.
928    ///
929    /// Existing directories are accepted; an existing non-directory component
930    /// is an error. This is not atomic and is not protected against concurrent
931    /// changes to ancestor paths.
932    ///
933    /// ```no_run
934    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
935    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
936    /// fs.create_dir_all("/workspace/results/2026")?;
937    /// # Ok(())
938    /// # }
939    /// ```
940    fn create_dir_all(&self, path: impl AsRef<Path>) -> Result<()> {
941        let mut current = std::path::PathBuf::new();
942        for component in path.as_ref().components() {
943            match component {
944                std::path::Component::RootDir => {
945                    current.push(Path::new("/"));
946                    continue;
947                }
948                std::path::Component::Normal(part) => current.push(part),
949                std::path::Component::CurDir => continue,
950                std::path::Component::ParentDir => {
951                    current.push("..");
952                    continue;
953                }
954                std::path::Component::Prefix(_) => {
955                    return Err(crate::api::Error::client(0, crate::ERR_INVAL));
956                }
957            }
958            match self.vmkdir(&[MkDirOp::new(&current, 0o777)]) {
959                Ok(()) => {}
960                Err(error) if error.raw_code() == crate::ERR_EXIST => {
961                    if !self.attrs(&current)?.is_dir() {
962                        return Err(crate::api::Error::client(0, crate::ERR_NOTDIR)
963                            .with_context("create_dir_all", &current));
964                    }
965                }
966                Err(error) => return Err(error),
967            }
968        }
969        Ok(())
970    }
971
972    /// Single-target convenience. For multiple source/destination pairs, prefer [`Vfsi::vrename`].
973    ///
974    /// Rename within supported namespaces; cross-filesystem moves can fail.
975    ///
976    /// Both paths use this client's namespace. Replacement semantics follow
977    /// the backend/filesystem; this is not a cross-file vector transaction or
978    /// a guarantee of durable directory updates. No copy fallback is implied.
979    ///
980    /// ```no_run
981    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
982    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
983    /// fs.rename("/old-name", "/new-name")?;
984    /// # Ok(())
985    /// # }
986    /// ```
987    fn rename(&self, source: impl AsRef<Path>, destination: impl AsRef<Path>) -> Result<()> {
988        self.vrename(&[(source, destination)], crate::api::RenameMode::Replace)
989    }
990
991    /// Single-target convenience. For multiple file pairs, prefer [`Vfsi::vcopy`].
992    ///
993    /// Copy a file's contents; this is not recursive tree copying.
994    ///
995    /// Destination creation/replacement and failures follow backend semantics.
996    /// A failure can leave a partial destination. Do not assume metadata,
997    /// sparse layout, or durability is preserved. Server-side copy is used
998    /// only where supported; callers need not build protocol COPY requests.
999    ///
1000    /// ```no_run
1001    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
1002    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1003    /// fs.copy("/source", "/destination")?;
1004    /// # Ok(())
1005    /// # }
1006    /// ```
1007    fn copy(&self, source: impl AsRef<Path>, destination: impl AsRef<Path>) -> Result<()> {
1008        self.copy_with_options(source, destination, CopyOptions::default())
1009    }
1010
1011    /// Single-target copy with explicit operation semantics; prefer [`Vfsi::vcopy`]
1012    /// when submitting multiple pairs.
1013    fn copy_with_options(
1014        &self,
1015        source: impl AsRef<Path>,
1016        destination: impl AsRef<Path>,
1017        options: CopyOptions,
1018    ) -> Result<()> {
1019        self.vcopy(&[(source, destination)], options)
1020    }
1021
1022    /// Single-target convenience. For multiple paths, prefer [`Vfsi::vremove`]. That vector API accepts both files and directories.
1023    ///
1024    /// Remove one file or symlink, not the symlink target.
1025    ///
1026    /// Missing paths are errors. To remove a directory use
1027    /// [`remove_dir`](VfsiExt::remove_dir) or [`remove_dir_all`](VfsiExt::remove_dir_all).
1028    ///
1029    /// ```no_run
1030    /// use vfsi_core::api::{Vfsi, VfsiExt, ErrorKind, WriteOp};
1031    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1032    /// match fs.remove_file("/temporary-file") {
1033    ///     Ok(()) => (),
1034    ///     Err(error) if error.kind() == ErrorKind::NotFound => (),
1035    ///     Err(error) => return Err(error),
1036    /// }
1037    /// # Ok(())
1038    /// # }
1039    /// ```
1040    fn remove_file(&self, path: impl AsRef<Path>) -> Result<()> {
1041        let path = path.as_ref();
1042        if self.symlink_attrs(path)?.is_dir() {
1043            return Err(
1044                crate::api::Error::client(0, crate::ERR_ISDIR).with_context("remove_file", path)
1045            );
1046        }
1047        self.vremove(
1048            &[path],
1049            crate::api::RemoveMode::Entry,
1050            crate::api::RemoveOptions::new(),
1051        )
1052    }
1053
1054    /// Single-target convenience. For multiple paths, prefer [`Vfsi::vremove`]. That vector API does not enforce directory-only inputs.
1055    ///
1056    /// Remove one empty directory; not a recursive operation.
1057    ///
1058    /// Nonempty directories fail. Use [`remove_dir_contents`](VfsiExt::remove_dir_contents)
1059    /// to empty a directory while retaining it, or [`remove_dir_all`](VfsiExt::remove_dir_all)
1060    /// to remove its tree. Only operate on paths whose removal you intend.
1061    ///
1062    /// ```no_run
1063    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
1064    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1065    /// fs.remove_dir("/empty-temporary-directory")?;
1066    /// # Ok(())
1067    /// # }
1068    /// ```
1069    fn remove_dir(&self, path: impl AsRef<Path>) -> Result<()> {
1070        let path = path.as_ref();
1071        require_directory(self, path, "remove_dir")?;
1072        self.vremove(
1073            &[path],
1074            crate::api::RemoveMode::Entry,
1075            crate::api::RemoveOptions::new(),
1076        )
1077    }
1078
1079    /// Single-target convenience. For multiple trees, prefer [`Vfsi::vremove`] with `RemoveMode::Tree`.
1080    ///
1081    /// Recursively remove a tree without following directory symlinks.
1082    /// Path-based removal is not a security sandbox or an atomic transaction.
1083    ///
1084    /// A failure can leave a partly removed tree. Use trusted ancestor paths;
1085    /// concurrent namespace substitutions are not a confinement guarantee.
1086    /// This is destructive: do not use it to prepare an arbitrary existing
1087    /// directory for an example or benchmark.
1088    ///
1089    /// ```no_run
1090    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
1091    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1092    /// // Remove only the tree that this application intentionally owns.
1093    /// fs.remove_dir_all("/owned-temporary-tree")?;
1094    /// # Ok(())
1095    /// # }
1096    /// ```
1097    fn remove_dir_all(&self, path: impl AsRef<Path>) -> Result<()> {
1098        let path = path.as_ref();
1099        require_directory(self, path, "remove_dir_all")?;
1100        self.vremove(
1101            &[path],
1102            crate::api::RemoveMode::Tree,
1103            crate::api::RemoveOptions::new(),
1104        )
1105    }
1106
1107    /// Recursively remove one directory using explicit batching and failure
1108    /// policy. The directory is validated before mutation; symlink roots are
1109    /// not followed. Prefer [`Vfsi::vremove`] when removing several trees.
1110    fn remove_dir_all_with_options(
1111        &self,
1112        path: impl AsRef<Path>,
1113        options: crate::api::RemoveOptions,
1114    ) -> Result<()> {
1115        let path = path.as_ref();
1116        require_directory(self, path, "remove_dir_all")?;
1117        self.vremove(&[path], crate::api::RemoveMode::Tree, options)
1118    }
1119
1120    /// Single-target convenience. For multiple directory roots, prefer [`Vfsi::vremove`].
1121    ///
1122    /// Recursively empty a directory while keeping its root.
1123    ///
1124    /// The root must be a directory, not a symlink. Child symlinks are removed
1125    /// without following their targets. Errors can leave partially removed
1126    /// contents; retaining the root does not make the operation transactional.
1127    /// The path is resolved for this call, not pinned across calls or renames.
1128    /// Use trusted ancestors; this is not a path-confinement guarantee.
1129    ///
1130    /// ```no_run
1131    /// use vfsi_core::api::{Vfsi, VfsiExt, WriteOp};
1132    /// # fn example(fs: &impl Vfsi) -> vfsi_core::api::Result<()> {
1133    /// fs.remove_dir_contents("/owned-scratch-directory")?;
1134    /// // The scratch directory remains available for subsequent work.
1135    /// # Ok(())
1136    /// # }
1137    /// ```
1138    fn remove_dir_contents(&self, path: impl AsRef<Path>) -> Result<()> {
1139        self.remove_dir_contents_with_options(path, crate::api::RemoveOptions::new())
1140    }
1141
1142    /// Recursively empty one directory while retaining its root, using
1143    /// explicit batching and failure policy. The root is validated without
1144    /// following a symlink. Prefer [`Vfsi::vremove`] for several roots.
1145    /// Like [`Self::remove_dir_contents`], this resolves the current pathname;
1146    /// it does not retain a previously opened directory's identity.
1147    fn remove_dir_contents_with_options(
1148        &self,
1149        path: impl AsRef<Path>,
1150        options: crate::api::RemoveOptions,
1151    ) -> Result<()> {
1152        let path = path.as_ref();
1153        require_directory(self, path, "remove_dir_contents")?;
1154        self.vremove(&[path], crate::api::RemoveMode::Contents, options)
1155    }
1156}
1157
1158impl<C: Vfsi + ?Sized> VfsiExt for C {}
1159
1160fn single_completion<T>(mut values: Vec<T>, operation: &'static str) -> Result<T> {
1161    if values.len() != 1 {
1162        return Err(crate::api::Error::transport(
1163            None,
1164            format!("{operation} returned an invalid result count"),
1165        ));
1166    }
1167    Ok(values.remove(0))
1168}
1169fn require_directory<C: Vfsi + ?Sized>(fs: &C, path: &Path, operation: &'static str) -> Result<()> {
1170    if !fs.symlink_attrs(path)?.is_dir() {
1171        return Err(crate::api::Error::client(0, crate::ERR_NOTDIR).with_context(operation, path));
1172    }
1173    Ok(())
1174}
1175fn attrs_query<C: Vfsi + ?Sized>(
1176    client: &C,
1177    target: impl crate::AsTarget<C::File>,
1178    options: crate::api::AttrsOptions,
1179    operation: &'static str,
1180) -> Result<Attrs> {
1181    let target = target.as_target();
1182    let path = match target {
1183        crate::Target::Path(path) => path,
1184        crate::Target::File(file) => file.path(),
1185    };
1186    let results = client
1187        .vgetattrs(&[target], options)
1188        .map_err(|error| error.with_context(operation, path))?;
1189    single_completion(results, "vgetattrs").map_err(|error| error.with_context(operation, path))
1190}
1191
1192fn single_tree(trees: &mut Vec<Vec<DirectoryListing>>) -> Result<Vec<DirectoryListing>> {
1193    if trees.len() != 1 {
1194        return Err(crate::api::Error::transport(
1195            None,
1196            "read_dirs returned an invalid root count",
1197        ));
1198    }
1199    Ok(trees.remove(0))
1200}
1201
1202fn require_page_options(options: crate::api::ListDirOptions) -> Result<()> {
1203    if options.enter_leave() || options.sort_by_name() {
1204        Err(crate::api::Error::client(0, libc::EINVAL as u32))
1205    } else {
1206        Ok(())
1207    }
1208}