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(¤t, 0o777)]) {
959 Ok(()) => {}
960 Err(error) if error.raw_code() == crate::ERR_EXIST => {
961 if !self.attrs(¤t)?.is_dir() {
962 return Err(crate::api::Error::client(0, crate::ERR_NOTDIR)
963 .with_context("create_dir_all", ¤t));
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}