plex_boot/path/mod.rs
1//! Utilities for reading block devices and locating files
2//! specified in config.
3use core::str::FromStr;
4
5use alloc::format;
6use alloc::string::String;
7use alloc::string::ToString;
8use alloc::vec::Vec;
9use log::error;
10use uefi::boot::OpenProtocolParams;
11use uefi::proto::ProtocolPointer;
12use uefi::proto::device_path::{DevicePath, PoolDevicePath};
13use uefi::proto::loaded_image::LoadedImage;
14use uefi::proto::media::partition::{GptPartitionEntry, MbrPartitionRecord};
15use uefi::{CString16, Handle, Identify};
16
17/// URI-style path reference for locating files across partitions
18///
19/// Supports two addressing modes:
20/// - `boot():/path` - The partition where bootloader was loaded from
21/// - `guid:PARTUUID:/path` - Partition identified by GPT PARTUUID
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct PathReference {
24 /// Which partition contains the file
25 pub location: PartitionReference,
26 /// Absolute path within that partition (must start with /)
27 pub path: String,
28}
29
30/// A reference to a specific partition.
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum PartitionReference {
33 /// The partition where the bootloader EFI executable was loaded from
34 ///
35 /// For UEFI systems: This is determined by examining the LoadedImage
36 /// protocol's DeviceHandle, which tells us which partition the firmware
37 /// loaded us from.
38 ///
39 /// Syntax: `boot():`
40 /// Example: `boot():/vmlinuz-linux`
41 Boot,
42
43 /// Partition identified by GPT Partition GUID (PARTUUID)
44 ///
45 /// This is the unique identifier from the GPT partition table entry,
46 /// NOT the filesystem UUID. Each partition in a GPT table has a unique
47 /// GUID assigned when the partition is created.
48 ///
49 /// To find the PARTUUID on Linux:
50 /// ```bash
51 /// blkid /dev/nvme0n1p2
52 /// # Shows: PARTUUID="550e8400-e29b-41d4-a716-446655440000"
53 /// ```
54 ///
55 /// Or inspect GPT directly:
56 /// ```bash
57 /// sgdisk -i 2 /dev/nvme0n1
58 /// # Shows partition unique GUID
59 /// ```
60 ///
61 /// Syntax: `guid(XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX)/path`
62 Guid(uefi::Guid),
63
64 /// Drive is an ISO existing in the partition that the EFI image was loaded
65 /// from. Kernel/executable is a path within this ISO filesystem that is provided
66 /// to the kernel through the BLOCK_IO and SimpleFileSystem UEFI Protocols.
67 ///
68 /// Syntax: `iso(/uwuntu.iso)/vmlinuz`
69 #[cfg(feature = "iso")]
70 Iso(String),
71}
72
73impl PathReference {
74 /// Parse a URI-style path reference
75 ///
76 /// # Rules
77 /// - Resource and path separated by `:`
78 ///
79 /// # Examples
80 /// ```
81 /// PathReference::parse("boot():/vmlinuz-linux")?;
82 /// PathReference::parse("boot():/EFI/BOOT/BOOTX64.EFI")?;
83 /// PathReference::parse("guid(550e8400-e29b-41d4-a716-446655440000)/vmlinuz")?;
84 /// PathReference::parse("iso(myfile.iso)/vmlinuz")?; // with feature 'iso'
85 /// ```
86 ///
87 /// # Errors
88 /// - `MissingDelimiter` - No `:` found
89 /// - `InvalidPath` - Path doesn't start with `/` or is empty after `:`
90 /// - `UnknownResource` - Resource type not recognized
91 /// - `InvalidGuid` - GUID format incorrect (wrong length, invalid hex, missing hyphens)
92 /// - `InvalidSyntax` - boot() has unexpected content in parens
93 pub fn parse(s: &str) -> Result<Self, PathRefParseError> {
94 let (resource, path) = s
95 .split_once(':')
96 .ok_or(PathRefParseError::MissingDelimiter)?;
97
98 let location = PartitionReference::parse(resource)?;
99
100 Ok(PathReference {
101 location,
102 path: path.to_string(),
103 })
104 }
105
106 /// Convert back to canonical URI string
107 ///
108 /// # Example
109 /// ```
110 /// let uri = path_ref.to_uri();
111 /// assert_eq!(uri, "boot():/vmlinuz");
112 /// ```
113 pub fn to_uri(&self) -> String {
114 format!("{}{}", self.location.to_uri_prefix(), self.path)
115 }
116}
117
118impl PartitionReference {
119 /// Parse just the partition reference portion (before the final `:`)
120 ///
121 /// # Examples
122 /// ```
123 /// PartitionReference::parse("boot")?;
124 /// PartitionReference::parse("guid:550e8400-e29b-41d4-a716-446655440000")?;
125 /// ```
126 pub fn parse(s: &str) -> Result<Self, PathRefParseError> {
127 let Some(lparen) = s.find('(') else {
128 return Err(PathRefParseError::InvalidSyntax);
129 };
130
131 let scheme = &s[..lparen];
132 let arg = s[lparen + 1..]
133 .strip_suffix(')')
134 .ok_or(PathRefParseError::MissingDelimiter)?;
135
136 match scheme {
137 "boot" => Ok(PartitionReference::Boot),
138 "guid" => Ok(PartitionReference::Guid(
139 uefi::Guid::from_str(arg).map_err(|_| PathRefParseError::InvalidGuid)?,
140 )),
141 #[cfg(feature = "iso")]
142 "iso" => Ok(PartitionReference::Iso(arg.to_string())),
143 _ => Err(PathRefParseError::UnknownResource(scheme.to_string())),
144 }
145 }
146 /// Convert to URI prefix (everything before the path)
147 ///
148 /// # Example
149 /// ```
150 /// assert_eq!(PartitionReference::Boot.to_uri_prefix(), "boot():");
151 /// ```
152 pub fn to_uri_prefix(&self) -> String {
153 match self {
154 PartitionReference::Boot => String::from("boot():"),
155 PartitionReference::Guid(guid) => {
156 format!("guid({}):", guid)
157 }
158 #[cfg(feature = "iso")]
159 PartitionReference::Iso(iso) => {
160 format!("iso({}):", iso)
161 }
162 }
163 }
164}
165
166/// Errors that can occur when parsing a `PathReference`.
167#[derive(Debug, Clone, PartialEq, Eq, thiserror_no_std::Error)]
168pub enum PathRefParseError {
169 /// No `:` separator found between resource and path
170 #[error("Missing Delimiter")]
171 MissingDelimiter,
172
173 /// Path component doesn't start with `/` or is empty
174 #[error("Invalid Path")]
175 InvalidPath,
176
177 #[error("Unknown Resource: {0}")]
178 /// Unknown resource type (not "boot" or "guid")
179 UnknownResource(String),
180
181 /// GUID format invalid
182 ///
183 /// Valid format: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
184 /// Must have exactly 36 chars (32 hex + 4 hyphens)
185 #[error("Invalid Guid")]
186 InvalidGuid,
187
188 /// boot() syntax error (something in the parens)
189 #[error("Invalid Syntax")]
190 InvalidSyntax,
191}
192
193/// Manages partition discovery and path resolution
194pub struct DiskManager {
195 /// All discovered partitions with their metadata
196 partitions: Vec<Partition>,
197}
198
199impl DiskManager {
200 /// Create a new DiskManager by discovering all partitions
201 ///
202 /// # Arguments
203 /// * `boot_handle` - The handle for the partition containing the bootloader
204 ///
205 /// (typically from LoadedImage protocol's device_handle)
206 ///
207 /// # Process
208 /// 1. Call LocateHandleBuffer for BlockIO protocol
209 /// 2. Filter to only logical partitions (media.is_logical_partition())
210 /// 3. For each partition, extract PARTUUID from device path
211 /// 4. Store mapping of PARTUUID -> Handle
212 ///
213 /// # Errors
214 /// Returns error if:
215 /// - LocateHandleBuffer fails
216 /// - Cannot open BlockIO protocol on any handle
217 /// - Cannot allocate memory for partition list
218 pub fn new(boot_handle: Handle) -> uefi::Result<Self> {
219 use uefi::proto::media::partition::PartitionInfo;
220 let mut partitions = Vec::new();
221
222 let boot_device_handle = open_protocol_get::<LoadedImage>(boot_handle)?.device();
223 let partition_handles = uefi::boot::locate_handle_buffer(
224 uefi::boot::SearchType::ByProtocol(&uefi::proto::media::partition::PartitionInfo::GUID),
225 )?;
226
227 for handle in partition_handles.iter() {
228 match uefi::boot::open_protocol_exclusive::<PartitionInfo>(*handle) {
229 Ok(partition_info) => {
230 partitions.push(Partition {
231 handle: *handle,
232 mbr_partition_info: partition_info.mbr_partition_record().cloned(),
233 gpt_partition_info: partition_info.gpt_partition_entry().cloned(),
234 is_system: partition_info.is_system(),
235 is_boot: boot_device_handle == Some(*handle),
236 #[cfg(feature = "iso")]
237 iso_path: None,
238 });
239 }
240 Err(e) => {
241 error!("failed to open protocol on a partition: {:?}", e)
242 }
243 }
244 }
245
246 Ok(DiskManager { partitions })
247 }
248 //
249 /// Resolve a partition reference to a UEFI handle
250 ///
251 /// # Arguments
252 /// * `reference` - The partition to locate
253 ///
254 /// # Returns
255 /// The UEFI handle for the partition, suitable for opening SimpleFileSystem
256 ///
257 /// # Behavior
258 /// - Boot: Returns cached boot_handle immediately (O(1))
259 /// - Guid: Linear search through partitions for matching GUID (O(n))
260 ///
261 /// # Errors
262 /// - `NOT_FOUND` if GUID doesn't match any discovered partition
263 pub fn resolve_path(&self, reference: &PathReference) -> uefi::Result<PoolDevicePath> {
264 match self
265 .partitions
266 .iter()
267 .find(|part| reference.location.matches(part))
268 {
269 Some(partition) => {
270 let device_path = open_protocol_get::<DevicePath>(partition.handle)?;
271 let mut v = Vec::new();
272 let root_to_executable =
273 uefi::proto::device_path::build::DevicePathBuilder::with_vec(&mut v)
274 .push(&uefi::proto::device_path::build::media::FilePath {
275 path_name: &CString16::try_from(reference.path.as_str())
276 .map_err(|_| uefi::Error::new(uefi::Status::NOT_FOUND, ()))?,
277 })
278 .map_err(|_| uefi::Error::new(uefi::Status::NOT_FOUND, ()))?
279 .finalize()
280 .map_err(|_| uefi::Error::new(uefi::Status::NOT_FOUND, ()))?;
281 Ok(device_path
282 .append_path(root_to_executable)
283 .map_err(|_| uefi::Error::new(uefi::Status::NOT_FOUND, ()))?)
284 }
285 None => Err(uefi::Error::new(uefi::Status::NOT_FOUND, ())),
286 }
287 }
288}
289
290/// Metadata about a discovered partition
291#[derive(Debug)]
292pub struct Partition {
293 /// UEFI handle for opening protocols on this partition.
294 pub handle: Handle,
295
296 /// GPT pt info, if avail.
297 pub gpt_partition_info: Option<GptPartitionEntry>,
298
299 /// MBR pt info, if avail.
300 #[allow(dead_code)]
301 pub mbr_partition_info: Option<MbrPartitionRecord>,
302
303 /// Whether marked as system partition (not necessarily boot partition).
304 #[allow(dead_code)]
305 pub is_system: bool,
306
307 /// Whether this is the partition from which the bootloader was launched.
308 pub is_boot: bool,
309
310 /// Optional path to an ISO file within this partition, if treating an ISO as a partition.
311 #[cfg(feature = "iso")]
312 pub iso_path: Option<String>,
313}
314
315impl Partition {
316 const fn guid(&self) -> Option<uefi::Guid> {
317 if let Some(gpt) = self.gpt_partition_info {
318 Some(gpt.unique_partition_guid)
319 } else {
320 None
321 }
322 }
323}
324
325impl PartitionReference {
326 fn matches(&self, p: &Partition) -> bool {
327 match &self {
328 PartitionReference::Boot => p.is_boot,
329 PartitionReference::Guid(id) => p.guid().as_ref() == Some(id),
330 #[cfg(feature = "iso")]
331 PartitionReference::Iso(iso) => p.iso_path.as_ref() == Some(iso),
332 }
333 }
334}
335
336/// Convenience function to safely open a UEFI protocol on a handle.
337pub fn open_protocol_get<P: ProtocolPointer + ?Sized>(
338 handle: Handle,
339) -> Result<uefi::boot::ScopedProtocol<P>, uefi::Error> {
340 unsafe {
341 uefi::boot::open_protocol::<P>(
342 OpenProtocolParams {
343 handle,
344 agent: uefi::boot::image_handle(),
345 controller: None,
346 },
347 uefi::boot::OpenProtocolAttributes::GetProtocol,
348 )
349 }
350}