Skip to content

Commit a24d931

Browse files
Merge pull request #1956 from rust-osdev/uefi-raw-time-v3-splitout2
uefi: Add integration with `jiff` crate
2 parents da39859 + 02b9306 commit a24d931

9 files changed

Lines changed: 261 additions & 6 deletions

File tree

Cargo.lock

Lines changed: 38 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ rust-version = "1.91"
2222
[workspace.dependencies]
2323
bitflags = "2.0.0"
2424
log = { version = "0.4.5", default-features = false }
25+
jiff = { version = "0.2", default-features = false }
2526
ptr_meta = { version = "0.3.0", default-features = false, features = ["derive"] }
2627
qemu-exit = { version = "4.0.0" }
2728
time = { version = "0.3", default-features = false }

uefi/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@
4747
- Integration of `Time` with `time` crate
4848
- `TryFrom`: `time::PrimitiveDateTime <--> Time` (without timezone)
4949
- `TryFrom`: `time::OffsetDateTime <--> Time` (with timezone)
50+
- Integration of `Time` with `jiff` crate
51+
- `TryFrom`: `jiff::DateTime <--> Time` (without timezone)
52+
- `TryFrom`: `jiff::Zoned <--> Time` (with timezone)
5053

5154
## Changed
5255
- export all `text::{input, output}::*` types

uefi/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ rust-version.workspace = true
2020
# KEEP this feature list in sync with doc in uefi/lib.rs!
2121
default = [ ]
2222
alloc = []
23+
# Integration with jiff crate `v0.2`
24+
jiff02 = ["dep:jiff"]
2325
# Integration with time crate `v0.3`
2426
time03 = ["dep:time"]
2527

@@ -41,6 +43,7 @@ log-debugcon = []
4143
bitflags.workspace = true
4244
log.workspace = true
4345
ptr_meta.workspace = true
46+
jiff = { workspace = true, optional = true }
4447
time = { workspace = true, optional = true }
4548
qemu-exit = { workspace = true, optional = true }
4649
uguid.workspace = true

uefi/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,9 @@
142142
//! using this feature, or no allocator at all if you don't need to
143143
//! dynamically allocate any memory. Note that even without that feature,
144144
//! some code might use the internal UEFI allocator.
145+
//! - `jiff02`: Integration of [`runtime::Time`] with the `jiff` crate
146+
//! (version 0.2 and possible above). Specifically, it integrates the time
147+
//! struct with `DateTime` and `Zoned` via `TryFrom`.
145148
//! - `logger`: Logging implementation for the standard [`log`] crate
146149
//! that prints output to the UEFI console. No buffering is done; this
147150
//! is not a high-performance logger.

uefi/src/runtime/time/integration_common.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ pub(super) enum ConversionErrorInner {
3838
/// Errors raised in the [`time`] crate.
3939
#[cfg(feature = "time03")]
4040
TimeCrateError(time::Error),
41+
/// Errors raised in the [`jiff`] crate.
42+
#[cfg(feature = "jiff02")]
43+
JiffCrateError(jiff::Error),
4144
}
4245

