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> {
159 let esp_device = self.find_partition_of_esp()?;
160 let devname = &esp_device.name;
161
162 let partition_path = Utf8PathBuf::from(format!("/sys/class/block/{devname}/partition"));
163 if partition_path.exists() {
164 return std::fs::read_to_string(&partition_path)
165 .with_context(|| format!("Failed to read {partition_path}"));
166 }
167
168 if self.is_mpath()? {
170 if let Some(partn) = esp_device.partn {
171 return Ok(partn.to_string());
172 }
173 }
174 anyhow::bail!("Not supported for {devname}")
175 }
176
177 pub fn find_partition_of_bios_boot(&self) -> Option<&Device> {
179 self.find_partition_of_type(BIOS_BOOT)
180 }
181
182 pub fn find_colocated_esps(&self) -> Result<Option<Vec<Device>>> {
186 let mut esps = Vec::new();
187 for root in &self.find_all_roots()? {
188 if let Some(esp) = root.find_partition_of_esp_optional()? {
189 esps.push(esp.clone());
190 }
191 }
192 Ok((!esps.is_empty()).then_some(esps))
193 }
194
195 pub fn find_first_colocated_esp(&self) -> Result<Device> {
201 self.find_colocated_esps()?
202 .and_then(|mut v| Some(v.remove(0)))
203 .ok_or_else(|| anyhow!("No ESP partition found among backing devices"))
204 }
205
206 pub fn find_colocated_bios_boot(&self) -> Result<Option<Vec<Device>>> {
210 let bios_boots: Vec<_> = self
211 .find_all_roots()?
212 .iter()
213 .filter_map(|root| root.find_partition_of_bios_boot())
214 .cloned()
215 .collect();
216 Ok((!bios_boots.is_empty()).then_some(bios_boots))
217 }
218
219 pub fn find_partition_of_type(&self, parttype: &str) -> Option<&Device> {
221 self.children.as_ref()?.iter().find(|child| {
222 child
223 .parttype
224 .as_ref()
225 .is_some_and(|pt| pt.eq_ignore_ascii_case(parttype))
226 })
227 }
228
229 pub fn find_partition_of_esp_optional(&self) -> Result<Option<&Device>> {
242 let Some(children) = self.children.as_ref() else {
243 return Ok(None);
244 };
245 let direct = match self.pttype.as_deref() {
246 Some("dos") => children.iter().find(|child| {
247 child
248 .parttype
249 .as_ref()
250 .and_then(|pt| {
251 let pt = pt.strip_prefix("0x").unwrap_or(pt);
252 u8::from_str_radix(pt, 16).ok()
253 })
254 .is_some_and(|pt| ESP_ID_MBR.contains(&pt))
255 }),
256 Some("gpt") | None => self.find_partition_of_type(ESP),
259 Some(other) => return Err(anyhow!("Unsupported partition table type: {other}")),
260 };
261 if direct.is_some() {
262 return Ok(direct);
263 }
264 for child in children {
267 if child.pttype.is_some() {
268 if let Some(esp) = child.find_partition_of_esp_optional()? {
269 return Ok(Some(esp));
270 }
271 }
272 }
273 Ok(None)
274 }
275
276 pub fn find_partition_of_esp(&self) -> Result<&Device> {
281 self.find_partition_of_esp_optional()?
282 .ok_or_else(|| anyhow!("ESP partition not found on {}", self.path()))
283 }
284
285 pub fn find_device_by_partno(&self, partno: u32) -> Result<&Device> {
287 self.children
288 .as_ref()
289 .ok_or_else(|| anyhow!("Device has no children"))?
290 .iter()
291 .find(|child| child.partn == Some(partno))
292 .ok_or_else(|| anyhow!("Missing partition for index {partno}"))
293 }
294
295 pub fn refresh(&mut self) -> Result<()> {
298 let path = self.path();
299 let new_device = list_dev(Utf8Path::new(&path))?;
300 *self = new_device;
301 Ok(())
302 }
303
304 fn read_sysfs_property<T>(&self, property: &str) -> Result<Option<T>>
306 where
307 T: std::str::FromStr,
308 T::Err: std::error::Error + Send + Sync + 'static,
309 {
310 let Some(majmin) = self.maj_min.as_deref() else {
311 return Ok(None);
312 };
313 let sysfs_path = format!("/sys/dev/block/{majmin}/{property}");
314 if !Utf8Path::new(&sysfs_path).try_exists()? {
315 return Ok(None);
316 }
317 let value = std::fs::read_to_string(&sysfs_path)
318 .with_context(|| format!("Reading {sysfs_path}"))?;
319 let parsed = value
320 .trim()
321 .parse()
322 .with_context(|| format!("Parsing sysfs {property} property"))?;
323 tracing::debug!("backfilled {property} to {value}");
324 Ok(Some(parsed))
325 }
326
327 pub fn backfill_missing(&mut self) -> Result<()> {
334 if self.start.is_none() {
337 self.start = self.read_sysfs_property("start")?;
338 }
339 if self.partn.is_none() {
342 self.partn = self.read_sysfs_property("partition")?;
343 }
344 if !have_udev() && (self.parttype.is_none() || self.pttype.is_none()) {
348 let props = blkid_probe(&self.path())?;
349 if self.parttype.is_none() {
350 self.parttype = props.get("PART_ENTRY_TYPE").cloned();
351 }
352 if self.pttype.is_none() {
353 self.pttype = props.get("PTTYPE").cloned();
354 }
355 }
356 for child in self.children.iter_mut().flatten() {
358 child.backfill_missing()?;
359 }
360 Ok(())
361 }
362
363 pub fn list_parents(&self) -> Result<Option<Vec<Device>>> {
370 let path = self.path();
371 let output: DevicesOutput = Command::new("lsblk")
372 .args(["-J", "-b", "-O", "--inverse"])
373 .arg(&path)
374 .log_debug()
375 .run_and_parse_json()?;
376
377 let device = output
378 .blockdevices
379 .into_iter()
380 .next()
381 .ok_or_else(|| anyhow!("no device output from lsblk --inverse for {path}"))?;
382
383 match device.children {
384 Some(mut children) if !children.is_empty() => {
385 for child in &mut children {
386 child.backfill_missing()?;
387 }
388 Ok(Some(children))
389 }
390 _ => Ok(None),
391 }
392 }
393
394 pub fn require_single_root(&self) -> Result<Device> {
400 let mut roots = self.find_all_roots()?;
401 match roots.len() {
402 1 => Ok(roots.remove(0)),
403 n => anyhow::bail!(
404 "Expected a single root device for {}, but found {n}",
405 self.path()
406 ),
407 }
408 }
409
410 pub fn find_all_roots(&self) -> Result<Vec<Device>> {
417 let Some(parents) = self.list_parents()? else {
418 return Ok(vec![list_dev(Utf8Path::new(&self.path()))?]);
420 };
421
422 let mut roots = Vec::new();
423 let mut seen = HashSet::new();
424 let mut queue = parents;
425 while let Some(mut device) = queue.pop() {
426 match device.children.take() {
427 Some(grandparents) if !grandparents.is_empty() => {
428 queue.extend(grandparents);
429 }
430 _ => {
431 let name = device.name.clone();
434 if seen.insert(name) {
435 roots.push(list_dev(Utf8Path::new(&device.path()))?);
437 }
438 }
439 }
440 }
441 Ok(roots)
442 }
443}
444
445#[context("Listing device {dev}")]
446pub fn list_dev(dev: &Utf8Path) -> Result<Device> {
447 let mut devs: DevicesOutput = Command::new("lsblk")
448 .args(["-J", "-b", "-O"])
449 .arg(dev)
450 .log_debug()
451 .run_and_parse_json()?;
452 for dev in devs.blockdevices.iter_mut() {
453 dev.backfill_missing()?;
454 }
455 devs.blockdevices
456 .into_iter()
457 .next()
458 .ok_or_else(|| anyhow!("no device output from lsblk for {dev}"))
459}
460
461#[context("Finding block device for ZFS dataset {dataset}")]
462fn list_dev_for_zfs_dataset(dataset: &str) -> Result<Device> {
463 let dataset = dataset.strip_prefix("ZFS=").unwrap_or(dataset);
464 let pool = dataset
465 .split('/')
466 .next()
467 .ok_or_else(|| anyhow!("Invalid ZFS dataset: {dataset}"))?;
468
469 let output = Command::new("zpool")
470 .args(["list", "-H", "-v", "-P", pool])
471 .run_get_string()
472 .with_context(|| format!("Querying ZFS pool {pool}"))?;
473
474 for line in output.lines() {
475 if line.starts_with('\t') || line.starts_with(' ') {
476 let dev_path = line.trim_start().split('\t').next().unwrap_or("").trim();
477 if dev_path.starts_with('/') {
478 return list_dev(Utf8Path::new(dev_path));
479 }
480 }
481 }
482
483 anyhow::bail!("Could not find a block device backing ZFS pool {pool}")
484}
485
486pub fn list_dev_by_dir(dir: &Dir) -> Result<Device> {
488 let fsinfo = bootc_mount::inspect_filesystem_of_dir(dir)?;
489 let source = &fsinfo.source;
490 if fsinfo.fstype == "zfs" || source.starts_with("ZFS=") {
491 return list_dev_for_zfs_dataset(source);
492 }
493 list_dev(&Utf8PathBuf::from(source))
494}
495
496pub struct LoopbackDevice {
497 pub dev: Option<Utf8PathBuf>,
498 cleanup_handle: Option<LoopbackCleanupHandle>,
500}
501
502struct LoopbackCleanupHandle {
504 child: std::process::Child,
506}
507
508impl LoopbackDevice {
509 pub fn new(path: &Path) -> Result<Self> {
511 let direct_io = match env::var("BOOTC_DIRECT_IO") {
512 Ok(val) => {
513 if val == "on" {
514 "on"
515 } else {
516 "off"
517 }
518 }
519 Err(_e) => "off",
520 };
521
522 let dev = Command::new("losetup")
523 .args([
524 "--show",
525 format!("--direct-io={direct_io}").as_str(),
526 "-P",
527 "--find",
528 ])
529 .arg(path)
530 .run_get_string()?;
531 let dev = Utf8PathBuf::from(dev.trim());
532 tracing::debug!("Allocated loopback {dev}");
533
534 let cleanup_handle = match Self::spawn_cleanup_helper(dev.as_str()) {
536 Ok(handle) => Some(handle),
537 Err(e) => {
538 tracing::warn!(
539 "Failed to spawn loopback cleanup helper for {}: {}. \
540 Loopback device may not be cleaned up if process is interrupted.",
541 dev,
542 e
543 );
544 None
545 }
546 };
547
548 Ok(Self {
549 dev: Some(dev),
550 cleanup_handle,
551 })
552 }
553
554 pub fn path(&self) -> &Utf8Path {
556 self.dev.as_deref().unwrap()
558 }
559
560 fn spawn_cleanup_helper(device_path: &str) -> Result<LoopbackCleanupHandle> {
563 let bootc_path = bootc_utils::reexec::executable_path()
565 .context("Failed to locate bootc binary for cleanup helper")?;
566
567 let mut cmd = Command::new(bootc_path);
569 cmd.args([
570 "internals",
571 "loopback-cleanup-helper",
572 "--device",
573 device_path,
574 ]);
575
576 cmd.env("BOOTC_LOOPBACK_CLEANUP_HELPER", "1");
578
579 cmd.stdin(Stdio::null());
581 cmd.stdout(Stdio::null());
582 let child = cmd
586 .spawn()
587 .context("Failed to spawn loopback cleanup helper")?;
588
589 Ok(LoopbackCleanupHandle { child })
590 }
591
592 fn impl_close(&mut self) -> Result<()> {
594 let Some(dev) = self.dev.take() else {
596 tracing::trace!("loopback device already deallocated");
597 return Ok(());
598 };
599
600 if let Some(mut cleanup_handle) = self.cleanup_handle.take() {
602 let _ = cleanup_handle.child.kill();
604 }
605
606 Command::new("losetup")
607 .args(["-d", dev.as_str()])
608 .run_capture_stderr()
609 }
610
611 pub fn close(mut self) -> Result<()> {
613 self.impl_close()
614 }
615}
616
617impl Drop for LoopbackDevice {
618 fn drop(&mut self) {
619 let _ = self.impl_close();
621 }
622}
623
624pub async fn run_loopback_cleanup_helper(device_path: &str) -> Result<()> {
627 if std::env::var("BOOTC_LOOPBACK_CLEANUP_HELPER").is_err() {
629 anyhow::bail!("This function should only be called as a cleanup helper");
630 }
631
632 rustix::process::set_parent_process_death_signal(Some(rustix::process::Signal::TERM))
634 .context("Failed to set parent death signal")?;
635
636 tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
638 .expect("Failed to create signal stream")
639 .recv()
640 .await;
641
642 let output = std::process::Command::new("losetup")
644 .args(["-d", device_path])
645 .output();
646
647 match output {
648 Ok(output) if output.status.success() => {
649 tracing::info!("Cleaned up leaked loopback device {}", device_path);
651 std::process::exit(0);
652 }
653 Ok(output) => {
654 let stderr = String::from_utf8_lossy(&output.stderr);
655 tracing::error!(
656 "Failed to clean up loopback device {}: {}. Stderr: {}",
657 device_path,
658 output.status,
659 stderr.trim()
660 );
661 std::process::exit(1);
662 }
663 Err(e) => {
664 tracing::error!(
665 "Error executing losetup to clean up loopback device {}: {}",
666 device_path,
667 e
668 );
669 std::process::exit(1);
670 }
671 }
672}
673
674pub fn parse_size_mib(mut s: &str) -> Result<u64> {
676 let suffixes = [
677 ("MiB", 1u64),
678 ("M", 1u64),
679 ("GiB", 1024),
680 ("G", 1024),
681 ("TiB", 1024 * 1024),
682 ("T", 1024 * 1024),
683 ];
684 let mut mul = 1u64;
685 for (suffix, imul) in suffixes {
686 if let Some((sv, rest)) = s.rsplit_once(suffix) {
687 if !rest.is_empty() {
688 anyhow::bail!("Trailing text after size: {rest}");
689 }
690 s = sv;
691 mul = imul;
692 }
693 }
694 let v = s.parse::<u64>()?;
695 Ok(v * mul)
696}
697
698#[cfg(test)]
699mod test {
700 use super::*;
701
702 #[test]
703 fn test_parse_size_mib() {
704 let ident_cases = [0, 10, 9, 1024].into_iter().map(|k| (k.to_string(), k));
705 let cases = [
706 ("0M", 0),
707 ("10M", 10),
708 ("10MiB", 10),
709 ("1G", 1024),
710 ("9G", 9216),
711 ("11T", 11 * 1024 * 1024),
712 ]
713 .into_iter()
714 .map(|(k, v)| (k.to_string(), v));
715 for (s, v) in ident_cases.chain(cases) {
716 assert_eq!(parse_size_mib(&s).unwrap(), v as u64, "Parsing {s}");
717 }
718 }
719
720 #[test]
721 fn test_parse_lsblk() {
722 let fixture = include_str!("../tests/fixtures/lsblk.json");
723 let devs: DevicesOutput = serde_json::from_str(fixture).unwrap();
724 let dev = devs.blockdevices.into_iter().next().unwrap();
725 assert_eq!(dev.partn, None);
727 let children = dev.children.as_deref().unwrap();
728 assert_eq!(children.len(), 3);
729 let first_child = &children[0];
730 assert_eq!(first_child.partn, Some(1));
731 assert_eq!(
732 first_child.parttype.as_deref().unwrap(),
733 "21686148-6449-6e6f-744e-656564454649"
734 );
735 assert_eq!(
736 first_child.partuuid.as_deref().unwrap(),
737 "3979e399-262f-4666-aabc-7ab5d3add2f0"
738 );
739 let part2 = dev.find_device_by_partno(2).unwrap();
741 assert_eq!(part2.partn, Some(2));
742 assert_eq!(part2.parttype.as_deref().unwrap(), ESP);
743 let esp = dev.find_partition_of_esp().unwrap();
745 assert_eq!(esp.partn, Some(2));
746 let bios = dev.find_partition_of_bios_boot().unwrap();
748 assert_eq!(bios.partn, Some(1));
749 assert_eq!(bios.parttype.as_deref().unwrap(), BIOS_BOOT);
750 }
751
752 #[test]
756 fn test_parse_lsblk_no_udev() {
757 let fixture = include_str!("../tests/fixtures/lsblk-no-udev.json");
758 let devs: DevicesOutput = serde_json::from_str(fixture).unwrap();
759 let dev = devs.blockdevices.into_iter().next().unwrap();
760 assert!(dev.pttype.is_none());
762 let children = dev.children.as_deref().unwrap();
763 assert_eq!(children.len(), 3);
764 assert!(children[0].parttype.is_none());
765 assert!(children[1].parttype.is_none());
766 assert!(children[2].parttype.is_none());
767 assert!(dev.find_partition_of_esp_optional().unwrap().is_none());
769 assert!(dev.find_partition_of_bios_boot().is_none());
770 }
771
772 #[test]
773 fn test_parse_lsblk_mbr() {
774 let fixture = include_str!("../tests/fixtures/lsblk-mbr.json");
775 let devs: DevicesOutput = serde_json::from_str(fixture).unwrap();
776 let dev = devs.blockdevices.into_iter().next().unwrap();
777 assert_eq!(dev.partn, None);
779 assert_eq!(dev.pttype.as_deref().unwrap(), "dos");
780 let children = dev.children.as_deref().unwrap();
781 assert_eq!(children.len(), 3);
782 let first_child = &children[0];
784 assert_eq!(first_child.partn, Some(1));
785 assert_eq!(first_child.parttype.as_deref().unwrap(), "0x06");
786 assert_eq!(first_child.partuuid.as_deref().unwrap(), "a1b2c3d4-01");
787 assert_eq!(first_child.fstype.as_deref().unwrap(), "vfat");
788 assert!(first_child.partlabel.is_none());
790 let second_child = &children[1];
792 assert_eq!(second_child.partn, Some(2));
793 assert_eq!(second_child.parttype.as_deref().unwrap(), "0x83");
794 assert_eq!(second_child.partuuid.as_deref().unwrap(), "a1b2c3d4-02");
795 let third_child = &children[2];
797 assert_eq!(third_child.partn, Some(3));
798 assert_eq!(third_child.parttype.as_deref().unwrap(), "0xef");
799 assert_eq!(third_child.partuuid.as_deref().unwrap(), "a1b2c3d4-03");
800 let part1 = dev.find_device_by_partno(1).unwrap();
802 assert_eq!(part1.partn, Some(1));
803 let esp = dev.find_partition_of_esp().unwrap();
805 assert_eq!(esp.partn, Some(1));
806 }
807
808 fn make_mbr_disk(parttypes: &[&str]) -> Device {
810 Device {
811 name: "vda".into(),
812 serial: None,
813 model: None,
814 partlabel: None,
815 parttype: None,
816 partuuid: None,
817 partn: None,
818 size: 10737418240,
819 maj_min: None,
820 start: None,
821 label: None,
822 fstype: None,
823 uuid: None,
824 path: Some("/dev/vda".into()),
825 pttype: Some("dos".into()),
826 children: Some(
827 parttypes
828 .iter()
829 .enumerate()
830 .map(|(i, pt)| Device {
831 name: format!("vda{}", i + 1),
832 serial: None,
833 model: None,
834 partlabel: None,
835 parttype: Some(pt.to_string()),
836 partuuid: None,
837 partn: Some(i as u32 + 1),
838 size: 1048576,
839 maj_min: None,
840 start: Some(2048),
841 label: None,
842 fstype: None,
843 uuid: None,
844 path: None,
845 pttype: Some("dos".into()),
846 children: None,
847 })
848 .collect(),
849 ),
850 }
851 }
852
853 #[test]
854 fn test_parse_lsblk_vroc() {
855 let fixture = include_str!("../tests/fixtures/lsblk-vroc.json");
856 let devs: DevicesOutput = serde_json::from_str(fixture).unwrap();
857 assert_eq!(devs.blockdevices.len(), 2);
858
859 for nvme in &devs.blockdevices {
863 let esp = nvme.find_partition_of_esp().unwrap();
864 assert_eq!(esp.name, "md126p1");
865 assert_eq!(esp.partn, Some(1));
866 assert_eq!(esp.parttype.as_deref().unwrap(), ESP);
867 assert_eq!(esp.fstype.as_deref().unwrap(), "vfat");
868 }
869 }
870
871 #[test]
872 fn test_parse_lsblk_swraid() {
873 let fixture = include_str!("../tests/fixtures/lsblk-swraid.json");
874 let devs: DevicesOutput = serde_json::from_str(fixture).unwrap();
875 assert_eq!(devs.blockdevices.len(), 2);
876
877 let sda = &devs.blockdevices[0];
883 let esp = sda.find_partition_of_esp().unwrap();
884 assert_eq!(esp.name, "sda1");
885 assert_eq!(esp.partn, Some(1));
886 assert_eq!(esp.parttype.as_deref().unwrap(), ESP);
887 assert_eq!(esp.fstype.as_deref().unwrap(), "vfat");
888
889 let sdb = &devs.blockdevices[1];
890 let esp = sdb.find_partition_of_esp().unwrap();
891 assert_eq!(esp.name, "sdb1");
892 assert_eq!(esp.partn, Some(1));
893 assert_eq!(esp.parttype.as_deref().unwrap(), ESP);
894 assert_eq!(esp.fstype.as_deref().unwrap(), "vfat");
895
896 let sda3 = sda
899 .children
900 .as_ref()
901 .unwrap()
902 .iter()
903 .find(|c| c.name == "sda3")
904 .unwrap();
905 assert_eq!(sda3.fstype.as_deref().unwrap(), "linux_raid_member");
906 let md0 = sda3
907 .children
908 .as_ref()
909 .unwrap()
910 .iter()
911 .find(|c| c.name == "md0")
912 .unwrap();
913 assert_eq!(md0.fstype.as_deref().unwrap(), "ext4");
914 }
915
916 #[test]
917 fn test_mbr_esp_detection() {
918 let dev = make_mbr_disk(&["0x06"]);
920 assert_eq!(dev.find_partition_of_esp().unwrap().partn, Some(1));
921
922 let dev = make_mbr_disk(&["0x83", "0xef"]);
924 assert_eq!(dev.find_partition_of_esp().unwrap().partn, Some(2));
925
926 let dev = make_mbr_disk(&["0x83", "0x82"]);
928 assert!(dev.find_partition_of_esp().is_err());
929 }
930}