rdirstat-core 0.1.0

Parallel directory scanner and snapshot pipeline behind the rdirstat TUI/GUI
Documentation
//! Cross-platform drive / mount-point enumeration for "pick a disk to scan"
//! UIs.
//!
//! Implementation differs per OS: drive letters on Windows, `/Volumes`
//! entries on macOS, and parsed `/proc/mounts` on Linux. The returned list
//! is intended for human selection, not as an authoritative filesystem
//! mount table.

#[cfg(not(target_os = "windows"))]
use std::fs;
use std::path::PathBuf;

/// One drive or mount point that the user might want to scan.
#[derive(Clone)]
pub struct MountPoint {
    /// Path to mount onto when selected (e.g. `/`, `/Volumes/External`,
    /// `C:\`).
    pub path: PathBuf,
    /// Human-friendly label for display in the UI.
    pub label: String,
}

/// Enumerate the user-visible drives / mount points on this machine.
///
/// Always returns at least `/` (Unix) or each present drive letter
/// (Windows). The exact set depends on the platform, current mounts, and
/// permissions.
pub fn list_mounts() -> Vec<MountPoint> {
    #[cfg(target_os = "windows")]
    {
        list_mounts_windows()
    }
    #[cfg(not(target_os = "windows"))]
    {
        list_mounts_unix()
    }
}

#[cfg(target_os = "windows")]
fn list_mounts_windows() -> Vec<MountPoint> {
    let mut mounts = Vec::new();
    for letter in b'A'..=b'Z' {
        let drive = format!("{}:\\", letter as char);
        let path = PathBuf::from(&drive);
        if path.exists() {
            mounts.push(MountPoint {
                path,
                label: drive,
            });
        }
    }
    mounts
}

#[cfg(not(target_os = "windows"))]
fn list_mounts_unix() -> Vec<MountPoint> {
    let mut mounts = Vec::new();

    mounts.push(MountPoint {
        path: PathBuf::from("/"),
        label: "/".to_string(),
    });

    let mount_files = ["/proc/mounts", "/etc/mtab"];
    for mf in &mount_files {
        if let Ok(content) = fs::read_to_string(mf) {
            for line in content.lines() {
                let parts: Vec<&str> = line.split_whitespace().collect();
                if parts.len() >= 2 {
                    let mount_path = parts[1];
                    if mount_path == "/"
                        || mount_path.starts_with("/proc")
                        || mount_path.starts_with("/sys")
                        || mount_path.starts_with("/dev")
                        || mount_path.starts_with("/run")
                        || mount_path.starts_with("/snap")
                    {
                        continue;
                    }
                    let path = PathBuf::from(mount_path);
                    if path.exists() {
                        mounts.push(MountPoint {
                            path,
                            label: format!("{} ({})", mount_path, parts[0]),
                        });
                    }
                }
            }
            break;
        }
    }

    if let Ok(read_dir) = fs::read_dir("/Volumes") {
        for entry in read_dir.flatten() {
            let path = entry.path();
            let name = entry.file_name().to_string_lossy().to_string();
            if !mounts.iter().any(|m| m.path == path) {
                mounts.push(MountPoint {
                    path,
                    label: format!("/Volumes/{name}"),
                });
            }
        }
    }

    mounts
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn list_mounts_returns_results() {
        let mounts = list_mounts();
        assert!(!mounts.is_empty());
    }

    #[test]
    fn list_mounts_paths_exist() {
        let mounts = list_mounts();
        for m in &mounts {
            assert!(m.path.exists(), "mount path {:?} should exist", m.path);
        }
    }

    #[test]
    fn list_mounts_labels_not_empty() {
        let mounts = list_mounts();
        for m in &mounts {
            assert!(!m.label.is_empty(), "mount label should not be empty");
        }
    }

    #[test]
    fn mount_point_clone() {
        let mp = MountPoint {
            path: PathBuf::from("/test"),
            label: "test".to_string(),
        };
        let cloned = mp.clone();
        assert_eq!(cloned.path, mp.path);
        assert_eq!(cloned.label, mp.label);
    }
}