Skip to content

Encode UEFI's peculiar string handling in the type system #61

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 9 commits into from
Oct 14, 2018
128 changes: 128 additions & 0 deletions src/data_types/chars.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
//! UEFI character handling
//!
//! UEFI uses both Latin-1 and UCS-2 character encoding, this module implements
//! support for the associated character types.

use core::convert::{TryFrom, TryInto};
use core::fmt;

/// Character conversion error
pub struct CharConversionError;

/// A Latin-1 character
#[derive(Clone, Copy, Default, Eq, PartialEq, PartialOrd, Ord)]
#[repr(transparent)]
pub struct Char8(u8);

impl TryFrom<char> for Char8 {
type Error = CharConversionError;

fn try_from(value: char) -> Result<Self, Self::Error> {
let code_point = value as u32;
if code_point <= 0xff {
Ok(Char8(code_point as u8))
} else {
Err(CharConversionError)
}
}
}

impl Into<char> for Char8 {
fn into(self) -> char {
self.0 as char
}
}

impl From<u8> for Char8 {
fn from(value: u8) -> Self {
Char8(value)
}
}

impl Into<u8> for Char8 {
fn into(self) -> u8 {
self.0 as u8
}
}

impl fmt::Debug for Char8 {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
<char as fmt::Debug>::fmt(&From::from(self.0), f)
}
}

impl fmt::Display for Char8 {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
<char as fmt::Display>::fmt(&From::from(self.0), f)
}
}

/// Latin-1 version of the NUL character
pub const NUL_8: Char8 = Char8(0);

/// An UCS-2 code point
#[derive(Clone, Copy, Default, Eq, PartialEq, PartialOrd, Ord)]
#[repr(transparent)]
pub struct Char16(u16);

impl TryFrom<char> for Char16 {
type Error = CharConversionError;

fn try_from(value: char) -> Result<Self, Self::Error> {
let code_point = value as u32;
if code_point <= 0xffff {
Ok(Char16(code_point as u16))
} else {
Err(CharConversionError)
}
}
}

impl Into<char> for Char16 {
fn into(self) -> char {
u32::from(self.0).try_into().unwrap()
}
}

impl TryFrom<u16> for Char16 {
type Error = CharConversionError;

fn try_from(value: u16) -> Result<Self, Self::Error> {
// We leverage char's TryFrom<u32> impl for Unicode validity checking
let res: Result<char, _> = u32::from(value).try_into();
if let Ok(ch) = res {
ch.try_into()
} else {
Err(CharConversionError)
}
}
}

impl Into<u16> for Char16 {
fn into(self) -> u16 {
self.0 as u16
}
}

impl fmt::Debug for Char16 {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if let Ok(c) = u32::from(self.0).try_into() {
<char as fmt::Debug>::fmt(&c, f)
} else {
write!(f, "Char16({:?})", self.0)
}
}
}

impl fmt::Display for Char16 {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if let Ok(c) = u32::from(self.0).try_into() {
<char as fmt::Display>::fmt(&c, f)
} else {
write!(f, "{}", core::char::REPLACEMENT_CHARACTER)
}
}
}

/// UCS-2 version of the NUL character
pub const NUL_16: Char16 = Char16(0);
10 changes: 10 additions & 0 deletions src/data_types/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
//! Data type definitions
//!
//! This module defines the basic data types that are used throughout uefi-rs

use core::ffi::c_void;

/// Opaque handle to an UEFI entity (protocol, image...)
Expand All @@ -13,5 +17,11 @@ pub struct Event(*mut c_void);
mod guid;
pub use self::guid::Guid;

pub mod chars;
pub use self::chars::{Char16, Char8};

#[macro_use]
mod enums;

mod strs;
pub use self::strs::{CStr16, CStr8};
134 changes: 134 additions & 0 deletions src/data_types/strs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
use super::chars::{Char16, Char8, NUL_16, NUL_8};
use core::convert::TryInto;
use core::result::Result;
use core::slice;

