#![allow(dead_code, unused_imports, unused_variables)] use crate::error::{exit_code, WipeError}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::process::Command; #[derive(Debug, Clone, Deserialize, Serialize)] pub struct LsblkOutput { pub blockdevices: Vec, } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct LsblkDevice { pub name: String, pub kname: Option, pub path: Option, #[serde(rename = "type")] pub devtype: Option, pub size: Option, #[serde(rename = "maj:min")] pub maj_min: Option, pub rota: Option, pub tran: Option, pub model: Option, pub serial: Option, pub fstype: Option, #[serde(default)] pub mountpoints: Option>>, #[serde(default)] pub children: Option>, // Additional fields that lsblk may emit #[serde(default)] pub mountpoint: Option, } #[derive(Debug, Clone)] pub struct DeviceInfo { pub path: PathBuf, pub resolved_path: PathBuf, pub kname: String, pub devtype: String, pub size_bytes: Option, pub rota: Option, pub tran: Option, pub model: Option, pub serial: Option, pub fstype: Option, pub is_whole_disk: bool, pub partitions: Vec, pub mountpoints: Vec, pub all_mountpoints: Vec, } #[derive(Debug, Clone)] pub struct PartitionInfo { pub path: PathBuf, pub kname: String, pub size_bytes: Option, pub fstype: Option, pub mountpoints: Vec, } /// Validate device path per spec #8: /// - path in /dev /// - exists /// - resolve symlink /// - target is block device /// - avoid symlink escape pub fn validate_device_path(input: &Path) -> Result { // Must be absolute and under /dev if !input.is_absolute() { return Err(WipeError::new( exit_code::INVALID_ARGS, format!("device path must be absolute: {}", input.display()), )); } let input_str = input.to_string_lossy(); if !input_str.starts_with("/dev/") { return Err(WipeError::new( exit_code::INVALID_ARGS, format!("device path must be in /dev: {}", input.display()), )); } if !input.exists() { return Err(WipeError::new( exit_code::TARGET_NOT_FOUND, format!("device does not exist: {}", input.display()), )); } // Resolve symlink (canonicalize) but check escape let canonical = std::fs::canonicalize(input).map_err(|e| { WipeError::with_source( exit_code::TARGET_NOT_FOUND, format!("failed to resolve device path: {}", input.display()), e, ) })?; let canonical_str = canonical.to_string_lossy(); if !canonical_str.starts_with("/dev/") { return Err(WipeError::new( exit_code::INVALID_ARGS, format!( "symlink escapes /dev: {} -> {}", input.display(), canonical.display() ), )); } // Check block device let metadata = std::fs::metadata(&canonical).map_err(|e| { WipeError::with_source( exit_code::TARGET_NOT_FOUND, format!("cannot stat device: {}", canonical.display()), e, ) })?; // Use nix to check file type is block device? Simpler: use std and check via libc // Use std::os::unix::fs::FileTypeExt use std::os::unix::fs::FileTypeExt; if !metadata.file_type().is_block_device() { return Err(WipeError::new( exit_code::NOT_A_BLOCK_DEVICE, format!("not a block device: {}", canonical.display()), )); } Ok(canonical) } /// Run lsblk --json with needed columns and parse output pub fn run_lsblk() -> Result { let output = Command::new("lsblk") .args([ "--json", "-o", "NAME,KNAME,PATH,TYPE,SIZE,ROTA,MOUNTPOINTS,FSTYPE,MODEL,SERIAL,TRAN,MAJ:MIN", ]) .output() .map_err(|e| { WipeError::with_source( exit_code::EXTERNAL_COMMAND_FAILED, "failed to execute lsblk", e, ) })?; if !output.status.success() { return Err(WipeError::new( exit_code::EXTERNAL_COMMAND_FAILED, format!( "lsblk failed: {}", String::from_utf8_lossy(&output.stderr).trim() ), )); } let stdout = String::from_utf8_lossy(&output.stdout); serde_json::from_str::(&stdout).map_err(|e| { WipeError::with_source( exit_code::EXTERNAL_COMMAND_FAILED, format!("failed to parse lsblk output: {e}"), e, ) }) } /// Find device in lsblk tree by canonical path or kname fn find_device<'a>(devices: &'a [LsblkDevice], target: &Path) -> Option<&'a LsblkDevice> { let target_str = target.to_string_lossy(); let target_kname = target.file_name().map(|n| n.to_string_lossy().to_string()); for dev in devices { if let Some(p) = &dev.path { if Path::new(p) == target { return Some(dev); } } if let Some(kname) = &dev.kname { if Some(kname.as_str()) == target_kname.as_deref() { // Check also if path matches or kname matches if dev.path.is_none() { // fallback to /dev/ let kpath = format!("/dev/{kname}"); if kpath == target_str { return Some(dev); } } } } if let Some(name) = dev.name.strip_prefix("/dev/") { let full = format!("/dev/{name}"); if full == target_str { return Some(dev); } } // Also match by NAME field if format!("/dev/{}", dev.name) == target_str { return Some(dev); } if let Some(children) = &dev.children { if let Some(found) = find_device(children, target) { return Some(found); } } } None } /// Also search recursively and return top-level disk if needed fn find_device_recursive<'a>(devices: &'a [LsblkDevice], target: &Path) -> Option<&'a LsblkDevice> { find_device(devices, target) } pub fn inspect_device(resolved_path: &Path) -> Result { let lsblk = run_lsblk()?; let dev = find_device_recursive(&lsblk.blockdevices, resolved_path).ok_or_else(|| { WipeError::new( exit_code::TARGET_NOT_FOUND, format!("device not found in lsblk: {}", resolved_path.display()), ) })?; let devtype = dev.devtype.clone().unwrap_or_else(|| "unknown".to_string()); let is_whole_disk = devtype == "disk" || devtype == "loop"; // Get mountpoints for this device let mountpoints = collect_mountpoints(dev); // Collect all mountpoints recursively (for whole disk, include children) let all_mountpoints = collect_all_mountpoints(dev); // Partitions: children where type == "part" let mut partitions = Vec::new(); if let Some(children) = &dev.children { for child in children { let ctype = child.devtype.as_deref().unwrap_or(""); if ctype == "part" { let cpath = child .path .clone() .unwrap_or_else(|| format!("/dev/{}", child.name)); partitions.push(PartitionInfo { path: PathBuf::from(cpath), kname: child.kname.clone().unwrap_or_else(|| child.name.clone()), size_bytes: child.size.as_deref().and_then(parse_lsblk_size), fstype: child.fstype.clone(), mountpoints: collect_mountpoints(child), }); } else if child.children.is_some() { // For nested, still collect part children if let Some(sub) = &child.children { for subchild in sub { if subchild.devtype.as_deref() == Some("part") { let cpath = subchild .path .clone() .unwrap_or_else(|| format!("/dev/{}", subchild.name)); partitions.push(PartitionInfo { path: PathBuf::from(cpath), kname: subchild .kname .clone() .unwrap_or_else(|| subchild.name.clone()), size_bytes: None, fstype: subchild.fstype.clone(), mountpoints: collect_mountpoints(subchild), }); } } } } } } // For partition devices, partitions is empty; but for whole-disk we have children // Also if target is partition, we don't need to populate partitions. // Try to get size_bytes via blockdev or sysfs let size_bytes = get_block_device_size(resolved_path).ok(); let kname = dev .kname .clone() .unwrap_or_else(|| dev.name.clone()) .trim() .to_string(); Ok(DeviceInfo { path: resolved_path.to_path_buf(), resolved_path: resolved_path.to_path_buf(), kname, devtype, size_bytes, rota: dev.rota, tran: dev.tran.clone(), model: dev.model.clone().map(|s| s.trim().to_string()), serial: dev.serial.clone().map(|s| s.trim().to_string()), fstype: dev.fstype.clone(), is_whole_disk, partitions, mountpoints: mountpoints.clone(), all_mountpoints, }) } fn collect_mountpoints(dev: &LsblkDevice) -> Vec { let mut out = Vec::new(); if let Some(mps) = &dev.mountpoints { for m in mps.iter().flatten() { if !m.is_empty() && m != "null" { // lsblk may return "[SWAP]" for swap out.push(m.clone()); } } } if let Some(mp) = &dev.mountpoint { if !mp.is_empty() && !out.contains(mp) { out.push(mp.clone()); } } out } fn collect_all_mountpoints(dev: &LsblkDevice) -> Vec { let mut out = collect_mountpoints(dev); if let Some(children) = &dev.children { for child in children { out.extend(collect_all_mountpoints(child)); } } out } /// Parse lsblk SIZE string like "476.9G" or "513M" into bytes (approximate) /// This is only fallback; primary size comes from blockdev ioctl pub fn parse_lsblk_size(s: &str) -> Option { let s = s.trim(); if s.is_empty() { return None; } // If it's plain number, treat as bytes if let Ok(n) = s.parse::() { return Some(n); } // Handle human sizes: number + unit let mut num_part = String::new(); let mut unit_part = String::new(); for c in s.chars() { if c.is_ascii_digit() || c == '.' { if unit_part.is_empty() { num_part.push(c); } else { // invalid interleaving return None; } } else { unit_part.push(c); } } let num: f64 = num_part.parse().ok()?; let unit = unit_part.trim().to_ascii_uppercase(); let mult: f64 = match unit.as_str() { "B" => 1.0, "K" | "KB" | "KIB" => 1024.0, "M" | "MB" | "MIB" => 1024.0 * 1024.0, "G" | "GB" | "GIB" => 1024.0 * 1024.0 * 1024.0, "T" | "TB" | "TIB" => 1024.0 * 1024.0 * 1024.0 * 1024.0, _ => return None, }; Some((num * mult) as u64) } /// Get block device size via ioctl BLKGETSIZE64 or fallback to sysfs / sys/block pub fn get_block_device_size(path: &Path) -> Result { // Try ioctl first if let Ok(size) = get_size_via_ioctl(path) { return Ok(size); } // Fallback to blockdev --getsize64 if let Ok(size) = get_size_via_blockdev(path) { return Ok(size); } // Fallback to /sys/class/block//size (sectors * 512) let kname = path .file_name() .and_then(|n| n.to_str()) .unwrap_or_default(); let sys_path = format!("/sys/class/block/{kname}/size"); if let Ok(content) = std::fs::read_to_string(&sys_path) { if let Ok(sectors) = content.trim().parse::() { return Ok(sectors * 512); } } // Try /sys/block variant let sys_path2 = format!("/sys/block/{kname}/size"); if let Ok(content) = std::fs::read_to_string(&sys_path2) { if let Ok(sectors) = content.trim().parse::() { return Ok(sectors * 512); } } Err(WipeError::new( exit_code::EXTERNAL_COMMAND_FAILED, format!("cannot determine size of {}", path.display()), )) } fn get_size_via_ioctl(path: &Path) -> Result { use std::os::unix::io::AsRawFd; let file = std::fs::OpenOptions::new() .read(true) .open(path) .map_err(|e| WipeError::with_source(exit_code::GENERIC_ERROR, "open for ioctl", e))?; let fd = file.as_raw_fd(); let mut size: u64 = 0; // BLKGETSIZE64 = _IOR(0x12,114,size_t) => 0x80081272 on x86_64 const BLKGETSIZE64: libc::c_ulong = 0x80081272; let ret = unsafe { libc::ioctl(fd, BLKGETSIZE64 as _, &mut size as *mut u64 as *mut libc::c_void) }; if ret == 0 { Ok(size) } else { Err(WipeError::new( exit_code::EXTERNAL_COMMAND_FAILED, format!( "ioctl BLKGETSIZE64 failed: {}", std::io::Error::last_os_error() ), )) } } fn get_size_via_blockdev(path: &Path) -> Result { let output = Command::new("blockdev") .arg("--getsize64") .arg(path) .output() .map_err(|e| { WipeError::with_source(exit_code::EXTERNAL_COMMAND_FAILED, "blockdev exec", e) })?; if !output.status.success() { return Err(WipeError::new( exit_code::EXTERNAL_COMMAND_FAILED, String::from_utf8_lossy(&output.stderr).to_string(), )); } let s = String::from_utf8_lossy(&output.stdout).trim().to_string(); s.parse::().map_err(|e| { WipeError::with_source(exit_code::EXTERNAL_COMMAND_FAILED, "parse blockdev size", e) }) } /// Human-readable size formatting pub fn format_human_size(bytes: u64) -> String { const UNITS: &[&str] = &["B", "KiB", "MiB", "GiB", "TiB", "PiB"]; let mut size = bytes as f64; let mut unit = 0; while size >= 1024.0 && unit + 1 < UNITS.len() { size /= 1024.0; unit += 1; } if unit == 0 { format!("{} {}", bytes, UNITS[unit]) } else { format!("{:.2} {}", size, UNITS[unit]) } } /// Format size for display in TiB as spec example pub fn format_size_tib(bytes: u64) -> String { let tib = bytes as f64 / (1024.0 * 1024.0 * 1024.0 * 1024.0); format!("{:.2} TiB", tib) } // For JSON output: collect device info into hashmap-like struct pub fn device_info_to_map(info: &DeviceInfo) -> HashMap { let mut m = HashMap::new(); m.insert( "path".to_string(), serde_json::Value::String(info.path.display().to_string()), ); m.insert( "kname".to_string(), serde_json::Value::String(info.kname.clone()), ); m.insert( "type".to_string(), serde_json::Value::String(info.devtype.clone()), ); if let Some(s) = info.size_bytes { m.insert("size".to_string(), serde_json::Value::Number(s.into())); } m } #[cfg(test)] mod tests { use super::*; #[test] fn test_parse_lsblk_size() { assert_eq!(parse_lsblk_size("512M"), Some(512 * 1024 * 1024)); assert_eq!( parse_lsblk_size("476.9G"), Some((476.9 * 1024.0 * 1024.0 * 1024.0) as u64) ); assert_eq!(parse_lsblk_size("1M"), Some(1024 * 1024)); assert!(parse_lsblk_size("3.64 TiB").is_some()); // Plain bytes assert_eq!(parse_lsblk_size("1024"), Some(1024)); } #[test] fn test_format_human_size() { assert_eq!(format_human_size(1024), "1.00 KiB"); assert_eq!(format_human_size(64 * 1024 * 1024), "64.00 MiB"); } #[test] fn test_parse_human_size_via_cli() { // cross-check with cli parser use crate::cli::parse_human_size as chs; assert_eq!(chs("64M"), Some(64 * 1024 * 1024)); } }