use super::{Partition, PhysicalDisk};
const SYSFS_SECTOR: u64 = 512;
pub(super) struct RawDisk {
pub name: String,
pub size: String,
pub removable: String,
pub ro: String,
pub logical: String,
pub physical: String,
pub model: Option<String>,
pub partitions: Vec<RawPart>,
}
pub(super) struct RawPart {
pub name: String,
pub size: String,
pub start: String,
}
pub(super) fn parse_mounts(content: &str) -> Vec<(String, String, String)> {
content
.lines()
.filter_map(|line| {
let mut f = line.split_whitespace();
let dev = f.next()?.strip_prefix("/dev/")?;
let mount = f.next()?;
let fs = f.next()?;
Some((dev.to_string(), decode_octal(mount), fs.to_string()))
})
.collect()
}
pub(super) fn build(disks: Vec<RawDisk>, mounts_content: &str) -> Vec<PhysicalDisk> {
let mounts = parse_mounts(mounts_content);
let lookup = |name: &str| {
mounts
.iter()
.find(|(dev, _, _)| dev == name)
.map(|(_, mp, fs)| (mp.clone(), fs.clone()))
};
let mut out: Vec<PhysicalDisk> = disks
.into_iter()
.map(|d| {
let logical = match parse_u64(&d.logical) as u32 {
0 => 512,
n => n,
};
let physical = match parse_u64(&d.physical) as u32 {
0 => logical,
n => n,
};
let mut partitions: Vec<Partition> = d
.partitions
.iter()
.map(|p| {
let (mount_point, filesystem) = match lookup(&p.name) {
Some((mp, fs)) => (Some(mp), Some(fs)),
None => (None, None),
};
Partition {
device_path: format!("/dev/{}", p.name),
name: p.name.clone(),
start_offset: parse_u64(&p.start) * SYSFS_SECTOR,
size_bytes: parse_u64(&p.size) * SYSFS_SECTOR,
partition_type: None,
mount_point,
filesystem,
label: None,
}
})
.collect();
partitions.sort_by_key(|p| p.start_offset);
PhysicalDisk {
device_path: format!("/dev/{}", d.name),
name: d.name.clone(),
size_bytes: parse_u64(&d.size) * SYSFS_SECTOR,
logical_sector_size: logical,
physical_sector_size: physical,
model: d
.model
.as_deref()
.map(str::trim)
.filter(|m| !m.is_empty())
.map(str::to_string),
serial: None,
removable: parse_u64(&d.removable) == 1,
read_only: parse_u64(&d.ro) == 1,
synthesized: d.name.starts_with("dm-"),
partitions,
}
})
.collect();
out.sort_by(|a, b| a.name.cmp(&b.name));
out
}
fn parse_u64(s: &str) -> u64 {
s.trim().parse().unwrap_or(0)
}
fn decode_octal(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
let decoded = if c == '\\' {
let digits: String = chars.clone().take(3).collect();
if digits.len() == 3 && digits.chars().all(|d| ('0'..='7').contains(&d)) {
u8::from_str_radix(&digits, 8).ok()
} else {
None
}
} else {
None
};
match decoded {
Some(byte) => {
out.push(byte as char);
for _ in 0..3 {
chars.next();
}
}
None => out.push(c),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn rd(name: &str, size: &str, parts: Vec<RawPart>) -> RawDisk {
RawDisk {
name: name.into(),
size: size.into(),
removable: "0".into(),
ro: "0".into(),
logical: "512".into(),
physical: "4096".into(),
model: Some(" Samsung SSD 990 ".into()),
partitions: parts,
}
}
fn rp(name: &str, start: &str, size: &str) -> RawPart {
RawPart {
name: name.into(),
start: start.into(),
size: size.into(),
}
}
#[test]
fn parse_mounts_keeps_dev_mounts_and_decodes_spaces() {
let content = "\
sysfs /sys sysfs rw 0 0
/dev/sda1 / ext4 rw 0 0
proc /proc proc rw 0 0
/dev/sdb1 /mnt/my\\040disk xfs rw 0 0
";
let m = parse_mounts(content);
assert_eq!(
m,
vec![
("sda1".into(), "/".into(), "ext4".into()),
("sdb1".into(), "/mnt/my disk".into(), "xfs".into()),
]
);
}
#[test]
fn build_converts_sectors_to_bytes_and_attaches_mounts() {
let disks = vec![rd("sda", "2048", vec![rp("sda1", "2048", "1024")])];
let out = build(disks, "/dev/sda1 /boot vfat rw 0 0\n");
assert_eq!(out.len(), 1);
let d = &out[0];
assert_eq!(d.device_path, "/dev/sda");
assert_eq!(d.size_bytes, 2048 * 512);
assert_eq!(d.logical_sector_size, 512);
assert_eq!(d.physical_sector_size, 4096);
assert_eq!(d.model.as_deref(), Some("Samsung SSD 990")); assert_eq!(d.partitions.len(), 1);
let p = &d.partitions[0];
assert_eq!(p.start_offset, 2048 * 512);
assert_eq!(p.size_bytes, 1024 * 512);
assert_eq!(p.mount_point.as_deref(), Some("/boot"));
assert_eq!(p.filesystem.as_deref(), Some("vfat"));
}
#[test]
fn build_sorts_partitions_by_offset() {
let disks = vec![rd(
"nvme0n1",
"100000",
vec![
rp("nvme0n1p2", "50000", "10"),
rp("nvme0n1p1", "2048", "10"),
],
)];
let out = build(disks, "");
let names: Vec<&str> = out[0].partitions.iter().map(|p| p.name.as_str()).collect();
assert_eq!(names, ["nvme0n1p1", "nvme0n1p2"]);
}
#[test]
fn build_flags_removable_ro_and_dm_synthesized() {
let mut d = rd("dm-0", "100", vec![]);
d.removable = "1".into();
d.ro = "1".into();
let out = build(vec![d], "");
assert!(out[0].removable);
assert!(out[0].read_only);
assert!(out[0].synthesized);
}
#[test]
fn build_defaults_physical_to_logical_when_zero() {
let mut d = rd("sda", "100", vec![]);
d.physical = "0".into();
d.logical = "512".into();
let out = build(vec![d], "");
assert_eq!(out[0].physical_sector_size, 512);
}
#[test]
fn build_defaults_logical_to_512_when_zero() {
let mut d = rd("sda", "100", vec![]);
d.logical = "0".into();
let out = build(vec![d], "");
assert_eq!(out[0].logical_sector_size, 512);
}
#[test]
fn parse_mounts_keeps_non_octal_backslash_literal() {
let m = parse_mounts("/dev/sda1 /mnt/a\\xb ext4 rw 0 0\n");
assert_eq!(m[0].1, "/mnt/a\\xb");
}
}