4346
impl Display for ConversionErrorInner {
@@ -48,6 +51,8 @@ impl Display for ConversionErrorInner {
4851
Self::UnspecifiedTimezone => write!(f, "Unspecified timezone"),
4952
#[cfg(feature = "time03")]
5053
Self::TimeCrateError(e) => write!(f, "Time crate error: {}", e),
54+
#[cfg(feature = "jiff02")]
55+
Self::JiffCrateError(e) => write!(f, "Jiff crate error: {}", e),
5156
}
5257
}
5358
}
@@ -60,6 +65,9 @@ impl Error for ConversionErrorInner {
6065
Self::UnspecifiedTimezone => None,
6166
#[cfg(feature = "time03")]
6267
Self::TimeCrateError(e) => Some(e),
68+
// None: Missing Error trait
69+
#[cfg(feature = "jiff02")]
70+
Self::JiffCrateError(_e) => None,
6371
}
6472
}
6573
}
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
// SPDX-License-Identifier: MIT OR Apache-2.0
2+
3+
//! Integration of the UEFI [`Time`] type with the [`jiff`] crate.
4+
5+
use super::Time;
6+
use super::integration_common::{ConversionErrorInner, TimeConversionError};
7+
use jiff::Zoned;
8+
use jiff::civil::DateTime;
9+
use jiff::tz::{Offset, TimeZone};
10+
use uefi::runtime::TimeParams;
11+
12+
// Timezone unaware
13+
impl TryFrom<Time> for DateTime {
14+
type Error = TimeConversionError;
15+
16+
fn try_from(value: Time) -> Result<Self, Self::Error> {
17+
if let Err(e) = value.is_valid() {
18+
return Err(TimeConversionError(ConversionErrorInner::InvalidUefiTime(
19+
e,
20+
)));
21+
}
22+
23+
let datetime = Self::new(
24+
// Cannot fail as the value is valid and in range (we checked that).
25+
i16::try_from(value.0.year).unwrap(),
26+
// Cannot fail as the value is valid and in range (we checked that).
27+
i8::try_from(value.0.month).unwrap(),
28+
// Cannot fail as the value is valid and in range (we checked that).
29+
i8::try_from(value.0.day).unwrap(),
30+
// Cannot fail as the value is valid and in range (we checked that).
31+
i8::try_from(value.0.hour).unwrap(),
32+
// Cannot fail as the value is valid and in range (we checked that).
33+
i8::try_from(value.0.minute).unwrap(),
34+
// Cannot fail as the value is valid and in range (we checked that).
35+
i8::try_from(value.0.second).unwrap(),
36+
// Cannot fail as the value is valid and in range (we checked that).
37+
i32::try_from(value.0.nanosecond).unwrap(),
38+
)
39+
.map_err(|e| TimeConversionError(ConversionErrorInner::JiffCrateError(e)))?;
40+
41+
Ok(datetime)
42+
}
43+
}
44+
45+
// Timezone aware
46+
impl TryFrom<Time> for Zoned {
47+
type Error = TimeConversionError;
48+
49+
fn try_from(value: Time) -> Result<Self, Self::Error> {
50+
if let Err(e) = value.is_valid() {
51+
return Err(TimeConversionError(ConversionErrorInner::InvalidUefiTime(
52+
e,
53+
)));
54+
}
55+
56+
if value.0.time_zone == Time::UNSPECIFIED_TIMEZONE {
57+
return Err(TimeConversionError(
58+
ConversionErrorInner::UnspecifiedTimezone,
59+
));
60+
}
61+
62+
let datetime = DateTime::try_from(value)?;
63+
let seconds = value.0.time_zone as i32 * 60 /* seconds per minute */;
64+
let offset = Offset::from_seconds(seconds)
65+
.map_err(|e| TimeConversionError(ConversionErrorInner::JiffCrateError(e)))?;
66+
let timezone = TimeZone::fixed(offset);
67+
let zoned = datetime
68+
.to_zoned(timezone)
69+
.map_err(|e| TimeConversionError(ConversionErrorInner::JiffCrateError(e)))?;
70+
Ok(zoned)
71+
}
72+
}
73+
74+
impl TryFrom<DateTime> for Time {
75+
type Error = TimeConversionError;
76+
77+
fn try_from(value: DateTime) -> Result<Self, Self::Error> {
78+
let params = TimeParams {
79+
year: u16::try_from(value.year())
80+
.map_err(|_e| TimeConversionError(ConversionErrorInner::InvalidComponent))?,
81+
month: u8::try_from(value.month())
82+
.map_err(|_e| TimeConversionError(ConversionErrorInner::InvalidComponent))?,
83+
day: u8::try_from(value.day())
84+
.map_err(|_e| TimeConversionError(ConversionErrorInner::InvalidComponent))?,
85+
hour: u8::try_from(value.hour())
86+
.map_err(|_e| TimeConversionError(ConversionErrorInner::InvalidComponent))?,
87+
minute: u8::try_from(value.minute())
88+
.map_err(|_e| TimeConversionError(ConversionErrorInner::InvalidComponent))?,
89+
second: u8::try_from(value.second())
90+
.map_err(|_e| TimeConversionError(ConversionErrorInner::InvalidComponent))?,
91+
nanosecond: u32::try_from(value.subsec_nanosecond())
92+
.map_err(|_e| TimeConversionError(ConversionErrorInner::InvalidComponent))?,
93+
time_zone: None,
94+
daylight: Default::default(),
95+
};
96+
Self::new(params).map_err(|e| TimeConversionError(ConversionErrorInner::InvalidUefiTime(e)))
97+
}
98+
}
99+
100+
impl TryFrom<Zoned> for Time {
101+
type Error = TimeConversionError;
102+
fn try_from(value: Zoned) -> Result<Self, Self::Error> {
103+
let params = TimeParams {
104+
year: u16::try_from(value.year())
105+
.map_err(|_e| TimeConversionError(ConversionErrorInner::InvalidComponent))?,
106+
month: u8::try_from(value.month())
107+
.map_err(|_e| TimeConversionError(ConversionErrorInner::InvalidComponent))?,
108+
day: u8::try_from(value.day())
109+
.map_err(|_e| TimeConversionError(ConversionErrorInner::InvalidComponent))?,
110+
hour: u8::try_from(value.hour())
111+
.map_err(|_e| TimeConversionError(ConversionErrorInner::InvalidComponent))?,
112+
minute: u8::try_from(value.minute())
113+
.map_err(|_e| TimeConversionError(ConversionErrorInner::InvalidComponent))?,
114+
second: u8::try_from(value.second())
115+
.map_err(|_e| TimeConversionError(ConversionErrorInner::InvalidComponent))?,
116+
nanosecond: u32::try_from(value.subsec_nanosecond())
117+
.map_err(|_e| TimeConversionError(ConversionErrorInner::InvalidComponent))?,
118+
time_zone: Some(
119+
i16::try_from(value.offset().seconds() / 60 /* seconds per minute */)
120+
.map_err(|_e| TimeConversionError(ConversionErrorInner::InvalidComponent))?,
121+
),
122+
daylight: Default::default(),
123+
};
124+
Self::new(params).map_err(|e| TimeConversionError(ConversionErrorInner::InvalidUefiTime(e)))
125+
}
126+
}
127+
128+
#[cfg(test)]
129+
mod tests {
130+
use super::super::integration_common::test_helpers;
131+
use super::*;
132+
133+
#[test]
134+
fn primitive_roundtrip_basic() {
135+
test_helpers::primitive_roundtrip::<DateTime>();
136+
}
137+
138+
#[test]
139+
fn zoned_roundtrip_positive_offset() {
140+
test_helpers::zoned_roundtrip::<Zoned>();
141+
}
142+
143+
#[test]
144+
fn zoned_roundtrip_negative_offset() {
145+
test_helpers::negative_offset_roundtrip::<Zoned>();
146+
}
147+
148+
#[test]
149+
fn nanoseconds_preserved() {
150+
test_helpers::preserves_nanoseconds::<Zoned>();
151+
}
152+
153+
#[test]
154+
fn unspecified_timezone_is_rejected() {
155+
test_helpers::unspecified_timezone_fails::<Zoned>();
156+
}
157+
158+
#[test]
159+
fn invalid_date_is_rejected() {
160+
test_helpers::invalid_calendar_date_fails::<DateTime>();
161+
}
162+
163+
// jiff-specific edge case: offset minute precision
164+
#[test]
165+
fn half_hour_timezone_roundtrip() {
166+
let t = test_helpers::sample_time(90); // +01:30
167+
168+
let z: Zoned = t.try_into().unwrap();
169+
let back: Time = z.try_into().unwrap();
170+
171+
assert_eq!(back.0.time_zone, 90);
172+
}
173+
174+
#[test]
175+
fn negative_half_hour_timezone_roundtrip() {
176+
let t = test_helpers::sample_time(-330); // -05:30
177+
178+
let z: Zoned = t.try_into().unwrap();
179+
let back: Time = z.try_into().unwrap();
180+
181+
assert_eq!(back.0.time_zone, -330);
182+
}
183+
}

