|
| 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 | +} |
0 commit comments