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()
    }
}