partition_identity/
lib.rs

1//! Find the ID of a device by its path, or find a device path by its ID.
2
3use thiserror::Error;
4use self::PartitionSource::Path as SourcePath;
5use self::PartitionSource::*;
6use std::borrow::Cow;
7use std::fmt::{self, Display, Formatter};
8use std::fs;
9use std::path::{Path, PathBuf};
10use std::str::FromStr;
11
12#[derive(Debug, Error, Hash, Eq, PartialEq)]
13pub enum Error {
14    #[error("the partition ID key was invalid")]
15    InvalidKey,
16    #[error("the provided path was not valid in this context")]
17    InvalidPath,
18    #[error("the provided `/dev/disk/by-` path was not supported")]
19    UnknownByPath,
20}
21
22/// Describes a partition identity.
23///
24/// A device path may be recovered from this.
25///
26/// # Notes
27///
28/// This is a struct instead of an enum to make access to the `id` string
29/// easier for situations where the variant does not need to be checked.
30#[derive(Clone, Debug, Hash, Eq, PartialEq)]
31pub struct PartitionID {
32    pub variant: PartitionSource,
33    pub id: String,
34}
35
36impl PartitionID {
37    /// Construct a new `PartitionID` as the given source.
38    pub fn new(variant: PartitionSource, id: String) -> Self {
39        Self { variant, id }
40    }
41
42    /// Construct a new `PartitionID` as a `ID` source.
43    pub fn new_id(id: String) -> Self {
44        Self::new(ID, id)
45    }
46
47    /// Construct a new `PartitionID` as a `Label` source.
48    pub fn new_label(id: String) -> Self {
49        Self::new(Label, id)
50    }
51
52    /// Construct a new `PartitionID` as a `UUID` source.
53    pub fn new_uuid(id: String) -> Self {
54        Self::new(UUID, id)
55    }
56
57    /// Construct a new `PartitionID` as a `PartLabel` source.
58    pub fn new_partlabel(id: String) -> Self {
59        Self::new(PartLabel, id)
60    }
61
62    /// Construct a new `PartitionID` as a `PartUUID` source.
63    pub fn new_partuuid(id: String) -> Self {
64        Self::new(PartUUID, id)
65    }
66
67    /// Construct a new `PartitionID` as a `Path` source.
68    pub fn new_path(id: String) -> Self {
69        Self::new(SourcePath, id)
70    }
71
72    /// Find the device path of this ID.
73    pub fn get_device_path(&self) -> Option<PathBuf> {
74        if self.variant == PartitionSource::Path && self.id.starts_with("/") {
75            Some(PathBuf::from(&self.id))
76        } else {
77            from_id(&self.id, &self.variant.disk_by_path())
78        }
79    }
80
81    /// Find the given source ID of the device at the given path.
82    pub fn get_source<P: AsRef<Path>>(variant: PartitionSource, path: P) -> Option<Self> {
83        Some(Self { variant, id: find_id(path.as_ref(), &variant.disk_by_path())? })
84    }
85
86    /// Find the UUID of the device at the given path.
87    pub fn get_uuid<P: AsRef<Path>>(path: P) -> Option<Self> {
88        Self::get_source(UUID, path)
89    }
90
91    /// Find the PARTUUID of the device at the given path.
92    pub fn get_partuuid<P: AsRef<Path>>(path: P) -> Option<Self> {
93        Self::get_source(PartUUID, path)
94    }
95
96    /// Fetch a partition ID by a `/dev/disk/by-` path.
97    pub fn from_disk_by_path<S: AsRef<str>>(path: S) -> Result<Self, Error> {
98        let path = path.as_ref();
99
100        let path = if path.starts_with("/dev/disk/by-") {
101            &path[13..]
102        } else {
103            return Err(Error::InvalidPath);
104        };
105
106        let id = if path.starts_with("id/") {
107            Self::new(ID, path[3..].into())
108        } else if path.starts_with("label/") {
109            Self::new(Label, path[6..].into())
110        } else if path.starts_with("partlabel/") {
111            Self::new(PartLabel, path[10..].into())
112        } else if path.starts_with("partuuid/") {
113            Self::new(PartUUID, path[9..].into())
114        } else if path.starts_with("path/") {
115            Self::new(Path, path[5..].into())
116        } else if path.starts_with("uuid/") {
117            Self::new(UUID, path[5..].into())
118        } else {
119            return Err(Error::UnknownByPath);
120        };
121
122        Ok(id)
123    }
124}
125
126impl Display for PartitionID {
127    fn fmt(&self, fmt: &mut Formatter) -> fmt::Result {
128        if let PartitionSource::Path = self.variant {
129            write!(fmt, "{}", self.id)
130        } else {
131            write!(fmt, "{}={}", <&'static str>::from(self.variant), self.id)
132        }
133    }
134}
135
136impl FromStr for PartitionID {
137    type Err = Error;
138
139    fn from_str(input: &str) -> Result<Self, Self::Err> {
140        if input.starts_with('/') {
141            Ok(PartitionID { variant: SourcePath, id: input.to_owned() })
142        } else if input.starts_with("ID=") {
143            Ok(PartitionID { variant: ID, id: input[3..].to_owned() })
144        } else if input.starts_with("LABEL=") {
145            Ok(PartitionID { variant: Label, id: input[6..].to_owned() })
146        } else if input.starts_with("PARTLABEL=") {
147            Ok(PartitionID { variant: PartLabel, id: input[10..].to_owned() })
148        } else if input.starts_with("PARTUUID=") {
149            Ok(PartitionID { variant: PartUUID, id: input[9..].to_owned() })
150        } else if input.starts_with("UUID=") {
151            Ok(PartitionID { variant: UUID, id: input[5..].to_owned() })
152        } else {
153            Err(Error::InvalidKey)
154        }
155    }
156}
157
158/// Describes the type of partition identity.
159#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
160pub enum PartitionSource {
161    ID,
162    Label,
163    PartLabel,
164    PartUUID,
165    Path,
166    UUID,
167}
168
169impl Display for PartitionSource {
170    fn fmt(&self, fmt: &mut Formatter) -> fmt::Result {
171        write!(fmt, "{}", <&'static str>::from(*self))
172    }
173}
174
175impl From<PartitionSource> for &'static str {
176    fn from(pid: PartitionSource) -> &'static str {
177        match pid {
178            PartitionSource::ID => "ID",
179            PartitionSource::Label => "LABEL",
180            PartitionSource::PartLabel => "PARTLABEL",
181            PartitionSource::PartUUID => "PARTUUID",
182            PartitionSource::Path => "PATH",
183            PartitionSource::UUID => "UUID",
184        }
185    }
186}
187
188impl PartitionSource {
189    fn disk_by_path(self) -> PathBuf {
190        PathBuf::from(["/dev/disk/by-", &<&'static str>::from(self).to_lowercase()].concat())
191    }
192}
193
194/// A collection of all discoverable identifiers for a partition.
195#[derive(Debug, Default, Clone, Hash, PartialEq)]
196pub struct PartitionIdentifiers {
197    pub id: Option<String>,
198    pub label: Option<String>,
199    pub part_label: Option<String>,
200    pub part_uuid: Option<String>,
201    pub path: Option<String>,
202    pub uuid: Option<String>,
203}
204
205impl PartitionIdentifiers {
206    /// Fetches all discoverable identifiers for a partition by the path to that partition.
207    pub fn from_path<P: AsRef<Path>>(path: P) -> PartitionIdentifiers {
208        let path = path.as_ref();
209
210        PartitionIdentifiers {
211            path: PartitionID::get_source(SourcePath, path).map(|id| id.id),
212            id: PartitionID::get_source(ID, path).map(|id| id.id),
213            label: PartitionID::get_source(Label, path).map(|id| id.id),
214            part_label: PartitionID::get_source(PartLabel, path).map(|id| id.id),
215            part_uuid: PartitionID::get_source(PartUUID, path).map(|id| id.id),
216            uuid: PartitionID::get_source(UUID, path).map(|id| id.id),
217        }
218    }
219
220    /// Checks if the given identity matches one of the available identifiers.
221    pub fn matches(&self, id: &PartitionID) -> bool {
222        match id.variant {
223            ID => self.id.as_ref().map_or(false, |s| &id.id == s),
224            Label => self.label.as_ref().map_or(false, |s| &id.id == s),
225            PartLabel => self.part_label.as_ref().map_or(false, |s| &id.id == s),
226            PartUUID => self.part_uuid.as_ref().map_or(false, |s| &id.id == s),
227            SourcePath => self.path.as_ref().map_or(false, |s| &id.id == s),
228            UUID => self.uuid.as_ref().map_or(false, |s| &id.id == s),
229        }
230    }
231}
232
233fn attempt<T, F: FnMut() -> Option<T>>(attempts: u8, wait: u64, mut func: F) -> Option<T> {
234    let mut tried = 0;
235    let mut result;
236
237    loop {
238        result = func();
239        if result.is_none() && tried != attempts {
240            ::std::thread::sleep(::std::time::Duration::from_millis(wait));
241            tried += 1;
242        } else {
243            return result;
244        }
245    }
246}
247
248fn canonicalize<'a>(path: &'a Path) -> Cow<'a, Path> {
249    // NOTE: It seems that the kernel may intermittently error.
250    match attempt::<PathBuf, _>(10, 1, || path.canonicalize().ok()) {
251        Some(path) => Cow::Owned(path),
252        None => Cow::Borrowed(path),
253    }
254}
255
256/// Attempts to find the ID from the given path.
257fn find_id(path: &Path, uuid_dir: &Path) -> Option<String> {
258    // NOTE: It seems that the kernel may sometimes intermittently skip directories.
259    attempt(10, 1, move || {
260        let dir = uuid_dir.read_dir().ok()?;
261        find_id_(path, dir)
262    })
263}
264
265fn from_id(uuid: &str, uuid_dir: &Path) -> Option<PathBuf> {
266    // NOTE: It seems that the kernel may sometimes intermittently skip directories.
267    attempt(10, 1, move || {
268        let dir = uuid_dir.read_dir().ok()?;
269        from_id_(uuid, dir)
270    })
271}
272
273fn find_id_(path: &Path, uuid_dir: fs::ReadDir) -> Option<String> {
274    let path = canonicalize(path);
275    for uuid_entry in uuid_dir.filter_map(|entry| entry.ok()) {
276        let uuid_path = uuid_entry.path();
277        let uuid_path = canonicalize(&uuid_path);
278        if &uuid_path == &path {
279            if let Some(uuid_entry) = uuid_entry.file_name().to_str() {
280                return Some(uuid_entry.into());
281            }
282        }
283    }
284
285    None
286}
287
288fn from_id_(uuid: &str, uuid_dir: fs::ReadDir) -> Option<PathBuf> {
289    for uuid_entry in uuid_dir.filter_map(|entry| entry.ok()) {
290        let uuid_entry = uuid_entry.path();
291        if let Some(name) = uuid_entry.file_name() {
292            if name == uuid {
293                if let Ok(uuid_entry) = uuid_entry.canonicalize() {
294                    return Some(uuid_entry);
295                }
296            }
297        }
298    }
299
300    None
301}
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306
307    #[test]
308    fn partition_id_from_str() {
309        assert_eq!(
310            "/dev/sda1".parse::<PartitionID>(),
311            Ok(PartitionID::new_path("/dev/sda1".into()))
312        );
313
314        assert_eq!("ID=abcd".parse::<PartitionID>(), Ok(PartitionID::new_id("abcd".into())));
315
316        assert_eq!("LABEL=abcd".parse::<PartitionID>(), Ok(PartitionID::new_label("abcd".into())));
317
318        assert_eq!(
319            "PARTLABEL=abcd".parse::<PartitionID>(),
320            Ok(PartitionID::new_partlabel("abcd".into()))
321        );
322
323        assert_eq!(
324            "PARTUUID=abcd".parse::<PartitionID>(),
325            Ok(PartitionID::new_partuuid("abcd".into()))
326        );
327
328        assert_eq!("UUID=abcd".parse::<PartitionID>(), Ok(PartitionID::new_uuid("abcd".into())));
329    }
330}