Skip to content

device_path: add more convenience (part 1) #827

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- Added `Event::from_ptr`, `Event::as_ptr`, and `Handle::as_ptr`.
- Added `ScopedProtocol::get` and `ScopedProtocol::get_mut` to access
potentially-null interfaces without panicking.
- `DevicePath::to_string` and `DevicePathNode::to_string`

### Changed
- Renamed `LoadImageSource::FromFilePath` to `LoadImageSource::FromDevicePath`
Expand Down
152 changes: 105 additions & 47 deletions uefi-test-runner/src/proto/device_path.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use uefi::prelude::*;
use uefi::proto::device_path::text::*;
use uefi::proto::device_path::{DevicePath, LoadedImageDevicePath};
Expand All @@ -7,57 +9,113 @@ use uefi::table::boot::BootServices;
pub fn test(image: Handle, bt: &BootServices) {
info!("Running device path protocol test");

let loaded_image = bt
.open_protocol_exclusive::<LoadedImage>(image)
.expect("Failed to open LoadedImage protocol");

let device_path = bt
.open_protocol_exclusive::<DevicePath>(loaded_image.device())
.expect("Failed to open DevicePath protocol");

let device_path_to_text = bt
.open_protocol_exclusive::<DevicePathToText>(
bt.get_handle_for_protocol::<DevicePathToText>()
.expect("Failed to get DevicePathToText handle"),
)
.expect("Failed to open DevicePathToText protocol");

let device_path_from_text = bt
.open_protocol_exclusive::<DevicePathFromText>(
bt.get_handle_for_protocol::<DevicePathFromText>()
.expect("Failed to get DevicePathFromText handle"),
)
.expect("Failed to open DevicePathFromText protocol");

for path in device_path.node_iter() {
info!(
"path: type={:?}, subtype={:?}, length={}",
path.device_type(),
path.sub_type(),
path.length(),
);
// test 1/2: test low-level API by directly opening all protocols
{
let loaded_image = bt
.open_protocol_exclusive::<LoadedImage>(image)
.expect("Failed to open LoadedImage protocol");

let device_path = bt
.open_protocol_exclusive::<DevicePath>(loaded_image.device())
.expect("Failed to open DevicePath protocol");

let device_path_to_text = bt
.open_protocol_exclusive::<DevicePathToText>(
bt.get_handle_for_protocol::<DevicePathToText>()
.expect("Failed to get DevicePathToText handle"),
)
.expect("Failed to open DevicePathToText protocol");

let device_path_from_text = bt
.open_protocol_exclusive::<DevicePathFromText>(
bt.get_handle_for_protocol::<DevicePathFromText>()
.expect("Failed to get DevicePathFromText handle"),
)
.expect("Failed to open DevicePathFromText protocol");

for path in device_path.node_iter() {
info!(
"path: type={:?}, subtype={:?}, length={}",
path.device_type(),
path.sub_type(),
path.length(),
);

let text = device_path_to_text
.convert_device_node_to_text(bt, path, DisplayOnly(true), AllowShortcuts(false))
.expect("Failed to convert device path to text");
let text = &*text;
info!("path name: {text}");

let text = device_path_to_text
.convert_device_node_to_text(bt, path, DisplayOnly(true), AllowShortcuts(false))
.expect("Failed to convert device path to text");
let text = &*text;
info!("path name: {text}");
let convert = device_path_from_text
.convert_text_to_device_node(text)
.expect("Failed to convert text to device path");
assert_eq!(path, convert);
}

let convert = device_path_from_text
.convert_text_to_device_node(text)
.expect("Failed to convert text to device path");
assert_eq!(path, convert);
// Get the `LoadedImageDevicePath`. Verify it start with the same nodes as
// `device_path`.
let loaded_image_device_path = bt
.open_protocol_exclusive::<LoadedImageDevicePath>(image)
.expect("Failed to open LoadedImageDevicePath protocol");

for (n1, n2) in device_path
.node_iter()
.zip(loaded_image_device_path.node_iter())
{
assert_eq!(n1, n2);
}
}

// Get the `LoadedImageDevicePath`. Verify it start with the same nodes as
// `device_path`.
let loaded_image_device_path = bt
.open_protocol_exclusive::<LoadedImageDevicePath>(image)
.expect("Failed to open LoadedImageDevicePath protocol");
for (n1, n2) in device_path
.node_iter()
.zip(loaded_image_device_path.node_iter())
// test 2/2: test high-level to-string api
{
assert_eq!(n1, n2);
let loaded_image_device_path = bt
.open_protocol_exclusive::<LoadedImageDevicePath>(image)
.expect("Failed to open LoadedImageDevicePath protocol");
let device_path: &DevicePath = &loaded_image_device_path;

let path_components = device_path
.node_iter()
.map(|node| node.to_string(bt, DisplayOnly(false), AllowShortcuts(false)))
.map(|str| str.unwrap().unwrap().to_string())
.collect::<Vec<_>>();

let expected_device_path_str_components = &[
"PciRoot(0x0)",
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
"Pci(0x1F,0x2)",
#[cfg(target_arch = "aarch64")]
"Pci(0x4,0x0)",
// Sata device only used on x86.
// See xtask utility.
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
"Sata(0x0,0xFFFF,0x0)",
"HD(1,MBR,0xBE1AFDFA,0x3F,0xFBFC1)",
"\\efi\\boot\\test_runner.efi",
];
let expected_device_path_str =
expected_device_path_str_components
.iter()
.fold(String::new(), |mut acc, next| {
if !acc.is_empty() {
acc.push('/');
}
acc.push_str(next);
acc
});

assert_eq!(
path_components.as_slice(),
expected_device_path_str_components
);

// Test that to_string works for device_paths
let path = device_path
.to_string(bt, DisplayOnly(false), AllowShortcuts(false))
.unwrap()
.unwrap()
.to_string();

assert_eq!(path, expected_device_path_str);
}
}
139 changes: 134 additions & 5 deletions uefi/src/proto/device_path/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,15 +80,21 @@ mod device_path_gen;
pub use device_path_gen::{
acpi, bios_boot_spec, end, hardware, media, messaging, DevicePathNodeEnum,
};
#[cfg(feature = "alloc")]
use {alloc::borrow::ToOwned, alloc::boxed::Box};

use crate::proto::{unsafe_protocol, ProtocolPointer};
use core::ffi::c_void;
use core::fmt::{self, Debug, Formatter};
use core::fmt::{self, Debug, Display, Formatter};
use core::mem;
use core::ops::Deref;
use ptr_meta::Pointee;
use uefi::table::boot::ScopedProtocol;
#[cfg(feature = "alloc")]
use {alloc::borrow::ToOwned, alloc::boxed::Box, uefi::CString16};

use crate::prelude::BootServices;
use crate::proto::device_path::text::{AllowShortcuts, DevicePathToText, DisplayOnly};
use crate::proto::{unsafe_protocol, ProtocolPointer};
use crate::table::boot::{OpenProtocolAttributes, OpenProtocolParams, SearchType};
use crate::Identify;

opaque_type! {
/// Opaque type that should be used to represent a pointer to a
Expand All @@ -113,7 +119,23 @@ pub struct DevicePathHeader {
/// A single node within a [`DevicePath`].
///
/// Each node starts with a [`DevicePathHeader`]. The rest of the data
/// in the node depends on the type of node.
/// in the node depends on the type of node. You can "cast" a node to a specific
/// one like this:
/// ```no_run
/// use uefi::proto::device_path::DevicePath;
/// use uefi::proto::device_path::media::FilePath;
///
/// let image_device_path: &DevicePath = unsafe { DevicePath::from_ffi_ptr(0x1337 as *const _) };
/// let file_path = image_device_path
/// .node_iter()
/// .find_map(|node| {
/// let node: &FilePath = node.try_into().ok()?;
/// let path = node.path_name().to_cstring16().ok()?;
/// Some(path.to_string().to_uppercase())
/// });
/// ```
/// More types are available in [`uefi::proto::device_path`]. Builder types
/// can be found in [`uefi::proto::device_path::build`]
///
/// See the [module-level documentation] for more details.
///
Expand Down Expand Up @@ -189,6 +211,31 @@ impl DevicePathNode {
pub fn as_enum(&self) -> Result<DevicePathNodeEnum, NodeConversionError> {
DevicePathNodeEnum::try_from(self)
}

/// Transforms the device path node to its string representation using the
/// [`DevicePathToText`] protocol.
///
/// The resulting string is only None, if there was not enough memory.
#[cfg(feature = "alloc")]
pub fn to_string(
&self,
bs: &BootServices,
display_only: DisplayOnly,
allow_shortcuts: AllowShortcuts,
) -> Result<Option<CString16>, DevicePathToTextError> {
let to_text_protocol = open_text_protocol(bs)?;

let cstring16 = to_text_protocol
.convert_device_node_to_text(bs, self, display_only, allow_shortcuts)
.ok()
.map(|pool_string| {
let cstr16 = &*pool_string;
// Another allocation; pool string is dropped. This overhead
// is negligible. CString16 is more convenient to use.
CString16::from(cstr16)
});
Ok(cstring16)
}
}

impl Debug for DevicePathNode {
Expand Down Expand Up @@ -377,6 +424,31 @@ impl DevicePath {
let data = data.into_boxed_slice();
unsafe { mem::transmute(data) }
}

/// Transforms the device path to its string representation using the
/// [`DevicePathToText`] protocol.
///
/// The resulting string is only None, if there was not enough memory.
#[cfg(feature = "alloc")]
pub fn to_string(
&self,
bs: &BootServices,
display_only: DisplayOnly,
allow_shortcuts: AllowShortcuts,
) -> Result<Option<CString16>, DevicePathToTextError> {
let to_text_protocol = open_text_protocol(bs)?;

let cstring16 = to_text_protocol
.convert_device_path_to_text(bs, self, display_only, allow_shortcuts)
.ok()
.map(|pool_string| {
let cstr16 = &*pool_string;
// Another allocation; pool string is dropped. This overhead
// is negligible. CString16 is more convenient to use.
CString16::from(cstr16)
});
Ok(cstring16)
}
}

impl Debug for DevicePath {
Expand Down Expand Up @@ -700,6 +772,63 @@ impl Deref for LoadedImageDevicePath {
}
}

/// Errors that may happen when a device path is transformed to a string
/// representation using:
/// - [`DevicePath::to_string`]
/// - [`DevicePathNode::to_string`]
#[derive(Debug)]
pub enum DevicePathToTextError {
/// Can't locate a handle buffer with handles associated with the
/// [`DevicePathToText`] protocol.
CantLocateHandleBuffer(crate::Error),
/// There is no handle supporting the [`DevicePathToText`] protocol.
NoHandle,
/// The handle supporting the [`DevicePathToText`] protocol exists but it
/// could not be opened.
CantOpenProtocol(crate::Error),
}

impl Display for DevicePathToTextError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{self:?}")
}
}

#[cfg(feature = "unstable")]
impl core::error::Error for DevicePathToTextError {
fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
match self {
DevicePathToTextError::CantLocateHandleBuffer(e) => Some(e),
DevicePathToTextError::CantOpenProtocol(e) => Some(e),
_ => None,
}
}
}

/// Helper function to open the [`DevicePathToText`] protocol using the boot
/// services.
fn open_text_protocol(
bs: &BootServices,
) -> Result<ScopedProtocol<DevicePathToText>, DevicePathToTextError> {
let &handle = bs
.locate_handle_buffer(SearchType::ByProtocol(&DevicePathToText::GUID))
.map_err(DevicePathToTextError::CantLocateHandleBuffer)?
.first()
.ok_or(DevicePathToTextError::NoHandle)?;

unsafe {
bs.open_protocol::<DevicePathToText>(
OpenProtocolParams {
handle,
agent: bs.image_handle(),
controller: None,
},
OpenProtocolAttributes::GetProtocol,
)
}
.map_err(DevicePathToTextError::CantOpenProtocol)
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
1 change: 1 addition & 0 deletions xtask/src/qemu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,7 @@ pub fn run_qemu(arch: UefiArch, opt: &QemuOpt) -> Result<()> {
None
};

// Print the actual used QEMU command for running the test.
println!("{}", command_to_string(&cmd));

cmd.stdin(Stdio::piped());
Expand Down