1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
use std::{collections::HashMap, path::PathBuf};
#[derive(thiserror::Error, Debug)]
pub enum LsblkError {
#[error("I/O Error: {0}")]
Io(#[from] std::io::Error),
#[error("Fail to strip prefix `/dev/` from path")]
StripPrefix(#[from] std::path::StripPrefixError),
}
fn ls_symlinks(
dir: &std::path::Path,
) -> std::io::Result<impl Iterator<Item = Result<(String, String), LsblkError>>> {
Ok(std::fs::read_dir(dir)?
.filter_map(|f| f.ok())
.filter(|f| f.metadata().is_ok_and(|f| f.is_symlink()))
.map(|f| {
let target = std::fs::read_link(f.path())?;
let target = target.strip_prefix("/dev/")?.to_string_lossy().to_string();
let source = f.file_name().to_string_lossy().to_string();
Ok((target, source))
}))
}
/// A representation of a block-device
#[derive(Debug, Clone, Default)]
pub struct BlockDevice {
/// the filename of the block-device.
///
/// If the drive is deemed to be storage by the kernel, this is usually prefixed by one of the
/// followings:
/// - `sd`
/// - `hd`
/// - `vd`
/// - `nvme`
/// - `mmcblk`
/// - `loop`
pub name: String,
/// The diskseq of the device as in `/dev/disk/by-diskseq/`.
pub diskseq: Option<String>,
/// The path (not the filesystem!) of the device as in `/dev/disk/by-path`.
pub path: Option<String>,
/// The device UUID.
pub uuid: Option<String>,
/// The UUID of a partition (not the same as device UUID).
pub partuuid: Option<String>,
/// The label of the partition.
pub label: Option<String>,
/// The partition label (not the same as `label`), as in `/dev/disk/by-partlabel`)
pub partlabel: Option<String>,
/// The id of the device as in `/dev/disk/by-id/`.
pub id: Option<String>,
}
impl BlockDevice {
/// List out all found block devices and populate all fields.
#[must_use]
pub fn list() -> Result<Vec<Self>, LsblkError> {
let mut result = HashMap::new();
macro_rules! insert {
($kind:ident) => {
for x in ls_symlinks(&PathBuf::from(concat!("/dev/disk/by-", stringify!($kind))))? {
let (name, blk) = x?;
if let Some(bd) = result.get_mut(&name) {
bd.$kind = Some(blk);
} else {
result.insert(
name.to_string(),
Self {
name,
$kind: Some(blk),
..Self::default()
},
);
}
}
};
}
for x in ls_symlinks(&PathBuf::from("/dev/disk/by-diskseq/"))? {
let (name, blk) = x?;
result.insert(
name.to_string(), // FIXME: clone shouldn't be needed theoretically
Self {
name,
diskseq: Some(blk),
..BlockDevice::default()
},
);
}
insert!(path);
insert!(uuid);
insert!(partuuid);
insert!(label);
insert!(partlabel);
insert!(id);
Ok(result.into_values().collect())
}
/// Returns true if and only if the device is a storage disk and is not a partition.
#[must_use]
pub fn is_disk(&self) -> bool {
!self.is_part()
&& (self.name.starts_with("sd")
|| self.name.starts_with("hd")
|| self.name.starts_with("vd")
|| self.name.starts_with("nvme")
|| self.name.starts_with("mmcblk")
|| self.name.starts_with("loop"))
}
/// Returns true if and only if the device is a partition.
///
/// The implementation currently is just:
/// ```rs
/// self.uuid.is_some()
/// ```
#[must_use]
pub fn is_part(&self) -> bool {
self.uuid.is_some()
}
}