uefi/src/runtime/time/mod.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ use core::fmt;
1010
use core::fmt::{Debug, Display, Formatter};
1111
use uefi_raw::time::Daylight;
1212

13-
#[cfg(feature = "time03")]
13+
#[cfg(any(feature = "jiff02", feature = "time03"))]
1414
mod integration_common;
15+
#[cfg(feature = "jiff02")]
16+
mod integration_jiff_crate;
1517
#[cfg(feature = "time03")]
1618
mod integration_time_crate;
1719

@@ -28,7 +30,13 @@ mod integration_time_crate;
2830
/// - [`TryFrom`]: `PrimitiveDateTime` <--> [`Time`] (without timezone)
2931
/// - [`TryFrom`]: `OffsetDateTime` <--> [`Time`] (with timezone)
3032
///
33+
/// ## Integration with [`jiff`][jiff crate] crate
34+
///
35+
/// - [`TryFrom`]: `DateTime` <--> [`Time`] (without timezone)
36+
/// - [`TryFrom`]: `Zoned` <--> [`Time`] (with timezone)
37+
///
3138
/// [time crate]: https://crates.io/crates/time
39+
/// [jiff crate]: https://crates.io/crates/jiff
3240
#[derive(Copy, Clone, Eq, PartialEq)]
3341
#[repr(transparent)]
3442
pub struct Time(uefi_raw::time::Time);

