uefi-raw: add time integration into module

This helps to better keep an overview.
This commit is contained in:
Philipp Schuster
2026-02-22 13:55:46 +01:00
parent 59ae1afb26
commit dbbf96e06d
+271 -271
View File
@@ -2,69 +2,11 @@
//! Date and time types.
#[cfg(feature = "time")]
pub use time_crate_integration::*;
use bitflags::bitflags;
use core::error;
use core::fmt::{self, Display, Formatter};
#[cfg(feature = "time")]
use time::{OffsetDateTime, UtcOffset};
/// The time is invalid as it doesn't fulfill the requirements of
/// [`Time::is_valid`].
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg(feature = "time")]
pub struct InvalidTimeError(Time);
#[cfg(feature = "time")]
impl Display for InvalidTimeError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "Invalid EFI time (not valid): {}", self.0)
}
}
#[cfg(feature = "time")]
impl error::Error for InvalidTimeError {}
/// Errors that can happen when converting a [`Time`] to a [`OffsetDateTime`].
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg(feature = "time")]
pub enum ToOffsetDateTimeError {
/// See [`InvalidTimeError`].
Invalid(InvalidTimeError),
/// The timezone is a valid EFI value but [`Time::UNSPECIFIED_TIMEZONE`]
/// means `local` which cannot be determined in a `no_std` context.
UnspecifiedTimezone,
/// The corresponding component has an invalid range (e.g. February 30th).
ComponentRange(time::error::ComponentRange),
}
#[cfg(feature = "time")]
impl Display for ToOffsetDateTimeError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Invalid(e) => {
write!(f, "Cannot convert to OffsetDateTime: {}", e)
}
Self::UnspecifiedTimezone => write!(
f,
"Cannot convert to OffsetDateTime: the timezone (local) can't be determined in a no_std context"
),
Self::ComponentRange(e) => {
write!(f, "Cannot convert to OffsetDateTime: {}", e)
}
}
}
}
#[cfg(feature = "time")]
impl error::Error for ToOffsetDateTimeError {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match self {
Self::Invalid(e) => Some(e),
Self::UnspecifiedTimezone => None,
Self::ComponentRange(e) => Some(e),
}
}
}
/// Date and time representation.
///
@@ -120,150 +62,6 @@ impl Time {
/// Indicates the time should be interpreted as local time.
pub const UNSPECIFIED_TIMEZONE: i16 = 0x07ff;
#[cfg(feature = "time")]
fn _to_offset_date_time(
&self,
local_timezone: Option<UtcOffset>,
) -> Result<OffsetDateTime, ToOffsetDateTimeError> {
if !self.is_valid() {
return Err(ToOffsetDateTimeError::Invalid(InvalidTimeError(*self)));
}
// Special handling for `UNSPECIFIED_TIMEZONE` (local):
// - specific time zone => all good
// - `UNSPECIFIED_TIMEZONE` and no default time zone => return Err
// - else: use the default time zone
let offset_hms: (i8, i8, i8) = {
if self.time_zone == Self::UNSPECIFIED_TIMEZONE {
if let Some(local_timezone) = local_timezone {
local_timezone.as_hms()
} else {
return Err(ToOffsetDateTimeError::UnspecifiedTimezone);
}
} else {
let h = self.time_zone / 60;
let m = self.time_zone % 60;
(h as i8, m as i8, 0)
}
};
// Emulated try {} block to keep the `?` error propagation scoped
// (we have a different error type here)
let datetime: Result<OffsetDateTime, time::error::ComponentRange> = (|| {
let month = time::Month::try_from(self.month)?;
let date = time::Date::from_calendar_date(self.year as i32, month, self.day)?;
let time =
time::Time::from_hms_nano(self.hour, self.minute, self.second, self.nanosecond)?;
let offset = UtcOffset::from_hms(offset_hms.0, offset_hms.1, offset_hms.2)?;
Ok(OffsetDateTime::new_in_offset(date, time, offset))
})();
datetime.map_err(ToOffsetDateTimeError::ComponentRange)
}
/// Converts this [`Time`] to a [`OffsetDateTime`] using the UTC offset
/// stored in `self.time_zone`.
///
/// # Returns
///
/// - `Ok(OffsetDateTime)` if the time is valid and the timezone is fully
/// specified (not `UNSPECIFIED_TIMEZONE`).
/// - `Err(ToOffsetDateTimeError::Invalid(_))` if any of the time fields are
/// out of valid EFI ranges (e.g., year < 1900, month > 12).
/// - `Err(ToOffsetDateTimeError::UnspecifiedTimezone)` if the timezone is
/// `UNSPECIFIED_TIMEZONE`, since no default local timezone was provided.
/// - `Err(ToOffsetDateTimeError::ComponentRange(_))` if the combination of
/// fields results in an invalid calendar date (e.g., February 30th).
///
/// # Examples
///
/// ```
/// # use uefi_raw::time::{Time, ToOffsetDateTimeError};
/// # use time::OffsetDateTime;
/// let t = Time {
/// year: 2024,
/// month: 3,
/// day: 15,
/// hour: 12,
/// minute: 0,
/// second: 0,
/// pad1: 0,
/// nanosecond: 0,
/// time_zone: 0, // UTC
/// daylight: Default::default(),
/// pad2: 0,
/// };
///
/// let odt: OffsetDateTime = t.to_offset_date_time().unwrap();
/// assert_eq!(odt.offset().whole_seconds(), 0);
/// ```
#[cfg(feature = "time")]
pub fn to_offset_date_time(&self) -> Result<OffsetDateTime, ToOffsetDateTimeError> {
self._to_offset_date_time(None)
}
/// Converts this [`Time`] to a [`OffsetDateTime`] using a provided default
/// timezone when `self.time_zone` is [`Self::UNSPECIFIED_TIMEZONE`].
///
/// If the stored `time_zone` field is `UNSPECIFIED_TIMEZONE`, this method
/// uses the provided `local` [`UtcOffset`] as the offset. Otherwise, the
/// stored `time_zone` in minutes is used.
///
/// # Arguments
///
/// - `local`: The default [`UtcOffset`] to use if `time_zone` is
/// [`Self::UNSPECIFIED_TIMEZONE`].
///
/// # Returns
///
/// - `Ok(OffsetDateTime)` if the time is valid and the offset can be
/// applied successfully.
/// - `Err(ToOffsetDateTimeError::Invalid(_))` if any of the time fields are
/// out of valid EFI ranges.
/// - `Err(ToOffsetDateTimeError::ComponentRange(_))` if the combination of
/// fields results in an invalid calendar date.
///
/// # Panics
///
/// This function does **not panic** under normal usage. It is impossible
/// for `UnspecifiedTimezone` to be returned because the caller provides
/// a valid default offset.
///
/// # Examples
///
/// ```
/// # use uefi_raw::time::{Time, ToOffsetDateTimeError};
/// # use time::{OffsetDateTime, UtcOffset};
/// let mut t = Time {
/// year: 2024,
/// month: 3,
/// day: 15,
/// hour: 12,
/// minute: 0,
/// second: 0,
/// pad1: 0,
/// nanosecond: 0,
/// time_zone: Time::UNSPECIFIED_TIMEZONE,
/// daylight: Default::default(),
/// pad2: 0,
/// };
///
/// let default_offset = UtcOffset::from_hms(1, 30, 0).unwrap();
/// let odt: OffsetDateTime = t.to_offset_date_time_with_default_timezone(default_offset).unwrap();
/// assert_eq!(odt.offset(), default_offset);
/// ```
#[cfg(feature = "time")]
pub fn to_offset_date_time_with_default_timezone(
&self,
local: UtcOffset,
) -> Result<OffsetDateTime, ToOffsetDateTimeError> {
match self._to_offset_date_time(Some(local)) {
Err(ToOffsetDateTimeError::UnspecifiedTimezone) => unreachable!(),
other => other,
}
}
/// Create an invalid `Time` with all fields set to zero.
#[must_use]
pub const fn invalid() -> Self {
@@ -360,79 +158,281 @@ bitflags! {
}
#[cfg(feature = "time")]
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum FromOffsetDateTimeError {
/// The year is not representable as `u16`.
InvalidYear(i32),
/// Time zone offsets with seconds are not representable.
OffsetWithSeconds(i8),
InvalidTime(InvalidTimeError),
}
mod time_crate_integration {
use super::*;
use core::error;
use time::{OffsetDateTime, UtcOffset};
#[cfg(feature = "time")]
impl Display for FromOffsetDateTimeError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidYear(y) => {
write!(
f,
"Cannot to convert OffsetDateTime to EFI Time: invalid year {y}"
)
}
Self::OffsetWithSeconds(s) => {
write!(
f,
"Cannot to convert OffsetDateTime to EFI Time: time zone offset has seconds ({s}) which is unsupported"
)
}
Self::InvalidTime(t) => {
write!(f, "The time is invalid: {t}")
}
/// The time is invalid as it doesn't fulfill the requirements of
/// [`Time::is_valid`].
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct InvalidTimeError(Time);
impl Display for InvalidTimeError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "Invalid EFI time (not valid): {}", self.0)
}
}
}
#[cfg(feature = "time")]
impl error::Error for FromOffsetDateTimeError {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match self {
Self::InvalidYear(_) => None,
Self::OffsetWithSeconds(_) => None,
Self::InvalidTime(e) => Some(e),
}
impl error::Error for InvalidTimeError {}
/// Errors that can happen when converting a [`Time`] to a [`OffsetDateTime`].
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum ToOffsetDateTimeError {
/// See [`InvalidTimeError`].
Invalid(InvalidTimeError),
/// The timezone is a valid EFI value but [`Time::UNSPECIFIED_TIMEZONE`]
/// means `local` which cannot be determined in a `no_std` context.
UnspecifiedTimezone,
/// The corresponding component has an invalid range (e.g. February 30th).
ComponentRange(time::error::ComponentRange),
}
}
#[cfg(feature = "time")]
impl TryFrom<OffsetDateTime> for Time {
type Error = FromOffsetDateTimeError;
fn try_from(value: OffsetDateTime) -> Result<Self, Self::Error> {
let this = Self {
year: u16::try_from(value.date().year())
.map_err(|_| Self::Error::InvalidYear(value.date().year()))?,
// No checks needed: underlying type has repr `u8`
month: value.date().month() as u8,
day: value.date().day(),
hour: value.time().hour(),
minute: value.time().minute(),
second: value.time().second(),
pad1: 0,
nanosecond: value.time().nanosecond(),
time_zone: {
let (h, m, s) = value.offset().as_hms();
if s != 0 {
return Err(Self::Error::OffsetWithSeconds(s));
impl Display for ToOffsetDateTimeError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Invalid(e) => {
write!(f, "Cannot convert to OffsetDateTime: {}", e)
}
(h as i16 * 60) + (m as i16)
},
daylight: Daylight::NONE,
pad2: 0,
};
if this.is_valid() {
Ok(this)
} else {
Err(Self::Error::InvalidTime(InvalidTimeError(this)))
Self::UnspecifiedTimezone => write!(
f,
"Cannot convert to OffsetDateTime: the timezone (local) can't be determined in a no_std context"
),
Self::ComponentRange(e) => {
write!(f, "Cannot convert to OffsetDateTime: {}", e)
}
}
}
}
impl error::Error for ToOffsetDateTimeError {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match self {
Self::Invalid(e) => Some(e),
Self::UnspecifiedTimezone => None,
Self::ComponentRange(e) => Some(e),
}
}
}
impl Time {
fn _to_offset_date_time(
&self,
local_timezone: Option<UtcOffset>,
) -> Result<OffsetDateTime, ToOffsetDateTimeError> {
if !self.is_valid() {
return Err(ToOffsetDateTimeError::Invalid(InvalidTimeError(*self)));
}
// Special handling for `UNSPECIFIED_TIMEZONE` (local):
// - specific time zone => all good
// - `UNSPECIFIED_TIMEZONE` and no default time zone => return Err
// - else: use the default time zone
let offset_hms: (i8, i8, i8) = {
if self.time_zone == Self::UNSPECIFIED_TIMEZONE {
if let Some(local_timezone) = local_timezone {
local_timezone.as_hms()
} else {
return Err(ToOffsetDateTimeError::UnspecifiedTimezone);
}
} else {
let h = self.time_zone / 60;
let m = self.time_zone % 60;
(h as i8, m as i8, 0)
}
};
// Emulated try {} block to keep the `?` error propagation scoped
// (we have a different error type here)
let datetime: Result<OffsetDateTime, time::error::ComponentRange> = (|| {
let month = time::Month::try_from(self.month)?;
let date = time::Date::from_calendar_date(self.year as i32, month, self.day)?;
let time = time::Time::from_hms_nano(
self.hour,
self.minute,
self.second,
self.nanosecond,
)?;
let offset = UtcOffset::from_hms(offset_hms.0, offset_hms.1, offset_hms.2)?;
Ok(OffsetDateTime::new_in_offset(date, time, offset))
})();
datetime.map_err(ToOffsetDateTimeError::ComponentRange)
}
/// Converts this [`Time`] to a [`OffsetDateTime`] using the UTC offset
/// stored in `self.time_zone`.
///
/// # Returns
///
/// - `Ok(OffsetDateTime)` if the time is valid and the timezone is fully
/// specified (not `UNSPECIFIED_TIMEZONE`).
/// - `Err(ToOffsetDateTimeError::Invalid(_))` if any of the time fields are
/// out of valid EFI ranges (e.g., year < 1900, month > 12).
/// - `Err(ToOffsetDateTimeError::UnspecifiedTimezone)` if the timezone is
/// `UNSPECIFIED_TIMEZONE`, since no default local timezone was provided.
/// - `Err(ToOffsetDateTimeError::ComponentRange(_))` if the combination of
/// fields results in an invalid calendar date (e.g., February 30th).
///
/// # Examples
///
/// ```
/// # use uefi_raw::time::{Time, ToOffsetDateTimeError};
/// # use time::OffsetDateTime;
/// let t = Time {
/// year: 2024,
/// month: 3,
/// day: 15,
/// hour: 12,
/// minute: 0,
/// second: 0,
/// pad1: 0,
/// nanosecond: 0,
/// time_zone: 0, // UTC
/// daylight: Default::default(),
/// pad2: 0,
/// };
///
/// let odt: OffsetDateTime = t.to_offset_date_time().unwrap();
/// assert_eq!(odt.offset().whole_seconds(), 0);
/// ```
pub fn to_offset_date_time(&self) -> Result<OffsetDateTime, ToOffsetDateTimeError> {
self._to_offset_date_time(None)
}
/// Converts this [`Time`] to a [`OffsetDateTime`] using a provided default
/// timezone when `self.time_zone` is [`Self::UNSPECIFIED_TIMEZONE`].
///
/// If the stored `time_zone` field is `UNSPECIFIED_TIMEZONE`, this method
/// uses the provided `local` [`UtcOffset`] as the offset. Otherwise, the
/// stored `time_zone` in minutes is used.
///
/// # Arguments
///
/// - `local`: The default [`UtcOffset`] to use if `time_zone` is
/// [`Self::UNSPECIFIED_TIMEZONE`].
///
/// # Returns
///
/// - `Ok(OffsetDateTime)` if the time is valid and the offset can be
/// applied successfully.
/// - `Err(ToOffsetDateTimeError::Invalid(_))` if any of the time fields are
/// out of valid EFI ranges.
/// - `Err(ToOffsetDateTimeError::ComponentRange(_))` if the combination of
/// fields results in an invalid calendar date.
///
/// # Panics
///
/// This function does **not panic** under normal usage. It is impossible
/// for `UnspecifiedTimezone` to be returned because the caller provides
/// a valid default offset.
///
/// # Examples
///
/// ```
/// # use uefi_raw::time::{Time, ToOffsetDateTimeError};
/// # use time::{OffsetDateTime, UtcOffset};
/// let mut t = Time {
/// year: 2024,
/// month: 3,
/// day: 15,
/// hour: 12,
/// minute: 0,
/// second: 0,
/// pad1: 0,
/// nanosecond: 0,
/// time_zone: Time::UNSPECIFIED_TIMEZONE,
/// daylight: Default::default(),
/// pad2: 0,
/// };
///
/// let default_offset = UtcOffset::from_hms(1, 30, 0).unwrap();
/// let odt: OffsetDateTime = t.to_offset_date_time_with_default_timezone(default_offset).unwrap();
/// assert_eq!(odt.offset(), default_offset);
/// ```
pub fn to_offset_date_time_with_default_timezone(
&self,
local: UtcOffset,
) -> Result<OffsetDateTime, ToOffsetDateTimeError> {
match self._to_offset_date_time(Some(local)) {
Err(ToOffsetDateTimeError::UnspecifiedTimezone) => unreachable!(),
other => other,
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum FromOffsetDateTimeError {
/// The year is not representable as `u16`.
InvalidYear(i32),
/// Time zone offsets with seconds are not representable.
OffsetWithSeconds(i8),
InvalidTime(InvalidTimeError),
}
impl Display for FromOffsetDateTimeError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidYear(y) => {
write!(
f,
"Cannot to convert OffsetDateTime to EFI Time: invalid year {y}"
)
}
Self::OffsetWithSeconds(s) => {
write!(
f,
"Cannot to convert OffsetDateTime to EFI Time: time zone offset has seconds ({s}) which is unsupported"
)
}
Self::InvalidTime(t) => {
write!(f, "The time is invalid: {t}")
}
}
}
}
impl error::Error for FromOffsetDateTimeError {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
match self {
Self::InvalidYear(_) => None,
Self::OffsetWithSeconds(_) => None,
Self::InvalidTime(e) => Some(e),
}
}
}
impl TryFrom<OffsetDateTime> for Time {
type Error = FromOffsetDateTimeError;
fn try_from(value: OffsetDateTime) -> Result<Self, Self::Error> {
let this = Self {
year: u16::try_from(value.date().year())
.map_err(|_| Self::Error::InvalidYear(value.date().year()))?,
// No checks needed: underlying type has repr `u8`
month: value.date().month() as u8,
day: value.date().day(),
hour: value.time().hour(),
minute: value.time().minute(),
second: value.time().second(),
pad1: 0,
nanosecond: value.time().nanosecond(),
time_zone: {
let (h, m, s) = value.offset().as_hms();
if s != 0 {
return Err(Self::Error::OffsetWithSeconds(s));
}
(h as i16 * 60) + (m as i16)
},
daylight: Daylight::NONE,
pad2: 0,
};
if this.is_valid() {
Ok(this)
} else {
Err(Self::Error::InvalidTime(InvalidTimeError(this)))
}
}
}
}