#[derive(Debug, Clone)]
pub struct UsbInfo {
pub vendor_id: u16,
pub product_id: u16,
pub manufacturer: Option<String>,
pub product: Option<String>,
pub serial: Option<String>,
pub speed: Option<String>,
}
impl UsbInfo {
pub fn display_label(&self) -> String {
match (&self.manufacturer, &self.product) {
(Some(mfr), Some(prd)) => format!("{} {}", mfr.trim(), prd.trim()),
(None, Some(prd)) => prd.trim().to_string(),
(Some(mfr), None) => mfr.trim().to_string(),
(None, None) => format!("{:04x}:{:04x}", self.vendor_id, self.product_id),
}
}
}
#[derive(Debug, Clone)]
pub struct DriveInfo {
pub name: String,
pub mount_point: String,
pub size_gb: f64,
pub device_path: String,
pub is_system: bool,
pub is_read_only: bool,
pub disabled: bool,
pub usb_info: Option<UsbInfo>,
}
impl DriveInfo {
pub fn new(name: String, mount_point: String, size_gb: f64, device_path: String) -> Self {
Self {
name,
mount_point,
size_gb,
device_path,
is_system: false,
is_read_only: false,
disabled: false,
usb_info: None,
}
}
pub fn with_constraints(
name: String,
mount_point: String,
size_gb: f64,
device_path: String,
is_system: bool,
is_read_only: bool,
) -> Self {
Self {
name,
mount_point,
size_gb,
device_path,
is_system,
is_read_only,
disabled: false,
usb_info: None,
}
}
pub fn with_usb_info(mut self, info: UsbInfo) -> Self {
self.usb_info = Some(info);
self
}
pub fn is_usb(&self) -> bool {
self.usb_info.is_some()
}
}
impl PartialEq for DriveInfo {
fn eq(&self, other: &Self) -> bool {
self.device_path == other.device_path
}
}
#[cfg(test)]
mod tests {
use super::*;
fn usb_info_fixture() -> UsbInfo {
UsbInfo {
vendor_id: 0x0781,
product_id: 0x5581,
manufacturer: Some("SanDisk".into()),
product: Some("Ultra".into()),
serial: Some("SN123456".into()),
speed: Some("SuperSpeed (5 Gbps)".into()),
}
}
#[test]
fn test_new_defaults() {
let d = DriveInfo::new(
"USB Drive".into(),
"/media/usb".into(),
32.0,
"/dev/sdb".into(),
);
assert!(!d.is_system);
assert!(!d.is_read_only);
assert!(!d.disabled);
assert!(d.usb_info.is_none());
assert!(!d.is_usb());
}
#[test]
fn test_with_constraints_defaults_usb_info_none() {
let d = DriveInfo::with_constraints(
"USB".into(),
"/media/usb".into(),
32.0,
"/dev/sdb".into(),
true,
false,
);
assert!(d.is_system);
assert!(!d.is_read_only);
assert!(d.usb_info.is_none());
}
#[test]
fn test_with_usb_info_builder() {
let d = DriveInfo::new(
"SanDisk Ultra".into(),
"/media/usb".into(),
32.0,
"/dev/sdb".into(),
)
.with_usb_info(usb_info_fixture());
assert!(d.is_usb());
let info = d.usb_info.as_ref().unwrap();
assert_eq!(info.vendor_id, 0x0781);
assert_eq!(info.product_id, 0x5581);
assert_eq!(info.serial.as_deref(), Some("SN123456"));
assert_eq!(info.speed.as_deref(), Some("SuperSpeed (5 Gbps)"));
}
#[test]
fn test_usb_info_display_label_both() {
let info = usb_info_fixture();
assert_eq!(info.display_label(), "SanDisk Ultra");
}
#[test]
fn test_usb_info_display_label_product_only() {
let info = UsbInfo {
vendor_id: 0x1234,
product_id: 0x5678,
manufacturer: None,
product: Some("MyDrive".into()),
serial: None,
speed: None,
};
assert_eq!(info.display_label(), "MyDrive");
}
#[test]
fn test_usb_info_display_label_fallback_to_ids() {
let info = UsbInfo {
vendor_id: 0x1234,
product_id: 0xabcd,
manufacturer: None,
product: None,
serial: None,
speed: None,
};
assert_eq!(info.display_label(), "1234:abcd");
}
#[test]
fn test_drive_equality_by_device_path() {
let d1 = DriveInfo::new("A".into(), "/mnt/a".into(), 32.0, "/dev/sdb".into());
let d2 = DriveInfo::new("B".into(), "/mnt/b".into(), 64.0, "/dev/sdb".into());
let d3 = DriveInfo::new("C".into(), "/mnt/c".into(), 32.0, "/dev/sdc".into());
assert_eq!(d1, d2, "same device_path → equal");
assert_ne!(d1, d3, "different device_path → not equal");
}
#[test]
fn test_drive_equality_ignores_usb_info() {
let d1 = DriveInfo::new("A".into(), "/mnt/a".into(), 32.0, "/dev/sdb".into())
.with_usb_info(usb_info_fixture());
let d2 = DriveInfo::new("A".into(), "/mnt/a".into(), 32.0, "/dev/sdb".into());
assert_eq!(d1, d2, "usb_info must not affect equality");
}
}