Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions src/app/toolbar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,16 @@ impl App {
.width(80.0)
.selected_text(egui::RichText::new(drive_text).monospace().size(13.0).color(t.highlight))
.show_ui(ui, |ui| {
for (i, (d, _)) in self.drives.iter().enumerate() {
for (i, (d, description)) in self.drives.iter().enumerate() {
let label = if cfg!(target_os = "linux") && !description.is_empty() {
format!("{d} — {description}")
} else {
d.clone()
};
ui.selectable_value(
&mut self.selected_drive,
i,
egui::RichText::new(d.as_str()).monospace().size(13.0).color(t.highlight),
egui::RichText::new(label).monospace().size(13.0).color(t.highlight),
);
}
});
Expand Down
67 changes: 66 additions & 1 deletion src/drives.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ pub fn detect_drives() -> Vec<(String, String)> {
if let Some(sg_entry) = sg_entries.flatten().next() {
if sg_entry.path().is_dir() {
let dev_path = format!("/dev/{}", sg_entry.file_name().to_string_lossy());
drives.push((dev_path, String::new()));
drives.push((dev_path, linux_drive_description(&path)));
continue;
}
}
Expand Down Expand Up @@ -244,3 +244,68 @@ pub fn detect_drives() -> Vec<(String, String)> {
drives
}
}

// sysfs attributes are optional: a disappearing drive or restricted sandbox must
// not prevent enumeration, and missing metadata leaves the plain device path.
#[cfg(target_os = "linux")]
fn linux_drive_description(device: &std::path::Path) -> String {
["vendor", "model"]
.iter()
.filter_map(|attribute| std::fs::read_to_string(device.join(attribute)).ok())
.flat_map(|value| value.split_whitespace().map(str::to_owned).collect::<Vec<_>>())
.collect::<Vec<_>>()
.join(" ")
}

#[cfg(all(test, target_os = "linux"))]
mod tests {
use super::*;
use std::{fs, path::PathBuf};

struct DeviceFixture(PathBuf);

impl DeviceFixture {
fn new() -> Self {
static NEXT_ID: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
let id = NEXT_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let path = std::env::temp_dir().join(format!("redumper-gui-drives-{}-{id}", std::process::id()));
fs::create_dir(&path).unwrap();
Self(path)
}
}

impl Drop for DeviceFixture {
fn drop(&mut self) {
fs::remove_dir_all(&self.0).unwrap();
}
}

#[test]
fn reads_and_normalizes_vendor_and_model() {
let device = DeviceFixture::new();
fs::write(device.0.join("vendor"), "PLEXTOR \n").unwrap();
fs::write(device.0.join("model"), "CD-R PX-W4012A\n").unwrap();
assert_eq!(linux_drive_description(&device.0), "PLEXTOR CD-R PX-W4012A");
}

#[test]
fn uses_available_metadata() {
let device = DeviceFixture::new();
fs::write(device.0.join("model"), "DVD-ROM SH-D163B\n").unwrap();
assert_eq!(linux_drive_description(&device.0), "DVD-ROM SH-D163B");
fs::remove_file(device.0.join("model")).unwrap();
fs::write(device.0.join("vendor"), "TSSTcorp\n").unwrap();
assert_eq!(linux_drive_description(&device.0), "TSSTcorp");
}

#[test]
fn missing_blank_or_unreadable_metadata_is_optional() {
let device = DeviceFixture::new();
assert_eq!(linux_drive_description(&device.0), "");
fs::write(device.0.join("vendor"), " \t\n").unwrap();
// A directory at an attribute path simulates a read failure even as root.
fs::create_dir(device.0.join("model")).unwrap();
assert_eq!(linux_drive_description(&device.0), "");
assert_eq!(linux_drive_description(&device.0.join("disconnected")), "");
}
}