1use 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#[derive(Clone, Debug, Hash, Eq, PartialEq)]
31pub struct PartitionID {
32 pub variant: PartitionSource,
33 pub id: String,
34}
35
36impl PartitionID {
37 pub fn new(variant: PartitionSource, id: String) -> Self {
39 Self { variant, id }
40 }
41
42 pub fn new_id(id: String) -> Self {
44 Self::new(ID, id)
45 }
46
47 pub fn new_label(id: String) -> Self {
49 Self::new(Label, id)
50 }
51
52 pub fn new_uuid(id: String) -> Self {
54 Self::new(UUID, id)
55 }
56
57 pub fn new_partlabel(id: String) -> Self {
59 Self::new(PartLabel, id)
60 }
61
62 pub fn new_partuuid(id: String) -> Self {
64 Self::new(PartUUID, id)
65 }
66
67 pub fn new_path(id: String) -> Self {
69 Self::new(SourcePath, id)
70 }
71
72 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 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 pub fn get_uuid<P: AsRef<Path>>(path: P) -> Option<Self> {
88 Self::get_source(UUID, path)
89 }
90
91 pub fn get_partuuid<P: AsRef<Path>>(path: P) -> Option<Self> {
93 Self::get_source(PartUUID, path)
94 }
95
96 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#[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#[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 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 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 match attempt::<PathBuf, _>(10, 1, || path.canonicalize().ok()) {
251 Some(path) => Cow::Owned(path),
252 None => Cow::Borrowed(path),
253 }
254}
255
256fn find_id(path: &Path, uuid_dir: &Path) -> Option<String> {
258 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 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}