|
| 1 | +use std::collections::HashSet; |
1 | 2 | use std::env; |
2 | 3 | use std::path::Path; |
3 | 4 | use std::process::{Command, Stdio}; |
@@ -123,15 +124,26 @@ impl Device { |
123 | 124 | /// Calls find_all_roots() to discover physical disks, then searches each for an ESP. |
124 | 125 | /// Returns None if no ESPs are found. |
125 | 126 | pub fn find_colocated_esps(&self) -> Result<Option<Vec<Device>>> { |
126 | | - let esps: Vec<_> = self |
127 | | - .find_all_roots()? |
128 | | - .iter() |
129 | | - .flat_map(|root| root.find_partition_of_esp().ok()) |
130 | | - .cloned() |
131 | | - .collect(); |
| 127 | + let mut esps = Vec::new(); |
| 128 | + for root in &self.find_all_roots()? { |
| 129 | + if let Some(esp) = root.find_partition_of_esp_optional()? { |
| 130 | + esps.push(esp.clone()); |
| 131 | + } |
| 132 | + } |
132 | 133 | Ok((!esps.is_empty()).then_some(esps)) |
133 | 134 | } |
134 | 135 |
|
| 136 | + /// Find a single ESP partition among all root devices backing this device. |
| 137 | + /// |
| 138 | + /// Walks the parent chain to find all backing disks, then looks for ESP |
| 139 | + /// partitions on each. Returns the first ESP found. This is the common |
| 140 | + /// case for composefs/UKI boot paths where exactly one ESP is expected. |
| 141 | + pub fn find_first_colocated_esp(&self) -> Result<Device> { |
| 142 | + self.find_colocated_esps()? |
| 143 | + .and_then(|mut v| Some(v.remove(0))) |
| 144 | + .ok_or_else(|| anyhow!("No ESP partition found among backing devices")) |
| 145 | + } |
| 146 | + |
135 | 147 | /// Find all BIOS boot partitions across all root devices backing this device. |
136 | 148 | /// Calls find_all_roots() to discover physical disks, then searches each for a BIOS boot partition. |
137 | 149 | /// Returns None if no BIOS boot partitions are found. |
@@ -159,34 +171,41 @@ impl Device { |
159 | 171 | /// |
160 | 172 | /// For GPT disks, this matches by the ESP partition type GUID. |
161 | 173 | /// For MBR (dos) disks, this matches by the MBR partition type IDs (0x06 or 0xEF). |
162 | | - pub fn find_partition_of_esp(&self) -> Result<&Device> { |
163 | | - let children = self |
164 | | - .children |
165 | | - .as_ref() |
166 | | - .ok_or_else(|| anyhow!("Device has no children"))?; |
| 174 | + /// |
| 175 | + /// Returns `Ok(None)` when there are no children or no ESP partition |
| 176 | + /// is present. Returns `Err` only for genuinely unexpected conditions |
| 177 | + /// (e.g. an unsupported partition table type). |
| 178 | + pub fn find_partition_of_esp_optional(&self) -> Result<Option<&Device>> { |
| 179 | + let Some(children) = self.children.as_ref() else { |
| 180 | + return Ok(None); |
| 181 | + }; |
167 | 182 | match self.pttype.as_deref() { |
168 | | - Some("dos") => children |
169 | | - .iter() |
170 | | - .find(|child| { |
171 | | - child |
172 | | - .parttype |
173 | | - .as_ref() |
174 | | - .and_then(|pt| { |
175 | | - let pt = pt.strip_prefix("0x").unwrap_or(pt); |
176 | | - u8::from_str_radix(pt, 16).ok() |
177 | | - }) |
178 | | - .is_some_and(|pt| ESP_ID_MBR.contains(&pt)) |
179 | | - }) |
180 | | - .ok_or_else(|| anyhow!("ESP not found in MBR partition table")), |
| 183 | + Some("dos") => Ok(children.iter().find(|child| { |
| 184 | + child |
| 185 | + .parttype |
| 186 | + .as_ref() |
| 187 | + .and_then(|pt| { |
| 188 | + let pt = pt.strip_prefix("0x").unwrap_or(pt); |
| 189 | + u8::from_str_radix(pt, 16).ok() |
| 190 | + }) |
| 191 | + .is_some_and(|pt| ESP_ID_MBR.contains(&pt)) |
| 192 | + })), |
181 | 193 | // When pttype is None (e.g. older lsblk or partition devices), default |
182 | 194 | // to GPT UUID matching which will simply not match MBR hex types. |
183 | | - Some("gpt") | None => self |
184 | | - .find_partition_of_type(ESP) |
185 | | - .ok_or_else(|| anyhow!("ESP not found in GPT partition table")), |
| 195 | + Some("gpt") | None => Ok(self.find_partition_of_type(ESP)), |
186 | 196 | Some(other) => Err(anyhow!("Unsupported partition table type: {other}")), |
187 | 197 | } |
188 | 198 | } |
189 | 199 |
|
| 200 | + /// Find the EFI System Partition (ESP) among children, or error if absent. |
| 201 | + /// |
| 202 | + /// This is a convenience wrapper around [`Self::find_partition_of_esp_optional`] |
| 203 | + /// for callers that require an ESP to be present. |
| 204 | + pub fn find_partition_of_esp(&self) -> Result<&Device> { |
| 205 | + self.find_partition_of_esp_optional()? |
| 206 | + .ok_or_else(|| anyhow!("ESP partition not found on {}", self.path())) |
| 207 | + } |
| 208 | + |
190 | 209 | /// Find a child partition by partition number (1-indexed). |
191 | 210 | pub fn find_device_by_partno(&self, partno: u32) -> Result<&Device> { |
192 | 211 | self.children |
@@ -308,15 +327,21 @@ impl Device { |
308 | 327 | }; |
309 | 328 |
|
310 | 329 | let mut roots = Vec::new(); |
| 330 | + let mut seen = HashSet::new(); |
311 | 331 | let mut queue = parents; |
312 | 332 | while let Some(mut device) = queue.pop() { |
313 | 333 | match device.children.take() { |
314 | 334 | Some(grandparents) if !grandparents.is_empty() => { |
315 | 335 | queue.extend(grandparents); |
316 | 336 | } |
317 | 337 | _ => { |
318 | | - // Found a root; re-query to populate its actual children |
319 | | - roots.push(list_dev(Utf8Path::new(&device.path()))?); |
| 338 | + // Deduplicate: in complex topologies (e.g. multipath) |
| 339 | + // multiple branches can converge on the same physical disk. |
| 340 | + let name = device.name.clone(); |
| 341 | + if seen.insert(name) { |
| 342 | + // Found a new root; re-query to populate its actual children |
| 343 | + roots.push(list_dev(Utf8Path::new(&device.path()))?); |
| 344 | + } |
320 | 345 | } |
321 | 346 | } |
322 | 347 | } |
|
0 commit comments