/// Errors which can occur during checked [uN] -> CStrN conversions
pub enum FromSliceWithNulError {
/// An invalid character was encountered before the end of the slice
InvalidChar(usize),

/// A null character was encountered before the end of the slice
InteriorNul(usize),

/// The slice was not null-terminated
NotNulTerminated,
}

/// A Latin-1 null-terminated string
///
/// This type is largely inspired by std::ffi::CStr, see documentation of CStr
/// for more details on its semantics.
///
#[repr(transparent)]
pub struct CStr8([Char8]);

impl CStr8 {
/// Wraps a raw UEFI string with a safe C string wrapper
pub unsafe fn from_ptr<'a>(ptr: *const Char8) -> &'a Self {
let mut len = 0;
while *ptr.add(len) != NUL_8 {
len += 1
}
let ptr = ptr as *const u8;
Self::from_bytes_with_nul_unchecked(slice::from_raw_parts(ptr, len + 1))
}

/// Creates a C string wrapper from bytes
pub fn from_bytes_with_nul(chars: &[u8]) -> Result<&Self, FromSliceWithNulError> {
let nul_pos = chars.iter().position(|&c| c == 0);
if let Some(nul_pos) = nul_pos {
if nul_pos + 1 != chars.len() {
return Err(FromSliceWithNulError::InteriorNul(nul_pos));
}
Ok(unsafe { Self::from_bytes_with_nul_unchecked(chars) })
} else {
Err(FromSliceWithNulError::NotNulTerminated)
}
}

/// Unsafely creates a C string wrapper from bytes
pub unsafe fn from_bytes_with_nul_unchecked(chars: &[u8]) -> &Self {
&*(chars as *const [u8] as *const Self)
}

/// Returns the inner pointer to this C string
pub fn as_ptr(&self) -> *const Char8 {
self.0.as_ptr()
}

/// Converts this C string to a slice of bytes
pub fn to_bytes(&self) -> &[u8] {
let chars = self.to_bytes_with_nul();
&chars[..chars.len() - 1]
}

/// Converts this C string to a slice of bytes containing the trailing 0 char
pub fn to_bytes_with_nul(&self) -> &[u8] {
unsafe { &*(&self.0 as *const [Char8] as *const [u8]) }
}
}

/// An UCS-2 null-terminated string
///
/// This type is largely inspired by std::ffi::CStr, see documentation of CStr
/// for more details on its semantics.
///
#[repr(transparent)]
pub struct CStr16([Char16]);

impl CStr16 {
/// Wraps a raw UEFI string with a safe C string wrapper
pub unsafe fn from_ptr<'a>(ptr: *const Char16) -> &'a Self {
let mut len = 0;
while *ptr.add(len) != NUL_16 {
len += 1
}
let ptr = ptr as *const u16;
Self::from_u16_with_nul_unchecked(slice::from_raw_parts(ptr, len + 1))
}

/// Creates a C string wrapper from a u16 slice
///
/// Since not every u16 value is a valid UCS-2 code point, this function
/// must do a bit more validity checking than CStr::from_bytes_with_nul
pub fn from_u16_with_nul(codes: &[u16]) -> Result<&Self, FromSliceWithNulError> {
for (pos, &code) in codes.iter().enumerate() {
match code.try_into() {
Ok(NUL_16) => {
if pos != codes.len() - 1 {
return Err(FromSliceWithNulError::InteriorNul(pos));
} else {
return Ok(unsafe { Self::from_u16_with_nul_unchecked(codes) });
}
}
Err(_) => {
return Err(FromSliceWithNulError::InvalidChar(pos));
}
_ => {}
}
}
Err(FromSliceWithNulError::NotNulTerminated)
}

/// Unsafely creates a C string wrapper from a u16 slice.
pub unsafe fn from_u16_with_nul_unchecked(codes: &[u16]) -> &Self {
&*(codes as *const [u16] as *const Self)
}

/// Returns the inner pointer to this C string
pub fn as_ptr(&self) -> *const Char16 {
self.0.as_ptr()
}

/// Converts this C string to a u16 slice
pub fn to_u16_slice(&self) -> &[u16] {
let chars = self.to_u16_slice_with_nul();
&chars[..chars.len() - 1]
}

/// Converts this C string to a u16 slice containing the trailing 0 char
pub fn to_u16_slice_with_nul(&self) -> &[u16] {
unsafe { &*(&self.0 as *const [Char16] as *const [u16]) }
}
}
5 changes: 3 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,16 @@
//! therefore all the network protocols will be unavailable.

