Skip to content

Commit 7d0601d

Browse files
committed
disks: Total revamp, disks is now primarily a sysfs walker
We differentiate between different types of disks to control exactly which ones we're interested in, and group the commonality into the new BasicDisk type. Using static dispatch etc. Signed-off-by: Ikey Doherty <[email protected]>
1 parent 4a17907 commit 7d0601d

File tree

4 files changed

+257
-77
lines changed

4 files changed

+257
-77
lines changed

crates/disks/src/lib.rs

Lines changed: 143 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,40 +2,162 @@
22
//
33
// SPDX-License-Identifier: MPL-2.0
44

5-
use std::{fs, path::PathBuf};
5+
use std::{
6+
fs, io,
7+
path::{Path, PathBuf},
8+
};
69

710
pub mod nvme;
811
pub mod scsi;
12+
mod sysfs;
913

1014
const SYSFS_DIR: &str = "/sys/class/block";
15+
const DEVFS_DIR: &str = "/dev";
1116

17+
/// A block device on the system which can be either a physical disk or a partition.
1218
#[derive(Debug)]
13-
pub struct Disk {
14-
/// Partial-name, ie "sda"
15-
pub name: String,
19+
pub enum BlockDevice {
20+
/// A physical disk device
21+
Disk(Box<Disk>),
22+
}
23+
24+
/// Represents the type of disk device.
25+
#[derive(Debug)]
26+
pub enum Disk {
27+
/// SCSI disk device (e.g. sda, sdb)
28+
Scsi(scsi::Disk),
29+
/// NVMe disk device (e.g. nvme0n1)
30+
Nvme(nvme::Disk),
31+
}
1632

17-
// Number of sectors (* 512 sector size for data size)
33+
/// A basic disk representation containing common attributes shared by all disk types.
34+
/// This serves as the base structure that specific disk implementations build upon.
35+
#[derive(Debug)]
36+
pub struct BasicDisk {
37+
/// Device name (e.g. sda, nvme0n1)
38+
pub name: String,
39+
/// Total number of sectors on the disk
1840
pub sectors: u64,
41+
/// Path to the device in sysfs
42+
pub node: PathBuf,
43+
/// Path to the device in /dev
44+
pub device: PathBuf,
45+
/// Optional disk model name
46+
pub model: Option<String>,
47+
/// Optional disk vendor name
48+
pub vendor: Option<String>,
1949
}
2050

2151
impl Disk {
22-
fn from_sysfs_block_name(name: impl AsRef<str>) -> Self {
23-
let name = name.as_ref().to_owned();
24-
let entry = PathBuf::from(SYSFS_DIR).join(&name);
25-
26-
// Determine number of blocks
27-
let block_file = entry.join("size");
28-
let sectors = fs::read_to_string(block_file)
29-
.ok()
30-
.and_then(|s| s.trim().parse::<u64>().ok())
31-
.unwrap_or(0);
32-
33-
Self { name, sectors }
52+
/// Returns the name of the disk device.
53+
///
54+
/// # Examples
55+
///
56+
/// ```
57+
/// // Returns strings like "sda" or "nvme0n1"
58+
/// let name = disk.name();
59+
/// ```
60+
pub fn name(&self) -> &str {
61+
match self {
62+
Disk::Scsi(disk) => disk.name(),
63+
Disk::Nvme(disk) => disk.name(),
64+
}
65+
}
66+
}
67+
68+
/// Trait for initializing different types of disk devices from sysfs.
69+
pub(crate) trait DiskInit: Sized {
70+
/// Creates a new disk instance by reading information from the specified sysfs path.
71+
///
72+
/// # Arguments
73+
///
74+
/// * `root` - The root sysfs directory path
75+
/// * `name` - The name of the disk device
76+
///
77+
/// # Returns
78+
///
79+
/// `Some(Self)` if the disk was successfully initialized, `None` otherwise
80+
fn from_sysfs_path(root: &Path, name: &str) -> Option<Self>;
81+
}
82+
83+
impl DiskInit for BasicDisk {
84+
fn from_sysfs_path(sysroot: &Path, name: &str) -> Option<Self> {
85+
let node = sysroot.join(name);
86+
Some(Self {
87+
name: name.to_owned(),
88+
sectors: sysfs::sysfs_read(sysroot, &node, "size").unwrap_or(0),
89+
device: PathBuf::from(DEVFS_DIR).join(name),
90+
model: sysfs::sysfs_read(sysroot, &node, "device/model"),
91+
vendor: sysfs::sysfs_read(sysroot, &node, "device/vendor"),
92+
node,
93+
})
3494
}
95+
}
96+
97+
impl BlockDevice {
98+
/// Discovers all block devices present in the system.
99+
///
100+
/// # Returns
101+
///
102+
/// A vector of discovered block devices or an IO error if the discovery fails.
103+
///
104+
/// # Examples
105+
///
106+
/// ```
107+
/// let devices = BlockDevice::discover()?;
108+
/// for device in devices {
109+
/// println!("Found device: {:?}", device);
110+
/// }
111+
/// ```
112+
pub fn discover() -> io::Result<Vec<BlockDevice>> {
113+
Self::discover_in_sysroot("/")
114+
}
115+
116+
/// Discovers block devices in a specified sysroot directory.
117+
///
118+
/// # Arguments
119+
///
120+
/// * `sysroot` - Path to the system root directory
121+
///
122+
/// # Returns
123+
///
124+
/// A vector of discovered block devices or an IO error if the discovery fails.
125+
pub fn discover_in_sysroot(sysroot: impl AsRef<str>) -> io::Result<Vec<BlockDevice>> {
126+
let sysroot = sysroot.as_ref();
127+
let sysfs_dir = PathBuf::from(sysroot).join(SYSFS_DIR);
128+
let mut devices = Vec::new();
129+
130+
// Iterate over all block devices in sysfs and collect their filenames
131+
let entries = fs::read_dir(&sysfs_dir)?
132+
.filter_map(Result::ok)
133+
.filter_map(|e| Some(e.file_name().to_str()?.to_owned()));
134+
135+
// For all the discovered block devices, try to create a Disk instance
136+
// At this point we completely ignore partitions. They come later.
137+
for entry in entries {
138+
let disk = if let Some(disk) = scsi::Disk::from_sysfs_path(&sysfs_dir, &entry) {
139+
Disk::Scsi(disk)
140+
} else if let Some(disk) = nvme::Disk::from_sysfs_path(&sysfs_dir, &entry) {
141+
Disk::Nvme(disk)
142+
} else {
143+
continue;
144+
};
145+
146+
devices.push(BlockDevice::Disk(Box::new(disk)));
147+
}
148+
149+
Ok(devices)
150+
}
151+
}
152+
153+
#[cfg(test)]
154+
mod tests {
155+
use super::*;
35156

36-
/// Return usable size
37-
/// TODO: Grab the block size from the system. We know Linux is built on 512s though.
38-
pub fn size_in_bytes(&self) -> u64 {
39-
self.sectors * 512
157+
#[test]
158+
fn test_discover() {
159+
let devices = BlockDevice::discover().unwrap();
160+
assert!(!devices.is_empty());
161+
eprintln!("devices: {devices:?}");
40162
}
41163
}

crates/disks/src/nvme.rs

Lines changed: 40 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -4,47 +4,52 @@
44

55
//! NVME device enumeration and handling
66
//!
7-
//! This module provides functionality to enumerate and handle NVME devices.
8-
9-
use std::{fs, io};
7+
//! This module provides functionality to enumerate and handle NVMe (Non-Volatile Memory Express)
8+
//! storage devices by parsing sysfs paths and device names.
109
10+
use std::{path::Path, sync::OnceLock};
1111
use regex::Regex;
12+
use crate::{BasicDisk, DiskInit};
1213

13-
use crate::{Disk, SYSFS_DIR};
14-
15-
pub fn enumerate() -> io::Result<Vec<Disk>> {
16-
// Filter for NVME block devices in format nvmeXnY where X and Y are digits
17-
// Exclude partitions (nvmeXnYpZ) and character devices
18-
let nvme_pattern = Regex::new(r"^nvme\d+n\d+$").unwrap();
14+
/// Regex pattern to match valid NVMe device names (e.g. nvme0n1)
15+
static NVME_PATTERN: OnceLock<Regex> = OnceLock::new();
1916

20-
let items = fs::read_dir(SYSFS_DIR)?
21-
.filter_map(Result::ok)
22-
.filter_map(|e| Some(e.file_name().to_str()?.to_owned()))
23-
.filter(|name| nvme_pattern.is_match(name))
24-
.map(Disk::from_sysfs_block_name)
25-
.collect();
26-
Ok(items)
17+
/// Represents an NVMe disk device
18+
#[derive(Debug)]
19+
pub struct Disk {
20+
/// The underlying basic disk implementation
21+
disk: BasicDisk,
2722
}
2823

29-
#[cfg(test)]
30-
mod tests {
31-
use super::*;
32-
33-
#[test]
34-
fn test_enumerate() {
35-
let devices = enumerate().expect("failed to collect nvme disks");
36-
eprintln!("nvme devices: {devices:?}");
37-
for device in devices.iter() {
38-
let mut size = device.size_in_bytes() as f64;
39-
size /= 1024.0 * 1024.0 * 1024.0;
40-
// Cheeky emulation of `fdisk -l` output
41-
eprintln!(
42-
"Disk /dev/{}: {:.2} GiB, {} bytes, {} sectors",
43-
device.name,
44-
size,
45-
device.size_in_bytes(),
46-
device.sectors
47-
);
24+
impl DiskInit for Disk {
25+
/// Creates a new NVMe disk from a sysfs path and device name
26+
///
27+
/// # Arguments
28+
/// * `sysroot` - The sysfs root path
29+
/// * `name` - The device name to check
30+
///
31+
/// # Returns
32+
/// * `Some(Disk)` if the device name matches NVMe pattern
33+
/// * `None` if name doesn't match or basic disk creation fails
34+
fn from_sysfs_path(sysroot: &Path, name: &str) -> Option<Self> {
35+
let regex = NVME_PATTERN
36+
.get_or_init(|| Regex::new(r"^nvme\d+n\d+$").expect("Failed to initialise known-working regex"));
37+
if regex.is_match(name) {
38+
Some(Self {
39+
disk: BasicDisk::from_sysfs_path(sysroot, name)?,
40+
})
41+
} else {
42+
None
4843
}
4944
}
5045
}
46+
47+
impl Disk {
48+
/// Returns the name of the NVMe disk (e.g. "nvme0n1")
49+
///
50+
/// # Returns
51+
/// * A string slice containing the disk name
52+
pub fn name(&self) -> &str {
53+
&self.disk.name
54+
}
55+
}

crates/disks/src/scsi.rs

Lines changed: 43 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,55 @@
22
//
33
// SPDX-License-Identifier: MPL-2.0
44

5-
//! SCSI device enumeration and handling
5+
//! SCSI device enumeration and handling.
66
//!
7-
//! OK. Not quite true. Per modern conventions, all libata devices are also considered SCSI devices.
8-
//! This means all `/dev/sd*` devices.
7+
//! In modern Linux systems, all libata devices are exposed as SCSI devices through
8+
//! the SCSI subsystem. This module handles enumeration and management of these devices,
9+
//! which appear as `/dev/sd*` block devices.
910
10-
use std::{fs, io};
11+
use std::path::Path;
1112

12-
use crate::{Disk, SYSFS_DIR};
13+
use crate::{BasicDisk, DiskInit};
1314

14-
pub fn enumerate() -> io::Result<Vec<Disk>> {
15-
// Filtered list of SCSI devices whose paths begin with "sd" but not ending with a digit
16-
let items = fs::read_dir(SYSFS_DIR)?
17-
.filter_map(Result::ok)
18-
.filter_map(|e| Some(e.file_name().to_str()?.to_owned()))
19-
.filter(|e| e.starts_with("sd") && e[2..].chars().all(char::is_alphabetic))
20-
.map(Disk::from_sysfs_block_name)
21-
.collect();
22-
Ok(items)
15+
/// Represents a SCSI disk device.
16+
///
17+
/// This struct wraps a BasicDisk to provide SCSI-specific functionality.
18+
#[derive(Debug)]
19+
pub struct Disk {
20+
disk: BasicDisk,
2321
}
2422

25-
#[cfg(test)]
26-
mod tests {
27-
use super::*;
23+
impl DiskInit for Disk {
24+
/// Creates a new Disk instance from a sysfs path if the device name matches SCSI naming pattern.
25+
///
26+
/// # Arguments
27+
///
28+
/// * `sysroot` - The root path of the sysfs filesystem
29+
/// * `name` - The device name to check (e.g. "sda", "sdb")
30+
///
31+
/// # Returns
32+
///
33+
/// * `Some(Disk)` if the name matches SCSI pattern (starts with "sd" followed by letters)
34+
/// * `None` if the name doesn't match or the device can't be initialized
35+
fn from_sysfs_path(sysroot: &Path, name: &str) -> Option<Self> {
36+
let matching = name.starts_with("sd") && name[2..].chars().all(char::is_alphabetic);
37+
if matching {
38+
Some(Self {
39+
disk: BasicDisk::from_sysfs_path(sysroot, name)?,
40+
})
41+
} else {
42+
None
43+
}
44+
}
45+
}
2846

29-
#[test]
30-
fn test_enumerate() {
31-
let devices = enumerate().expect("Failed to enumerate SCSI devices");
32-
eprintln!("scsi devices: {devices:?}");
47+
impl Disk {
48+
/// Returns the name of the disk device.
49+
///
50+
/// # Returns
51+
///
52+
/// The device name (e.g. "sda", "sdb")
53+
pub fn name(&self) -> &str {
54+
&self.disk.name
3355
}
3456
}

crates/disks/src/sysfs.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// SPDX-FileCopyrightText: Copyright © 2025 Serpent OS Developers
2+
//
3+
// SPDX-License-Identifier: MPL-2.0
4+
5+
//! Helper functions for interacting with Linux sysfs interfaces
6+
7+
use std::{fs, path::Path, str::FromStr};
8+
9+
/// Reads a value from a sysfs node and attempts to parse it to type T
10+
///
11+
/// # Arguments
12+
///
13+
/// * `sysroot` - Base path of the sysfs mount point
14+
/// * `node` - Path to specific sysfs node relative to sysroot
15+
/// * `key` - Name of the sysfs attribute to read
16+
///
17+
/// # Returns
18+
///
19+
/// * `Some(T)` if the value was successfully read and parsed
20+
/// * `None` if the file could not be read or parsed
21+
///
22+
/// # Type Parameters
23+
///
24+
/// * `T` - Target type that implements FromStr for parsing the raw value
25+
pub(crate) fn sysfs_read<T>(sysroot: &Path, node: &Path, key: &str) -> Option<T>
26+
where
27+
T: FromStr,
28+
{
29+
let path = sysroot.join(node).join(key);
30+
fs::read_to_string(&path).ok()?.trim().parse().ok()
31+
}

0 commit comments

Comments
 (0)