diff --git a/.gitignore b/.gitignore index 088ba6b..356d973 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk + +.vscode/* diff --git a/Cargo.toml b/Cargo.toml index 5b78af5..ec846d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,12 +11,12 @@ authors = ["Stefan Rabmund "] readme = "crates-io.md" [dependencies] +gpt = {version = "3.1.0", optional=true} thiserror = "1.0" [dev-dependencies] tempfile = "3" - [[example]] name = "simple_main" path = "examples/simple_main.rs" diff --git a/README.md b/README.md index c3d4559..fd70a4a 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,8 @@ A rust library (crate) for listing mounted or mountable drives on linux (flash drives, sd-cards, etc.) -Uses the virtual sysfs filesystem (/sys) to gather information about the block devices known by the linux kernel. +Uses the virtual kernel filesystems (/sys, /proc and /dev) to gather information about the block devices known by the linux kernel. +Optionally reads the GUID Partition Table (GPT) to enrich gathered data with informations from the partition table. ## Data @@ -17,10 +18,12 @@ Uses the virtual sysfs filesystem (/sys) to gather information about the block d * size * partitions * is removable + * uuid (optionally from GPT) * partition * name * size * mountpoint (path, filesystem) + * part_uuid (optionally from GPT) ## Example @@ -30,6 +33,9 @@ For an simple example see [simple_main.rs](examples/simple_main.rs): cargo run --example simple_main ``` +## Optional Data from GUID Partition Table (GPT) + +Currently only the UUID for a device and the PART_UUID of partitions are retreived using the GPT. This needs the feature "gpt" to be enabled. ## License diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..3380cf9 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,6 @@ +coverage: + status: + project: + default: + target: 80% + threshold: 1% diff --git a/crates-io.md b/crates-io.md index 6d59a47..ad94a9a 100644 --- a/crates-io.md +++ b/crates-io.md @@ -5,7 +5,8 @@ A rust library (crate) for listing mounted or mountable drives on linux (flash drives, sd-cards, etc.) -Uses the virtual sysfs filesystem (/sys) to gather information about the block devices known by the linux kernel. +Uses the virtual kernel filesystems (/sys, /proc and /dev) to gather information about the block devices known by the linux kernel. +Optionally reads the GUID Partition Table (GPT) to enrich gathered data with informations from the partition table. ## Data @@ -15,10 +16,12 @@ Uses the virtual sysfs filesystem (/sys) to gather information about the block d * size * partitions * is removable + * uuid (optionally from GPT) * partition * name * size * mountpoint (path, filesystem) + * part_uuid (optionally from GPT) ## Example @@ -32,6 +35,10 @@ cargo run --example simple_main Documentation can be found on [docs.rs](https://docs.rs/drives/latest/drives/). +## Optional Data from GUID Partition Table (GPT) + +Currently only the UUID for a device and the PART_UUID of partitions are retreived using the GPT. This needs the feature "gpt" to be enabled. + ## License diff --git a/resources/test/gptdisk.img b/resources/test/gptdisk.img new file mode 100644 index 0000000..ff6f764 Binary files /dev/null and b/resources/test/gptdisk.img differ diff --git a/src/error.rs b/src/error.rs index e43bbcc..8c48f3c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -14,6 +14,8 @@ pub enum DrivesError { PathAppendFailed, #[error("failed to convert file content to u64")] ConversionToU64Failed, + #[error("failed to convert file content to u32")] + ConversionToU32Failed, #[error("failed to access directory {directory:?}")] DiraccessError { directory: String }, #[error("reading mounts from /proc/mounts failed")] diff --git a/src/fs_wrap.rs b/src/fs_wrap.rs index 7f01ed3..302c6fe 100644 --- a/src/fs_wrap.rs +++ b/src/fs_wrap.rs @@ -62,6 +62,17 @@ pub fn read_file_to_u64(path: &str) -> Result { Ok(size_as_u64) } +pub fn read_file_to_u32(path: &str) -> Result { + let content = read_file_to_string(Path::new(path))?; + + let size_as_u32 = if let Ok(size) = content.parse() { + size + } else { + return Err(DrivesError::ConversionToU32Failed); + }; + Ok(size_as_u32) +} + pub fn read_lines

(filename: P) -> io::Result>> where P: AsRef, @@ -113,4 +124,28 @@ mod tests { let result = read_file_to_string(test_file.path()); assert_eq!("content", result.unwrap()); } + + #[test] + fn test_read_file_to_32() { + // prepare a temporary file to read from + let mut test_file = NamedTempFile::new().unwrap(); + test_file.write_all("2".as_bytes()).unwrap(); + + // call the method under test + let result = read_file_to_u32(test_file.path().to_str().unwrap()); + let expected: u32 = 2; + assert_eq!(expected, result.unwrap()); + } + + #[test] + fn test_read_file_to_64() { + // prepare a temporary file to read from + let mut test_file = NamedTempFile::new().unwrap(); + test_file.write_all("42".as_bytes()).unwrap(); + + // call the method under test + let result = read_file_to_u64(test_file.path().to_str().unwrap()); + let expected: u64 = 42; + assert_eq!(expected, result.unwrap()); + } } diff --git a/src/gpt.rs b/src/gpt.rs new file mode 100644 index 0000000..9d1f312 --- /dev/null +++ b/src/gpt.rs @@ -0,0 +1,123 @@ +use crate::Device; +#[cfg(feature = "gpt")] +use gpt; + +#[cfg(not(test))] +const DEV_DIR: &str = "/dev/"; + +#[cfg(test)] +const DEV_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/resources", "/test"); + +/// Enumeration for holding the gpt UUID or a reason why it is not available +#[derive(Debug)] +pub enum GptUUID { + /// an io error happened when opening the device for read access + IoError(std::io::Error), + /// the UUID from the partition table (gpt) as a hyphenated string + UUID(String), + /// the feature "gpt" was not enabled + FeatureNotEnabled, + /// reading the gpt was successful, but no header was found + NotAvailable, +} + +// when the feature "gpt" is not enabled this function is used +// to set the GptUUID::FeatureNotEnabled value +#[cfg(not(feature = "gpt"))] +pub fn enrich_with_gpt_uuid(mut device: Device) -> Device { + device.uuid = GptUUID::FeatureNotEnabled; + device +} + +// When the feature "gpt" is enabled then this function will actually read the +// partition table (gpt) to get the UUID for the device and the partitions +#[cfg(feature = "gpt")] +pub fn enrich_with_gpt_uuid(mut device: Device) -> Device { + let diskpath = std::path::Path::new(DEV_DIR).join(device.name.to_string()); + let cfg = gpt::GptConfig::new().writable(false); + match cfg.open(diskpath) { + Err(error) => device.uuid = GptUUID::IoError(error), + Ok(disk) => { + match disk.primary_header() { + None => device.uuid = GptUUID::NotAvailable, + Some(disk_header) => { + device.uuid = GptUUID::UUID(disk_header.disk_guid.as_hyphenated().to_string()); + } + }; + for partition in device.partitions.iter_mut() { + match disk.partitions().get(&partition.number) { + Some(gpt_partition) => { + partition.part_uuid = + GptUUID::UUID(gpt_partition.part_guid.as_hyphenated().to_string()) + } + None => partition.part_uuid = GptUUID::NotAvailable, + } + } + } + }; + + device +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[cfg(feature = "gpt")] + #[test] + fn test_enrich_with_gpt_uuid() { + use crate::{get_devices, Partition, Size}; + + let partition1 = Partition { + name: "sda1".to_string(), + size: Size::new(512), + number: 1, + mountpoint: None, + part_uuid: GptUUID::NotAvailable, + }; + let partition2 = Partition { + name: "sda2".to_string(), + size: Size::new(512), + number: 2, + mountpoint: None, + part_uuid: GptUUID::NotAvailable, + }; + + let mut device = Device { + name: "gptdisk.img".to_string(), + partitions: vec![partition1, partition2], + is_removable: false, + model: None, + serial: None, + size: Size::new(42), + uuid: GptUUID::NotAvailable, + }; + device = enrich_with_gpt_uuid(device); + + match device.uuid { + GptUUID::UUID(uuid) => assert_eq!("f0ce7b2c-74af-47e4-8141-b2fe24ac20cc", uuid), + _ => panic!("No UUID"), + } + match &device + .partitions + .iter() + .find(|&partition| partition.number == 1) + .unwrap() + .part_uuid + { + GptUUID::UUID(uuid) => assert_eq!("3cdd6997-9b47-46f1-a160-49546976c24e", uuid), + _ => panic!("Partition 1 - no UUID"), + } + match &device + .partitions + .iter() + .find(|&partition| partition.number == 2) + .unwrap() + .part_uuid + { + GptUUID::UUID(uuid) => assert_eq!("4d3adf65-ff1b-473c-8f5e-b6c8d228b8d4", uuid), + _ => panic!("Partition 2 - no UUID"), + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 49c1b9a..bce0735 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,12 +10,14 @@ use mounts::Mounts; mod error; mod fs_wrap; +mod gpt; mod mounts; mod size; pub use error::DrivesError; pub use mounts::Mount; pub use size::{Size, Unit}; +pub use gpt::GptUUID; use std::fs::DirEntry; @@ -33,7 +35,10 @@ pub struct Device { pub model: Option, /// the hardware serial string pub serial: Option, + /// size of the device pub size: Size, + /// the GUID from GPT (needs feature "gpt" to be enabled) + pub uuid: GptUUID, } /// partition of a device @@ -43,8 +48,12 @@ pub struct Partition { pub name: String, /// size of the partition on 512 byte blocks pub size: Size, + /// the partition number + pub number: u32, /// the mountpoint if mounted pub mountpoint: Option, + /// the PartUUID from GPT (needs feature "gpt" to be enabled) + pub part_uuid: GptUUID, } struct Drives { @@ -72,10 +81,13 @@ impl Drives { if dir_name.starts_with(&base_dir_name) { let size = fs_wrap::read_file_to_u64(&build_path(&entry, "/size")?)?; let mount = self.find_mountpoint_for_partition(&mount_points, &dir_name)?; + let number = fs_wrap::read_file_to_u32(&build_path(&entry, "/partition")?)?; partitions.push(Partition { name: dir_name, size: Size::new(size), + number, mountpoint: mount, + part_uuid: GptUUID::NotAvailable, }); } } @@ -136,14 +148,16 @@ impl Drives { let model_and_serial = self.read_model_and_serial_if_available(&entry); let size = fs_wrap::read_file_to_u64(&build_path(&entry, "/size")?)?; - let device = Device { + let mut device = Device { name: device_name.clone(), partitions, is_removable: removable, model: model_and_serial.0, serial: model_and_serial.1, size: Size::new(size), + uuid: GptUUID::NotAvailable, }; + device = gpt::enrich_with_gpt_uuid(device); devices.push(device); } Ok(devices) @@ -194,11 +208,20 @@ mod tests { fs::create_dir(&part_one_dir_path).unwrap(); size_file = fs::File::create(part_one_dir_path.as_path().join("size")).unwrap(); size_file.write_all("1050624".as_bytes()).unwrap(); + + let mut partition_file = fs::File::create(part_one_dir_path.as_path().join("partition")).unwrap(); + partition_file.write_all("1".as_bytes()).unwrap(); + + let part_two_dir_path = next_dir_path.join("nvme0n1p2"); fs::create_dir(&part_two_dir_path).unwrap(); size_file = fs::File::create(part_two_dir_path.as_path().join("size")).unwrap(); size_file.write_all("999162511".as_bytes()).unwrap(); + let mut partition_file = fs::File::create(part_two_dir_path.as_path().join("partition")).unwrap(); + partition_file.write_all("2".as_bytes()).unwrap(); + + // and create a third dir that isn't following the partition name schema // and should therefor not be identified as a partition let power_dir_path = next_dir_path.join("power");