1use std::path::Path;
8
9pub fn space(path: &Path) -> Option<(u64, u64)> {
12 let existing = path.ancestors().find(|p| p.exists())?;
14 platform(existing)
15}
16
17#[cfg(unix)]
18fn platform(path: &Path) -> Option<(u64, u64)> {
19 let stat = rustix::fs::statvfs(path).ok()?;
20 let block = stat.f_frsize;
21 Some((stat.f_bavail.saturating_mul(block), stat.f_blocks.saturating_mul(block)))
22}
23
24#[cfg(windows)]
25fn platform(path: &Path) -> Option<(u64, u64)> {
26 use std::os::windows::ffi::OsStrExt;
27 let wide: Vec<u16> = path.as_os_str().encode_wide().chain(std::iter::once(0)).collect();
28 let (mut free, mut total) = (0u64, 0u64);
29 let done = unsafe { windows::Win32::Storage::FileSystem::GetDiskFreeSpaceExW(windows::core::PCWSTR(wide.as_ptr()), Some(&mut free), Some(&mut total), None) };
31 done.ok().map(|()| (free, total))
32}
33
34#[cfg(not(any(unix, windows)))]
35fn platform(_path: &Path) -> Option<(u64, u64)> {
36 None
37}
38
39pub fn reserve(total: u64) -> u64 {
41 (5u64 << 30).max(total / 20)
42}
43
44pub fn kept_free(total: u64) -> u64 {
49 (256u64 << 20).max(total / 100)
50}
51
52#[cfg(test)]
53thread_local! {
54 pub(crate) static ROOM: std::cell::Cell<Option<u64>> = const { std::cell::Cell::new(Some(u64::MAX)) };
57}
58
59pub fn fits(path: &Path, bytes: u64) -> bool {
62 #[cfg(test)]
63 if let Some(room) = ROOM.with(std::cell::Cell::get) {
64 return room >= bytes;
65 }
66 space(path).is_none_or(|(free, total)| free.saturating_sub(kept_free(total)) >= bytes)
67}
68
69pub fn room(path: &Path) -> u64 {
72 space(path).map_or(u64::MAX, |(free, total)| free.saturating_sub(reserve(total)))
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78
79 #[test]
80 fn the_reserve_grows_with_the_disk() {
81 assert_eq!(reserve(64 << 30), 5 << 30);
82 assert_eq!(reserve(1000 << 30), 50 << 30);
83 let (free, total) = space(&std::env::temp_dir().join("sioul-not-there/at-all")).expect("the system says");
84 assert!(total >= free && total > 0);
85 }
86}