Skip to content

std: sys: pal: uefi: Overhaul Time #139806

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

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
55 changes: 49 additions & 6 deletions library/std/src/sys/pal/uefi/tests.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use super::alloc::*;
use super::time::*;
use super::time::system_time_internal::{from_uefi, to_uefi};
use crate::time::Duration;

const SECS_IN_MINUTE: u64 = 60;

#[test]
fn align() {
// UEFI ABI specifies that allocation alignment minimum is always 8. So this can be
Expand All @@ -23,19 +25,60 @@ fn align() {
}

#[test]
fn epoch() {
let t = r_efi::system::Time {
year: 1970,
fn systemtime_start() {
let t = r_efi::efi::Time {
year: 1900,
month: 1,
day: 1,
hour: 0,
minute: 0,
second: 0,
pad1: 0,
nanosecond: 0,
timezone: -1440,
daylight: 0,
pad2: 0,
};
assert_eq!(from_uefi(&t), Duration::new(0, 0));
assert_eq!(t, to_uefi(&from_uefi(&t), -1440, 0).unwrap());
assert!(to_uefi(&from_uefi(&t), 0, 0).is_none());
}

#[test]
fn systemtime_utc_start() {
let t = r_efi::efi::Time {
year: 1900,
month: 1,
day: 1,
hour: 0,
minute: 0,
second: 0,
pad1: 0,
nanosecond: 0,
timezone: r_efi::efi::UNSPECIFIED_TIMEZONE,
timezone: 0,
daylight: 0,
pad2: 0,
};
assert_eq!(from_uefi(&t), Duration::new(1440 * SECS_IN_MINUTE, 0));
assert_eq!(t, to_uefi(&from_uefi(&t), 0, 0).unwrap());
assert!(to_uefi(&from_uefi(&t), -1440, 0).is_some());
}

#[test]
fn systemtime_end() {
let t = r_efi::efi::Time {
year: 9999,
month: 12,
day: 31,
hour: 23,
minute: 59,
second: 59,
pad1: 0,
nanosecond: 0,
timezone: 1440,
daylight: 0,
pad2: 0,
};
assert_eq!(system_time_internal::uefi_time_to_duration(t), Duration::new(0, 0));
assert!(to_uefi(&from_uefi(&t), 1440, 0).is_some());
assert!(to_uefi(&from_uefi(&t), 1439, 0).is_none());
}
159 changes: 139 additions & 20 deletions library/std/src/sys/pal/uefi/time.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,42 @@
use crate::time::Duration;

const SECS_IN_MINUTE: u64 = 60;
const SECS_IN_HOUR: u64 = SECS_IN_MINUTE * 60;
const SECS_IN_DAY: u64 = SECS_IN_HOUR * 24;

#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
pub struct Instant(Duration);

/// When a Timezone is specified, the stored Duration is in UTC. If timezone is unspecified, then
/// the timezone is assumed to be in UTC.
///
/// UEFI SystemTime is stored as Duration from 1900-01-01-00:00:00 with timezone -1440 as anchor
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
pub struct SystemTime(Duration);

pub const UNIX_EPOCH: SystemTime = SystemTime(Duration::from_secs(0));
pub const UNIX_EPOCH: SystemTime = SystemTime::from_uefi(r_efi::efi::Time {
year: 1970,
month: 1,
day: 1,
hour: 0,
minute: 0,
second: 0,
nanosecond: 0,
timezone: 0,
daylight: 0,
pad1: 0,
pad2: 0,
});

const MAX_UEFI_TIME: SystemTime = SystemTime::from_uefi(r_efi::efi::Time {
year: 9999,
month: 12,
day: 31,
hour: 23,
minute: 59,
second: 59,
nanosecond: 999_999_999,
timezone: 1440,
daylight: 0,
pad1: 0,
pad2: 0,
});

impl Instant {
pub fn now() -> Instant {
Expand Down Expand Up @@ -40,6 +66,15 @@ impl Instant {
}

impl SystemTime {
pub(crate) const fn from_uefi(t: r_efi::efi::Time) -> Self {
Self(system_time_internal::from_uefi(&t))
}

#[expect(dead_code)]
pub(crate) const fn to_uefi(self, timezone: i16, daylight: u8) -> Option<r_efi::efi::Time> {
system_time_internal::to_uefi(&self.0, timezone, daylight)
}

pub fn now() -> SystemTime {
system_time_internal::now()
.unwrap_or_else(|| panic!("time not implemented on this platform"))
Expand All @@ -50,11 +85,14 @@ impl SystemTime {
}

pub fn checked_add_duration(&self, other: &Duration) -> Option<SystemTime> {
Some(SystemTime(self.0.checked_add(*other)?))
let temp = Self(self.0.checked_add(*other)?);

// Check if can be represented in UEFI
if temp <= MAX_UEFI_TIME { Some(temp) } else { None }
}

pub fn checked_sub_duration(&self, other: &Duration) -> Option<SystemTime> {
Some(SystemTime(self.0.checked_sub(*other)?))
self.0.checked_sub(*other).map(Self)
}
}

Expand All @@ -66,51 +104,132 @@ pub(crate) mod system_time_internal {
use crate::mem::MaybeUninit;
use crate::ptr::NonNull;

const SECS_IN_MINUTE: u64 = 60;
const SECS_IN_HOUR: u64 = SECS_IN_MINUTE * 60;
const SECS_IN_DAY: u64 = SECS_IN_HOUR * 24;
const TIMEZONE_DELTA: u64 = 1440 * SECS_IN_MINUTE;

pub fn now() -> Option<SystemTime> {
let runtime_services: NonNull<RuntimeServices> = helpers::runtime_services()?;
let mut t: MaybeUninit<Time> = MaybeUninit::uninit();
let r = unsafe {
((*runtime_services.as_ptr()).get_time)(t.as_mut_ptr(), crate::ptr::null_mut())
};

if r.is_error() {
return None;
}

let t = unsafe { t.assume_init() };

Some(SystemTime(uefi_time_to_duration(t)))
Some(SystemTime::from_uefi(t))
}

// This algorithm is based on the one described in the post
// https://blog.reverberate.org/2020/05/12/optimizing-date-algorithms.html
pub(crate) const fn uefi_time_to_duration(t: r_efi::system::Time) -> Duration {
assert!(t.month <= 12);
assert!(t.month != 0);
/// This algorithm is a modified form of the one described in the post
/// https://blog.reverberate.org/2020/05/12/optimizing-date-algorithms.html
///
/// The changes are to use 1900-01-01-00:00:00 with timezone -1440 as anchor instead of UNIX
/// epoch used in the original algorithm.
pub(crate) const fn from_uefi(t: &Time) -> Duration {
assert!(t.month <= 12 && t.month != 0);
assert!(t.year >= 1900 && t.year <= 9999);
assert!(t.day <= 31 && t.day != 0);

assert!(t.second < 60);
assert!(t.minute < 60);
assert!(t.hour < 24);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing minute check?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added

assert!(t.nanosecond < 1_000_000_000);

assert!(
(t.timezone <= 1440 && t.timezone >= -1440)
|| t.timezone == r_efi::efi::UNSPECIFIED_TIMEZONE
);

const YEAR_BASE: u32 = 4800; /* Before min year, multiple of 400. */

// Calculate the number of days since 1/1/1970
// Calculate the number of days since 1/1/1900. This is the earliest supported date in UEFI
// time.
// Use 1 March as the start
let (m_adj, overflow): (u32, bool) = (t.month as u32).overflowing_sub(3);
let (carry, adjust): (u32, u32) = if overflow { (1, 12) } else { (0, 0) };
let y_adj: u32 = (t.year as u32) + YEAR_BASE - carry;
let month_days: u32 = (m_adj.wrapping_add(adjust) * 62719 + 769) / 2048;
let leap_days: u32 = y_adj / 4 - y_adj / 100 + y_adj / 400;
let days: u32 = y_adj * 365 + leap_days + month_days + (t.day as u32 - 1) - 2472632;
let days: u32 = y_adj * 365 + leap_days + month_days + (t.day as u32 - 1) - 2447065;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might be worth it to add a CONST for the magic 2447065

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, I am not sure if it's useful to do that. The current code is almost a one-to-one translation of the algorithms I have linked in the comments, and thus is easier to reason about.

Besides, the offset in this particular case is derived by collapsing multiple calculations into a single one (the one in to_uefi is more straightforward). So not sure if there is any good descriptive name. And just calling it const MAGIC is probably not any more useful/readable than current form.


let localtime_epoch: u64 = (days as u64) * SECS_IN_DAY
+ (t.second as u64)
+ (t.minute as u64) * SECS_IN_MINUTE
+ (t.hour as u64) * SECS_IN_HOUR;

let utc_epoch: u64 = if t.timezone == r_efi::efi::UNSPECIFIED_TIMEZONE {
localtime_epoch
// Calculate the offset from 1/1/1900 at timezone -1440 min
let adjusted_localtime_epoc: u64 = localtime_epoch + TIMEZONE_DELTA;

let epoch: u64 = if t.timezone == r_efi::efi::UNSPECIFIED_TIMEZONE {
adjusted_localtime_epoc
} else {
(localtime_epoch as i64 + (t.timezone as i64) * SECS_IN_MINUTE as i64) as u64
adjusted_localtime_epoc
.checked_add_signed((t.timezone as i64) * SECS_IN_MINUTE as i64)
.unwrap()
};

Duration::new(utc_epoch, t.nanosecond)
Duration::new(epoch, t.nanosecond)
}

/// This algorithm is a modifed version of the one described in the post:
/// https://howardhinnant.github.io/date_algorithms.html#clive_from_days
///
/// The changes are to use 1900-01-01-00:00:00 with timezone -1440 as anchor instead of UNIX
/// epoch used in the original algorithm.
pub(crate) const fn to_uefi(dur: &Duration, timezone: i16, daylight: u8) -> Option<Time> {
// Check timzone validity
assert!(timezone <= 1440 && timezone >= -1440);

// FIXME(#126043): use checked_sub_signed once stablized
let secs =
dur.as_secs().checked_add_signed((-timezone as i64) * SECS_IN_MINUTE as i64).unwrap();

// Convert to seconds since 1900-01-01-00:00:00 in timezone.
let Some(secs) = secs.checked_sub(TIMEZONE_DELTA) else { return None };

let days = secs / SECS_IN_DAY;
let remaining_secs = secs % SECS_IN_DAY;

let z = days + 693901;
Copy link

@phip1611 phip1611 Jun 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

more magic constants (693901), might be worth it to add CONST values

let era = z / 146097;
let doe = z - (era * 146097);
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let mut y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };

if m <= 2 {
y += 1;
}

let hour = (remaining_secs / SECS_IN_HOUR) as u8;
let minute = ((remaining_secs % SECS_IN_HOUR) / SECS_IN_MINUTE) as u8;
let second = (remaining_secs % SECS_IN_MINUTE) as u8;

// Check Bounds
if y >= 1900 && y <= 9999 {
Some(Time {
year: y as u16,
month: m as u8,
day: d as u8,
hour,
minute,
second,
nanosecond: dur.subsec_nanos(),
timezone,
daylight,
pad1: 0,
pad2: 0,
})
} else {
None
}
}
}

Expand Down
Loading