fs4/lib.rs
1#![doc = include_str!("../README.md")]
2#![cfg_attr(docsrs, feature(doc_cfg))]
3#![cfg_attr(docsrs, allow(unused_attributes))]
4// The `cfg_<feature>!` macros below are only invoked inside
5// feature-gated modules -- every call site is itself behind
6// `#[cfg(feature = "...")]` or inside the Unix/Windows backend
7// trees. With `--no-default-features` (or on targets where neither
8// `cfg(unix)` nor `cfg(windows)` matches, e.g. `wasm32-wasi*`), all
9// call sites compile out, so the macros appear unused. Silence the
10// lint at the crate level rather than shadowing each definition.
11#![allow(unexpected_cfgs, unstable_name_collisions, unused_macros)]
12
13#[cfg(windows)]
14extern crate windows_sys;
15
16macro_rules! cfg_async_std {
17 ($($item:item)*) => {
18 $(
19 #[cfg(feature = "async-std")]
20 #[cfg_attr(docsrs, doc(cfg(feature = "async-std")))]
21 $item
22 )*
23 }
24}
25
26macro_rules! cfg_fs_err2 {
27 ($($item:item)*) => {
28 $(
29 #[cfg(feature = "fs-err2")]
30 #[cfg_attr(docsrs, doc(cfg(feature = "fs-err2")))]
31 $item
32 )*
33 }
34}
35
36macro_rules! cfg_fs_err2_tokio {
37 ($($item:item)*) => {
38 $(
39 #[cfg(feature = "fs-err2-tokio")]
40 #[cfg_attr(docsrs, doc(cfg(feature = "fs-err2-tokio")))]
41 $item
42 )*
43 }
44}
45
46macro_rules! cfg_fs_err3 {
47 ($($item:item)*) => {
48 $(
49 #[cfg(feature = "fs-err3")]
50 #[cfg_attr(docsrs, doc(cfg(feature = "fs-err3")))]
51 $item
52 )*
53 }
54}
55
56macro_rules! cfg_fs_err3_tokio {
57 ($($item:item)*) => {
58 $(
59 #[cfg(feature = "fs-err3-tokio")]
60 #[cfg_attr(docsrs, doc(cfg(feature = "fs-err3-tokio")))]
61 $item
62 )*
63 }
64}
65
66macro_rules! cfg_smol {
67 ($($item:item)*) => {
68 $(
69 #[cfg(feature = "smol")]
70 #[cfg_attr(docsrs, doc(cfg(feature = "smol")))]
71 $item
72 )*
73 }
74}
75
76macro_rules! cfg_tokio {
77 ($($item:item)*) => {
78 $(
79 #[cfg(feature = "tokio")]
80 #[cfg_attr(docsrs, doc(cfg(feature = "tokio")))]
81 $item
82 )*
83 }
84}
85
86macro_rules! cfg_sync {
87 ($($item:item)*) => {
88 $(
89 #[cfg(feature = "sync")]
90 #[cfg_attr(docsrs, doc(cfg(feature = "sync")))]
91 $item
92 )*
93 }
94}
95
96macro_rules! cfg_async {
97 ($($item:item)*) => {
98 $(
99 #[cfg(any(
100 feature = "smol",
101 feature = "async-std",
102 feature = "tokio",
103 feature = "fs-err2-tokio",
104 feature = "fs-err3-tokio",
105 ))]
106 #[cfg_attr(docsrs, doc(cfg(any(
107 feature = "smol",
108 feature = "async-std",
109 feature = "tokio",
110 feature = "fs-err2-tokio",
111 feature = "fs-err3-tokio",
112 ))))]
113 $item
114 )*
115 }
116}
117
118#[cfg(unix)]
119mod unix;
120#[cfg(unix)]
121use unix as sys;
122
123#[cfg(windows)]
124mod windows;
125
126#[cfg(windows)]
127use windows as sys;
128
129// The file-extension traits (`FileExt`, `AsyncFileExt`) and the stats
130// API are only implementable on targets with a real `sys` backend.
131// Anywhere else (notably `wasm32-wasi*`, where `target_family = "wasm"`
132// so neither `cfg(unix)` nor `cfg(windows)` matches and rustix does
133// not expose `statvfs` / `flock` / `fallocate`) the crate compiles
134// down to just the shared data types below.
135#[cfg(any(unix, windows))]
136mod file_ext;
137
138#[cfg(all(feature = "fs-err2", any(unix, windows)))]
139#[cfg_attr(docsrs, doc(cfg(feature = "fs-err2")))]
140pub mod fs_err2 {
141 pub use crate::FileExt;
142}
143
144#[cfg(all(feature = "fs-err3", any(unix, windows)))]
145#[cfg_attr(docsrs, doc(cfg(feature = "fs-err3")))]
146pub mod fs_err3 {
147 pub use crate::FileExt;
148}
149
150#[cfg(all(feature = "async-std", any(unix, windows)))]
151#[cfg_attr(docsrs, doc(cfg(feature = "async-std")))]
152pub mod async_std {
153 pub use crate::{AsyncFileExt, DynAsyncFileExt};
154}
155
156#[cfg(all(feature = "fs-err2-tokio", any(unix, windows)))]
157#[cfg_attr(docsrs, doc(cfg(feature = "fs-err2-tokio")))]
158pub mod fs_err2_tokio {
159 pub use crate::{AsyncFileExt, DynAsyncFileExt};
160}
161
162#[cfg(all(feature = "fs-err3-tokio", any(unix, windows)))]
163#[cfg_attr(docsrs, doc(cfg(feature = "fs-err3-tokio")))]
164pub mod fs_err3_tokio {
165 pub use crate::{AsyncFileExt, DynAsyncFileExt};
166}
167
168#[cfg(all(feature = "smol", any(unix, windows)))]
169#[cfg_attr(docsrs, doc(cfg(feature = "smol")))]
170pub mod smol {
171 pub use crate::{AsyncFileExt, DynAsyncFileExt};
172}
173
174#[cfg(all(feature = "tokio", any(unix, windows)))]
175#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))]
176pub mod tokio {
177 pub use crate::{AsyncFileExt, DynAsyncFileExt};
178}
179
180mod fs_stats;
181pub use fs_stats::FsStats;
182
183mod try_lock_error;
184pub use try_lock_error::TryLockError;
185
186use std::io::Result;
187#[cfg(any(unix, windows))]
188use std::path::Path;
189
190/// Get the stats of the file system containing the provided path.
191#[cfg(any(unix, windows))]
192pub fn statvfs<P>(path: P) -> Result<FsStats>
193where
194 P: AsRef<Path>,
195{
196 sys::statvfs(path.as_ref())
197}
198
199/// Returns the number of free bytes in the file system containing the provided
200/// path.
201#[cfg(any(unix, windows))]
202pub fn free_space<P>(path: P) -> Result<u64>
203where
204 P: AsRef<Path>,
205{
206 statvfs(path).map(|stat| stat.free_space)
207}
208
209/// Returns the available space in bytes to non-privileged users in the file
210/// system containing the provided path.
211#[cfg(any(unix, windows))]
212pub fn available_space<P>(path: P) -> Result<u64>
213where
214 P: AsRef<Path>,
215{
216 statvfs(path).map(|stat| stat.available_space)
217}
218
219/// Returns the total space in bytes in the file system containing the provided
220/// path.
221#[cfg(any(unix, windows))]
222pub fn total_space<P>(path: P) -> Result<u64>
223where
224 P: AsRef<Path>,
225{
226 statvfs(path).map(|stat| stat.total_space)
227}
228
229/// Returns the filesystem's disk space allocation granularity in bytes.
230/// The provided path may be for any file in the filesystem.
231///
232/// On Posix, this is equivalent to the filesystem's block size.
233/// On Windows, this is equivalent to the filesystem's cluster size.
234#[cfg(any(unix, windows))]
235pub fn allocation_granularity<P>(path: P) -> Result<u64>
236where
237 P: AsRef<Path>,
238{
239 statvfs(path).map(|stat| stat.allocation_granularity)
240}
241
242mod sealed {
243 pub trait Sealed {}
244
245 impl<F: Sealed + ?Sized> Sealed for &F {}
246}
247
248/// Extension trait for file which provides allocation and locking methods.
249///
250/// This trait is sealed and cannot be implemented for types outside of `fs4`.
251///
252/// ## Notes on File Locks
253///
254/// This library provides whole-file locks in both shared (read) and exclusive
255/// (read-write) varieties.
256///
257/// File locks are a cross-platform hazard since the file lock APIs exposed by
258/// operating system kernels vary in subtle and not-so-subtle ways.
259///
260/// The API exposed by this library can be safely used across platforms as long
261/// as the following rules are followed:
262///
263/// * Multiple locks should not be created on an individual `File` instance
264/// concurrently.
265/// * Duplicated files should not be locked without great care.
266/// * Files to be locked should be opened with at least read or write
267/// permissions.
268/// * File locks may only be relied upon to be advisory.
269///
270/// File locks are released automatically when the file handle is closed (for
271/// example when the owning `File` is dropped), so calling [`FileExt::unlock`]
272/// explicitly is optional.
273///
274/// File locks are implemented with
275/// [`flock(2)`](http://man7.org/linux/man-pages/man2/flock.2.html) on Unix and
276/// [`LockFileEx`](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-lockfileex)
277/// on Windows.
278pub trait FileExt: sealed::Sealed {
279 /// Returns the amount of physical space allocated for a file.
280 fn allocated_size(&self) -> Result<u64>;
281
282 /// Ensures that at least `len` bytes of disk space are allocated for the
283 /// file. After a successful call to `allocate`, subsequent writes to the
284 /// file within the specified length are guaranteed not to fail because of
285 /// lack of disk space.
286 ///
287 /// On most platforms the file's logical size is also extended to `len`
288 /// bytes. On Windows, if the file's existing cluster-aligned allocation
289 /// already covers `len`, the logical size is left unchanged to work around
290 /// buffered-I/O quirks observed when the end-of-file pointer is moved
291 /// inside an already-allocated cluster.
292 fn allocate(&self, len: u64) -> Result<()>;
293
294 /// Acquires a shared lock on the file, blocking until the lock can be
295 /// acquired.
296 fn lock_shared(&self) -> Result<()>;
297
298 /// Acquires an exclusive lock on the file, blocking until the lock can be
299 /// acquired.
300 ///
301 /// This is the blocking counterpart of [`FileExt::try_lock`]. It mirrors
302 /// [`std::fs::File::lock`].
303 fn lock(&self) -> Result<()>;
304
305 /// Attempts to acquire a shared lock on the file, without blocking.
306 ///
307 /// Returns `Ok(())` if the lock was acquired, or
308 /// `Err(`[`TryLockError::WouldBlock`](crate::TryLockError::WouldBlock)`)`
309 /// if the file is currently locked. Mirrors
310 /// [`std::fs::File::try_lock_shared`].
311 fn try_lock_shared(&self) -> std::result::Result<(), TryLockError>;
312
313 /// Attempts to acquire an exclusive lock on the file, without blocking.
314 ///
315 /// Returns `Ok(())` if the lock was acquired, or
316 /// `Err(`[`TryLockError::WouldBlock`](crate::TryLockError::WouldBlock)`)`
317 /// if the file is currently locked. Mirrors [`std::fs::File::try_lock`].
318 fn try_lock(&self) -> std::result::Result<(), TryLockError>;
319
320 /// Releases any lock held on the file. The lock is also released
321 /// automatically when the file handle is closed.
322 fn unlock(&self) -> Result<()>;
323}
324
325impl<F: FileExt + ?Sized> FileExt for &F {
326 #[cfg_attr(not(tarpaulin), inline(always))]
327 fn allocated_size(&self) -> Result<u64> {
328 <F as FileExt>::allocated_size(*self)
329 }
330
331 #[cfg_attr(not(tarpaulin), inline(always))]
332 fn allocate(&self, len: u64) -> Result<()> {
333 <F as FileExt>::allocate(*self, len)
334 }
335
336 #[cfg_attr(not(tarpaulin), inline(always))]
337 fn lock_shared(&self) -> Result<()> {
338 <F as FileExt>::lock_shared(*self)
339 }
340
341 #[cfg_attr(not(tarpaulin), inline(always))]
342 fn lock(&self) -> Result<()> {
343 <F as FileExt>::lock(*self)
344 }
345
346 #[cfg_attr(not(tarpaulin), inline(always))]
347 fn try_lock_shared(&self) -> std::result::Result<(), TryLockError> {
348 <F as FileExt>::try_lock_shared(*self)
349 }
350
351 #[cfg_attr(not(tarpaulin), inline(always))]
352 fn try_lock(&self) -> std::result::Result<(), TryLockError> {
353 <F as FileExt>::try_lock(*self)
354 }
355
356 #[cfg_attr(not(tarpaulin), inline(always))]
357 fn unlock(&self) -> Result<()> {
358 <F as FileExt>::unlock(*self)
359 }
360}
361
362/// Extension trait for file which provides allocation and locking methods.
363///
364/// ## Notes on File Locks
365///
366/// This library provides whole-file locks in both shared (read) and exclusive
367/// (read-write) varieties.
368///
369/// File locks are a cross-platform hazard since the file lock APIs exposed by
370/// operating system kernels vary in subtle and not-so-subtle ways.
371///
372/// The API exposed by this library can be safely used across platforms as long
373/// as the following rules are followed:
374///
375/// * Multiple locks should not be created on an individual `File` instance
376/// concurrently.
377/// * Duplicated files should not be locked without great care.
378/// * Files to be locked should be opened with at least read or write
379/// permissions.
380/// * File locks may only be relied upon to be advisory.
381///
382/// File locks are released automatically when the file handle is closed (for
383/// example when the owning `File` is dropped), so calling [`AsyncFileExt::unlock`]
384/// explicitly is optional.
385///
386/// File locks are implemented with
387/// [`flock(2)`](http://man7.org/linux/man-pages/man2/flock.2.html) on Unix and
388/// [`LockFileEx`](https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-lockfileex)
389/// on Windows. The `lock_*` and `try_lock_*` methods are synchronous because
390/// the underlying system calls are blocking. The separate
391/// [`AsyncFileExt::unlock_async`] method is provided for convenience inside
392/// async code, but the underlying `unlock` syscall is still blocking.
393///
394/// This trait is sealed and cannot be implemented for types outside of `fs4`.
395pub trait AsyncFileExt: sealed::Sealed {
396 /// Returns the amount of physical space allocated for a file.
397 fn allocated_size(&self) -> impl core::future::Future<Output = Result<u64>>;
398
399 /// Ensures that at least `len` bytes of disk space are allocated for the
400 /// file. After a successful call to `allocate`, subsequent writes to the
401 /// file within the specified length are guaranteed not to fail because of
402 /// lack of disk space.
403 ///
404 /// On most platforms the file's logical size is also extended to `len`
405 /// bytes. On Windows, if the file's existing cluster-aligned allocation
406 /// already covers `len`, the logical size is left unchanged to work around
407 /// buffered-I/O quirks observed when the end-of-file pointer is moved
408 /// inside an already-allocated cluster.
409 fn allocate(&self, len: u64) -> impl core::future::Future<Output = Result<()>>;
410
411 /// Acquires a shared lock on the file, blocking until the lock can be
412 /// acquired.
413 fn lock_shared(&self) -> Result<()>;
414
415 /// Acquires an exclusive lock on the file, blocking until the lock can be
416 /// acquired. Mirrors [`std::fs::File::lock`].
417 fn lock(&self) -> Result<()>;
418
419 /// Attempts to acquire a shared lock on the file, without blocking.
420 ///
421 /// Returns `Ok(())` if the lock was acquired, or
422 /// `Err(`[`TryLockError::WouldBlock`](crate::TryLockError::WouldBlock)`)`
423 /// if the file is currently locked.
424 fn try_lock_shared(&self) -> std::result::Result<(), crate::TryLockError>;
425
426 /// Attempts to acquire an exclusive lock on the file, without blocking.
427 ///
428 /// Returns `Ok(())` if the lock was acquired, or
429 /// `Err(`[`TryLockError::WouldBlock`](crate::TryLockError::WouldBlock)`)`
430 /// if the file is currently locked.
431 fn try_lock(&self) -> std::result::Result<(), crate::TryLockError>;
432
433 /// Releases any lock held on the file. The lock is also released
434 /// automatically when the file handle is closed.
435 fn unlock(&self) -> Result<()>;
436
437 /// Releases any lock held on the file.
438 ///
439 /// **Note:** This method is not truly async; the underlying system call is
440 /// still blocking. It exists for convenience when used from an async
441 /// context.
442 fn unlock_async(&self) -> impl core::future::Future<Output = Result<()>>;
443}
444
445impl<F: AsyncFileExt + ?Sized> AsyncFileExt for &F {
446 #[cfg_attr(not(tarpaulin), inline(always))]
447 async fn allocated_size(&self) -> Result<u64> {
448 <F as AsyncFileExt>::allocated_size(*self).await
449 }
450
451 #[cfg_attr(not(tarpaulin), inline(always))]
452 async fn allocate(&self, len: u64) -> Result<()> {
453 <F as AsyncFileExt>::allocate(*self, len).await
454 }
455
456 #[cfg_attr(not(tarpaulin), inline(always))]
457 fn lock_shared(&self) -> Result<()> {
458 <F as AsyncFileExt>::lock_shared(*self)
459 }
460
461 #[cfg_attr(not(tarpaulin), inline(always))]
462 fn lock(&self) -> Result<()> {
463 <F as AsyncFileExt>::lock(*self)
464 }
465
466 #[cfg_attr(not(tarpaulin), inline(always))]
467 fn try_lock_shared(&self) -> std::result::Result<(), crate::TryLockError> {
468 <F as AsyncFileExt>::try_lock_shared(*self)
469 }
470
471 #[cfg_attr(not(tarpaulin), inline(always))]
472 fn try_lock(&self) -> std::result::Result<(), crate::TryLockError> {
473 <F as AsyncFileExt>::try_lock(*self)
474 }
475
476 #[cfg_attr(not(tarpaulin), inline(always))]
477 fn unlock(&self) -> Result<()> {
478 <F as AsyncFileExt>::unlock(*self)
479 }
480
481 #[cfg_attr(not(tarpaulin), inline(always))]
482 async fn unlock_async(&self) -> Result<()> {
483 <F as AsyncFileExt>::unlock_async(*self).await
484 }
485}
486
487/// A heap-allocated, dynamically-typed `Send` future used by
488/// [`DynAsyncFileExt`] to keep its methods object-safe.
489pub type BoxFuture<'a, T> = core::pin::Pin<Box<dyn core::future::Future<Output = T> + Send + 'a>>;
490
491/// Object-safe variant of [`AsyncFileExt`] returning boxed `Send` futures, so
492/// it can be used behind a trait object (e.g. `Box<dyn DynAsyncFileExt>` or
493/// `&dyn DynAsyncFileExt`).
494///
495/// [`AsyncFileExt`] uses return-position `impl Future`, which is not
496/// object-safe; this trait wraps the same operations behind
497/// [`BoxFuture`]s so the trait *can* be used as a trait object. Every type
498/// that implements [`AsyncFileExt`] also implements `DynAsyncFileExt`.
499///
500/// Prefer [`AsyncFileExt`] for generic code (no allocation, no dynamic
501/// dispatch); reach for `DynAsyncFileExt` only when type erasure is
502/// required.
503///
504/// This trait is sealed and cannot be implemented for types outside of `fs4`.
505pub trait DynAsyncFileExt: sealed::Sealed {
506 /// Returns the amount of physical space allocated for a file.
507 fn allocated_size(&self) -> BoxFuture<'_, Result<u64>>;
508
509 /// Ensures that at least `len` bytes of disk space are allocated for the
510 /// file. After a successful call to `allocate`, subsequent writes to the
511 /// file within the specified length are guaranteed not to fail because of
512 /// lack of disk space.
513 ///
514 /// On most platforms the file's logical size is also extended to `len`
515 /// bytes. On Windows, if the file's existing cluster-aligned allocation
516 /// already covers `len`, the logical size is left unchanged to work around
517 /// buffered-I/O quirks observed when the end-of-file pointer is moved
518 /// inside an already-allocated cluster.
519 fn allocate(&self, len: u64) -> BoxFuture<'_, Result<()>>;
520
521 /// Acquires a shared lock on the file, blocking until the lock can be
522 /// acquired.
523 fn lock_shared(&self) -> Result<()>;
524
525 /// Acquires an exclusive lock on the file, blocking until the lock can be
526 /// acquired. Mirrors [`std::fs::File::lock`].
527 fn lock(&self) -> Result<()>;
528
529 /// Attempts to acquire a shared lock on the file, without blocking.
530 ///
531 /// Returns `Ok(())` if the lock was acquired, or
532 /// `Err(`[`TryLockError::WouldBlock`](crate::TryLockError::WouldBlock)`)`
533 /// if the file is currently locked.
534 fn try_lock_shared(&self) -> std::result::Result<(), crate::TryLockError>;
535
536 /// Attempts to acquire an exclusive lock on the file, without blocking.
537 ///
538 /// Returns `Ok(())` if the lock was acquired, or
539 /// `Err(`[`TryLockError::WouldBlock`](crate::TryLockError::WouldBlock)`)`
540 /// if the file is currently locked.
541 fn try_lock(&self) -> std::result::Result<(), crate::TryLockError>;
542
543 /// Releases any lock held on the file. The lock is also released
544 /// automatically when the file handle is closed.
545 fn unlock(&self) -> Result<()>;
546
547 /// Releases any lock held on the file.
548 ///
549 /// **Note:** This method is not truly async; the underlying system call is
550 /// still blocking. It exists for convenience when used from an async
551 /// context.
552 fn unlock_async(&self) -> BoxFuture<'_, Result<()>>;
553}
554
555impl<F: DynAsyncFileExt + ?Sized> DynAsyncFileExt for &F {
556 #[cfg_attr(not(tarpaulin), inline(always))]
557 fn allocated_size(&self) -> BoxFuture<'_, Result<u64>> {
558 <F as DynAsyncFileExt>::allocated_size(*self)
559 }
560
561 #[cfg_attr(not(tarpaulin), inline(always))]
562 fn allocate(&self, len: u64) -> BoxFuture<'_, Result<()>> {
563 <F as DynAsyncFileExt>::allocate(*self, len)
564 }
565
566 #[cfg_attr(not(tarpaulin), inline(always))]
567 fn lock_shared(&self) -> Result<()> {
568 <F as DynAsyncFileExt>::lock_shared(*self)
569 }
570
571 #[cfg_attr(not(tarpaulin), inline(always))]
572 fn lock(&self) -> Result<()> {
573 <F as DynAsyncFileExt>::lock(*self)
574 }
575
576 #[cfg_attr(not(tarpaulin), inline(always))]
577 fn try_lock_shared(&self) -> std::result::Result<(), crate::TryLockError> {
578 <F as DynAsyncFileExt>::try_lock_shared(*self)
579 }
580
581 #[cfg_attr(not(tarpaulin), inline(always))]
582 fn try_lock(&self) -> std::result::Result<(), crate::TryLockError> {
583 <F as DynAsyncFileExt>::try_lock(*self)
584 }
585
586 #[cfg_attr(not(tarpaulin), inline(always))]
587 fn unlock(&self) -> Result<()> {
588 <F as DynAsyncFileExt>::unlock(*self)
589 }
590
591 #[cfg_attr(not(tarpaulin), inline(always))]
592 fn unlock_async(&self) -> BoxFuture<'_, Result<()>> {
593 <F as DynAsyncFileExt>::unlock_async(*self)
594 }
595}
596
597#[cfg(all(test, any(unix, windows)))]
598mod tests {
599 //! The `free_space` / `available_space` / `total_space` helpers
600 //! each forward to `statvfs(...).map(|s| s.<field>)`. The
601 //! `FsStats` getter tests in `fs_stats.rs` cover the field
602 //! accessors; these tests cover the top-level forwarders (which
603 //! were previously uncovered in CI per Codecov).
604 //!
605 //! Assertions are intentionally loose: we don't compare the three
606 //! numbers across separate `statvfs` calls because that races
607 //! with concurrent filesystem activity (other tests, the OS,
608 //! etc.). Proving the call returned `Ok` with a plausible value
609 //! is enough to exercise the forwarding path.
610 extern crate tempfile;
611
612 use super::*;
613
614 fn tempdir() -> tempfile::TempDir {
615 tempfile::TempDir::with_prefix("fs4").unwrap()
616 }
617
618 #[test]
619 fn free_space_returns_ok() {
620 let dir = tempdir();
621 let free = free_space(dir.path()).unwrap();
622 let total = total_space(dir.path()).unwrap();
623 assert!(
624 free <= total,
625 "free_space ({free}) must not exceed total_space ({total})",
626 );
627 }
628
629 #[test]
630 fn available_space_returns_ok() {
631 let dir = tempdir();
632 let available = available_space(dir.path()).unwrap();
633 let total = total_space(dir.path()).unwrap();
634 assert!(
635 available <= total,
636 "available_space ({available}) must not exceed total_space ({total})",
637 );
638 }
639
640 #[test]
641 fn total_space_is_non_zero() {
642 let dir = tempdir();
643 assert!(
644 total_space(dir.path()).unwrap() > 0,
645 "total_space on a tempdir's volume should be non-zero",
646 );
647 }
648
649 /// POSIX `statvfs` returns `ENOENT` for a path that doesn't
650 /// exist, which is how we exercise the error-propagation branch
651 /// of the three forwarders. Windows has different semantics:
652 /// `GetVolumePathNameW` resolves any syntactically valid path to
653 /// its volume root regardless of whether the path itself exists,
654 /// so `statvfs(missing)` returns `Ok` on that platform.
655 #[cfg(unix)]
656 #[test]
657 fn missing_path_errors() {
658 let dir = tempdir();
659 let missing = dir.path().join("definitely-does-not-exist");
660 assert!(free_space(&missing).is_err());
661 assert!(available_space(&missing).is_err());
662 assert!(total_space(&missing).is_err());
663 }
664}