nt_time/file_time/
dos_date_time.rs

1// SPDX-FileCopyrightText: 2023 Shun Sakai
2//
3// SPDX-License-Identifier: Apache-2.0 OR MIT
4
5//! Implementations of conversions between [`FileTime`] and [MS-DOS date and
6//! time].
7//!
8//! [MS-DOS date and time]: https://learn.microsoft.com/en-us/windows/win32/sysinfo/ms-dos-date-and-time
9
10use time::{Date, OffsetDateTime, Time, error::ComponentRange};
11
12use super::FileTime;
13use crate::error::{DosDateTimeRangeError, DosDateTimeRangeErrorKind};
14
15impl FileTime {
16    #[allow(clippy::missing_panics_doc)]
17    /// Returns [MS-DOS date and time] which represents the same date and time
18    /// as this `FileTime`. This date and time is used as the timestamp such as
19    /// [FAT] or [ZIP] file format.
20    ///
21    /// This method returns a `(date, time)` tuple if the result is [`Ok`].
22    ///
23    /// `date` and `time` represents the local date and time, and has no notion
24    /// of time zone.
25    ///
26    /// <div class="warning">
27    ///
28    /// The resolution of MS-DOS date and time is 2 seconds. So this method
29    /// rounds towards zero, truncating any fractional part of the exact result
30    /// of dividing seconds by 2.
31    ///
32    /// </div>
33    ///
34    /// # Errors
35    ///
36    /// Returns [`Err`] if the resulting date and time is out of range for
37    /// MS-DOS date and time.
38    ///
39    /// # Examples
40    ///
41    /// ```
42    /// # use nt_time::FileTime;
43    /// #
44    /// // From `1980-01-01 00:00:00 UTC` to `1980-01-01 00:00:00`.
45    /// assert_eq!(
46    ///     FileTime::new(119_600_064_000_000_000).to_dos_date_time(),
47    ///     Ok((0x0021, u16::MIN))
48    /// );
49    /// // From `2107-12-31 23:59:59 UTC` to `2107-12-31 23:59:58`.
50    /// assert_eq!(
51    ///     FileTime::new(159_992_927_990_000_000).to_dos_date_time(),
52    ///     Ok((0xff9f, 0xbf7d))
53    /// );
54    ///
55    /// // Before `1980-01-01 00:00:00 UTC`.
56    /// assert!(
57    ///     FileTime::new(119_600_063_990_000_000)
58    ///         .to_dos_date_time()
59    ///         .is_err()
60    /// );
61    /// // After `2107-12-31 23:59:59.999999900 UTC`.
62    /// assert!(
63    ///     FileTime::new(159_992_928_000_000_000)
64    ///         .to_dos_date_time()
65    ///         .is_err()
66    /// );
67    /// ```
68    ///
69    /// [MS-DOS date and time]: https://learn.microsoft.com/en-us/windows/win32/sysinfo/ms-dos-date-and-time
70    /// [FAT]: https://en.wikipedia.org/wiki/File_Allocation_Table
71    /// [ZIP]: https://en.wikipedia.org/wiki/ZIP_(file_format)
72    pub fn to_dos_date_time(self) -> Result<(u16, u16), DosDateTimeRangeError> {
73        let dt = OffsetDateTime::try_from(self).map_err(|_| DosDateTimeRangeErrorKind::Overflow)?;
74
75        match dt.year() {
76            ..=1979 => Err(DosDateTimeRangeErrorKind::Negative.into()),
77            2108.. => Err(DosDateTimeRangeErrorKind::Overflow.into()),
78            year => {
79                let (year, month, day) = (
80                    u16::try_from(year - 1980).expect("year should be in the range of `u16`"),
81                    u16::from(u8::from(dt.month())),
82                    u16::from(dt.day()),
83                );
84                let date = (year << 9) | (month << 5) | day;
85
86                let (hour, minute, second) = (dt.hour(), dt.minute(), dt.second() / 2);
87                let time = (u16::from(hour) << 11) | (u16::from(minute) << 5) | u16::from(second);
88
89                Ok((date, time))
90            }
91        }
92    }
93
94    #[allow(clippy::missing_panics_doc)]
95    /// Creates a `FileTime` with the given [MS-DOS date and time]. This date
96    /// and time is used as the timestamp such as [FAT] or [ZIP] file format.
97    ///
98    /// <div class="warning">
99    ///
100    /// The time zone for the local date and time is assumed to be UTC.
101    ///
102    /// </div>
103    ///
104    /// # Errors
105    ///
106    /// Returns [`Err`] if `date` or `time` is an invalid date and time.
107    ///
108    /// # Examples
109    ///
110    /// ```
111    /// # use nt_time::FileTime;
112    /// #
113    /// // From `1980-01-01 00:00:00` to `1980-01-01 00:00:00 UTC`.
114    /// assert_eq!(
115    ///     FileTime::from_dos_date_time(0x0021, u16::MIN),
116    ///     Ok(FileTime::new(119_600_064_000_000_000))
117    /// );
118    /// // From `2107-12-31 23:59:58` to `2107-12-31 23:59:58 UTC`.
119    /// assert_eq!(
120    ///     FileTime::from_dos_date_time(0xff9f, 0xbf7d),
121    ///     Ok(FileTime::new(159_992_927_980_000_000))
122    /// );
123    ///
124    /// // The Day field is 0.
125    /// assert!(FileTime::from_dos_date_time(0x0020, u16::MIN).is_err());
126    /// // The DoubleSeconds field is 30.
127    /// assert!(FileTime::from_dos_date_time(0x0021, 0x001e).is_err());
128    /// ```
129    ///
130    /// [MS-DOS date and time]: https://learn.microsoft.com/en-us/windows/win32/sysinfo/ms-dos-date-and-time
131    /// [FAT]: https://en.wikipedia.org/wiki/File_Allocation_Table
132    /// [ZIP]: https://en.wikipedia.org/wiki/ZIP_(file_format)
133    pub fn from_dos_date_time(date: u16, time: u16) -> Result<Self, ComponentRange> {
134        let (year, month, day) = (
135            (1980 + (date >> 9)).into(),
136            u8::try_from((date >> 5) & 0x0f)
137                .expect("month should be in the range of `u8`")
138                .try_into()?,
139            (date & 0x1f)
140                .try_into()
141                .expect("day should be in the range of `u8`"),
142        );
143        let date = Date::from_calendar_date(year, month, day)?;
144
145        let (hour, minute, second) = (
146            (time >> 11)
147                .try_into()
148                .expect("hour should be in the range of `u8`"),
149            ((time >> 5) & 0x3f)
150                .try_into()
151                .expect("minute should be in the range of `u8`"),
152            ((time & 0x1f) * 2)
153                .try_into()
154                .expect("second should be in the range of `u8`"),
155        );
156        let time = Time::from_hms(hour, minute, second)?;
157
158        let ft = OffsetDateTime::new_utc(date, time)
159            .try_into()
160            .expect("MS-DOS date and time should be in the range of `FileTime`");
161        Ok(ft)
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[test]
170    fn to_dos_date_time_before_dos_date_time_epoch() {
171        // `1979-12-31 23:59:58 UTC`.
172        assert_eq!(
173            FileTime::new(119_600_063_980_000_000)
174                .to_dos_date_time()
175                .unwrap_err(),
176            DosDateTimeRangeErrorKind::Negative.into()
177        );
178        // `1979-12-31 23:59:59 UTC`.
179        assert_eq!(
180            FileTime::new(119_600_063_990_000_000)
181                .to_dos_date_time()
182                .unwrap_err(),
183            DosDateTimeRangeErrorKind::Negative.into()
184        );
185    }
186
187    #[cfg(feature = "std")]
188    #[test_strategy::proptest]
189    fn to_dos_date_time_before_dos_date_time_epoch_roundtrip(
190        #[strategy(..=119_600_063_980_000_000_u64)] ft: u64,
191    ) {
192        use proptest::prop_assert_eq;
193
194        prop_assert_eq!(
195            FileTime::new(ft).to_dos_date_time().unwrap_err(),
196            DosDateTimeRangeErrorKind::Negative.into()
197        );
198    }
199
200    #[test]
201    fn to_dos_date_time() {
202        // From `1980-01-01 00:00:00 UTC` to `1980-01-01 00:00:00`.
203        assert_eq!(
204            FileTime::new(119_600_064_000_000_000)
205                .to_dos_date_time()
206                .unwrap(),
207            (0x0021, u16::MIN)
208        );
209        // From `1980-01-01 00:00:01 UTC` to `1980-01-01 00:00:00`.
210        assert_eq!(
211            FileTime::new(119_600_064_010_000_000)
212                .to_dos_date_time()
213                .unwrap(),
214            (0x0021, u16::MIN)
215        );
216        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
217        //
218        // From `2002-11-27 03:25:00 UTC` to `2002-11-27 03:25:00`.
219        assert_eq!(
220            FileTime::new(126_828_411_000_000_000)
221                .to_dos_date_time()
222                .unwrap(),
223            (0x2d7b, 0x1b20)
224        );
225        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
226        //
227        // From `2018-11-17 10:38:30 UTC` to `2018-11-17 10:38:30`.
228        assert_eq!(
229            FileTime::new(131_869_247_100_000_000)
230                .to_dos_date_time()
231                .unwrap(),
232            (0x4d71, 0x54cf)
233        );
234        // From `2107-12-31 23:59:58 UTC` to `2107-12-31 23:59:58`.
235        assert_eq!(
236            FileTime::new(159_992_927_980_000_000)
237                .to_dos_date_time()
238                .unwrap(),
239            (0xff9f, 0xbf7d)
240        );
241        // From `2107-12-31 23:59:59 UTC` to `2107-12-31 23:59:58`.
242        assert_eq!(
243            FileTime::new(159_992_927_990_000_000)
244                .to_dos_date_time()
245                .unwrap(),
246            (0xff9f, 0xbf7d)
247        );
248    }
249
250    #[cfg(feature = "std")]
251    #[test_strategy::proptest]
252    fn to_dos_date_time_roundtrip(
253        #[strategy(119_600_064_000_000_000..=159_992_927_980_000_000_u64)] ft: u64,
254    ) {
255        use proptest::prop_assert;
256
257        prop_assert!(FileTime::new(ft).to_dos_date_time().is_ok());
258    }
259
260    #[test]
261    fn to_dos_date_time_with_too_big_date_time() {
262        // `2108-01-01 00:00:00 UTC`.
263        assert_eq!(
264            FileTime::new(159_992_928_000_000_000)
265                .to_dos_date_time()
266                .unwrap_err(),
267            DosDateTimeRangeErrorKind::Overflow.into()
268        );
269    }
270
271    #[cfg(feature = "std")]
272    #[test_strategy::proptest]
273    fn to_dos_date_time_with_too_big_date_time_roundtrip(
274        #[strategy(159_992_928_000_000_000_u64..)] ft: u64,
275    ) {
276        use proptest::prop_assert_eq;
277
278        prop_assert_eq!(
279            FileTime::new(ft).to_dos_date_time().unwrap_err(),
280            DosDateTimeRangeErrorKind::Overflow.into()
281        );
282    }
283
284    #[test]
285    fn from_dos_date_time() {
286        // From `1980-01-01 00:00:00` to `1980-01-01 00:00:00 UTC`.
287        assert_eq!(
288            FileTime::from_dos_date_time(0x0021, u16::MIN).unwrap(),
289            FileTime::new(119_600_064_000_000_000)
290        );
291        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
292        //
293        // From `2002-11-26 19:25:00` to `2002-11-26 19:25:00 UTC`.
294        assert_eq!(
295            FileTime::from_dos_date_time(0x2d7a, 0x9b20).unwrap(),
296            FileTime::new(126_828_123_000_000_000)
297        );
298        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
299        //
300        // From `2018-11-17 10:38:30` to `2018-11-17 10:38:30 UTC`.
301        assert_eq!(
302            FileTime::from_dos_date_time(0x4d71, 0x54cf).unwrap(),
303            FileTime::new(131_869_247_100_000_000)
304        );
305        // From `2107-12-31 23:59:58` to `2107-12-31 23:59:58 UTC`.
306        assert_eq!(
307            FileTime::from_dos_date_time(0xff9f, 0xbf7d).unwrap(),
308            FileTime::new(159_992_927_980_000_000)
309        );
310    }
311
312    #[cfg(feature = "std")]
313    #[test_strategy::proptest]
314    fn from_dos_date_time_roundtrip(
315        #[strategy(1980..=2107_u16)] year: u16,
316        #[strategy(1..=12_u8)] month: u8,
317        #[strategy(1..=31_u8)] day: u8,
318        #[strategy(..=23_u8)] hour: u8,
319        #[strategy(..=59_u8)] minute: u8,
320        #[strategy(..=58_u8)] second: u8,
321    ) {
322        use proptest::{prop_assert, prop_assume};
323
324        prop_assume!(Date::from_calendar_date(year.into(), month.try_into().unwrap(), day).is_ok());
325        prop_assume!(Time::from_hms(hour, minute, second).is_ok());
326
327        let date = u16::from(day) + (u16::from(month) << 5) + ((year - 1980) << 9);
328        let time = u16::from(second / 2) + (u16::from(minute) << 5) + (u16::from(hour) << 11);
329        prop_assert!(FileTime::from_dos_date_time(date, time).is_ok());
330    }
331
332    #[test]
333    fn from_dos_date_time_with_invalid_date_time() {
334        // The Day field is 0.
335        assert!(FileTime::from_dos_date_time(0x0020, u16::MIN).is_err());
336        // The Day field is 30, which is after the last day of February.
337        assert!(FileTime::from_dos_date_time(0x005e, u16::MIN).is_err());
338        // The Month field is 0.
339        assert!(FileTime::from_dos_date_time(0x0001, u16::MIN).is_err());
340        // The Month field is 13.
341        assert!(FileTime::from_dos_date_time(0x01a1, u16::MIN).is_err());
342
343        // The DoubleSeconds field is 30.
344        assert!(FileTime::from_dos_date_time(0x0021, 0x001e).is_err());
345        // The Minute field is 60.
346        assert!(FileTime::from_dos_date_time(0x0021, 0x0780).is_err());
347        // The Hour field is 24.
348        assert!(FileTime::from_dos_date_time(0x0021, 0xc000).is_err());
349    }
350}