2025-11-01 17:47:41 -04:00
|
|
|
use anyhow::{Context, Result, bail};
|
2025-10-13 00:55:11 -07:00
|
|
|
use std::ops::Deref;
|
2025-10-30 21:38:49 -04:00
|
|
|
use uefi::boot::SearchType;
|
2025-10-01 21:30:43 -07:00
|
|
|
use uefi::fs::{FileSystem, Path};
|
2025-10-02 00:24:19 -07:00
|
|
|
use uefi::proto::device_path::text::{AllowShortcuts, DevicePathFromText, DisplayOnly};
|
|
|
|
|
use uefi::proto::device_path::{DevicePath, PoolDevicePath};
|
2025-10-01 21:30:43 -07:00
|
|
|
use uefi::proto::media::fs::SimpleFileSystem;
|
2025-10-28 21:05:22 -04:00
|
|
|
use uefi::proto::media::partition::PartitionInfo;
|
|
|
|
|
use uefi::{CString16, Guid, Handle};
|
|
|
|
|
use uefi_raw::Status;
|
2025-10-01 21:30:43 -07:00
|
|
|
|
2025-10-19 23:03:28 -07:00
|
|
|
/// Support code for the EFI framebuffer.
|
2025-10-11 14:11:31 -07:00
|
|
|
pub mod framebuffer;
|
2025-10-19 23:03:28 -07:00
|
|
|
|
|
|
|
|
/// Support code for the media loader protocol.
|
2025-10-14 18:11:41 -07:00
|
|
|
pub mod media_loader;
|
2025-10-11 14:11:31 -07:00
|
|
|
|
2025-10-30 18:57:26 -04:00
|
|
|
/// Support code for EFI variables.
|
|
|
|
|
pub mod variables;
|
|
|
|
|
|
2025-11-01 21:34:55 -04:00
|
|
|
/// Implements a version comparison algorithm according to the BLS specification.
|
|
|
|
|
pub mod vercmp;
|
|
|
|
|
|
2025-10-24 15:54:58 -07:00
|
|
|
/// Parses the input `path` as a [DevicePath].
|
2025-10-14 12:47:33 -07:00
|
|
|
/// Uses the [DevicePathFromText] protocol exclusively, and will fail if it cannot acquire the protocol.
|
2025-10-11 14:35:29 -07:00
|
|
|
pub fn text_to_device_path(path: &str) -> Result<PoolDevicePath> {
|
|
|
|
|
let path = CString16::try_from(path).context("unable to convert path to CString16")?;
|
2025-10-01 21:30:43 -07:00
|
|
|
let device_path_from_text = uefi::boot::open_protocol_exclusive::<DevicePathFromText>(
|
|
|
|
|
uefi::boot::get_handle_for_protocol::<DevicePathFromText>()
|
2025-10-11 14:35:29 -07:00
|
|
|
.context("no device path from text protocol")?,
|
2025-10-01 21:30:43 -07:00
|
|
|
)
|
2025-10-11 14:35:29 -07:00
|
|
|
.context("unable to open device path from text protocol")?;
|
2025-10-01 21:30:43 -07:00
|
|
|
|
|
|
|
|
device_path_from_text
|
|
|
|
|
.convert_text_to_device_path(&path)
|
2025-10-11 14:35:29 -07:00
|
|
|
.context("unable to convert text to device path")
|
2025-10-01 21:30:43 -07:00
|
|
|
}
|
|
|
|
|
|
2025-10-24 19:11:17 -07:00
|
|
|
/// Checks if a [CString16] contains a char `c`.
|
|
|
|
|
/// We need to call to_string() because CString16 doesn't support `contains` with a char.
|
|
|
|
|
fn cstring16_contains_char(string: &CString16, c: char) -> bool {
|
|
|
|
|
string.to_string().contains(c)
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-24 15:54:58 -07:00
|
|
|
/// Grabs the root part of the `path`.
|
2025-10-14 12:47:33 -07:00
|
|
|
/// For example, given "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)/\EFI\BOOT\BOOTX64.efi"
|
|
|
|
|
/// it will give "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)"
|
2025-10-11 14:35:29 -07:00
|
|
|
pub fn device_path_root(path: &DevicePath) -> Result<String> {
|
2025-10-02 00:24:19 -07:00
|
|
|
let mut path = path
|
|
|
|
|
.node_iter()
|
|
|
|
|
.filter_map(|item| {
|
2025-10-11 14:35:29 -07:00
|
|
|
let item = item.to_string(DisplayOnly(false), AllowShortcuts(false));
|
|
|
|
|
if item
|
|
|
|
|
.as_ref()
|
2025-10-24 19:11:17 -07:00
|
|
|
.map(|item| cstring16_contains_char(item, '('))
|
2025-10-11 14:35:29 -07:00
|
|
|
.unwrap_or(false)
|
|
|
|
|
{
|
|
|
|
|
Some(item.unwrap_or_default())
|
2025-10-02 00:24:19 -07:00
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.map(|item| item.to_string())
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
.join("/");
|
|
|
|
|
path.push('/');
|
2025-10-11 14:35:29 -07:00
|
|
|
Ok(path)
|
2025-10-02 00:24:19 -07:00
|
|
|
}
|
|
|
|
|
|
2025-10-24 15:54:58 -07:00
|
|
|
/// Grabs the part of the `path` after the root.
|
2025-10-14 12:47:33 -07:00
|
|
|
/// For example, given "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)/\EFI\BOOT\BOOTX64.efi"
|
|
|
|
|
/// it will give "\EFI\BOOT\BOOTX64.efi"
|
2025-10-13 00:55:11 -07:00
|
|
|
pub fn device_path_subpath(path: &DevicePath) -> Result<String> {
|
|
|
|
|
let path = path
|
|
|
|
|
.node_iter()
|
|
|
|
|
.filter_map(|item| {
|
|
|
|
|
let item = item.to_string(DisplayOnly(false), AllowShortcuts(false));
|
|
|
|
|
if item
|
|
|
|
|
.as_ref()
|
2025-10-24 19:11:17 -07:00
|
|
|
.map(|item| cstring16_contains_char(item, '('))
|
2025-10-13 00:55:11 -07:00
|
|
|
.unwrap_or(false)
|
|
|
|
|
{
|
|
|
|
|
None
|
|
|
|
|
} else {
|
|
|
|
|
Some(item.unwrap_or_default())
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.map(|item| item.to_string())
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
.join("\\");
|
|
|
|
|
Ok(path)
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-14 12:47:33 -07:00
|
|
|
/// Represents the components of a resolved path.
|
2025-10-13 00:55:11 -07:00
|
|
|
pub struct ResolvedPath {
|
2025-10-14 12:47:33 -07:00
|
|
|
/// The root path of the resolved path. This is the device itself.
|
|
|
|
|
/// For example, "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)/"
|
2025-10-13 00:55:11 -07:00
|
|
|
pub root_path: Box<DevicePath>,
|
2025-10-14 12:47:33 -07:00
|
|
|
/// The subpath of the resolved path. This is the path to the file.
|
|
|
|
|
/// For example, "\EFI\BOOT\BOOTX64.efi"
|
2025-10-13 00:55:11 -07:00
|
|
|
pub sub_path: Box<DevicePath>,
|
2025-10-14 12:47:33 -07:00
|
|
|
/// The full path of the resolved path. This is the safest path to use.
|
|
|
|
|
/// For example, "PciRoot(0x0)/Pci(0x4,0x0)/NVMe(0x1,00-00-00-00-00-00-00-00)/HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)/\EFI\BOOT\BOOTX64.efi"
|
2025-10-13 00:55:11 -07:00
|
|
|
pub full_path: Box<DevicePath>,
|
2025-10-14 12:47:33 -07:00
|
|
|
/// The handle of the filesystem containing the path.
|
|
|
|
|
/// This can be used to acquire a [SimpleFileSystem] protocol to read the file.
|
2025-10-13 00:55:11 -07:00
|
|
|
pub filesystem_handle: Handle,
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-30 21:38:49 -04:00
|
|
|
impl ResolvedPath {
|
|
|
|
|
/// Read the file specified by this path into a buffer and return it.
|
|
|
|
|
pub fn read_file(&self) -> Result<Vec<u8>> {
|
|
|
|
|
let fs = uefi::boot::open_protocol_exclusive::<SimpleFileSystem>(self.filesystem_handle)
|
|
|
|
|
.context("unable to open filesystem protocol")?;
|
|
|
|
|
let mut fs = FileSystem::new(fs);
|
|
|
|
|
let path = self
|
|
|
|
|
.sub_path
|
|
|
|
|
.to_string(DisplayOnly(false), AllowShortcuts(false))?;
|
|
|
|
|
let content = fs.read(Path::new(&path));
|
|
|
|
|
content.context("unable to read file contents")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-24 15:54:58 -07:00
|
|
|
/// Resolve a path specified by `input` to its various components.
|
|
|
|
|
/// Uses `default_root_path` as the base root if one is not specified in the path.
|
2025-10-14 12:47:33 -07:00
|
|
|
/// Returns [ResolvedPath] which contains the resolved components.
|
2025-10-30 22:56:01 -04:00
|
|
|
pub fn resolve_path(default_root_path: Option<&DevicePath>, input: &str) -> Result<ResolvedPath> {
|
2025-10-14 12:47:33 -07:00
|
|
|
let mut path = text_to_device_path(input).context("unable to convert text to path")?;
|
2025-10-13 00:55:11 -07:00
|
|
|
let path_has_device = path
|
|
|
|
|
.node_iter()
|
|
|
|
|
.next()
|
|
|
|
|
.map(|it| {
|
|
|
|
|
it.to_string(DisplayOnly(false), AllowShortcuts(false))
|
|
|
|
|
.unwrap_or_default()
|
|
|
|
|
})
|
2025-10-24 18:59:15 -07:00
|
|
|
.map(|it| it.to_string().contains('('))
|
2025-10-13 00:55:11 -07:00
|
|
|
.unwrap_or(false);
|
|
|
|
|
if !path_has_device {
|
|
|
|
|
let mut input = input.to_string();
|
2025-10-24 18:59:15 -07:00
|
|
|
if !input.starts_with('\\') {
|
2025-10-13 00:55:11 -07:00
|
|
|
input.insert(0, '\\');
|
|
|
|
|
}
|
2025-10-30 22:56:01 -04:00
|
|
|
|
|
|
|
|
let default_root_path = default_root_path.context("unable to get default root path")?;
|
|
|
|
|
|
2025-10-13 00:55:11 -07:00
|
|
|
input.insert_str(
|
|
|
|
|
0,
|
|
|
|
|
device_path_root(default_root_path)
|
2025-10-14 12:47:33 -07:00
|
|
|
.context("unable to get loaded image device root")?
|
2025-10-13 00:55:11 -07:00
|
|
|
.as_str(),
|
|
|
|
|
);
|
2025-10-14 12:47:33 -07:00
|
|
|
path = text_to_device_path(input.as_str()).context("unable to convert text to path")?;
|
2025-10-13 00:55:11 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let path = path.to_boxed();
|
2025-10-14 12:47:33 -07:00
|
|
|
let root = device_path_root(path.as_ref()).context("unable to convert root to path")?;
|
2025-10-13 00:55:11 -07:00
|
|
|
let root_path = text_to_device_path(root.as_str())
|
2025-10-14 12:47:33 -07:00
|
|
|
.context("unable to convert root to path")?
|
2025-10-13 00:55:11 -07:00
|
|
|
.to_boxed();
|
2025-11-01 01:20:45 -04:00
|
|
|
let root_path = root_path.as_ref();
|
|
|
|
|
|
|
|
|
|
// locate_device_path modifies the path, so we need to clone it.
|
|
|
|
|
let root_path_modifiable = root_path.to_owned();
|
|
|
|
|
let handle = uefi::boot::locate_device_path::<SimpleFileSystem>(&mut &*root_path_modifiable)
|
2025-10-14 12:47:33 -07:00
|
|
|
.context("unable to locate filesystem device path")?;
|
|
|
|
|
let subpath = device_path_subpath(path.deref()).context("unable to get device subpath")?;
|
2025-10-13 00:55:11 -07:00
|
|
|
Ok(ResolvedPath {
|
|
|
|
|
root_path: root_path.to_boxed(),
|
|
|
|
|
sub_path: text_to_device_path(subpath.as_str())?.to_boxed(),
|
|
|
|
|
full_path: path,
|
|
|
|
|
filesystem_handle: handle,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-24 15:54:58 -07:00
|
|
|
/// Read the contents of a file at the location specified with the `input` path.
|
2025-10-14 12:47:33 -07:00
|
|
|
/// Internally, this uses [resolve_path] to resolve the path to its various components.
|
2025-10-24 15:54:58 -07:00
|
|
|
/// [resolve_path] is passed the `default_root_path` which should specify a base root.
|
2025-10-14 12:47:33 -07:00
|
|
|
///
|
|
|
|
|
/// This acquires exclusive protocol access to the [SimpleFileSystem] protocol of the resolved
|
|
|
|
|
/// filesystem handle, so care must be taken to call this function outside a scope with
|
|
|
|
|
/// the filesystem handle protocol acquired.
|
2025-10-30 22:56:01 -04:00
|
|
|
pub fn read_file_contents(default_root_path: Option<&DevicePath>, input: &str) -> Result<Vec<u8>> {
|
2025-10-13 00:55:11 -07:00
|
|
|
let resolved = resolve_path(default_root_path, input)?;
|
2025-10-30 21:38:49 -04:00
|
|
|
resolved.read_file()
|
2025-10-01 21:30:43 -07:00
|
|
|
}
|
2025-10-27 17:44:30 -04:00
|
|
|
|
|
|
|
|
/// Filter a string-like Option `input` such that an empty string is [None].
|
|
|
|
|
pub fn empty_is_none<T: AsRef<str>>(input: Option<T>) -> Option<T> {
|
|
|
|
|
input.filter(|input| !input.as_ref().is_empty())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Combine a sequence of strings into a single string, separated by spaces, ignoring empty strings.
|
|
|
|
|
pub fn combine_options<T: AsRef<str>>(options: impl Iterator<Item = T>) -> String {
|
|
|
|
|
options
|
|
|
|
|
.flat_map(|item| empty_is_none(Some(item)))
|
|
|
|
|
.map(|item| item.as_ref().to_string())
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
.join(" ")
|
|
|
|
|
}
|
2025-10-27 18:21:28 -04:00
|
|
|
|
|
|
|
|
/// Produce a unique hash for the input.
|
|
|
|
|
/// This uses SHA-256, which is unique enough but relatively short.
|
|
|
|
|
pub fn unique_hash(input: &str) -> String {
|
|
|
|
|
sha256::digest(input.as_bytes())
|
|
|
|
|
}
|
2025-10-28 21:05:22 -04:00
|
|
|
|
|
|
|
|
/// Represents the type of partition GUID that can be retrieved.
|
|
|
|
|
#[derive(PartialEq, Eq)]
|
|
|
|
|
pub enum PartitionGuidForm {
|
2025-10-30 23:42:47 -04:00
|
|
|
/// The partition GUID is the unique partition GUID.
|
2025-10-28 21:05:22 -04:00
|
|
|
Partition,
|
2025-10-30 23:42:47 -04:00
|
|
|
/// The partition GUID is the partition type GUID.
|
2025-10-28 21:05:22 -04:00
|
|
|
PartitionType,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Retrieve the partition / partition type GUID of the device root `path`.
|
|
|
|
|
/// This only works on GPT partitions. If the root is not a GPT partition, None is returned.
|
2025-10-30 23:42:47 -04:00
|
|
|
/// If the GUID is all zeros, this will return None.
|
2025-10-28 21:05:22 -04:00
|
|
|
pub fn partition_guid(path: &DevicePath, form: PartitionGuidForm) -> Result<Option<Guid>> {
|
|
|
|
|
// Clone the path so we can pass it to the UEFI stack.
|
|
|
|
|
let path = path.to_boxed();
|
|
|
|
|
let result = uefi::boot::locate_device_path::<PartitionInfo>(&mut &*path);
|
|
|
|
|
let handle = match result {
|
|
|
|
|
Ok(handle) => Ok(Some(handle)),
|
|
|
|
|
Err(error) => {
|
|
|
|
|
// If the error is NOT_FOUND or UNSUPPORTED, we can return None.
|
|
|
|
|
// These are non-fatal errors.
|
|
|
|
|
if error.status() == Status::NOT_FOUND || error.status() == Status::UNSUPPORTED {
|
|
|
|
|
Ok(None)
|
|
|
|
|
} else {
|
|
|
|
|
Err(error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
.context("unable to locate device path")?;
|
|
|
|
|
|
|
|
|
|
// If we have the handle, we can try to open the partition info protocol.
|
|
|
|
|
if let Some(handle) = handle {
|
|
|
|
|
// Open the partition info protocol.
|
|
|
|
|
let partition_info = uefi::boot::open_protocol_exclusive::<PartitionInfo>(handle)
|
|
|
|
|
.context("unable to open partition info protocol")?;
|
|
|
|
|
// Find the unique partition GUID.
|
|
|
|
|
// If this is not a GPT partition, this will produce None.
|
|
|
|
|
Ok(partition_info
|
|
|
|
|
.gpt_partition_entry()
|
|
|
|
|
.map(|entry| match form {
|
|
|
|
|
// Match the form of the partition GUID.
|
|
|
|
|
PartitionGuidForm::Partition => entry.unique_partition_guid,
|
|
|
|
|
PartitionGuidForm::PartitionType => entry.partition_type_guid.0,
|
2025-10-30 23:42:47 -04:00
|
|
|
})
|
|
|
|
|
.filter(|guid| !guid.is_zero()))
|
2025-10-28 21:05:22 -04:00
|
|
|
} else {
|
|
|
|
|
Ok(None)
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-30 21:38:49 -04:00
|
|
|
|
|
|
|
|
/// Find a handle that provides the specified `protocol`.
|
|
|
|
|
pub fn find_handle(protocol: &Guid) -> Result<Option<Handle>> {
|
|
|
|
|
// Locate the requested protocol handle.
|
|
|
|
|
match uefi::boot::locate_handle_buffer(SearchType::ByProtocol(protocol)) {
|
|
|
|
|
// If a handle is found, the protocol is available.
|
|
|
|
|
Ok(handles) => Ok(if handles.is_empty() {
|
|
|
|
|
None
|
|
|
|
|
} else {
|
|
|
|
|
Some(handles[0])
|
|
|
|
|
}),
|
|
|
|
|
// If an error occurs, check if it is because the protocol is not available.
|
|
|
|
|
// If so, return false. Otherwise, return the error.
|
|
|
|
|
Err(error) => {
|
|
|
|
|
if error.status() == Status::NOT_FOUND {
|
|
|
|
|
Ok(None)
|
|
|
|
|
} else {
|
|
|
|
|
Err(error).context("unable to determine if the protocol is available")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-11-01 17:47:41 -04:00
|
|
|
|
|
|
|
|
/// Convert a byte slice into a CString16.
|
|
|
|
|
pub fn utf16_bytes_to_cstring16(bytes: &[u8]) -> Result<CString16> {
|
|
|
|
|
// Validate the input bytes are the right length.
|
|
|
|
|
if !bytes.len().is_multiple_of(2) {
|
|
|
|
|
bail!("utf16 bytes must be a multiple of 2");
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-01 18:49:10 -04:00
|
|
|
// Convert the bytes to UTF-16 data.
|
|
|
|
|
let data = bytes
|
|
|
|
|
// Chunk everything into two bytes.
|
|
|
|
|
.chunks_exact(2)
|
|
|
|
|
// Reinterpret the bytes as u16 little-endian.
|
|
|
|
|
.map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
|
|
|
|
|
// Collect the result into a vector.
|
|
|
|
|
.collect::<Vec<_>>();
|
2025-11-01 17:47:41 -04:00
|
|
|
|
2025-11-01 18:49:10 -04:00
|
|
|
CString16::try_from(data).context("unable to convert utf16 bytes to CString16")
|
2025-11-01 17:47:41 -04:00
|
|
|
}
|