#![feature(optin_builtin_traits)]
#![feature(try_from)]
#![feature(try_trait)]
#![no_std]
// Enable some additional warnings and lints.
#![warn(missing_docs, unused)]
#![deny(clippy::all)]

#[macro_use]
mod data_types;
pub use self::data_types::{Event, Guid, Handle};
pub mod data_types;
pub use self::data_types::{CStr16, CStr8, Char16, Char8, Event, Guid, Handle};

mod error;
pub use self::error::{Completion, Result, ResultExt, Status};
Expand Down
4 changes: 2 additions & 2 deletions src/proto/console/text/input.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use core::mem;
use crate::{Event, Result, Status};
use crate::{Char16, Event, Result, Status};

/// Interface for text-based input devices.
#[repr(C)]
Expand Down Expand Up @@ -54,7 +54,7 @@ pub struct Key {
pub scan_code: ScanCode,
/// Associated Unicode character,
/// or 0 if not printable.
pub unicode_char: u16,
pub unicode_char: Char16,
}

newtype_enum! {
Expand Down
19 changes: 11 additions & 8 deletions src/proto/console/text/output.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use core::fmt;
use crate::prelude::*;
use crate::{Completion, Result, Status};
use crate::{CStr16, Char16, Completion, Result, Status};

/// Interface for text-based output devices.
///
Expand All @@ -9,8 +9,8 @@ use crate::{Completion, Result, Status};
#[repr(C)]
pub struct Output {
reset: extern "win64" fn(this: &Output, extended: bool) -> Status,
output_string: extern "win64" fn(this: &Output, string: *const u16) -> Status,
test_string: extern "win64" fn(this: &Output, string: *const u16) -> Status,
output_string: extern "win64" fn(this: &Output, string: *const Char16) -> Status,
test_string: extern "win64" fn(this: &Output, string: *const Char16) -> Status,
query_mode: extern "win64" fn(this: &Output, mode: i32, columns: &mut usize, rows: &mut usize)
-> Status,
set_mode: extern "win64" fn(this: &mut Output, mode: i32) -> Status,
Expand All @@ -36,17 +36,17 @@ impl Output {
}

/// Writes a string to the output device.
pub fn output_string(&mut self, string: *const u16) -> Result<()> {
(self.output_string)(self, string).into()
pub fn output_string(&mut self, string: &CStr16) -> Result<()> {
(self.output_string)(self, string.as_ptr()).into()
}

/// Checks if a string contains only supported characters.
/// True indicates success.
///
/// UEFI applications are encouraged to try to print a string even if it contains
/// some unsupported characters.
pub fn test_string(&mut self, string: *const u16) -> bool {
match (self.test_string)(self, string) {
pub fn test_string(&mut self, string: &CStr16) -> bool {
match (self.test_string)(self, string.as_ptr()) {
Status::SUCCESS => true,
_ => false,
}
Expand Down Expand Up @@ -146,9 +146,12 @@ impl fmt::Write for Output {
// This closure writes the local buffer to the output and resets the buffer.
let mut flush_buffer = |buf: &mut [u16], i: &mut usize| {
buf[*i] = 0;
let codes = &buf[..=*i];
*i = 0;

self.output_string(buf.as_ptr())
let text = CStr16::from_u16_with_nul(codes).map_err(|_| fmt::Error)?;

self.output_string(text)
.warning_as_error()
.map_err(|_| fmt::Error)
};
Expand Down
Loading