1use std::collections::{HashMap, HashSet};
2use std::env;
3use std::path::Path;
4use std::process::{Command, Stdio};
5use std::sync::OnceLock;
6
7use anyhow::{Context, Result, anyhow};
8use camino::{Utf8Path, Utf8PathBuf};
9use cap_std_ext::cap_std::fs::Dir;
10use fn_error_context::context;
11use serde::Deserialize;
12
13use bootc_utils::CommandRunExt;
14
15fn have_udev() -> bool {
25 static HAVE_UDEV: OnceLock<bool> = OnceLock::new();
26 *HAVE_UDEV.get_or_init(|| {
27 let r = Path::new("/run/udev/data").exists();
28 if !r {
29 tracing::debug!(
30 "udev database not available, will use blkid -p for partition metadata"
31 );
32 }
33 r
34 })
35}
36
37fn blkid_probe(dev: &str) -> Result<HashMap<String, String>> {
48 let mut cmd = Command::new("blkid");
49 cmd.args(["-p", "-o", "export"]).arg(dev);
50 cmd.log_debug();
51 let output = cmd.output().context("Failed to run blkid")?;
52 if !output.status.success() {
53 if output.status.code() == Some(2) {
55 return Ok(HashMap::new());
56 }
57 let stderr = String::from_utf8_lossy(&output.stderr);
58 anyhow::bail!(
59 "blkid -p failed on {dev} (exit status {}): {stderr}",
60 output.status
61 );
62 }
63 let text = String::from_utf8(output.stdout).context("blkid output is not UTF-8")?;
64 let mut props = HashMap::new();
65 for line in text.lines() {
66 if let Some((key, value)) = line.split_once('=') {
67 props.insert(key.to_string(), value.to_string());
68 }
69 }
70 Ok(props)
71}
72
73pub const ESP_ID_MBR: &[u8] = &[0x06, 0xEF];
78
79pub const ESP: &str = "c12a7328-f81f-11d2-ba4b-00a0c93ec93b";
81
82pub const BIOS_BOOT: &str = "21686148-6449-6e6f-744e-656564454649";
84
85#[derive(Debug, Deserialize)]
86struct DevicesOutput {
87 blockdevices: Vec<Device>,
88}
89
90#[allow(dead_code)]
91#[derive(Debug, Clone, serde::Serialize, Deserialize)]
92pub struct Device {
93 pub name: String,
94 pub serial: Option<String>,
95 pub model: Option<String>,
96 pub partlabel: Option<String>,
97 pub parttype: Option<String>,
98 pub partuuid: Option<String>,
99 pub partn: Option<u32>,
101 pub children: Option<Vec<Device>>,
102 pub size: u64,
103 #[serde(rename = "maj:min")]
104 pub maj_min: Option<String>,
105 pub start: Option<u64>,
108
109 pub label: Option<String>,
111 pub fstype: Option<String>,
112 pub uuid: Option<String>,
113 pub path: Option<String>,
114 pub pttype: Option<String>,
116 pub ro: Option<bool>,
119}
120
121impl Device {
122 pub fn path(&self) -> String {
124 self.path.clone().unwrap_or(format!("/dev/{}", &self.name))
125 }
126
127 #[allow(dead_code)]
129 pub fn node(&self) -> String {
130 self.path()
131 }
132
133 #[allow(dead_code)]
134 pub fn has_children(&self) -> bool {
135 self.children.as_ref().is_some_and(|v| !v.is_empty())
136 }
137
138 pub fn is_mpath(&self) -> Result<bool> {
140 let dm_path = Utf8PathBuf::from_path_buf(std::fs::canonicalize(self.path())?)
141 .map_err(|_| anyhow::anyhow!("Non-UTF8 path"))?;
142 let dm_name = dm_path.file_name().unwrap_or("");
143 let uuid_path = Utf8PathBuf::from(format!("/sys/class/block/{dm_name}/dm/uuid"));
144
145 if uuid_path.exists() {
146 let uuid = std::fs::read_to_string(&uuid_path)
147 .with_context(|| format!("Failed to read {uuid_path}"))?;
148 if uuid.trim_start().starts_with("mpath-") {
149 return Ok(true);
150 }
151 }
152 Ok(false)
153 }
154
155 pub fn get_esp_partition_number(&self) -> Result<String> {
164 let esp_device = self.find_partition_of_esp()?;
165 let devname = &esp_device.name;
166
167 let partition_path = Utf8PathBuf::from(format!("/sys/class/block/{devname}/partition"));
168 if partition_path.exists() {
169 return std::fs::read_to_string(&partition_path)
170 .with_context(|| format!("Failed to read {partition_path}"));
171 }
172
173 if self.is_mpath()? {
175 if let Some(partn) = esp_device.partn {
176 return Ok(partn.to_string());
177 }
178 let parent_path = self.path();
183 let esp_path = esp_device.path();
184 if let Some(n) = parse_partition_number_from_suffix(&parent_path, &esp_path) {
185 return Ok(n);
186 }
187 }
188 anyhow::bail!("Not supported for {devname}")
189 }
190
191 pub fn find_partition_of_bios_boot(&self) -> Option<&Device> {
193 self.find_partition_of_type(BIOS_BOOT)
194 }
195
196 pub fn find_colocated_esps(&self) -> Result<Option<Vec<Device>>> {
200 let mut esps = Vec::new();
201 for root in &self.find_all_roots()? {
202 if let Some(esp) = root.find_partition_of_esp_optional()? {
203 esps.push(esp.clone());
204 }
205 }
206 Ok((!esps.is_empty()).then_some(esps))
207 }
208
209 pub fn find_first_colocated_esp(&self) -> Result<Device> {
215 self.find_colocated_esps()?
216 .and_then(|mut v| Some(v.remove(0)))
217 .ok_or_else(|| anyhow!("No ESP partition found among backing devices"))
218 }
219
220 pub fn find_colocated_bios_boot(&self) -> Result<Option<Vec<Device>>> {
224 let bios_boots: Vec<_> = self
225 .find_all_roots()?
226 .iter()
227 .filter_map(|root| root.find_partition_of_bios_boot())
228 .cloned()
229 .collect();
230 Ok((!bios_boots.is_empty()).then_some(bios_boots))
231 }
232
233 pub fn find_partition_of_type(&self, parttype: &str) -> Option<&Device> {
235 self.children.as_ref()?.iter().find(|child| {
236 child
237 .parttype
238 .as_ref()
239 .is_some_and(|pt| pt.eq_ignore_ascii_case(parttype))
240 })
241 }
242
243 pub fn find_partition_of_esp_optional(&self) -> Result<Option<&Device>> {
256 let Some(children) = self.children.as_ref() else {
257 return Ok(None);
258 };
259 let direct = match self.pttype.as_deref() {
260 Some("dos") => children.iter().find(|child| {
261 child
262 .parttype
263 .as_ref()
264 .and_then(|pt| {
265 let pt = pt.strip_prefix("0x").unwrap_or(pt);
266 u8::from_str_radix(pt, 16).ok()
267 })
268 .is_some_and(|pt| ESP_ID_MBR.contains(&pt))
269 }),
270 Some("gpt") | None => self.find_partition_of_type(ESP),
273 Some(other) => return Err(anyhow!("Unsupported partition table type: {other}")),
274 };
275 if direct.is_some() {
276 return Ok(direct);
277 }
278 for child in children {
281 if child.pttype.is_some() {
282 if let Some(esp) = child.find_partition_of_esp_optional()? {
283 return Ok(Some(esp));
284 }
285 }
286 }
287 Ok(None)
288 }
289
290 pub fn find_partition_of_esp(&self) -> Result<&Device> {
295 self.find_partition_of_esp_optional()?
296 .ok_or_else(|| anyhow!("ESP partition not found on {}", self.path()))
297 }
298
299 pub fn find_device_by_partno(&self, partno: u32) -> Result<&Device> {
301 self.children
302 .as_ref()
303 .ok_or_else(|| anyhow!("Device has no children"))?
304 .iter()
305 .find(|child| child.partn == Some(partno))
306 .ok_or_else(|| anyhow!("Missing partition for index {partno}"))
307 }
308
309 pub fn refresh(&mut self) -> Result<()> {
312 let path = self.path();
313 let new_device = list_dev(Utf8Path::new(&path))?;
314 *self = new_device;
315 Ok(())
316 }
317
318 fn read_sysfs_property<T>(&self, property: &str) -> Result<Option<T>>
320 where
321 T: std::str::FromStr,
322 T::Err: std::error::Error + Send + Sync + 'static,
323 {
324 let Some(majmin) = self.maj_min.as_deref() else {
325 return Ok(None);
326 };
327 let sysfs_path = format!("/sys/dev/block/{majmin}/{property}");
328 if !Utf8Path::new(&sysfs_path).try_exists()? {
329 return Ok(None);
330 }
331 let value = std::fs::read_to_string(&sysfs_path)
332 .with_context(|| format!("Reading {sysfs_path}"))?;
333 let parsed = value
334 .trim()
335 .parse()
336 .with_context(|| format!("Parsing sysfs {property} property"))?;
337 tracing::debug!("backfilled {property} to {value}");
338 Ok(Some(parsed))
339 }
340
341 pub fn backfill_missing(&mut self) -> Result<()> {
348 if self.start.is_none() {
351 self.start = self.read_sysfs_property("start")?;
352 }
353 if self.partn.is_none() {
356 self.partn = self.read_sysfs_property("partition")?;
357 }
358 if !have_udev() && (self.parttype.is_none() || self.pttype.is_none()) {
362 let props = blkid_probe(&self.path())?;
363 if self.parttype.is_none() {
364 self.parttype = props.get("PART_ENTRY_TYPE").cloned();
365 }
366 if self.pttype.is_none() {
367 self.pttype = props.get("PTTYPE").cloned();
368 }
369 }
370 for child in self.children.iter_mut().flatten() {
372 child.backfill_missing()?;
373 }
374 Ok(())
375 }
376
377 pub fn list_parents(&self) -> Result<Option<Vec<Device>>> {
384 let path = self.path();
385 let output: DevicesOutput = Command::new("lsblk")
386 .args(["-J", "-b", "-O", "--inverse"])
387 .arg(&path)
388 .log_debug()
389 .run_and_parse_json()?;
390
391 let device = output
392 .blockdevices
393 .into_iter()
394 .next()
395 .ok_or_else(|| anyhow!("no device output from lsblk --inverse for {path}"))?;
396
397 match device.children {
398 Some(mut children) if !children.is_empty() => {
399 for child in &mut children {
400 child.backfill_missing()?;
401 }
402 Ok(Some(children))
403 }
404 _ => Ok(None),
405 }
406 }
407
408 pub fn require_single_root(&self) -> Result<Device> {
414 let mut roots = self.find_all_roots()?;
415 match roots.len() {
416 1 => Ok(roots.remove(0)),
417 n => anyhow::bail!(
418 "Expected a single root device for {}, but found {n}",
419 self.path()
420 ),
421 }
422 }
423
424 pub fn find_all_roots(&self) -> Result<Vec<Device>> {
431 let Some(parents) = self.list_parents()? else {
432 return Ok(vec![list_dev(Utf8Path::new(&self.path()))?]);
434 };
435
436 let mut roots = Vec::new();
437 let mut seen = HashSet::new();
438 let mut queue = parents;
439 while let Some(mut device) = queue.pop() {
440 match device.children.take() {
441 Some(grandparents) if !grandparents.is_empty() => {
442 queue.extend(grandparents);
443 }
444 _ => {
445 let name = device.name.clone();
448 if seen.insert(name) {
449 roots.push(list_dev(Utf8Path::new(&device.path()))?);
451 }
452 }
453 }
454 }
455 Ok(roots)
456 }
457}
458
459#[context("Listing device {dev}")]
460pub fn list_dev(dev: &Utf8Path) -> Result<Device> {
461 let mut devs: DevicesOutput = Command::new("lsblk")
462 .args(["-J", "-b", "-O"])
463 .arg(dev)
464 .log_debug()
465 .run_and_parse_json()?;
466 for dev in devs.blockdevices.iter_mut() {
467 dev.backfill_missing()?;
468 }
469 devs.blockdevices
470 .into_iter()
471 .next()
472 .ok_or_else(|| anyhow!("no device output from lsblk for {dev}"))
473}
474
475#[context("Finding block device for ZFS dataset {dataset}")]
476fn list_dev_for_zfs_dataset(dataset: &str) -> Result<Device> {
477 let dataset = dataset.strip_prefix("ZFS=").unwrap_or(dataset);
478 let pool = dataset
479 .split('/')
480 .next()
481 .ok_or_else(|| anyhow!("Invalid ZFS dataset: {dataset}"))?;
482
483 let output = Command::new("zpool")
484 .args(["list", "-H", "-v", "-P", pool])
485 .run_get_string()
486 .with_context(|| format!("Querying ZFS pool {pool}"))?;
487
488 for line in output.lines() {
489 if line.starts_with('\t') || line.starts_with(' ') {
490 let dev_path = line.trim_start().split('\t').next().unwrap_or("").trim();
491 if dev_path.starts_with('/') {
492 return list_dev(Utf8Path::new(dev_path));
493 }
494 }
495 }
496
497 anyhow::bail!("Could not find a block device backing ZFS pool {pool}")
498}
499
500pub fn list_dev_by_dir(dir: &Dir) -> Result<Device> {
502 let fsinfo = bootc_mount::inspect_filesystem_of_dir(dir)?;
503 let source = &fsinfo.source;
504 if fsinfo.fstype == "zfs" || source.starts_with("ZFS=") {
505 return list_dev_for_zfs_dataset(source);
506 }
507 list_dev(&Utf8PathBuf::from(source))
508}
509
510pub fn is_dir_backing_device_ro(dir: &Dir) -> Result<Option<bool>> {
521 let fsinfo = bootc_mount::inspect_filesystem_of_dir(dir)?;
522 let source = &fsinfo.source;
523 if !source.starts_with("/dev/") {
527 tracing::debug!("Filesystem source {source} is not a block device node");
528 return Ok(None);
529 }
530 let dev = list_dev(&Utf8PathBuf::from(source))?;
531 Ok(dev.ro)
532}
533
534pub struct LoopbackDevice {
535 pub dev: Option<Utf8PathBuf>,
536 cleanup_handle: Option<LoopbackCleanupHandle>,
538}
539
540struct LoopbackCleanupHandle {
542 child: std::process::Child,
544}
545
546impl LoopbackDevice {
547 pub fn new(path: &Path) -> Result<Self> {
549 let direct_io = match env::var("BOOTC_DIRECT_IO") {
550 Ok(val) => {
551 if val == "on" {
552 "on"
553 } else {
554 "off"
555 }
556 }
557 Err(_e) => "off",
558 };
559
560 let dev = Command::new("losetup")
561 .args([
562 "--show",
563 format!("--direct-io={direct_io}").as_str(),
564 "-P",
565 "--find",
566 ])
567 .arg(path)
568 .run_get_string()?;
569 let dev = Utf8PathBuf::from(dev.trim());
570 tracing::debug!("Allocated loopback {dev}");
571
572 let cleanup_handle = match Self::spawn_cleanup_helper(dev.as_str()) {
574 Ok(handle) => Some(handle),
575 Err(e) => {
576 tracing::warn!(
577 "Failed to spawn loopback cleanup helper for {}: {}. \
578 Loopback device may not be cleaned up if process is interrupted.",
579 dev,
580 e
581 );
582 None
583 }
584 };
585
586 Ok(Self {
587 dev: Some(dev),
588 cleanup_handle,
589 })
590 }
591
592 pub fn path(&self) -> &Utf8Path {
594 self.dev.as_deref().unwrap()
596 }
597
598 fn spawn_cleanup_helper(device_path: &str) -> Result<LoopbackCleanupHandle> {
601 let bootc_path = bootc_utils::reexec::executable_path()
603 .context("Failed to locate bootc binary for cleanup helper")?;
604
605 let mut cmd = Command::new(bootc_path);
607 cmd.args([
608 "internals",
609 "loopback-cleanup-helper",
610 "--device",
611 device_path,
612 ]);
613
614 cmd.env("BOOTC_LOOPBACK_CLEANUP_HELPER", "1");
616
617 cmd.stdin(Stdio::null());
619 cmd.stdout(Stdio::null());
620 let child = cmd
624 .spawn()
625 .context("Failed to spawn loopback cleanup helper")?;
626
627 Ok(LoopbackCleanupHandle { child })
628 }
629
630 fn impl_close(&mut self) -> Result<()> {
632 let Some(dev) = self.dev.take() else {
634 tracing::trace!("loopback device already deallocated");
635 return Ok(());
636 };
637
638 if let Some(mut cleanup_handle) = self.cleanup_handle.take() {
640 let _ = cleanup_handle.child.kill();
642 }
643
644 Command::new("losetup")
645 .args(["-d", dev.as_str()])
646 .run_capture_stderr()
647 }
648
649 pub fn close(mut self) -> Result<()> {
651 self.impl_close()
652 }
653}
654
655impl Drop for LoopbackDevice {
656 fn drop(&mut self) {
657 let _ = self.impl_close();
659 }
660}
661
662pub async fn run_loopback_cleanup_helper(device_path: &str) -> Result<()> {
665 if std::env::var("BOOTC_LOOPBACK_CLEANUP_HELPER").is_err() {
667 anyhow::bail!("This function should only be called as a cleanup helper");
668 }
669
670 rustix::process::set_parent_process_death_signal(Some(rustix::process::Signal::TERM))
672 .context("Failed to set parent death signal")?;
673
674 tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
676 .expect("Failed to create signal stream")
677 .recv()
678 .await;
679
680 let output = std::process::Command::new("losetup")
682 .args(["-d", device_path])
683 .output();
684
685 match output {
686 Ok(output) if output.status.success() => {
687 tracing::info!("Cleaned up leaked loopback device {}", device_path);
689 std::process::exit(0);
690 }
691 Ok(output) => {
692 let stderr = String::from_utf8_lossy(&output.stderr);
693 tracing::error!(
694 "Failed to clean up loopback device {}: {}. Stderr: {}",
695 device_path,
696 output.status,
697 stderr.trim()
698 );
699 std::process::exit(1);
700 }
701 Err(e) => {
702 tracing::error!(
703 "Error executing losetup to clean up loopback device {}: {}",
704 device_path,
705 e
706 );
707 std::process::exit(1);
708 }
709 }
710}
711
712pub fn parse_size_mib(mut s: &str) -> Result<u64> {
714 let suffixes = [
715 ("MiB", 1u64),
716 ("M", 1u64),
717 ("GiB", 1024),
718 ("G", 1024),
719 ("TiB", 1024 * 1024),
720 ("T", 1024 * 1024),
721 ];
722 let mut mul = 1u64;
723 for (suffix, imul) in suffixes {
724 if let Some((sv, rest)) = s.rsplit_once(suffix) {
725 if !rest.is_empty() {
726 anyhow::bail!("Trailing text after size: {rest}");
727 }
728 s = sv;
729 mul = imul;
730 }
731 }
732 let v = s.parse::<u64>()?;
733 Ok(v * mul)
734}
735
736fn parse_partition_number_from_suffix(parent_path: &str, esp_path: &str) -> Option<String> {
748 let suffix = esp_path.strip_prefix(parent_path)?;
749 let digits = suffix.trim_start_matches(|c: char| !c.is_ascii_digit());
750 if digits.is_empty() {
751 return None;
752 }
753 Some(digits.to_string())
754}
755
756#[cfg(test)]
757mod test {
758 use super::*;
759
760 #[test]
761 fn test_parse_size_mib() {
762 let ident_cases = [0, 10, 9, 1024].into_iter().map(|k| (k.to_string(), k));
763 let cases = [
764 ("0M", 0),
765 ("10M", 10),
766 ("10MiB", 10),
767 ("1G", 1024),
768 ("9G", 9216),
769 ("11T", 11 * 1024 * 1024),
770 ]
771 .into_iter()
772 .map(|(k, v)| (k.to_string(), v));
773 for (s, v) in ident_cases.chain(cases) {
774 assert_eq!(parse_size_mib(&s).unwrap(), v as u64, "Parsing {s}");
775 }
776 }
777
778 #[test]
779 fn test_parse_lsblk() {
780 let fixture = include_str!("../tests/fixtures/lsblk.json");
781 let devs: DevicesOutput = serde_json::from_str(fixture).unwrap();
782 let dev = devs.blockdevices.into_iter().next().unwrap();
783 assert_eq!(dev.partn, None);
785 let children = dev.children.as_deref().unwrap();
786 assert_eq!(children.len(), 3);
787 let first_child = &children[0];
788 assert_eq!(first_child.partn, Some(1));
789 assert_eq!(
790 first_child.parttype.as_deref().unwrap(),
791 "21686148-6449-6e6f-744e-656564454649"
792 );
793 assert_eq!(
794 first_child.partuuid.as_deref().unwrap(),
795 "3979e399-262f-4666-aabc-7ab5d3add2f0"
796 );
797 let part2 = dev.find_device_by_partno(2).unwrap();
799 assert_eq!(part2.partn, Some(2));
800 assert_eq!(part2.parttype.as_deref().unwrap(), ESP);
801 let esp = dev.find_partition_of_esp().unwrap();
803 assert_eq!(esp.partn, Some(2));
804 let bios = dev.find_partition_of_bios_boot().unwrap();
806 assert_eq!(bios.partn, Some(1));
807 assert_eq!(bios.parttype.as_deref().unwrap(), BIOS_BOOT);
808 }
809
810 #[test]
814 fn test_parse_lsblk_no_udev() {
815 let fixture = include_str!("../tests/fixtures/lsblk-no-udev.json");
816 let devs: DevicesOutput = serde_json::from_str(fixture).unwrap();
817 let dev = devs.blockdevices.into_iter().next().unwrap();
818 assert!(dev.pttype.is_none());
820 let children = dev.children.as_deref().unwrap();
821 assert_eq!(children.len(), 3);
822 assert!(children[0].parttype.is_none());
823 assert!(children[1].parttype.is_none());
824 assert!(children[2].parttype.is_none());
825 assert!(dev.find_partition_of_esp_optional().unwrap().is_none());
827 assert!(dev.find_partition_of_bios_boot().is_none());
828 }
829
830 #[test]
831 fn test_parse_lsblk_mbr() {
832 let fixture = include_str!("../tests/fixtures/lsblk-mbr.json");
833 let devs: DevicesOutput = serde_json::from_str(fixture).unwrap();
834 let dev = devs.blockdevices.into_iter().next().unwrap();
835 assert_eq!(dev.partn, None);
837 assert_eq!(dev.pttype.as_deref().unwrap(), "dos");
838 let children = dev.children.as_deref().unwrap();
839 assert_eq!(children.len(), 3);
840 let first_child = &children[0];
842 assert_eq!(first_child.partn, Some(1));
843 assert_eq!(first_child.parttype.as_deref().unwrap(), "0x06");
844 assert_eq!(first_child.partuuid.as_deref().unwrap(), "a1b2c3d4-01");
845 assert_eq!(first_child.fstype.as_deref().unwrap(), "vfat");
846 assert!(first_child.partlabel.is_none());
848 let second_child = &children[1];
850 assert_eq!(second_child.partn, Some(2));
851 assert_eq!(second_child.parttype.as_deref().unwrap(), "0x83");
852 assert_eq!(second_child.partuuid.as_deref().unwrap(), "a1b2c3d4-02");
853 let third_child = &children[2];
855 assert_eq!(third_child.partn, Some(3));
856 assert_eq!(third_child.parttype.as_deref().unwrap(), "0xef");
857 assert_eq!(third_child.partuuid.as_deref().unwrap(), "a1b2c3d4-03");
858 let part1 = dev.find_device_by_partno(1).unwrap();
860 assert_eq!(part1.partn, Some(1));
861 let esp = dev.find_partition_of_esp().unwrap();
863 assert_eq!(esp.partn, Some(1));
864 }
865
866 fn make_mbr_disk(parttypes: &[&str]) -> Device {
868 Device {
869 name: "vda".into(),
870 serial: None,
871 model: None,
872 partlabel: None,
873 parttype: None,
874 partuuid: None,
875 partn: None,
876 size: 10737418240,
877 maj_min: None,
878 start: None,
879 label: None,
880 fstype: None,
881 uuid: None,
882 path: Some("/dev/vda".into()),
883 pttype: Some("dos".into()),
884 ro: None,
885 children: Some(
886 parttypes
887 .iter()
888 .enumerate()
889 .map(|(i, pt)| Device {
890 name: format!("vda{}", i + 1),
891 serial: None,
892 model: None,
893 partlabel: None,
894 parttype: Some(pt.to_string()),
895 partuuid: None,
896 partn: Some(i as u32 + 1),
897 size: 1048576,
898 maj_min: None,
899 start: Some(2048),
900 label: None,
901 fstype: None,
902 uuid: None,
903 path: None,
904 pttype: Some("dos".into()),
905 ro: None,
906 children: None,
907 })
908 .collect(),
909 ),
910 }
911 }
912
913 #[test]
914 fn test_parse_lsblk_vroc() {
915 let fixture = include_str!("../tests/fixtures/lsblk-vroc.json");
916 let devs: DevicesOutput = serde_json::from_str(fixture).unwrap();
917 assert_eq!(devs.blockdevices.len(), 2);
918
919 for nvme in &devs.blockdevices {
923 let esp = nvme.find_partition_of_esp().unwrap();
924 assert_eq!(esp.name, "md126p1");
925 assert_eq!(esp.partn, Some(1));
926 assert_eq!(esp.parttype.as_deref().unwrap(), ESP);
927 assert_eq!(esp.fstype.as_deref().unwrap(), "vfat");
928 }
929 }
930
931 #[test]
932 fn test_parse_lsblk_swraid() {
933 let fixture = include_str!("../tests/fixtures/lsblk-swraid.json");
934 let devs: DevicesOutput = serde_json::from_str(fixture).unwrap();
935 assert_eq!(devs.blockdevices.len(), 2);
936
937 let sda = &devs.blockdevices[0];
943 let esp = sda.find_partition_of_esp().unwrap();
944 assert_eq!(esp.name, "sda1");
945 assert_eq!(esp.partn, Some(1));
946 assert_eq!(esp.parttype.as_deref().unwrap(), ESP);
947 assert_eq!(esp.fstype.as_deref().unwrap(), "vfat");
948
949 let sdb = &devs.blockdevices[1];
950 let esp = sdb.find_partition_of_esp().unwrap();
951 assert_eq!(esp.name, "sdb1");
952 assert_eq!(esp.partn, Some(1));
953 assert_eq!(esp.parttype.as_deref().unwrap(), ESP);
954 assert_eq!(esp.fstype.as_deref().unwrap(), "vfat");
955
956 let sda3 = sda
959 .children
960 .as_ref()
961 .unwrap()
962 .iter()
963 .find(|c| c.name == "sda3")
964 .unwrap();
965 assert_eq!(sda3.fstype.as_deref().unwrap(), "linux_raid_member");
966 let md0 = sda3
967 .children
968 .as_ref()
969 .unwrap()
970 .iter()
971 .find(|c| c.name == "md0")
972 .unwrap();
973 assert_eq!(md0.fstype.as_deref().unwrap(), "ext4");
974 }
975
976 #[test]
977 fn test_mbr_esp_detection() {
978 let dev = make_mbr_disk(&["0x06"]);
980 assert_eq!(dev.find_partition_of_esp().unwrap().partn, Some(1));
981
982 let dev = make_mbr_disk(&["0x83", "0xef"]);
984 assert_eq!(dev.find_partition_of_esp().unwrap().partn, Some(2));
985
986 let dev = make_mbr_disk(&["0x83", "0x82"]);
988 assert!(dev.find_partition_of_esp().is_err());
989 }
990
991 #[test]
992 fn test_parse_partition_number_from_suffix() {
993 assert_eq!(
995 parse_partition_number_from_suffix("/dev/mapper/mpatha", "/dev/mapper/mpatha2"),
996 Some("2".into())
997 );
998 assert_eq!(
1000 parse_partition_number_from_suffix("/dev/mapper/mpatha", "/dev/mapper/mpathap2"),
1001 Some("2".into())
1002 );
1003 assert_eq!(
1005 parse_partition_number_from_suffix(
1006 "/dev/mapper/3600508b4001",
1007 "/dev/mapper/3600508b4001-part1"
1008 ),
1009 Some("1".into())
1010 );
1011 assert_eq!(
1013 parse_partition_number_from_suffix("/dev/mapper/mpatha", "/dev/mapper/mpatha12"),
1014 Some("12".into())
1015 );
1016 assert_eq!(
1018 parse_partition_number_from_suffix("/dev/mapper/mpatha", "/dev/sda1"),
1019 None
1020 );
1021 assert_eq!(
1023 parse_partition_number_from_suffix("/dev/mapper/mpatha", "/dev/mapper/mpathap"),
1024 None
1025 );
1026 assert_eq!(
1028 parse_partition_number_from_suffix("/dev/mapper/mpatha", "/dev/mapper/mpatha"),
1029 None
1030 );
1031 }
1032}