liblitho 0.2.0

cli tool to flash/clone the images to storage devices
Documentation
//! Linux storage enumeration via sysfs.

use crate::devices::DeviceInfo;
use crate::platform::traits::DeviceInventory;
use anyhow::{Context, Result};
use log::{debug, warn};
use std::fs;
use std::path::{Path, PathBuf};

use super::LinuxPlatform;

impl DeviceInventory for LinuxPlatform {
    fn list_storage_devices() -> Result<Vec<DeviceInfo>> {
        let paths = fs::read_dir("/sys/block/").context("Failed to read /sys/block/ directory")?;
        let mut devices: Vec<DeviceInfo> = Vec::new();

        for entry in paths {
            let entry = entry.context("Failed to read directory entry")?;
            let sys_path = entry.path();
            let Some(device_end_name) = entry.file_name().to_str().map(str::to_string) else {
                warn!("Could not convert device name to string");
                continue;
            };

            if !sys_path.join("device").exists() {
                continue;
            }

            let vendor_name = read_sysfs_trimmed(&sys_path.join("device/vendor"));
            let model_name = read_sysfs_trimmed(&sys_path.join("device/model"));
            let removable_raw = read_sysfs_trimmed(&sys_path.join("removable"));
            let size = read_sysfs_trimmed(&sys_path.join("size"))
                .parse::<u64>()
                .unwrap_or(0);

            let removable = match removable_raw.as_str() {
                "1" => 1u8,
                "0" => 0u8,
                _ => continue,
            };

            let dev_path = PathBuf::from("/dev/").join(&device_end_name);
            if !dev_path.exists() {
                continue;
            }

            devices.push(DeviceInfo {
                device_name: dev_path.display().to_string(),
                vendor_name,
                model_name,
                removable,
                size,
            });
        }
        debug!("Found {} devices", devices.len());
        Ok(devices)
    }

    fn device_size_bytes(path: &str) -> Option<u64> {
        Self::device_size_sectors(path).map(|sectors| sectors.saturating_mul(512))
    }

    fn device_size_sectors(path: &str) -> Option<u64> {
        let block_name = Path::new(path).file_name().and_then(|n| n.to_str())?;
        let size_path = format!("/sys/block/{block_name}/size");
        fs::read_to_string(size_path).ok()?.trim().parse().ok()
    }

    fn is_removable(device_path: &str) -> Result<bool> {
        let device_name = Path::new(device_path)
            .file_name()
            .context("Invalid device path")?
            .to_str()
            .context("Non UTF-8 device name")?;

        let removable_path = PathBuf::from(format!("/sys/block/{device_name}/removable"));
        debug!("Checking removable path: {}", removable_path.display());
        let contents = fs::read_to_string(&removable_path)
            .with_context(|| format!("Failed to open {}", removable_path.display()))?;
        Ok(contents.trim() == "1")
    }
}

fn read_sysfs_trimmed(path: &Path) -> String {
    fs::read_to_string(path)
        .map(|s| s.trim().to_string())
        .unwrap_or_default()
}