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}
117
118impl Device {
119 pub fn path(&self) -> String {
121 self.path.clone().unwrap_or(format!("/dev/{}", &self.name))
122 }
123
124 #[allow(dead_code)]
126 pub fn node(&self) -> String {
127 self.path()
128 }
129
130 #[allow(dead_code)]
131 pub fn has_children(&self) -> bool {
132 self.children.as_ref().is_some_and(|v| !v.is_empty())
133 }
134
135 pub fn is_mpath(&self) -> Result<bool> {
137 let dm_path = Utf8PathBuf::from_path_buf(std::fs::canonicalize(self.path())?)
138 .map_err(|_| anyhow::anyhow!("Non-UTF8 path"))?;
139 let dm_name = dm_path.file_name().unwrap_or("");
140 let uuid_path = Utf8PathBuf::from(format!("/sys/class/block/{dm_name}/dm/uuid"));
141
142 if uuid_path.exists() {
143 let uuid = std::fs::read_to_string(&uuid_path)
144 .with_context(|| format!("Failed to read {uuid_path}"))?;
145 if uuid.trim_start().starts_with("mpath-") {
146 return Ok(true);
147 }
148 }
149 Ok(false)
150 }
151
152 pub fn get_esp_partition_number(&self) -> Result<String> {
161 let esp_device = self.find_partition_of_esp()?;
162 let devname = &esp_device.name;
163
164 let partition_path = Utf8PathBuf::from(format!("/sys/class/block/{devname}/partition"));
165 if partition_path.exists() {
166 return std::fs::read_to_string(&partition_path)
167 .with_context(|| format!("Failed to read {partition_path}"));
168 }
169
170 if self.is_mpath()? {
172 if let Some(partn) = esp_device.partn {
173 return Ok(partn.to_string());
174 }
175 let parent_path = self.path();
180 let esp_path = esp_device.path();
181 if let Some(n) = parse_partition_number_from_suffix(&parent_path, &esp_path) {
182 return Ok(n);
183 }
184 }
185 anyhow::bail!("Not supported for {devname}")
186 }
187
188 pub fn find_partition_of_bios_boot(&self) -> Option<&Device> {
190 self.find_partition_of_type(BIOS_BOOT)
191 }
192
193 pub fn find_colocated_esps(&self) -> Result<Option<Vec<Device>>> {
197 let mut esps = Vec::new();
198 for root in &self.find_all_roots()? {
199 if let Some(esp) = root.find_partition_of_esp_optional()? {
200 esps.push(esp.clone());
201 }
202 }
203 Ok((!esps.is_empty()).then_some(esps))
204 }
205
206 pub fn find_first_colocated_esp(&self) -> Result<Device> {
212 self.find_colocated_esps()?
213 .and_then(|mut v| Some(v.remove(0)))
214 .ok_or_else(|| anyhow!("No ESP partition found among backing devices"))
215 }
216
217 pub fn find_colocated_bios_boot(&self) -> Result<Option<Vec<Device>>> {
221 let bios_boots: Vec<_> = self
222 .find_all_roots()?
223 .iter()
224 .filter_map(|root| root.find_partition_of_bios_boot())
225 .cloned()
226 .collect();
227 Ok((!bios_boots.is_empty()).then_some(bios_boots))
228 }
229
230 pub fn find_partition_of_type(&self, parttype: &str) -> Option<&Device> {
232 self.children.as_ref()?.iter().find(|child| {
233 child
234 .parttype
235 .as_ref()
236 .is_some_and(|pt| pt.eq_ignore_ascii_case(parttype))
237 })
238 }
239
240 pub fn find_partition_of_esp_optional(&self) -> Result<Option<&Device>> {
253 let Some(children) = self.children.as_ref() else {
254 return Ok(None);
255 };
256 let direct = match self.pttype.as_deref() {
257 Some("dos") => children.iter().find(|child| {
258 child
259 .parttype
260 .as_ref()
261 .and_then(|pt| {
262 let pt = pt.strip_prefix("0x").unwrap_or(pt);
263 u8::from_str_radix(pt, 16).ok()
264 })
265 .is_some_and(|pt| ESP_ID_MBR.contains(&pt))
266 }),
267 Some("gpt") | None => self.find_partition_of_type(ESP),
270 Some(other) => return Err(anyhow!("Unsupported partition table type: {other}")),
271 };
272 if direct.is_some() {
273 return Ok(direct);
274 }
275 for child in children {
278 if child.pttype.is_some() {
279 if let Some(esp) = child.find_partition_of_esp_optional()? {
280 return Ok(Some(esp));
281 }
282 }
283 }
284 Ok(None)
285 }
286
287 pub fn find_partition_of_esp(&self) -> Result<&Device> {
292 self.find_partition_of_esp_optional()?
293 .ok_or_else(|| anyhow!("ESP partition not found on {}", self.path()))
294 }
295
296 pub fn find_device_by_partno(&self, partno: u32) -> Result<&Device> {
298 self.children
299 .as_ref()
300 .ok_or_else(|| anyhow!("Device has no children"))?
301 .iter()
302 .find(|child| child.partn == Some(partno))
303 .ok_or_else(|| anyhow!("Missing partition for index {partno}"))
304 }
305
306 pub fn refresh(&mut self) -> Result<()> {
309 let path = self.path();
310 let new_device = list_dev(Utf8Path::new(&path))?;
311 *self = new_device;
312 Ok(())
313 }
314
315 fn read_sysfs_property<T>(&self, property: &str) -> Result<Option<T>>
317 where
318 T: std::str::FromStr,
319 T::Err: std::error::Error + Send + Sync + 'static,
320 {
321 let Some(majmin) = self.maj_min.as_deref() else {
322 return Ok(None);
323 };
324 let sysfs_path = format!("/sys/dev/block/{majmin}/{property}");
325 if !Utf8Path::new(&sysfs_path).try_exists()? {
326 return Ok(None);
327 }
328 let value = std::fs::read_to_string(&sysfs_path)
329 .with_context(|| format!("Reading {sysfs_path}"))?;
330 let parsed = value
331 .trim()
332 .parse()
333 .with_context(|| format!("Parsing sysfs {property} property"))?;
334 tracing::debug!("backfilled {property} to {value}");
335 Ok(Some(parsed))
336 }
337
338 pub fn backfill_missing(&mut self) -> Result<()> {
345 if self.start.is_none() {
348 self.start = self.read_sysfs_property("start")?;
349 }
350 if self.partn.is_none() {
353 self.partn = self.read_sysfs_property("partition")?;
354 }
355 if !have_udev() && (self.parttype.is_none() || self.pttype.is_none()) {
359 let props = blkid_probe(&self.path())?;
360 if self.parttype.is_none() {
361 self.parttype = props.get("PART_ENTRY_TYPE").cloned();
362 }
363 if self.pttype.is_none() {
364 self.pttype = props.get("PTTYPE").cloned();
365 }
366 }
367 for child in self.children.iter_mut().flatten() {
369 child.backfill_missing()?;
370 }
371 Ok(())
372 }
373
374 pub fn list_parents(&self) -> Result<Option<Vec<Device>>> {
381 let path = self.path();
382 let output: DevicesOutput = Command::new("lsblk")
383 .args(["-J", "-b", "-O", "--inverse"])
384 .arg(&path)
385 .log_debug()
386 .run_and_parse_json()?;
387
388 let device = output
389 .blockdevices
390 .into_iter()
391 .next()
392 .ok_or_else(|| anyhow!("no device output from lsblk --inverse for {path}"))?;
393
394 match device.children {
395 Some(mut children) if !children.is_empty() => {
396 for child in &mut children {
397 child.backfill_missing()?;
398 }
399 Ok(Some(children))
400 }
401 _ => Ok(None),
402 }
403 }
404
405 pub fn require_single_root(&self) -> Result<Device> {
411 let mut roots = self.find_all_roots()?;
412 match roots.len() {
413 1 => Ok(roots.remove(0)),
414 n => anyhow::bail!(
415 "Expected a single root device for {}, but found {n}",
416 self.path()
417 ),
418 }
419 }
420
421 pub fn find_all_roots(&self) -> Result<Vec<Device>> {
428 let Some(parents) = self.list_parents()? else {
429 return Ok(vec![list_dev(Utf8Path::new(&self.path()))?]);
431 };
432
433 let mut roots = Vec::new();
434 let mut seen = HashSet::new();
435 let mut queue = parents;
436 while let Some(mut device) = queue.pop() {
437 match device.children.take() {
438 Some(grandparents) if !grandparents.is_empty() => {
439 queue.extend(grandparents);
440 }
441 _ => {
442 let name = device.name.clone();
445 if seen.insert(name) {
446 roots.push(list_dev(Utf8Path::new(&device.path()))?);
448 }
449 }
450 }
451 }
452 Ok(roots)
453 }
454}
455
456#[context("Listing device {dev}")]
457pub fn list_dev(dev: &Utf8Path) -> Result<Device> {
458 let mut devs: DevicesOutput = Command::new("lsblk")
459 .args(["-J", "-b", "-O"])
460 .arg(dev)
461 .log_debug()
462 .run_and_parse_json()?;
463 for dev in devs.blockdevices.iter_mut() {
464 dev.backfill_missing()?;
465 }
466 devs.blockdevices
467 .into_iter()
468 .next()
469 .ok_or_else(|| anyhow!("no device output from lsblk for {dev}"))
470}
471
472#[context("Finding block device for ZFS dataset {dataset}")]
473fn list_dev_for_zfs_dataset(dataset: &str) -> Result<Device> {
474 let dataset = dataset.strip_prefix("ZFS=").unwrap_or(dataset);
475 let pool = dataset
476 .split('/')
477 .next()
478 .ok_or_else(|| anyhow!("Invalid ZFS dataset: {dataset}"))?;
479
480 let output = Command::new("zpool")
481 .args(["list", "-H", "-v", "-P", pool])
482 .run_get_string()
483 .with_context(|| format!("Querying ZFS pool {pool}"))?;
484
485 for line in output.lines() {
486 if line.starts_with('\t') || line.starts_with(' ') {
487 let dev_path = line.trim_start().split('\t').next().unwrap_or("").trim();
488 if dev_path.starts_with('/') {
489 return list_dev(Utf8Path::new(dev_path));
490 }
491 }
492 }
493
494 anyhow::bail!("Could not find a block device backing ZFS pool {pool}")
495}
496
497pub fn list_dev_by_dir(dir: &Dir) -> Result<Device> {
499 let fsinfo = bootc_mount::inspect_filesystem_of_dir(dir)?;
500 let source = &fsinfo.source;
501 if fsinfo.fstype == "zfs" || source.starts_with("ZFS=") {
502 return list_dev_for_zfs_dataset(source);
503 }
504 list_dev(&Utf8PathBuf::from(source))
505}
506
507pub struct LoopbackDevice {
508 pub dev: Option<Utf8PathBuf>,
509 cleanup_handle: Option<LoopbackCleanupHandle>,
511}
512
513struct LoopbackCleanupHandle {
515 child: std::process::Child,
517}
518
519impl LoopbackDevice {
520 pub fn new(path: &Path) -> Result<Self> {
522 let direct_io = match env::var("BOOTC_DIRECT_IO") {
523 Ok(val) => {
524 if val == "on" {
525 "on"
526 } else {
527 "off"
528 }
529 }
530 Err(_e) => "off",
531 };
532
533 let dev = Command::new("losetup")
534 .args([
535 "--show",
536 format!("--direct-io={direct_io}").as_str(),
537 "-P",
538 "--find",
539 ])
540 .arg(path)
541 .run_get_string()?;
542 let dev = Utf8PathBuf::from(dev.trim());
543 tracing::debug!("Allocated loopback {dev}");
544
545 let cleanup_handle = match Self::spawn_cleanup_helper(dev.as_str()) {
547 Ok(handle) => Some(handle),
548 Err(e) => {
549 tracing::warn!(
550 "Failed to spawn loopback cleanup helper for {}: {}. \
551 Loopback device may not be cleaned up if process is interrupted.",
552 dev,
553 e
554 );
555 None
556 }
557 };
558
559 Ok(Self {
560 dev: Some(dev),
561 cleanup_handle,
562 })
563 }
564
565 pub fn path(&self) -> &Utf8Path {
567 self.dev.as_deref().unwrap()
569 }
570
571 fn spawn_cleanup_helper(device_path: &str) -> Result<LoopbackCleanupHandle> {
574 let bootc_path = bootc_utils::reexec::executable_path()
576 .context("Failed to locate bootc binary for cleanup helper")?;
577
578 let mut cmd = Command::new(bootc_path);
580 cmd.args([
581 "internals",
582 "loopback-cleanup-helper",
583 "--device",
584 device_path,
585 ]);
586
587 cmd.env("BOOTC_LOOPBACK_CLEANUP_HELPER", "1");
589
590 cmd.stdin(Stdio::null());
592 cmd.stdout(Stdio::null());
593 let child = cmd
597 .spawn()
598 .context("Failed to spawn loopback cleanup helper")?;
599
600 Ok(LoopbackCleanupHandle { child })
601 }
602
603 fn impl_close(&mut self) -> Result<()> {
605 let Some(dev) = self.dev.take() else {
607 tracing::trace!("loopback device already deallocated");
608 return Ok(());
609 };
610
611 if let Some(mut cleanup_handle) = self.cleanup_handle.take() {
613 let _ = cleanup_handle.child.kill();
615 }
616
617 Command::new("losetup")
618 .args(["-d", dev.as_str()])
619 .run_capture_stderr()
620 }
621
622 pub fn close(mut self) -> Result<()> {
624 self.impl_close()
625 }
626}
627
628impl Drop for LoopbackDevice {
629 fn drop(&mut self) {
630 let _ = self.impl_close();
632 }
633}
634
635pub async fn run_loopback_cleanup_helper(device_path: &str) -> Result<()> {
638 if std::env::var("BOOTC_LOOPBACK_CLEANUP_HELPER").is_err() {
640 anyhow::bail!("This function should only be called as a cleanup helper");
641 }
642
643 rustix::process::set_parent_process_death_signal(Some(rustix::process::Signal::TERM))
645 .context("Failed to set parent death signal")?;
646
647 tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
649 .expect("Failed to create signal stream")
650 .recv()
651 .await;
652
653 let output = std::process::Command::new("losetup")
655 .args(["-d", device_path])
656 .output();
657
658 match output {
659 Ok(output) if output.status.success() => {
660 tracing::info!("Cleaned up leaked loopback device {}", device_path);
662 std::process::exit(0);
663 }
664 Ok(output) => {
665 let stderr = String::from_utf8_lossy(&output.stderr);
666 tracing::error!(
667 "Failed to clean up loopback device {}: {}. Stderr: {}",
668 device_path,
669 output.status,
670 stderr.trim()
671 );
672 std::process::exit(1);
673 }
674 Err(e) => {
675 tracing::error!(
676 "Error executing losetup to clean up loopback device {}: {}",
677 device_path,
678 e
679 );
680 std::process::exit(1);
681 }
682 }
683}
684
685pub fn parse_size_mib(mut s: &str) -> Result<u64> {
687 let suffixes = [
688 ("MiB", 1u64),
689 ("M", 1u64),
690 ("GiB", 1024),
691 ("G", 1024),
692 ("TiB", 1024 * 1024),
693 ("T", 1024 * 1024),
694 ];
695 let mut mul = 1u64;
696 for (suffix, imul) in suffixes {
697 if let Some((sv, rest)) = s.rsplit_once(suffix) {
698 if !rest.is_empty() {
699 anyhow::bail!("Trailing text after size: {rest}");
700 }
701 s = sv;
702 mul = imul;
703 }
704 }
705 let v = s.parse::<u64>()?;
706 Ok(v * mul)
707}
708
709fn parse_partition_number_from_suffix(parent_path: &str, esp_path: &str) -> Option<String> {
721 let suffix = esp_path.strip_prefix(parent_path)?;
722 let digits = suffix.trim_start_matches(|c: char| !c.is_ascii_digit());
723 if digits.is_empty() {
724 return None;
725 }
726 Some(digits.to_string())
727}
728
729#[cfg(test)]
730mod test {
731 use super::*;
732
733 #[test]
734 fn test_parse_size_mib() {
735 let ident_cases = [0, 10, 9, 1024].into_iter().map(|k| (k.to_string(), k));
736 let cases = [
737 ("0M", 0),
738 ("10M", 10),
739 ("10MiB", 10),
740 ("1G", 1024),
741 ("9G", 9216),
742 ("11T", 11 * 1024 * 1024),
743 ]
744 .into_iter()
745 .map(|(k, v)| (k.to_string(), v));
746 for (s, v) in ident_cases.chain(cases) {
747 assert_eq!(parse_size_mib(&s).unwrap(), v as u64, "Parsing {s}");
748 }
749 }
750
751 #[test]
752 fn test_parse_lsblk() {
753 let fixture = include_str!("../tests/fixtures/lsblk.json");
754 let devs: DevicesOutput = serde_json::from_str(fixture).unwrap();
755 let dev = devs.blockdevices.into_iter().next().unwrap();
756 assert_eq!(dev.partn, None);
758 let children = dev.children.as_deref().unwrap();
759 assert_eq!(children.len(), 3);
760 let first_child = &children[0];
761 assert_eq!(first_child.partn, Some(1));
762 assert_eq!(
763 first_child.parttype.as_deref().unwrap(),
764 "21686148-6449-6e6f-744e-656564454649"
765 );
766 assert_eq!(
767 first_child.partuuid.as_deref().unwrap(),
768 "3979e399-262f-4666-aabc-7ab5d3add2f0"
769 );
770 let part2 = dev.find_device_by_partno(2).unwrap();
772 assert_eq!(part2.partn, Some(2));
773 assert_eq!(part2.parttype.as_deref().unwrap(), ESP);
774 let esp = dev.find_partition_of_esp().unwrap();
776 assert_eq!(esp.partn, Some(2));
777 let bios = dev.find_partition_of_bios_boot().unwrap();
779 assert_eq!(bios.partn, Some(1));
780 assert_eq!(bios.parttype.as_deref().unwrap(), BIOS_BOOT);
781 }
782
783 #[test]
787 fn test_parse_lsblk_no_udev() {
788 let fixture = include_str!("../tests/fixtures/lsblk-no-udev.json");
789 let devs: DevicesOutput = serde_json::from_str(fixture).unwrap();
790 let dev = devs.blockdevices.into_iter().next().unwrap();
791 assert!(dev.pttype.is_none());
793 let children = dev.children.as_deref().unwrap();
794 assert_eq!(children.len(), 3);
795 assert!(children[0].parttype.is_none());
796 assert!(children[1].parttype.is_none());
797 assert!(children[2].parttype.is_none());
798 assert!(dev.find_partition_of_esp_optional().unwrap().is_none());
800 assert!(dev.find_partition_of_bios_boot().is_none());
801 }
802
803 #[test]
804 fn test_parse_lsblk_mbr() {
805 let fixture = include_str!("../tests/fixtures/lsblk-mbr.json");
806 let devs: DevicesOutput = serde_json::from_str(fixture).unwrap();
807 let dev = devs.blockdevices.into_iter().next().unwrap();
808 assert_eq!(dev.partn, None);
810 assert_eq!(dev.pttype.as_deref().unwrap(), "dos");
811 let children = dev.children.as_deref().unwrap();
812 assert_eq!(children.len(), 3);
813 let first_child = &children[0];
815 assert_eq!(first_child.partn, Some(1));
816 assert_eq!(first_child.parttype.as_deref().unwrap(), "0x06");
817 assert_eq!(first_child.partuuid.as_deref().unwrap(), "a1b2c3d4-01");
818 assert_eq!(first_child.fstype.as_deref().unwrap(), "vfat");
819 assert!(first_child.partlabel.is_none());
821 let second_child = &children[1];
823 assert_eq!(second_child.partn, Some(2));
824 assert_eq!(second_child.parttype.as_deref().unwrap(), "0x83");
825 assert_eq!(second_child.partuuid.as_deref().unwrap(), "a1b2c3d4-02");
826 let third_child = &children[2];
828 assert_eq!(third_child.partn, Some(3));
829 assert_eq!(third_child.parttype.as_deref().unwrap(), "0xef");
830 assert_eq!(third_child.partuuid.as_deref().unwrap(), "a1b2c3d4-03");
831 let part1 = dev.find_device_by_partno(1).unwrap();
833 assert_eq!(part1.partn, Some(1));
834 let esp = dev.find_partition_of_esp().unwrap();
836 assert_eq!(esp.partn, Some(1));
837 }
838
839 fn make_mbr_disk(parttypes: &[&str]) -> Device {
841 Device {
842 name: "vda".into(),
843 serial: None,
844 model: None,
845 partlabel: None,
846 parttype: None,
847 partuuid: None,
848 partn: None,
849 size: 10737418240,
850 maj_min: None,
851 start: None,
852 label: None,
853 fstype: None,
854 uuid: None,
855 path: Some("/dev/vda".into()),
856 pttype: Some("dos".into()),
857 children: Some(
858 parttypes
859 .iter()
860 .enumerate()
861 .map(|(i, pt)| Device {
862 name: format!("vda{}", i + 1),
863 serial: None,
864 model: None,
865 partlabel: None,
866 parttype: Some(pt.to_string()),
867 partuuid: None,
868 partn: Some(i as u32 + 1),
869 size: 1048576,
870 maj_min: None,
871 start: Some(2048),
872 label: None,
873 fstype: None,
874 uuid: None,
875 path: None,
876 pttype: Some("dos".into()),
877 children: None,
878 })
879 .collect(),
880 ),
881 }
882 }
883
884 #[test]
885 fn test_parse_lsblk_vroc() {
886 let fixture = include_str!("../tests/fixtures/lsblk-vroc.json");
887 let devs: DevicesOutput = serde_json::from_str(fixture).unwrap();
888 assert_eq!(devs.blockdevices.len(), 2);
889
890 for nvme in &devs.blockdevices {
894 let esp = nvme.find_partition_of_esp().unwrap();
895 assert_eq!(esp.name, "md126p1");
896 assert_eq!(esp.partn, Some(1));
897 assert_eq!(esp.parttype.as_deref().unwrap(), ESP);
898 assert_eq!(esp.fstype.as_deref().unwrap(), "vfat");
899 }
900 }
901
902 #[test]
903 fn test_parse_lsblk_swraid() {
904 let fixture = include_str!("../tests/fixtures/lsblk-swraid.json");
905 let devs: DevicesOutput = serde_json::from_str(fixture).unwrap();
906 assert_eq!(devs.blockdevices.len(), 2);
907
908 let sda = &devs.blockdevices[0];
914 let esp = sda.find_partition_of_esp().unwrap();
915 assert_eq!(esp.name, "sda1");
916 assert_eq!(esp.partn, Some(1));
917 assert_eq!(esp.parttype.as_deref().unwrap(), ESP);
918 assert_eq!(esp.fstype.as_deref().unwrap(), "vfat");
919
920 let sdb = &devs.blockdevices[1];
921 let esp = sdb.find_partition_of_esp().unwrap();
922 assert_eq!(esp.name, "sdb1");
923 assert_eq!(esp.partn, Some(1));
924 assert_eq!(esp.parttype.as_deref().unwrap(), ESP);
925 assert_eq!(esp.fstype.as_deref().unwrap(), "vfat");
926
927 let sda3 = sda
930 .children
931 .as_ref()
932 .unwrap()
933 .iter()
934 .find(|c| c.name == "sda3")
935 .unwrap();
936 assert_eq!(sda3.fstype.as_deref().unwrap(), "linux_raid_member");
937 let md0 = sda3
938 .children
939 .as_ref()
940 .unwrap()
941 .iter()
942 .find(|c| c.name == "md0")
943 .unwrap();
944 assert_eq!(md0.fstype.as_deref().unwrap(), "ext4");
945 }
946
947 #[test]
948 fn test_mbr_esp_detection() {
949 let dev = make_mbr_disk(&["0x06"]);
951 assert_eq!(dev.find_partition_of_esp().unwrap().partn, Some(1));
952
953 let dev = make_mbr_disk(&["0x83", "0xef"]);
955 assert_eq!(dev.find_partition_of_esp().unwrap().partn, Some(2));
956
957 let dev = make_mbr_disk(&["0x83", "0x82"]);
959 assert!(dev.find_partition_of_esp().is_err());
960 }
961
962 #[test]
963 fn test_parse_partition_number_from_suffix() {
964 assert_eq!(
966 parse_partition_number_from_suffix("/dev/mapper/mpatha", "/dev/mapper/mpatha2"),
967 Some("2".into())
968 );
969 assert_eq!(
971 parse_partition_number_from_suffix("/dev/mapper/mpatha", "/dev/mapper/mpathap2"),
972 Some("2".into())
973 );
974 assert_eq!(
976 parse_partition_number_from_suffix(
977 "/dev/mapper/3600508b4001",
978 "/dev/mapper/3600508b4001-part1"
979 ),
980 Some("1".into())
981 );
982 assert_eq!(
984 parse_partition_number_from_suffix("/dev/mapper/mpatha", "/dev/mapper/mpatha12"),
985 Some("12".into())
986 );
987 assert_eq!(
989 parse_partition_number_from_suffix("/dev/mapper/mpatha", "/dev/sda1"),
990 None
991 );
992 assert_eq!(
994 parse_partition_number_from_suffix("/dev/mapper/mpatha", "/dev/mapper/mpathap"),
995 None
996 );
997 assert_eq!(
999 parse_partition_number_from_suffix("/dev/mapper/mpatha", "/dev/mapper/mpatha"),
1000 None
1001 );
1002 }
1003}