xtask/src/cargo.rs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ pub enum Feature {
5656
Unstable,
5757
PanicHandler,
5858
Qemu,
59+
Jiff02,
5960
Time03,
6061

6162
// `uefi-test-runner` features.
@@ -77,6 +78,7 @@ impl Feature {
7778
Self::Unstable => "unstable",
7879
Self::PanicHandler => "panic_handler",
7980
Self::Qemu => "qemu",
81+
Self::Jiff02 => "jiff02",
8082
Self::Time03 => "time03",
8183

8284
Self::DebugSupport => "uefi-test-runner/debug_support",
@@ -119,7 +121,13 @@ impl Feature {
119121
/// - `include_unstable` - add all functionality behind the `unstable` feature
120122
/// - `runtime_features` - add all functionality that effect the runtime of Rust
121123
pub fn more_code(include_unstable: bool, runtime_features: bool) -> Vec<Self> {
122-
let mut base_features = vec![Self::Alloc, Self::LogDebugcon, Self::Logger, Self::Time03];
124+
let mut base_features = vec![
125+
Self::Alloc,
126+
Self::Jiff02,
127+
Self::LogDebugcon,
128+
Self::Logger,
129+
Self::Time03,
130+
];
123131
if include_unstable {
124132
base_features.extend([Self::Unstable])
125133
}
@@ -389,19 +397,19 @@ mod tests {
389397
fn test_comma_separated_features() {
390398
assert_eq!(
391399
Feature::comma_separated_string(&Feature::more_code(false, false)),
392-
"alloc,log-debugcon,logger,time03"
400+
"alloc,jiff02,log-debugcon,logger,time03"
393401
);
394402
assert_eq!(
395403
Feature::comma_separated_string(&Feature::more_code(false, true)),
396-
"alloc,log-debugcon,logger,time03,global_allocator"
404+
"alloc,jiff02,log-debugcon,logger,time03,global_allocator"
397405
);
398406
assert_eq!(
399407
Feature::comma_separated_string(&Feature::more_code(true, false)),
400-
"alloc,log-debugcon,logger,time03,unstable"
408+
"alloc,jiff02,log-debugcon,logger,time03,unstable"
401409
);
402410
assert_eq!(
403411
Feature::comma_separated_string(&Feature::more_code(true, true)),
404-
"alloc,log-debugcon,logger,time03,unstable,global_allocator"
412+
"alloc,jiff02,log-debugcon,logger,time03,unstable,global_allocator"
405413
);
406414
}
407415

0 commit comments

Comments
 (0)