Skip to content

Commit

Permalink
disks: Total revamp, disks is now primarily a sysfs walker
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
ikeycode committed Jan 20, 2025
1 parent 4a17907 commit 7d0601d
Show file tree
Hide file tree
Showing 4 changed files with 257 additions and 77 deletions.
164 changes: 143 additions & 21 deletions crates/disks/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,162 @@
//
// SPDX-License-Identifier: MPL-2.0

use std::{fs, path::PathBuf};
use std::{
fs, io,
path::{Path, PathBuf},
};

pub mod nvme;
pub mod scsi;
mod sysfs;

const SYSFS_DIR: &str = "/sys/class/block";
const DEVFS_DIR: &str = "/dev";

/// A block device on the system which can be either a physical disk or a partition.
#[derive(Debug)]
pub struct Disk {
/// Partial-name, ie "sda"
pub name: String,
pub enum BlockDevice {
/// A physical disk device
Disk(Box<Disk>),
}

/// Represents the type of disk device.
#[derive(Debug)]
pub enum Disk {
/// SCSI disk device (e.g. sda, sdb)
Scsi(scsi::Disk),
/// NVMe disk device (e.g. nvme0n1)
Nvme(nvme::Disk),
}

// Number of sectors (* 512 sector size for data size)
/// A basic disk representation containing common attributes shared by all disk types.
/// This serves as the base structure that specific disk implementations build upon.
#[derive(Debug)]
pub struct BasicDisk {
/// Device name (e.g. sda, nvme0n1)
pub name: String,
/// Total number of sectors on the disk
pub sectors: u64,
/// Path to the device in sysfs
pub node: PathBuf,
/// Path to the device in /dev
pub device: PathBuf,
/// Optional disk model name
pub model: Option<String>,
/// Optional disk vendor name
pub vendor: Option<String>,
}

impl Disk {
fn from_sysfs_block_name(name: impl AsRef<str>) -> Self {
let name = name.as_ref().to_owned();
let entry = PathBuf::from(SYSFS_DIR).join(&name);

// Determine number of blocks
let block_file = entry.join("size");
let sectors = fs::read_to_string(block_file)
.ok()
.and_then(|s| s.trim().parse::<u64>().ok())
.unwrap_or(0);

Self { name, sectors }
/// Returns the name of the disk device.
///
/// # Examples
///
/// ```
/// // Returns strings like "sda" or "nvme0n1"
/// let name = disk.name();
/// ```
pub fn name(&self) -> &str {
match self {
Disk::Scsi(disk) => disk.name(),
Disk::Nvme(disk) => disk.name(),
}
}
}

/// Trait for initializing different types of disk devices from sysfs.
pub(crate) trait DiskInit: Sized {
/// Creates a new disk instance by reading information from the specified sysfs path.
///
/// # Arguments
///
/// * `root` - The root sysfs directory path
/// * `name` - The name of the disk device
///
/// # Returns
///
/// `Some(Self)` if the disk was successfully initialized, `None` otherwise
fn from_sysfs_path(root: &Path, name: &str) -> Option<Self>;
}

impl DiskInit for BasicDisk {
fn from_sysfs_path(sysroot: &Path, name: &str) -> Option<Self> {
let node = sysroot.join(name);
Some(Self {
name: name.to_owned(),
sectors: sysfs::sysfs_read(sysroot, &node, "size").unwrap_or(0),
device: PathBuf::from(DEVFS_DIR).join(name),
model: sysfs::sysfs_read(sysroot, &node, "device/model"),
vendor: sysfs::sysfs_read(sysroot, &node, "device/vendor"),
node,
})
}
}

impl BlockDevice {
/// Discovers all block devices present in the system.
///
/// # Returns
///
/// A vector of discovered block devices or an IO error if the discovery fails.
///
/// # Examples
///
/// ```
/// let devices = BlockDevice::discover()?;
/// for device in devices {
/// println!("Found device: {:?}", device);
/// }
/// ```
pub fn discover() -> io::Result<Vec<BlockDevice>> {
Self::discover_in_sysroot("/")
}

/// Discovers block devices in a specified sysroot directory.
///
/// # Arguments
///
/// * `sysroot` - Path to the system root directory
///
/// # Returns
///
/// A vector of discovered block devices or an IO error if the discovery fails.
pub fn discover_in_sysroot(sysroot: impl AsRef<str>) -> io::Result<Vec<BlockDevice>> {
let sysroot = sysroot.as_ref();
let sysfs_dir = PathBuf::from(sysroot).join(SYSFS_DIR);
let mut devices = Vec::new();

// Iterate over all block devices in sysfs and collect their filenames
let entries = fs::read_dir(&sysfs_dir)?
.filter_map(Result::ok)
.filter_map(|e| Some(e.file_name().to_str()?.to_owned()));

// For all the discovered block devices, try to create a Disk instance
// At this point we completely ignore partitions. They come later.
for entry in entries {
let disk = if let Some(disk) = scsi::Disk::from_sysfs_path(&sysfs_dir, &entry) {
Disk::Scsi(disk)
} else if let Some(disk) = nvme::Disk::from_sysfs_path(&sysfs_dir, &entry) {
Disk::Nvme(disk)
} else {
continue;
};

devices.push(BlockDevice::Disk(Box::new(disk)));
}

Ok(devices)
}
}

#[cfg(test)]
mod tests {
use super::*;

/// Return usable size
/// TODO: Grab the block size from the system. We know Linux is built on 512s though.
pub fn size_in_bytes(&self) -> u64 {
self.sectors * 512
#[test]
fn test_discover() {
let devices = BlockDevice::discover().unwrap();
assert!(!devices.is_empty());
eprintln!("devices: {devices:?}");
}
}
75 changes: 40 additions & 35 deletions crates/disks/src/nvme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,47 +4,52 @@

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

use crate::{Disk, SYSFS_DIR};

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

let items = fs::read_dir(SYSFS_DIR)?
.filter_map(Result::ok)
.filter_map(|e| Some(e.file_name().to_str()?.to_owned()))
.filter(|name| nvme_pattern.is_match(name))
.map(Disk::from_sysfs_block_name)
.collect();
Ok(items)
/// Represents an NVMe disk device
#[derive(Debug)]
pub struct Disk {
/// The underlying basic disk implementation
disk: BasicDisk,
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_enumerate() {
let devices = enumerate().expect("failed to collect nvme disks");
eprintln!("nvme devices: {devices:?}");
for device in devices.iter() {
let mut size = device.size_in_bytes() as f64;
size /= 1024.0 * 1024.0 * 1024.0;
// Cheeky emulation of `fdisk -l` output
eprintln!(
"Disk /dev/{}: {:.2} GiB, {} bytes, {} sectors",
device.name,
size,
device.size_in_bytes(),
device.sectors
);
impl DiskInit for Disk {
/// Creates a new NVMe disk from a sysfs path and device name
///
/// # Arguments
/// * `sysroot` - The sysfs root path
/// * `name` - The device name to check
///
/// # Returns
/// * `Some(Disk)` if the device name matches NVMe pattern
/// * `None` if name doesn't match or basic disk creation fails
fn from_sysfs_path(sysroot: &Path, name: &str) -> Option<Self> {
let regex = NVME_PATTERN
.get_or_init(|| Regex::new(r"^nvme\d+n\d+$").expect("Failed to initialise known-working regex"));
if regex.is_match(name) {
Some(Self {
disk: BasicDisk::from_sysfs_path(sysroot, name)?,
})
} else {
None
}
}
}

impl Disk {
/// Returns the name of the NVMe disk (e.g. "nvme0n1")
///
/// # Returns
/// * A string slice containing the disk name
pub fn name(&self) -> &str {
&self.disk.name
}
}
64 changes: 43 additions & 21 deletions crates/disks/src/scsi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,55 @@
//
// SPDX-License-Identifier: MPL-2.0

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

use crate::{Disk, SYSFS_DIR};
use crate::{BasicDisk, DiskInit};

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

#[cfg(test)]
mod tests {
use super::*;
impl DiskInit for Disk {
/// Creates a new Disk instance from a sysfs path if the device name matches SCSI naming pattern.
///
/// # Arguments
///
/// * `sysroot` - The root path of the sysfs filesystem
/// * `name` - The device name to check (e.g. "sda", "sdb")
///
/// # Returns
///
/// * `Some(Disk)` if the name matches SCSI pattern (starts with "sd" followed by letters)
/// * `None` if the name doesn't match or the device can't be initialized
fn from_sysfs_path(sysroot: &Path, name: &str) -> Option<Self> {
let matching = name.starts_with("sd") && name[2..].chars().all(char::is_alphabetic);
if matching {
Some(Self {
disk: BasicDisk::from_sysfs_path(sysroot, name)?,
})
} else {
None
}
}
}

#[test]
fn test_enumerate() {
let devices = enumerate().expect("Failed to enumerate SCSI devices");
eprintln!("scsi devices: {devices:?}");
impl Disk {
/// Returns the name of the disk device.
///
/// # Returns
///
/// The device name (e.g. "sda", "sdb")
pub fn name(&self) -> &str {
&self.disk.name
}
}
31 changes: 31 additions & 0 deletions crates/disks/src/sysfs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// SPDX-FileCopyrightText: Copyright © 2025 Serpent OS Developers
//
// SPDX-License-Identifier: MPL-2.0

//! Helper functions for interacting with Linux sysfs interfaces
use std::{fs, path::Path, str::FromStr};

/// Reads a value from a sysfs node and attempts to parse it to type T
///
/// # Arguments
///
/// * `sysroot` - Base path of the sysfs mount point
/// * `node` - Path to specific sysfs node relative to sysroot
/// * `key` - Name of the sysfs attribute to read
///
/// # Returns
///
/// * `Some(T)` if the value was successfully read and parsed
/// * `None` if the file could not be read or parsed
///
/// # Type Parameters
///
/// * `T` - Target type that implements FromStr for parsing the raw value
pub(crate) fn sysfs_read<T>(sysroot: &Path, node: &Path, key: &str) -> Option<T>
where
T: FromStr,
{
let path = sysroot.join(node).join(key);
fs::read_to_string(&path).ok()?.trim().parse().ok()
}

0 comments on commit 7d0601d

Please sign in to comment.