Files
dap-tui/src/disk.rs
T
Sebastian f566162c29 review pass: mount safety, abort/quit robustness, UI fixes
Engine / safety:
- verify mount points against /proc/self/mountinfo instead of path
  existence; refuse to sync into a leftover (unmounted) directory that
  would otherwise receive the library on the root filesystem
- resolve udisks block devices by label when the path is not mounted,
  and never let findmnt fall back to the containing root filesystem
- abort now SIGTERMs the whole child process group (sh -c/podkit and
  rsync children included), escalating to SIGKILL after 2s
- quitting while a sync runs aborts and waits for the engine instead of
  orphaning the child
- atomically mark the engine busy before spawning to prevent double
  syncs; persist state before publishing completion
- don't follow symlinked directories while scanning (recursion loops)
- prune empty directories recursively when removing stale files

UI:
- help overlay was clipped (hardcoded height): size it from content
- speed/ETA were pushed onto a line the progress panel clipped; show
  them right-aligned on the bytes row
- leaving log follow with u/d now scrolls from the bottom instead of
  jumping to the top of the log
- reload config with r (label updated), refresh mounts on completion so
  last-sync ages are current
- fix width underflows on narrow terminals; terminal-aware sidebar
  scrolling

Tests: mountinfo unescape, process-group abort, and a TestBackend render
smoke test covering all tabs, small terminals and the help overlay.
2026-09-10 19:35:03 +02:00

105 lines
3.2 KiB
Rust

use std::path::Path;
/// Whether `path` is currently a mount point.
///
/// Scanning `/proc/self/mountinfo` is used instead of `path.exists()` so a
/// leftover mount-point directory (e.g. `/media/sebastian/IPOD` after an
/// unclean eject) is not mistaken for a mounted device. Writing a device sync
/// into an unmounted directory would silently fill the root filesystem.
pub fn is_mounted(path: &Path) -> bool {
let Some(target) = path.to_str() else {
return false;
};
let target = target.trim_end_matches('/');
#[cfg(target_os = "linux")]
if let Ok(text) = std::fs::read_to_string("/proc/self/mountinfo") {
return text.lines().any(|line| {
let Some(field) = line.split(' ').nth(4) else {
return false;
};
unescape_octal(field).trim_end_matches('/') == target
});
}
// Fallback for platforms without /proc: best effort only.
path.exists()
}
/// Undo the octal escaping (`\040` etc.) used by /proc mount tables.
fn unescape_octal(s: &str) -> String {
let bytes = s.as_bytes();
let mut out = String::with_capacity(s.len());
let mut i = 0;
while i < bytes.len() {
let octal = bytes[i] == b'\\'
&& i + 3 < bytes.len()
&& bytes[i + 1..=i + 3].iter().all(|b| (b'0'..=b'7').contains(b));
if octal {
let v = (bytes[i + 1] - b'0') * 64 + (bytes[i + 2] - b'0') * 8 + (bytes[i + 3] - b'0');
out.push(v as char);
i += 4;
} else {
out.push(bytes[i] as char);
i += 1;
}
}
out
}
/// Total and available bytes on a mounted filesystem (statvfs).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DiskStats {
pub total: u64,
pub available: u64,
}
pub fn disk_stats(path: &Path) -> Option<DiskStats> {
let c = std::ffi::CString::new(path.to_str()?).ok()?;
let mut s: libc::statvfs = unsafe { std::mem::zeroed() };
if unsafe { libc::statvfs(c.as_ptr(), &mut s) } != 0 {
return None;
}
let frsize = (s.f_frsize.max(1)) as u64;
Some(DiskStats {
total: s.f_blocks as u64 * frsize,
available: s.f_bavail as u64 * frsize,
})
}
pub fn fmt_bytes(n: u64) -> String {
const UNITS: [&str; 5] = ["B", "KiB", "MiB", "GiB", "TiB"];
let mut v = n as f64;
let mut u = 0;
while v >= 1024.0 && u < 4 {
v /= 1024.0;
u += 1;
}
if u == 0 {
format!("{v:.0} {}", UNITS[u])
} else {
format!("{v:.1} {}", UNITS[u])
}
}
#[cfg(test)]
mod tests {
use super::{fmt_bytes, unescape_octal};
#[test]
fn unescapes_mountinfo_paths() {
assert_eq!(unescape_octal("/media/sebastian/IPOD"), "/media/sebastian/IPOD");
assert_eq!(unescape_octal("/media/My\\040Disk"), "/media/My Disk");
assert_eq!(unescape_octal("/media/back\\134slash"), "/media/back\\slash");
}
#[test]
fn formats_bytes() {
assert_eq!(fmt_bytes(0), "0 B");
assert_eq!(fmt_bytes(512), "512 B");
assert_eq!(fmt_bytes(1024), "1.0 KiB");
assert_eq!(fmt_bytes(137438953472), "128.0 GiB");
assert_eq!(fmt_bytes(1_099_511_627_776), "1.0 TiB");
}
}