@@ -6,13 +6,16 @@ use std::sync::OnceLock;
66
77use anyhow:: { Context , Result , anyhow} ;
88use camino:: { Utf8Path , Utf8PathBuf } ;
9+ use cap_std_ext:: cap_std:: fs:: Dir ;
910use fn_error_context:: context;
1011use regex:: Regex ;
1112use serde:: Deserialize ;
1213
1314use bootc_utils:: CommandRunExt ;
1415
15- /// EFI System Partition (ESP) on MBR
16+ /// MBR partition type IDs that indicate an EFI System Partition.
17+ /// 0x06 is FAT16 (used as ESP on some MBR systems), 0xEF is the
18+ /// explicit EFI System Partition type.
1619/// Refer to <https://en.wikipedia.org/wiki/Partition_type>
1720pub const ESP_ID_MBR : & [ u8 ] = & [ 0x06 , 0xEF ] ;
1821
@@ -25,7 +28,7 @@ struct DevicesOutput {
2528}
2629
2730#[ allow( dead_code) ]
28- #[ derive( Debug , Deserialize ) ]
31+ #[ derive( Debug , Clone , Deserialize ) ]
2932pub struct Device {
3033 pub name : String ,
3134 pub serial : Option < String > ,
@@ -48,6 +51,8 @@ pub struct Device {
4851 pub fstype : Option < String > ,
4952 pub uuid : Option < String > ,
5053 pub path : Option < String > ,
54+ /// Partition table type (e.g., "gpt", "dos"). Only present on whole disk devices.
55+ pub pttype : Option < String > ,
5156}
5257
5358impl Device {
@@ -62,6 +67,67 @@ impl Device {
6267 self . children . as_ref ( ) . is_some_and ( |v| !v. is_empty ( ) )
6368 }
6469
70+ /// Find a child partition by partition type (case-insensitive).
71+ pub fn find_partition_of_type ( & self , parttype : & str ) -> Option < & Device > {
72+ self . children . as_ref ( ) ?. iter ( ) . find ( |child| {
73+ child
74+ . parttype
75+ . as_ref ( )
76+ . is_some_and ( |pt| pt. eq_ignore_ascii_case ( parttype) )
77+ } )
78+ }
79+
80+ /// Find the EFI System Partition (ESP) among children.
81+ ///
82+ /// For GPT disks, this matches by the ESP partition type GUID.
83+ /// For MBR (dos) disks, this matches by the MBR partition type IDs (0x06 or 0xEF).
84+ pub fn find_partition_of_esp ( & self ) -> Result < & Device > {
85+ let children = self
86+ . children
87+ . as_ref ( )
88+ . ok_or_else ( || anyhow ! ( "Device has no children" ) ) ?;
89+ match self . pttype . as_deref ( ) {
90+ Some ( "dos" ) => children
91+ . iter ( )
92+ . find ( |child| {
93+ child
94+ . parttype
95+ . as_ref ( )
96+ . and_then ( |pt| {
97+ let pt = pt. strip_prefix ( "0x" ) . unwrap_or ( pt) ;
98+ u8:: from_str_radix ( pt, 16 ) . ok ( )
99+ } )
100+ . is_some_and ( |pt| ESP_ID_MBR . contains ( & pt) )
101+ } )
102+ . ok_or_else ( || anyhow ! ( "ESP not found in MBR partition table" ) ) ,
103+ // When pttype is None (e.g. older lsblk or partition devices), default
104+ // to GPT UUID matching which will simply not match MBR hex types.
105+ Some ( "gpt" ) | None => self
106+ . find_partition_of_type ( ESP )
107+ . ok_or_else ( || anyhow ! ( "ESP not found in GPT partition table" ) ) ,
108+ Some ( other) => Err ( anyhow ! ( "Unsupported partition table type: {other}" ) ) ,
109+ }
110+ }
111+
112+ /// Find a child partition by partition number (1-indexed).
113+ pub fn find_device_by_partno ( & self , partno : u32 ) -> Result < & Device > {
114+ self . children
115+ . as_ref ( )
116+ . ok_or_else ( || anyhow ! ( "Device has no children" ) ) ?
117+ . iter ( )
118+ . find ( |child| child. partn == Some ( partno) )
119+ . ok_or_else ( || anyhow ! ( "Missing partition for index {partno}" ) )
120+ }
121+
122+ /// Re-query this device's information from lsblk, updating all fields.
123+ /// This is useful after partitioning when the device's children have changed.
124+ pub fn refresh ( & mut self ) -> Result < ( ) > {
125+ let path = self . path ( ) ;
126+ let new_device = list_dev ( Utf8Path :: new ( & path) ) ?;
127+ * self = new_device;
128+ Ok ( ( ) )
129+ }
130+
65131 /// Read a sysfs property for this device and parse it as the target type.
66132 fn read_sysfs_property < T > ( & self , property : & str ) -> Result < Option < T > >
67133 where
@@ -103,6 +169,67 @@ impl Device {
103169 }
104170 Ok ( ( ) )
105171 }
172+
173+ /// Query parent devices via `lsblk --inverse`.
174+ ///
175+ /// Returns `Ok(None)` if this device is already a root device (no parents).
176+ /// In the returned `Vec<Device>`, each device's `children` field contains
177+ /// *its own* parents (grandparents, etc.), forming the full chain to the
178+ /// root device(s). A device can have multiple parents (e.g. RAID, LVM).
179+ pub fn list_parents ( & self ) -> Result < Option < Vec < Device > > > {
180+ let path = self . path ( ) ;
181+ let output: DevicesOutput = Command :: new ( "lsblk" )
182+ . args ( [ "-J" , "-b" , "-O" , "--inverse" ] )
183+ . arg ( & path)
184+ . log_debug ( )
185+ . run_and_parse_json ( ) ?;
186+
187+ let device = output
188+ . blockdevices
189+ . into_iter ( )
190+ . next ( )
191+ . ok_or_else ( || anyhow ! ( "no device output from lsblk --inverse for {path}" ) ) ?;
192+
193+ match device. children {
194+ Some ( mut children) if !children. is_empty ( ) => {
195+ for child in & mut children {
196+ child. backfill_missing ( ) ?;
197+ }
198+ Ok ( Some ( children) )
199+ }
200+ _ => Ok ( None ) ,
201+ }
202+ }
203+
204+ /// Walk the parent chain to find the root (whole disk) device.
205+ ///
206+ /// Returns the root device with its children (partitions) populated.
207+ /// If this device is already a root device, returns a clone of `self`.
208+ /// Fails if the device has multiple parents at any level.
209+ pub fn root_disk ( & self ) -> Result < Device > {
210+ let Some ( parents) = self . list_parents ( ) ? else {
211+ // Already a root device; re-query to ensure children are populated
212+ return list_dev ( Utf8Path :: new ( & self . path ( ) ) ) ;
213+ } ;
214+ let mut current = parents;
215+ loop {
216+ anyhow:: ensure!(
217+ current. len( ) == 1 ,
218+ "Device {} has multiple parents; cannot determine root disk" ,
219+ self . path( )
220+ ) ;
221+ let mut parent = current. into_iter ( ) . next ( ) . unwrap ( ) ;
222+ match parent. children . take ( ) {
223+ Some ( grandparents) if !grandparents. is_empty ( ) => {
224+ current = grandparents;
225+ }
226+ _ => {
227+ // Found the root; re-query to populate its actual children
228+ return list_dev ( Utf8Path :: new ( & parent. path ( ) ) ) ;
229+ }
230+ }
231+ }
232+ }
106233}
107234
108235#[ context( "Listing device {dev}" ) ]
@@ -121,6 +248,12 @@ pub fn list_dev(dev: &Utf8Path) -> Result<Device> {
121248 . ok_or_else ( || anyhow ! ( "no device output from lsblk for {dev}" ) )
122249}
123250
251+ /// List the device containing the filesystem mounted at the given directory.
252+ pub fn list_dev_by_dir ( dir : & Dir ) -> Result < Device > {
253+ let fsinfo = bootc_mount:: inspect_filesystem_of_dir ( dir) ?;
254+ list_dev ( & Utf8PathBuf :: from ( & fsinfo. source ) )
255+ }
256+
124257#[ derive( Debug , Deserialize ) ]
125258struct SfDiskOutput {
126259 partitiontable : PartitionTable ,
@@ -697,4 +830,100 @@ mod test {
697830 assert_eq ! ( esp1. node, "/dev/mmcblk0p1" ) ;
698831 Ok ( ( ) )
699832 }
833+
834+ #[ test]
835+ fn test_parse_lsblk_mbr ( ) {
836+ let fixture = include_str ! ( "../tests/fixtures/lsblk-mbr.json" ) ;
837+ let devs: DevicesOutput = serde_json:: from_str ( fixture) . unwrap ( ) ;
838+ let dev = devs. blockdevices . into_iter ( ) . next ( ) . unwrap ( ) ;
839+ // The parent device has no partition number and is MBR
840+ assert_eq ! ( dev. partn, None ) ;
841+ assert_eq ! ( dev. pttype. as_deref( ) . unwrap( ) , "dos" ) ;
842+ let children = dev. children . as_deref ( ) . unwrap ( ) ;
843+ assert_eq ! ( children. len( ) , 3 ) ;
844+ // First partition: FAT16 boot partition (MBR type 0x06, an ESP type)
845+ let first_child = & children[ 0 ] ;
846+ assert_eq ! ( first_child. partn, Some ( 1 ) ) ;
847+ assert_eq ! ( first_child. parttype. as_deref( ) . unwrap( ) , "0x06" ) ;
848+ assert_eq ! ( first_child. partuuid. as_deref( ) . unwrap( ) , "a1b2c3d4-01" ) ;
849+ assert_eq ! ( first_child. fstype. as_deref( ) . unwrap( ) , "vfat" ) ;
850+ // MBR partitions have no partlabel
851+ assert ! ( first_child. partlabel. is_none( ) ) ;
852+ // Second partition: Linux root (MBR type 0x83)
853+ let second_child = & children[ 1 ] ;
854+ assert_eq ! ( second_child. partn, Some ( 2 ) ) ;
855+ assert_eq ! ( second_child. parttype. as_deref( ) . unwrap( ) , "0x83" ) ;
856+ assert_eq ! ( second_child. partuuid. as_deref( ) . unwrap( ) , "a1b2c3d4-02" ) ;
857+ // Third partition: EFI System Partition (MBR type 0xef)
858+ let third_child = & children[ 2 ] ;
859+ assert_eq ! ( third_child. partn, Some ( 3 ) ) ;
860+ assert_eq ! ( third_child. parttype. as_deref( ) . unwrap( ) , "0xef" ) ;
861+ assert_eq ! ( third_child. partuuid. as_deref( ) . unwrap( ) , "a1b2c3d4-03" ) ;
862+ // Verify find_device_by_partno works on MBR
863+ let part1 = dev. find_device_by_partno ( 1 ) . unwrap ( ) ;
864+ assert_eq ! ( part1. partn, Some ( 1 ) ) ;
865+ // find_partition_of_esp returns the first matching ESP type (0x06 on partition 1)
866+ let esp = dev. find_partition_of_esp ( ) . unwrap ( ) ;
867+ assert_eq ! ( esp. partn, Some ( 1 ) ) ;
868+ }
869+
870+ /// Helper to construct a minimal MBR disk Device with given child partition types.
871+ fn make_mbr_disk ( parttypes : & [ & str ] ) -> Device {
872+ Device {
873+ name : "vda" . into ( ) ,
874+ serial : None ,
875+ model : None ,
876+ partlabel : None ,
877+ parttype : None ,
878+ partuuid : None ,
879+ partn : None ,
880+ size : 10737418240 ,
881+ maj_min : None ,
882+ start : None ,
883+ label : None ,
884+ fstype : None ,
885+ uuid : None ,
886+ path : Some ( "/dev/vda" . into ( ) ) ,
887+ pttype : Some ( "dos" . into ( ) ) ,
888+ children : Some (
889+ parttypes
890+ . iter ( )
891+ . enumerate ( )
892+ . map ( |( i, pt) | Device {
893+ name : format ! ( "vda{}" , i + 1 ) ,
894+ serial : None ,
895+ model : None ,
896+ partlabel : None ,
897+ parttype : Some ( pt. to_string ( ) ) ,
898+ partuuid : None ,
899+ partn : Some ( i as u32 + 1 ) ,
900+ size : 1048576 ,
901+ maj_min : None ,
902+ start : Some ( 2048 ) ,
903+ label : None ,
904+ fstype : None ,
905+ uuid : None ,
906+ path : None ,
907+ pttype : Some ( "dos" . into ( ) ) ,
908+ children : None ,
909+ } )
910+ . collect ( ) ,
911+ ) ,
912+ }
913+ }
914+
915+ #[ test]
916+ fn test_mbr_esp_detection ( ) {
917+ // 0x06 (FAT16) is recognized as ESP
918+ let dev = make_mbr_disk ( & [ "0x06" ] ) ;
919+ assert_eq ! ( dev. find_partition_of_esp( ) . unwrap( ) . partn, Some ( 1 ) ) ;
920+
921+ // 0xef (EFI System Partition) is recognized as ESP
922+ let dev = make_mbr_disk ( & [ "0x83" , "0xef" ] ) ;
923+ assert_eq ! ( dev. find_partition_of_esp( ) . unwrap( ) . partn, Some ( 2 ) ) ;
924+
925+ // No ESP types present: 0x83 (Linux) and 0x82 (swap)
926+ let dev = make_mbr_disk ( & [ "0x83" , "0x82" ] ) ;
927+ assert ! ( dev. find_partition_of_esp( ) . is_err( ) ) ;
928+ }
700929}
0 commit comments