diff --git a/uefi-raw/CHANGELOG.md b/uefi-raw/CHANGELOG.md index e0c875bdb..cfb8898a7 100644 --- a/uefi-raw/CHANGELOG.md +++ b/uefi-raw/CHANGELOG.md @@ -3,6 +3,8 @@ ## Added - Added `AllocateType`. - Added `PciRootBridgeIoProtocol`. +- Added `HiiKeywordHandlerProtocol`. +- Added `HiiConfigAccessProtocol`. # uefi-raw - 0.11.0 (2025-05-04) diff --git a/uefi-raw/src/protocol/hii/config.rs b/uefi-raw/src/protocol/hii/config.rs new file mode 100644 index 000000000..f77db2805 --- /dev/null +++ b/uefi-raw/src/protocol/hii/config.rs @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +//! Bindings for HII protocols relating to system configuration. + +use core::fmt::Debug; + +use crate::{Char16, Guid, Status, guid}; + +/// EFI_KEYWORD_HANDLER_PROTOCOL +#[derive(Debug)] +#[repr(C)] +pub struct HiiKeywordHandlerProtocol { + pub set_data: unsafe extern "efiapi" fn( + this: *mut Self, + keyword_string: *const Char16, + progress: *mut *const Char16, + progress_err: *mut u32, + ) -> Status, + pub get_data: unsafe extern "efiapi" fn( + this: *const Self, + namespace_id: *const Char16, + keyword_string: *const Char16, + progress: *mut *const Char16, + progress_err: *mut u32, + results: *mut *const Char16, + ) -> Status, +} + +impl HiiKeywordHandlerProtocol { + pub const GUID: Guid = guid!("0a8badd5-03b8-4d19-b128-7b8f0edaa596"); +} + +newtype_enum! { + /// Type of action taken by the form browser + #[derive(Default)] + pub enum EfiBrowserAction: u32 => { + /// Called before the browser changes the value in the display (for questions which have a value) + /// or takes an action (in the case of an action button or cross-reference). + /// If EFI_SUCCESS is returned, the browser uses the value returned by Callback(). + CHANGING = 0, + /// Called after the browser has changed its internal copy of the question value and displayed it (if appropriate). + /// For action buttons, this is called after processing. Errors are ignored. + CHANGED = 1, + /// Called after the browser has read the current question value but before displaying it. + /// If EFI_SUCCESS is returned, the updated value is used. + RETRIEVE = 2, + /// Called for each question on a form prior to its value being retrieved or displayed. + /// If a question appears on more than one form, this may be called more than once. + FORM_OPEN = 3, + /// Called for each question on a form after processing any submit actions for that form. + /// If a question appears on multiple forms, this will be called more than once. + FORM_CLOSE = 4, + /// Called after the browser submits the modified question value. + /// ActionRequest is ignored. + SUBMITTED = 5, + /// Represents the standard default action, selecting a default value based on lower-priority methods. + DEFAULT_STANDARD = 0x1000, + /// Represents the manufacturing default action, selecting a default value relevant to manufacturing. + DEFAULT_MANUFACTURING = 0x1001, + /// Represents the safe default action, selecting the safest possible default value. + DEFAULT_SAFE = 0x1002, + /// Represents platform-defined default values within a range of possible store identifiers. + DEFAULT_PLATFORM = 0x2000, + /// Represents hardware-defined default values within a range of possible store identifiers. + DEFAULT_HARDWARE = 0x3000, + /// Represents firmware-defined default values within a range of possible store identifiers. + DEFAULT_FIRMWARE = 0x4000, + } +} + +newtype_enum! { + /// Represents actions requested by the Forms Browser in response to user interactions. + #[derive(Default)] + pub enum EfiBrowserActionRequest: usize => { + /// No special behavior is taken by the Forms Browser. + NONE = 0, + /// The Forms Browser will exit and request the platform to reset. + RESET = 1, + /// The Forms Browser will save all modified question values to storage and exit. + SUBMIT = 2, + /// The Forms Browser will discard all modified question values and exit. + EXIT = 3, + /// The Forms Browser will write all modified question values on the selected form to storage and exit the form. + FORM_SUBMIT_EXIT = 4, + /// The Forms Browser will discard the modified question values on the selected form and exit the form. + FORM_DISCARD_EXIT = 5, + /// The Forms Browser will write all modified current question values on the selected form to storage. + FORM_APPLY = 6, + /// The Forms Browser will discard the current question values on the selected form and replace them with the original values. + FORM_DISCARD = 7, + /// The user performed a hardware or software configuration change, requiring controller reconnection. + /// The Forms Browser calls `DisconnectController()` followed by `ConnectController()`. + RECONNECT = 8, + /// The Forms Browser will write the current modified question value on the selected form to storage. + QUESTION_APPLY = 9, + } +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct EfiHiiTime { + pub hour: u8, + pub minute: u8, + pub second: u8, +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct EfiHiiDate { + pub year: u16, + pub month: u8, + pub day: u8, +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct EfiHiiRef { + pub question_id: EfiQuestionId, + pub form_id: EfiFormId, + pub guid: Guid, + pub string_id: EfiStringId, +} + +#[repr(C)] +#[derive(Copy, Clone)] +pub union EfiIfrTypeValue { + pub u8: u8, // EFI_IFR_TYPE_NUM_SIZE_8 + pub u16: u16, // EFI_IFR_TYPE_NUM_SIZE_16 + pub u32: u32, // EFI_IFR_TYPE_NUM_SIZE_32 + pub u64: u64, // EFI_IFR_TYPE_NUM_SIZE_64 + pub b: bool, // EFI_IFR_TYPE_BOOLEAN + pub time: EfiHiiTime, // EFI_IFR_TYPE_TIME + pub date: EfiHiiDate, // EFI_IFR_TYPE_DATE + pub string: EfiStringId, // EFI_IFR_TYPE_STRING, EFI_IFR_TYPE_ACTION + pub hii_ref: EfiHiiRef, // EFI_IFR_TYPE_REF +} +impl core::fmt::Debug for EfiIfrTypeValue { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("EfiIfrTypeValue").finish() + } +} + +pub type EfiQuestionId = u16; +pub type EfiFormId = u16; +pub type EfiStringId = u16; + +/// EFI_HII_CONFIG_ACCESS_PROTOCOL +#[derive(Debug)] +#[repr(C)] +pub struct HiiConfigAccessProtocol { + pub extract_config: unsafe extern "efiapi" fn( + this: *const Self, + request: *const Char16, + progress: *mut *const Char16, + results: *mut *const Char16, + ) -> Status, + pub route_config: unsafe extern "efiapi" fn( + this: *const Self, + configuration: *const Char16, + progress: *mut *const Char16, + ) -> Status, + pub callback: unsafe extern "efiapi" fn( + this: *const Self, + action: EfiBrowserAction, + question_id: u16, + value_type: u8, + value: *mut EfiIfrTypeValue, + action_request: *mut EfiBrowserActionRequest, + ) -> Status, +} + +impl HiiConfigAccessProtocol { + pub const GUID: Guid = guid!("330d4706-f2a0-4e4f-a369-b66fa8d54385"); +} diff --git a/uefi-raw/src/protocol/hii/mod.rs b/uefi-raw/src/protocol/hii/mod.rs index a224364f2..25d68300e 100644 --- a/uefi-raw/src/protocol/hii/mod.rs +++ b/uefi-raw/src/protocol/hii/mod.rs @@ -2,6 +2,7 @@ //! HII Protocols +pub mod config; pub mod database; use crate::{Char16, Guid}; diff --git a/uefi/CHANGELOG.md b/uefi/CHANGELOG.md index 480f5e798..821a533c0 100644 --- a/uefi/CHANGELOG.md +++ b/uefi/CHANGELOG.md @@ -4,6 +4,9 @@ - Added `ConfigTableEntry::MEMORY_ATTRIBUTES_GUID` and `ConfigTableEntry::IMAGE_SECURITY_DATABASE_GUID`. - Added `proto::usb::io::UsbIo`. - Added `proto::pci::PciRootBridgeIo`. +- Added `proto::hii::config::HiiKeywordHandler`. +- Added `proto::hii::config::HiiConfigAccess`. +- Added `proto::hii::config_str::ConfigurationString`. ## Changed - **Breaking:** `boot::stall` now take `core::time::Duration` instead of `usize`. diff --git a/uefi/src/proto/hii/config.rs b/uefi/src/proto/hii/config.rs new file mode 100644 index 000000000..db001549c --- /dev/null +++ b/uefi/src/proto/hii/config.rs @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +//! HII Configuration protocols. + +use uefi_macros::unsafe_protocol; +use uefi_raw::protocol::hii::config::{HiiConfigAccessProtocol, HiiKeywordHandlerProtocol}; + +/// The HII Keyword Handler Protocol. +/// +/// # UEFI Spec Description +/// +/// This protocol provides the mechanism to set and get the values associated +/// with a keyword exposed through a x-UEFI- prefixed configuration language namespace. +#[derive(Debug)] +#[repr(transparent)] +#[unsafe_protocol(HiiKeywordHandlerProtocol::GUID)] +pub struct HiiKeywordHandler(HiiKeywordHandlerProtocol); + +/// The HII Configuration Access Protocol. +/// +/// # UEFI Spec Description +/// +/// This protocol is responsible for facilitating access to configuration data from HII. +/// It is typically invoked by the HII Configuration Routing Protocol for handling +/// configuration requests. Forms browsers also interact with this protocol through +/// the `Callback()` function. +#[derive(Debug)] +#[repr(transparent)] +#[unsafe_protocol(HiiConfigAccessProtocol::GUID)] +pub struct HiiConfigAccess(HiiConfigAccessProtocol); diff --git a/uefi/src/proto/hii/config_str.rs b/uefi/src/proto/hii/config_str.rs new file mode 100644 index 000000000..91040000d --- /dev/null +++ b/uefi/src/proto/hii/config_str.rs @@ -0,0 +1,261 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +//! UEFI Configuration String parsing according to Spec 35.2.1 + +use alloc::boxed::Box; +use alloc::string::{String, ToString}; +use alloc::vec::Vec; +use core::slice; +use core::str::{self, FromStr}; +use uguid::Guid; + +use crate::proto::device_path::DevicePath; +use crate::{CStr16, Char16}; + +/// A helper struct to split and parse a UEFI Configuration String. +/// +/// Configuration strings consist of key-value pairs separated by `&`. Keys +/// and values are separated by `=`. This struct provides an iterator for +/// easy traversal of the key-value pairs. +/// +/// For reasons of developer sanity, this is operating on &str instead of &CStr16. +#[derive(Debug)] +pub struct ConfigurationStringSplitter<'a> { + bfr: &'a str, +} + +impl<'a> ConfigurationStringSplitter<'a> { + /// Creates a new splitter instance for a given configuration string buffer. + #[must_use] + pub const fn new(bfr: &'a str) -> Self { + Self { bfr } + } +} + +impl<'a> Iterator for ConfigurationStringSplitter<'a> { + type Item = (&'a str, Option<&'a str>); + + fn next(&mut self) -> Option { + if self.bfr.is_empty() { + return None; + } + let (keyval, remainder) = self + .bfr + .split_once('&') + .unwrap_or((self.bfr, &self.bfr[0..0])); + self.bfr = remainder; + let (key, value) = keyval + .split_once('=') + .map(|(key, value)| (key, Some(value))) + .unwrap_or((keyval, None)); + Some((key, value)) + } +} + +/// Enum representing different sections of a UEFI Configuration Header. +/// +/// These sections include GUID, Name, and Path elements, which provide +/// routing and identification information for UEFI components. +#[derive(Debug, PartialEq, Eq)] +pub enum ConfigHdrSection { + /// UEFI ConfigurationString {GuidHdr} element + Guid, + /// UEFI ConfigurationString {NameHdr} element + Name, + /// UEFI ConfigurationString {PathHdr} element + Path, +} + +/// Enum representing possible parsing errors encountered when processing +/// UEFI Configuration Strings. +#[derive(Debug)] +pub enum ParseError { + /// Error while parsing the UEFI {ConfigHdr} configuration string section. + ConfigHdr(ConfigHdrSection), + /// Error while parsing the UEFI {BlockName} configuration string section. + BlockName, + /// Error while parsing the UEFI {BlockConfig} configuration string section. + BlockConfig, +} + +/// Represents an individual element within a UEFI Configuration String. +/// +/// Each element contains an offset, width, and value, defining the data +/// stored at specific memory locations within the configuration. +#[derive(Debug, Default)] +pub struct ConfigurationStringElement { + /// Byte offset in the configuration block + pub offset: u64, + /// Length of the value starting at offset + pub width: u64, + /// Value bytes + pub value: Vec, + // TODO + // nvconfig: HashMap>, +} + +/// A full UEFI Configuration String representation. +/// +/// This structure contains routing information such as GUID and device path, +/// along with the parsed configuration elements. +#[derive(Debug)] +pub struct ConfigurationString { + /// GUID used for identifying the configuration + pub guid: Guid, + /// Name field (optional identifier) + pub name: String, + /// Associated UEFI device path + pub device_path: Box, + /// Parsed UEFI {ConfigElement} sections + pub elements: Vec, +} + +impl ConfigurationString { + fn try_parse_with Option>( + err: ParseError, + parse_fn: F, + ) -> Result { + parse_fn().ok_or(err) + } + + /// Parses a hexadecimal string into an iterator of bytes. + /// + /// # Arguments + /// + /// * `hex` - The hexadecimal string representing binary data. + /// + /// # Returns + /// + /// An iterator over bytes. + pub fn parse_bytes_from_hex(hex: &str) -> impl Iterator { + hex.as_bytes().chunks(2).map(|chunk| { + let chunk = str::from_utf8(chunk).unwrap_or_default(); + u8::from_str_radix(chunk, 16).unwrap_or_default() + }) + } + + /// Converts a hexadecimal string representation into a numeric value. + /// + /// # Arguments + /// + /// * `data` - The hexadecimal string to convert. + /// + /// # Returns + /// + /// An `Option` representing the parsed number. + #[must_use] + pub fn parse_number_from_hex(data: &str) -> Option { + let data: Vec<_> = Self::parse_bytes_from_hex(data).collect(); + match data.len() { + 8 => Some(u64::from_be_bytes(data.try_into().unwrap())), + 4 => Some(u32::from_be_bytes(data.try_into().unwrap()) as u64), + 2 => Some(u16::from_be_bytes(data.try_into().unwrap()) as u64), + 1 => Some(u8::from_be_bytes(data.try_into().unwrap()) as u64), + _ => None, + } + } + + /// Converts a hexadecimal string into a UTF-16 string. + /// + /// # Arguments + /// + /// * `data` - The hexadecimal representation of a string. + /// + /// # Returns + /// + /// An `Option` containing the parsed string. + #[must_use] + pub fn parse_string_from_hex(data: &str) -> Option { + if data.len() % 2 != 0 { + return None; + } + let mut data: Vec<_> = Self::parse_bytes_from_hex(data).collect(); + data.chunks_exact_mut(2).for_each(|c| c.swap(0, 1)); + data.extend_from_slice(&[0, 0]); + let data: &[Char16] = + unsafe { slice::from_raw_parts(data.as_slice().as_ptr().cast(), data.len() / 2) }; + Some(CStr16::from_char16_with_nul(data).ok()?.to_string()) + } + + /// Parses a hexadecimal string into a UEFI GUID. + /// + /// # Arguments + /// + /// * `data` - The hexadecimal GUID representation. + /// + /// # Returns + /// + /// An `Option` containing the parsed GUID. + #[must_use] + pub fn parse_guid_from_hex(data: &str) -> Option { + let v: Vec<_> = Self::parse_bytes_from_hex(data).collect(); + Some(Guid::from_bytes(v.try_into().ok()?)) + } +} + +impl FromStr for ConfigurationString { + type Err = ParseError; + + fn from_str(bfr: &str) -> Result { + let mut splitter = ConfigurationStringSplitter::new(bfr).peekable(); + + let guid = Self::try_parse_with(ParseError::ConfigHdr(ConfigHdrSection::Guid), || { + let v = splitter.next()?; + let v = (v.0 == "GUID").then_some(v.1).flatten()?; + Self::parse_guid_from_hex(v) + })?; + let name = Self::try_parse_with(ParseError::ConfigHdr(ConfigHdrSection::Name), || { + let v = splitter.next()?; + let v = (v.0 == "NAME").then_some(v.1).flatten()?; + Self::parse_string_from_hex(v) + })?; + let device_path = + Self::try_parse_with(ParseError::ConfigHdr(ConfigHdrSection::Path), || { + let v = splitter.next()?.1?; + let v: Vec<_> = Self::parse_bytes_from_hex(v).collect(); + let v = <&DevicePath>::try_from(v.as_slice()).ok()?; + Some(v.to_boxed()) + })?; + + let mut elements = Vec::new(); + loop { + let offset = match splitter.next() { + Some(("OFFSET", Some(data))) => { + Self::parse_number_from_hex(data).ok_or(ParseError::BlockName)? + } + None => break, + _ => return Err(ParseError::BlockName), + }; + let width = match splitter.next() { + Some(("WIDTH", Some(data))) => { + Self::parse_number_from_hex(data).ok_or(ParseError::BlockName)? + } + _ => return Err(ParseError::BlockName), + }; + let value = match splitter.next() { + Some(("VALUE", Some(data))) => Self::parse_bytes_from_hex(data).collect(), + _ => return Err(ParseError::BlockConfig), + }; + + while let Some(next) = splitter.peek() { + if next.0 == "OFFSET" { + break; + } + let _ = splitter.next(); // drop nvconfig entries for now + } + + elements.push(ConfigurationStringElement { + offset, + width, + value, + }); + } + + Ok(Self { + guid, + name, + device_path, + elements, + }) + } +} diff --git a/uefi/src/proto/hii/mod.rs b/uefi/src/proto/hii/mod.rs new file mode 100644 index 000000000..bdece1069 --- /dev/null +++ b/uefi/src/proto/hii/mod.rs @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +//! HII Protocols + +pub mod config; +#[cfg(feature = "alloc")] +pub mod config_str; diff --git a/uefi/src/proto/mod.rs b/uefi/src/proto/mod.rs index 2ac72c3e8..68260b3d4 100644 --- a/uefi/src/proto/mod.rs +++ b/uefi/src/proto/mod.rs @@ -16,6 +16,7 @@ pub mod console; pub mod debug; pub mod device_path; pub mod driver; +pub mod hii; pub mod loaded_image; pub mod media; pub mod misc;