Skip to main content

aria2_core/filesystem/
disk_space.rs

1use crate::error::{Aria2Error, FatalError, Result};
2use std::fmt;
3use std::path::Path;
4
5const DEFAULT_MARGIN_MB: u64 = 100;
6
7/// Convert a Path to a null-terminated CString for libc functions on Unix.
8///
9/// `statvfs` and similar C APIs require null-terminated strings, but
10/// `OsStr::as_bytes()` does NOT include a null terminator. Passing
11/// `as_bytes().as_ptr()` directly causes `statvfs` to read past the path
12/// until it finds a `\0` byte in memory — undefined behavior that causes
13/// failures on macOS and intermittently on Linux.
14///
15/// Returns `None` if the path contains an embedded null byte.
16#[cfg(unix)]
17fn path_to_cstring(path: &Path) -> Option<std::ffi::CString> {
18    use std::os::unix::ffi::OsStrExt;
19    std::ffi::CString::new(path.as_os_str().as_bytes()).ok()
20}
21
22// =========================================================================
23// K5.2 — DiskError Enum for Structured Error Handling
24// =========================================================================
25
26/// Structured error type for disk-related failures during download.
27///
28/// Provides specific error variants for different disk failure scenarios,
29/// enabling callers to handle each case appropriately (e.g., show user-friendly
30/// messages, trigger cleanup, or retry with smaller files).
31///
32/// # Examples
33///
34/// ```ignore
35/// use aria2_core::filesystem::disk_space::DiskError;
36///
37/// match check_disk_space(path, required_bytes) {
38///     Ok(()) => println!("Sufficient space"),
39///     Err(DiskError::InsufficientSpace { required, available }) => {
40///         eprintln!("Need {} but only {} available", required, available.unwrap_or(0));
41///     }
42///     Err(DiskError::IoError(msg)) => eprintln!("I/O error: {}", msg),
43///     Err(DiskError::PermissionDenied(p)) => eprintln!("Permission denied: {}", p),
44/// }
45/// ```
46#[derive(Debug, Clone)]
47pub enum DiskError {
48    /// Not enough disk space available
49    InsufficientSpace {
50        /// Bytes required for the operation
51        required: u64,
52        /// Bytes currently available (None if unknown)
53        available: Option<u64>,
54    },
55    /// General I/O error during disk operation
56    IoError(String),
57    /// Permission denied when accessing path
58    PermissionDenied(String),
59}
60
61impl fmt::Display for DiskError {
62    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63        match self {
64            DiskError::InsufficientSpace {
65                required,
66                available,
67            } => {
68                write!(
69                    f,
70                    "Not enough disk space: need {}, have {}",
71                    format_bytes(*required),
72                    available.map_or_else(|| "unknown".into(), format_bytes)
73                )
74            }
75            DiskError::IoError(msg) => write!(f, "Disk I/O error: {}", msg),
76            DiskError::PermissionDenied(path) => write!(f, "Permission denied: {}", path),
77        }
78    }
79}
80
81impl std::error::Error for DiskError {}
82
83/// Format byte count as human-readable string.
84///
85/// Automatically selects appropriate unit (B, KiB, MiB, GiB)
86/// based on magnitude for user-friendly display.
87///
88/// # Arguments
89///
90/// * `bytes` - Number of bytes to format
91///
92/// # Returns
93///
94/// Human-readable string like "1.50 GiB" or "256.00 KiB"
95fn format_bytes(bytes: u64) -> String {
96    if bytes >= 1024 * 1024 * 1024 {
97        format!("{:.2} GiB", bytes as f64 / (1024.0 * 1024.0 * 1024.0))
98    } else if bytes >= 1024 * 1024 {
99        format!("{:.2} MiB", bytes as f64 / (1024.0 * 1024.0))
100    } else if bytes >= 1024 {
101        format!("{:.2} KiB", bytes as f64 / 1024.0)
102    } else {
103        format!("{} B", bytes)
104    }
105}
106
107// =========================================================================
108// K5.1 — Enhanced Disk Space Checking Functions
109// =========================================================================
110
111/// Check if sufficient disk space exists for a download.
112///
113/// Performs platform-specific disk space verification with a 10% headroom
114/// margin beyond the requested size to account for filesystem overhead and
115/// metadata. This prevents downloads from failing near completion due to
116/// running out of space.
117///
118/// # Platform Behavior
119///
120/// - **Unix/Linux/macOS**: Uses `statvfs` syscall to query actual available
121///   blocks. Returns error if insufficient space.
122/// - **Windows**: Logs warning but returns Ok(()) (space check skipped).
123///   Windows disk space APIs are less reliable for this use case.
124/// - **Other**: Returns Ok(()) without checking.
125///
126/// # Arguments
127///
128/// * `path` - Directory or file path to check for available space
129/// * `required_bytes` - Minimum bytes needed for the download
130///
131/// # Returns
132///
133/// * `Ok(())` - Sufficient space available (or check skipped on non-Unix)
134/// * `Err(String)` - Descriptive error message if insufficient space
135///
136/// # Example
137///
138/// ```ignore
139/// use aria2_core::filesystem::disk_space::check_disk_space;
140/// use std::path::Path;
141///
142/// let path = Path::new("/downloads");
143/// match check_disk_space(path, 1024 * 1024 * 100) { // 100 MB
144///     Ok(()) => println!("Proceeding with download"),
145///     Err(e) => eprintln!("Cannot download: {}", e),
146/// }
147/// ```
148pub fn check_disk_space(path: &Path, required_bytes: u64) -> std::result::Result<(), String> {
149    #[cfg(unix)]
150    {
151        // Handle empty/invalid paths gracefully
152        let check_path = if path.as_os_str().is_empty() {
153            Path::new(".")
154        } else {
155            path
156        };
157
158        // statvfs requires a null-terminated C string; OsStr::as_bytes() is NOT
159        // null-terminated (undefined behavior if passed directly).
160        let Some(c_path) = path_to_cstring(check_path) else {
161            tracing::warn!(
162                path = %check_path.display(),
163                required = required_bytes,
164                "path contains embedded null byte, skipping disk space check"
165            );
166            return Ok(());
167        };
168
169        let mut stat: libc::statvfs = unsafe { std::mem::zeroed() };
170        let ret = unsafe { libc::statvfs(c_path.as_ptr(), &mut stat) };
171
172        if ret != 0 {
173            // Failed to get disk space info, log warning but don't block
174            // This matches Windows behavior where GetDiskFreeSpaceExW failure is non-fatal
175            tracing::warn!(
176                path = %check_path.display(),
177                required = required_bytes,
178                "statvfs failed, skipping disk space check: {}",
179                std::io::Error::last_os_error()
180            );
181            return Ok(());
182        }
183
184        // Available blocks × block size = available bytes
185        let available = stat.f_bavail as u64 * stat.f_frsize as u64;
186
187        // Require 10% headroom beyond requested size to prevent near-completion failures
188        let needed_with_headroom = required_bytes.saturating_add(required_bytes / 10);
189
190        if available < needed_with_headroom {
191            let available_gb = available as f64 / (1024.0 * 1024.0 * 1024.0);
192            let required_gb = required_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
193            return Err(format!(
194                "Insufficient disk space: need {:.2} GiB but only {:.2} GiB available on '{}'",
195                required_gb,
196                available_gb,
197                check_path.display()
198            ));
199        }
200
201        Ok(())
202    }
203
204    #[cfg(windows)]
205    {
206        use std::os::windows::ffi::OsStrExt;
207
208        // Handle empty/invalid paths gracefully
209        let check_path = if path.as_os_str().is_empty() {
210            Path::new(".")
211        } else {
212            path
213        };
214
215        // Get absolute path for GetDiskFreeSpaceEx
216        let abs_path = match std::fs::canonicalize(check_path) {
217            Ok(p) => p,
218            Err(_) => {
219                // If path doesn't exist, use current directory
220                std::env::current_dir().unwrap_or_else(|_| Path::new(".").to_path_buf())
221            }
222        };
223
224        // Convert path to wide string with null terminator
225        let mut wide_path: Vec<u16> = abs_path.as_os_str().encode_wide().collect();
226        wide_path.push(0);
227
228        let mut free_bytes_available: u64 = 0;
229        let mut total_number_of_bytes: u64 = 0;
230        let mut total_number_of_free_bytes: u64 = 0;
231
232        let result = unsafe {
233            windows_sys::Win32::Storage::FileSystem::GetDiskFreeSpaceExW(
234                wide_path.as_ptr(),
235                &mut free_bytes_available as *mut u64 as *mut _,
236                &mut total_number_of_bytes as *mut u64 as *mut _,
237                &mut total_number_of_free_bytes as *mut u64 as *mut _,
238            )
239        };
240
241        if result == 0 {
242            // Failed to get disk space, log warning but don't block
243            tracing::warn!(
244                path = %check_path.display(),
245                required = required_bytes,
246                "GetDiskFreeSpaceEx failed, skipping disk space check"
247            );
248            return Ok(());
249        }
250
251        // Require 10% headroom beyond requested size
252        let needed_with_headroom = required_bytes.saturating_add(required_bytes / 10);
253
254        if free_bytes_available < needed_with_headroom {
255            let available_gb = free_bytes_available as f64 / (1024.0 * 1024.0 * 1024.0);
256            let required_gb = required_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
257            return Err(format!(
258                "Insufficient disk space: need {:.2} GiB but only {:.2} GiB available on '{}'",
259                required_gb,
260                available_gb,
261                check_path.display()
262            ));
263        }
264
265        Ok(())
266    }
267
268    #[cfg(not(any(unix, windows)))]
269    {
270        // Other platforms: log warning but don't block download
271        tracing::warn!(
272            path = %path.display(),
273            required = required_bytes,
274            "Disk space check skipped on unsupported platform"
275        );
276        Ok(())
277    }
278}
279
280/// Check disk space and return structured DiskError on failure.
281///
282/// Similar to `check_disk_space()` but returns typed `DiskError` enum
283/// instead of String, enabling pattern matching on error types.
284///
285/// # Arguments
286///
287/// * `path` - Directory or file path to check
288/// * `required_bytes` - Minimum bytes needed
289///
290/// # Returns
291///
292/// * `Ok(())` - Sufficient space available
293/// * `Err(DiskError)` - Typed error indicating specific failure reason
294pub fn check_disk_space_typed(
295    path: &Path,
296    required_bytes: u64,
297) -> std::result::Result<(), DiskError> {
298    #[cfg(unix)]
299    {
300        let check_path = if path.as_os_str().is_empty() {
301            Path::new(".")
302        } else {
303            path
304        };
305
306        // statvfs requires a null-terminated C string; OsStr::as_bytes() is NOT
307        // null-terminated (undefined behavior if passed directly).
308        let Some(c_path) = path_to_cstring(check_path) else {
309            tracing::warn!(
310                path = %check_path.display(),
311                required = required_bytes,
312                "path contains embedded null byte, skipping disk space check"
313            );
314            return Ok(());
315        };
316
317        let mut stat: libc::statvfs = unsafe { std::mem::zeroed() };
318        let ret = unsafe { libc::statvfs(c_path.as_ptr(), &mut stat) };
319
320        if ret != 0 {
321            // Failed to get disk space info, log warning but don't block
322            // This matches Windows behavior where GetDiskFreeSpaceExW failure is non-fatal
323            tracing::warn!(
324                path = %check_path.display(),
325                required = required_bytes,
326                "statvfs failed, skipping disk space check: {}",
327                std::io::Error::last_os_error()
328            );
329            return Ok(());
330        }
331
332        let available = stat.f_bavail as u64 * stat.f_frsize as u64;
333        let needed_with_headroom = required_bytes.saturating_add(required_bytes / 10);
334
335        if available < needed_with_headroom {
336            return Err(DiskError::InsufficientSpace {
337                required: needed_with_headroom,
338                available: Some(available),
339            });
340        }
341
342        Ok(())
343    }
344
345    #[cfg(windows)]
346    {
347        use std::os::windows::ffi::OsStrExt;
348
349        let check_path = if path.as_os_str().is_empty() {
350            Path::new(".")
351        } else {
352            path
353        };
354
355        // Get absolute path for GetDiskFreeSpaceEx
356        let abs_path = match std::fs::canonicalize(check_path) {
357            Ok(p) => p,
358            Err(_) => std::env::current_dir().unwrap_or_else(|_| Path::new(".").to_path_buf()),
359        };
360
361        // Convert path to wide string with null terminator
362        let mut wide_path: Vec<u16> = abs_path.as_os_str().encode_wide().collect();
363        wide_path.push(0);
364
365        let mut free_bytes_available: u64 = 0;
366        let mut total_number_of_bytes: u64 = 0;
367        let mut total_number_of_free_bytes: u64 = 0;
368
369        let result = unsafe {
370            windows_sys::Win32::Storage::FileSystem::GetDiskFreeSpaceExW(
371                wide_path.as_ptr(),
372                &mut free_bytes_available as *mut u64 as *mut _,
373                &mut total_number_of_bytes as *mut u64 as *mut _,
374                &mut total_number_of_free_bytes as *mut u64 as *mut _,
375            )
376        };
377
378        if result == 0 {
379            // Failed to get disk space, don't block
380            return Ok(());
381        }
382
383        let needed_with_headroom = required_bytes.saturating_add(required_bytes / 10);
384
385        if free_bytes_available < needed_with_headroom {
386            return Err(DiskError::InsufficientSpace {
387                required: needed_with_headroom,
388                available: Some(free_bytes_available),
389            });
390        }
391
392        Ok(())
393    }
394
395    #[cfg(not(any(unix, windows)))]
396    {
397        let _ = path;
398        let _ = required_bytes;
399        Ok(())
400    }
401}
402
403pub fn available_space(path: &Path) -> Result<u64> {
404    let path = if path.as_os_str().is_empty() {
405        Path::new(".")
406    } else {
407        path
408    };
409
410    #[cfg(unix)]
411    {
412        // statvfs requires a null-terminated C string; OsStr::as_bytes() is NOT
413        // null-terminated (undefined behavior if passed directly).
414        let c_path = path_to_cstring(path).ok_or_else(|| {
415            Aria2Error::Fatal(FatalError::Config(format!(
416                "path contains embedded null byte: {}",
417                path.display()
418            )))
419        })?;
420        let mut stat: libc::statvfs = unsafe { std::mem::zeroed() };
421        let ret = unsafe { libc::statvfs(c_path.as_ptr(), &mut stat) };
422        if ret != 0 {
423            return Err(Aria2Error::Fatal(FatalError::Config(format!(
424                "statvfs failed: {}",
425                std::io::Error::last_os_error()
426            ))));
427        }
428        Ok(stat.f_bavail as u64 * stat.f_frsize as u64)
429    }
430
431    #[cfg(windows)]
432    {
433        use std::os::windows::ffi::OsStrExt;
434
435        // Get absolute path for GetDiskFreeSpaceEx
436        let abs_path = match std::fs::canonicalize(path) {
437            Ok(p) => p,
438            Err(_) => std::env::current_dir().unwrap_or_else(|_| Path::new(".").to_path_buf()),
439        };
440
441        // Convert path to wide string with null terminator
442        let mut wide_path: Vec<u16> = abs_path.as_os_str().encode_wide().collect();
443        wide_path.push(0);
444
445        let mut free_bytes_available: u64 = 0;
446        let mut total_number_of_bytes: u64 = 0;
447        let mut total_number_of_free_bytes: u64 = 0;
448
449        let result = unsafe {
450            windows_sys::Win32::Storage::FileSystem::GetDiskFreeSpaceExW(
451                wide_path.as_ptr(),
452                &mut free_bytes_available as *mut u64 as *mut _,
453                &mut total_number_of_bytes as *mut u64 as *mut _,
454                &mut total_number_of_free_bytes as *mut u64 as *mut _,
455            )
456        };
457
458        if result == 0 {
459            return Err(Aria2Error::Fatal(FatalError::Config(
460                "GetDiskFreeSpaceExW failed".to_string(),
461            )));
462        }
463
464        Ok(free_bytes_available)
465    }
466
467    #[cfg(not(any(unix, windows)))]
468    {
469        let _ = path;
470        Ok(u64::MAX)
471    }
472}
473
474pub fn has_enough_space(path: &Path, required: u64) -> bool {
475    available_space(path).is_ok_and(|avail| avail >= required)
476}
477
478pub fn check_with_margin(path: &Path, required: u64, margin_mb: Option<u64>) -> Result<()> {
479    let margin = margin_mb.unwrap_or(DEFAULT_MARGIN_MB) * 1024 * 1024;
480    let total_needed = required.saturating_add(margin);
481    // If available space cannot be queried (e.g. statvfs fails with ENOENT on
482    // some CI tempdir paths), skip the check instead of failing the operation.
483    // This mirrors the graceful degradation already performed in
484    // `check_disk_space` and `check_disk_space_typed`.
485    let avail = match available_space(path) {
486        Ok(v) => v,
487        Err(err) => {
488            tracing::warn!(
489                path = %path.display(),
490                required = total_needed,
491                "available_space failed, skipping disk space check: {}",
492                err
493            );
494            return Ok(());
495        }
496    };
497    if avail < total_needed {
498        Err(Aria2Error::Fatal(FatalError::DiskSpaceExhausted))
499    } else {
500        Ok(())
501    }
502}
503
504pub fn total_space(path: &Path) -> Result<u64> {
505    let path = if path.as_os_str().is_empty() {
506        Path::new(".")
507    } else {
508        path
509    };
510
511    #[cfg(unix)]
512    {
513        // statvfs requires a null-terminated C string; OsStr::as_bytes() is NOT
514        // null-terminated (undefined behavior if passed directly).
515        let c_path = path_to_cstring(path).ok_or_else(|| {
516            Aria2Error::Fatal(FatalError::Config(format!(
517                "path contains embedded null byte: {}",
518                path.display()
519            )))
520        })?;
521        let mut stat: libc::statvfs = unsafe { std::mem::zeroed() };
522        let ret = unsafe { libc::statvfs(c_path.as_ptr(), &mut stat) };
523        if ret != 0 {
524            return Err(Aria2Error::Fatal(FatalError::Config(format!(
525                "statvfs failed: {}",
526                std::io::Error::last_os_error()
527            ))));
528        }
529        Ok(stat.f_blocks as u64 * stat.f_frsize as u64)
530    }
531
532    #[cfg(windows)]
533    {
534        use std::os::windows::ffi::OsStrExt;
535
536        // Get absolute path for GetDiskFreeSpaceEx
537        let abs_path = match std::fs::canonicalize(path) {
538            Ok(p) => p,
539            Err(_) => std::env::current_dir().unwrap_or_else(|_| Path::new(".").to_path_buf()),
540        };
541
542        // Convert path to wide string with null terminator
543        let mut wide_path: Vec<u16> = abs_path.as_os_str().encode_wide().collect();
544        wide_path.push(0);
545
546        let mut free_bytes_available: u64 = 0;
547        let mut total_number_of_bytes: u64 = 0;
548        let mut total_number_of_free_bytes: u64 = 0;
549
550        let result = unsafe {
551            windows_sys::Win32::Storage::FileSystem::GetDiskFreeSpaceExW(
552                wide_path.as_ptr(),
553                &mut free_bytes_available as *mut u64 as *mut _,
554                &mut total_number_of_bytes as *mut u64 as *mut _,
555                &mut total_number_of_free_bytes as *mut u64 as *mut _,
556            )
557        };
558
559        if result == 0 {
560            return Err(Aria2Error::Fatal(FatalError::Config(
561                "GetDiskFreeSpaceExW failed".to_string(),
562            )));
563        }
564
565        Ok(total_number_of_bytes)
566    }
567
568    #[cfg(not(any(unix, windows)))]
569    {
570        let _ = path;
571        Ok(u64::MAX)
572    }
573}
574
575// =========================================================================
576// K5.4 — Tests for Disk Space Pre-check
577// =========================================================================
578
579#[cfg(test)]
580mod tests {
581    use super::*;
582
583    /// Test K5.4 #1: Sufficient space check returns Ok.
584    ///
585    /// Verifies that when ample disk space is available (simulated by requesting
586    /// a small amount), the check passes successfully.
587    #[test]
588    fn test_sufficient_space_ok() {
589        // Request 100 MB - should succeed on any reasonable system
590        let required = 100 * 1024 * 1024; // 100 MB
591        let result = check_disk_space(Path::new("."), required);
592
593        // On Unix/Windows, this should succeed unless disk is critically low
594        // We just verify it doesn't panic and returns a Result
595        assert!(
596            result.is_ok() || result.is_err(),
597            "Should return Ok or Err without panicking"
598        );
599    }
600
601    /// Test K5.4 #2: Insufficient space error with descriptive message.
602    ///
603    /// Tests that when space is insufficient, the error message contains
604    /// useful information about required vs available space.
605    #[test]
606    fn test_insufficient_space_err() {
607        // Request an impossibly large amount to force failure on most systems
608        let required = u64::MAX / 2; // Exabytes - will certainly fail
609        let result = check_disk_space(Path::new("."), required);
610
611        // On both Unix and Windows, this should fail with insufficient space
612        // or succeed if the check is skipped (very unlikely for exabytes)
613        if let Err(error_msg) = result {
614            assert!(
615                error_msg.to_lowercase().contains("insufficient")
616                    || error_msg.to_lowercase().contains("space"),
617                "Error message should mention space: {}",
618                error_msg
619            );
620        }
621        // If result.is_ok(), the check was skipped or disk reported huge space
622    }
623
624    /// Test K5.4 #3: DiskError Display trait shows readable sizes.
625    ///
626    /// Verifies that the DiskError enum's Display implementation produces
627    /// human-readable output with proper byte formatting.
628    #[test]
629    fn test_disk_error_display() {
630        // Test InsufficientSpace variant
631        let err = DiskError::InsufficientSpace {
632            required: 1024 * 1024 * 1024,       // 1 GiB
633            available: Some(512 * 1024 * 1024), // 512 MiB
634        };
635        let display_str = format!("{}", err);
636        assert!(
637            display_str.contains("Not enough disk space"),
638            "Should mention insufficient space"
639        );
640        assert!(
641            display_str.contains("1.00 GiB") || display_str.contains("GiB"),
642            "Should show required size in GiB"
643        );
644        assert!(
645            display_str.contains("512.00 MiB") || display_str.contains("MiB"),
646            "Should show available size in MiB"
647        );
648
649        // Test IoError variant
650        let io_err = DiskError::IoError("Failed to write block".to_string());
651        let io_display = format!("{}", io_err);
652        assert!(
653            io_display.contains("Disk I/O error"),
654            "Should mention I/O error"
655        );
656        assert!(
657            io_display.contains("Failed to write block"),
658            "Should include original message"
659        );
660
661        // Test PermissionDenied variant
662        let perm_err = DiskError::PermissionDenied("/root/secret".to_string());
663        let perm_display = format!("{}", perm_err);
664        assert!(
665            perm_display.contains("Permission denied"),
666            "Should mention permission denied"
667        );
668        assert!(perm_display.contains("/root/secret"), "Should include path");
669
670        // Test format_bytes helper function
671        assert_eq!(format_bytes(0), "0 B", "Zero bytes");
672        assert_eq!(format_bytes(500), "500 B", "Small bytes");
673        assert_eq!(format_bytes(1024), "1.00 KiB", "Exactly 1 KiB");
674        assert_eq!(format_bytes(1024 * 1024), "1.00 MiB", "Exactly 1 MiB");
675        assert_eq!(
676            format_bytes(1024 * 1024 * 1024),
677            "1.00 GiB",
678            "Exactly 1 GiB"
679        );
680        assert_eq!(
681            format_bytes(1536 * 1024 * 1024),
682            "1.50 GiB",
683            "1.5 GiB with decimal"
684        );
685    }
686
687    /// Additional test: check_disk_space_typed returns typed errors.
688    #[test]
689    fn test_check_disk_space_typed_returns_structured_errors() {
690        let huge_request = u64::MAX / 2;
691
692        match check_disk_space_typed(Path::new("."), huge_request) {
693            Ok(()) => {
694                // Sufficient space or check skipped - acceptable
695            }
696            Err(DiskError::InsufficientSpace {
697                required,
698                available,
699            }) => {
700                // Should have structured data
701                assert!(required > 0, "Required should be positive");
702                // Available may be Some or None depending on platform
703                if let Some(avail) = available {
704                    // avail is u64, always valid by type guarantee
705                    let _ = avail;
706                }
707            }
708            Err(DiskError::IoError(msg)) => {
709                // I/O error during statvfs
710                assert!(!msg.is_empty(), "Error message should not be empty");
711            }
712            Err(DiskError::PermissionDenied(_)) => {
713                // Permission error - unlikely but possible
714            }
715        }
716    }
717
718    /// Additional test: Empty path handled gracefully.
719    #[test]
720    fn test_check_disk_space_empty_path() {
721        let result = check_disk_space(Path::new(""), 1024);
722        // Should not panic - either succeed (using ".") or fail gracefully
723        assert!(
724            result.is_ok() || result.is_err(),
725            "Empty path should be handled gracefully"
726        );
727    }
728
729    /// Additional test: Zero bytes request always succeeds.
730    #[test]
731    fn test_check_disk_space_zero_bytes() {
732        let result = check_disk_space(Path::new("."), 0);
733        assert!(
734            result.is_ok(),
735            "Requesting zero bytes should always succeed"
736        );
737    }
738}