Skip to main content

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}