popcap_pak/
file_time.rs

1#![warn(clippy::arithmetic_side_effects)]
2
3use std::time::Duration;
4use std::time::SystemTime;
5
6/// The number of nanos per second
7const NANOSECONDS_PER_SECOND: u64 = 1_000_000_000;
8
9/// The difference between the unix epoch and the microsoft epoch in seconds.
10///
11/// See https://devblogs.microsoft.com/oldnewthing/20220602-00/?p=106706.
12const EPOCH_DIFFERENCE_SECONDS: u64 = 11_644_473_600;
13
14/// The difference between the unix epoch and the microsoft epoch in nanoseconds.
15const EPOCH_DIFFERENCE_NANOSECONDS: u64 = EPOCH_DIFFERENCE_SECONDS * NANOSECONDS_PER_SECOND;
16
17/// The difference between the unix epoch and the microsoft epoch in ticks.
18const EPOCH_DIFFERENCE_TICKS: u64 = EPOCH_DIFFERENCE_NANOSECONDS / NANOSECONDS_PER_TICK;
19
20/// The number of nanoseconds per tick in a FileTime.
21const NANOSECONDS_PER_TICK: u64 = 100;
22
23/// A file time.
24///
25/// This is a wrapper for Microsoft's FILETIME struct.
26/// This is a UTC timestamp.
27/// This type's resolution is 100 nanoseconds.
28#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, PartialOrd, Ord)]
29pub struct FileTime {
30    time: u64,
31}
32
33impl FileTime {
34    /// Make a [`FileTime`] from the given raw value.
35    pub fn from_raw(time: u64) -> Self {
36        Self { time }
37    }
38
39    /// Get the raw value
40    pub fn into_raw(self) -> u64 {
41        self.time
42    }
43}
44
45/// A failure occured while converting from a file time into a system time
46#[derive(Debug)]
47pub enum TryFromFileTimeError {
48    /// Failed to adjust epochs.
49    Adjust,
50
51    /// Failed to convert ticks into nanos.
52    Nanos,
53
54    /// The SystemTime cannot hold the final timestamp
55    SystemTime,
56
57    /// An unspecified error occured
58    Unspecified,
59}
60
61impl std::fmt::Display for TryFromFileTimeError {
62    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63        match self {
64            Self::Adjust => f.write_str(
65                "failed to adjust epochs while converting a file time into a system time",
66            ),
67            Self::Nanos => f.write_str(
68                "failed to convert ticks into nanoseconds while converting a file time into a system time"
69            ),
70            Self::SystemTime => f.write_str(
71                "failed to convert ticks into a system time while converting a file time into a system time"
72            ),
73            Self::Unspecified => f.write_str(
74                "an unspecified error occured while converting a file time into a system time",
75            ),
76        }
77    }
78}
79
80impl std::error::Error for TryFromFileTimeError {}
81
82impl TryFrom<FileTime> for SystemTime {
83    type Error = TryFromFileTimeError;
84
85    fn try_from(file_time: FileTime) -> Result<Self, Self::Error> {
86        // This is a u64 representing the # of 100 nanos since
87        // January 1, 1601.
88        let ticks = file_time.into_raw();
89
90        // Adjust to unix epoch.
91        //
92        // We adjust first,
93        // as that will lead to a small value and hopefully prevent some overflows.
94        let adjusted = ticks
95            .checked_sub(EPOCH_DIFFERENCE_TICKS)
96            .ok_or(TryFromFileTimeError::Adjust)?;
97
98        // Convert ticks to nanoseconds.
99        let nanos = u128::from(adjusted)
100            .checked_mul(NANOSECONDS_PER_TICK.into())
101            .ok_or(TryFromFileTimeError::Nanos)?;
102
103        // Work around a backwards-compat issue in Rust stdlib.
104        //
105        // We can get nanos from a Duration as a u128,
106        // but we can only set it as a u64.
107        // To work-around, split into seconds and nanoseconds while creating a duration, then add.
108        let secs_part = u64::try_from(nanos.checked_div(NANOSECONDS_PER_SECOND.into()).unwrap())
109            .map_err(|_| TryFromFileTimeError::Unspecified)?;
110        let offset_seconds = Duration::from_secs(secs_part);
111        // 0 < the # of nanoseonds in a second < u64::MAX
112        let nanos_part =
113            u64::try_from(nanos.rem_euclid(u128::from(NANOSECONDS_PER_SECOND))).unwrap();
114        let offset_nanos = Duration::from_nanos(nanos_part);
115        let offset = offset_seconds
116            .checked_add(offset_nanos)
117            .ok_or(TryFromFileTimeError::Unspecified)?;
118
119        // Convert to SystemTime.
120        Self::UNIX_EPOCH
121            .checked_add(offset)
122            .ok_or(TryFromFileTimeError::SystemTime)
123    }
124}
125
126/// An error that may occur while converting a SystemTime into a FileTime.
127#[derive(Debug)]
128pub enum TryFromSystemTimeError {
129    /// An unspecified error occured
130    Unspecified,
131}
132
133impl std::fmt::Display for TryFromSystemTimeError {
134    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135        match self {
136            Self::Unspecified => f.write_str(
137                "an unspecified error occured whil converting a system time into a file time",
138            ),
139        }
140    }
141}
142
143impl std::error::Error for TryFromSystemTimeError {}
144
145impl TryFrom<SystemTime> for FileTime {
146    type Error = TryFromSystemTimeError;
147
148    fn try_from(time: SystemTime) -> Result<Self, Self::Error> {
149        // Get the nanos from the unix epoch.
150        //
151        // TODO: Technically we can recover from a failure here by assuming the timestamp is before the unix epoch.
152        // Those timestamps won't be too common though.
153        let nanos = time
154            .duration_since(SystemTime::UNIX_EPOCH)
155            .map_err(|_e| TryFromSystemTimeError::Unspecified)?
156            .as_nanos();
157
158        // Convert to the microsoft epoch.
159        let adjusted = nanos
160            .checked_add(EPOCH_DIFFERENCE_NANOSECONDS.into())
161            .ok_or(TryFromSystemTimeError::Unspecified)?;
162
163        // Convert to ticks.
164        let ticks_u128 = adjusted.checked_div(NANOSECONDS_PER_TICK.into()).unwrap();
165
166        // Fit the ticks value into a u64.
167        // If it doesn't fit, it isn't a valid FileTime.
168        let raw = u64::try_from(ticks_u128).map_err(|_| TryFromSystemTimeError::Unspecified)?;
169
170        Ok(Self::from_raw(raw))
171    }
172}
173
174#[cfg(test)]
175mod test {
176    use super::*;
177
178    #[test]
179    fn system_time_round() {
180        let now = SystemTime::now();
181        let file_time: FileTime = now.try_into().unwrap();
182        let round: SystemTime = file_time.try_into().unwrap();
183
184        // now is first, as it will be bigger if there are precision issues.
185        let diff = now.duration_since(round).unwrap() < Duration::from_nanos(NANOSECONDS_PER_TICK);
186        assert!(diff, "(original) {now:?} != (new) {round:?}");
187
188        // let time: SystemTime = FileTime::from_raw(u64::MAX).try_into().unwrap();
189    }
190}