Skip to main content

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}