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 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185
//! A library that lists out block-devices.
//!
//! Check out [`BlockDevice::list`].
#![warn(clippy::complexity)]
#![warn(clippy::correctness)]
#![warn(clippy::nursery)]
#![warn(clippy::pedantic)]
#![warn(clippy::perf)]
#![warn(clippy::style)]
#![warn(clippy::suspicious)]
// followings are from clippy::restriction
#![warn(clippy::missing_errors_doc)]
#![warn(clippy::missing_panics_doc)]
#![warn(clippy::missing_safety_doc)]
#![warn(clippy::unwrap_used)]
#![warn(clippy::format_push_string)]
#![warn(clippy::get_unwrap)]
#![allow(clippy::missing_inline_in_public_items)]
#![allow(clippy::implicit_return)]
#![allow(clippy::blanket_clippy_restriction_lints)]
#![allow(clippy::pattern_type_mismatch)]
pub mod mountpoints;
pub use mountpoints::Mount;
use std::{collections::HashMap, path::PathBuf};
#[derive(thiserror::Error, Debug)]
pub enum LsblkError {
#[error("Cannot read directory {0:?}: {1}")]
ReadDir(PathBuf, std::io::Error),
#[error("Cannot canonicalize broken symlink for {0:?}: {1}")]
BadSymlink(PathBuf, std::io::Error),
#[error("Cannot read file content from {0:?}: {1}")]
ReadFile(PathBuf, std::io::Error),
}
pub(crate) type Res<T> = Result<T, LsblkError>;
pub(crate) type ItRes<T> = dyn Iterator<Item = Res<T>>;
fn ls_symlinks(dir: &std::path::Path) -> Res<Box<ItRes<(PathBuf, String)>>> {
Ok(if dir.exists() {
Box::new(
std::fs::read_dir(dir)
.map_err(|e| LsblkError::ReadDir(dir.to_path_buf(), e))?
.filter_map(Result::ok)
.filter(|f| f.metadata().is_ok_and(|f| f.is_symlink()))
.map(|f| {
let dest = (f.path().canonicalize()) // this also resolves the symlink
.map_err(|e| LsblkError::BadSymlink(f.path(), e))?;
let src = f.file_name().to_string_lossy().to_string();
Ok((dest, src))
}),
)
} else {
Box::new(std::iter::empty())
})
}
/// A representation of a block-device
#[derive(Debug, Clone, Default)]
pub struct BlockDevice {
/// the filename of the block-device.
pub name: String,
/// The full name of the block-device, which is basically `/dev/{name}`.
pub fullname: PathBuf,
/// 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.
///
/// # Panics
/// If somehow there exists a device that isn't in `/dev/`, the function panics.
///
/// # Errors
/// There are no particular errors other than IO / symlink resolution failures, etc.
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 (fullname, blk) = x?;
let name = fullname
.strip_prefix("/dev/")
.expect("Cannot strip /dev")
.to_string_lossy()
.to_string();
if let Some(bd) = result.get_mut(&name) {
bd.$kind = Some(blk);
} else {
result.insert(
name.to_string(),
Self {
name,
fullname,
$kind: Some(blk),
..Self::default()
},
);
}
}
};
}
for x in ls_symlinks(&PathBuf::from("/dev/disk/by-diskseq/"))? {
let (fullname, blk) = x?;
let name = fullname
.strip_prefix("/dev/")
.expect("Cannot strip /dev")
.to_string_lossy()
.to_string();
result.insert(
name.to_string(), // FIXME: clone shouldn't be needed theoretically
Self {
name,
fullname,
diskseq: Some(blk),
..Self::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.
///
/// The implementation currently is just:
/// ```rs
/// !self.is_part()
/// ```
#[must_use]
pub const fn is_disk(&self) -> bool {
!self.is_part()
}
/// Determines if the block-device is considered to be physical.
/// This can be a partition or a disk.
///
/// A "physical" disk is one that has a path as in `/dev/disk/by-path`
///
/// The implementation currently is just:
/// ```rs
/// self.path.is_some()
/// ```
#[must_use]
pub const fn is_physical(&self) -> bool {
self.path.is_some()
}
/// Returns true if and only if the device is a partition.
///
/// The implementation currently is just:
/// ```rs
/// self.partuuid.is_some()
/// ```
#[must_use]
pub const fn is_part(&self) -> bool {
self.partuuid.is_some()
}
}
#[cfg(test)]
#[cfg(target_os = "linux")]
#[test]
fn test_lsblk_smoke() {
BlockDevice::list().expect("Valid lsblk");
}