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, UtcOffset, error::ComponentRange, macros::offset};
11
12use super::FileTime;
13use crate::error::{DosDateTimeRangeError, DosDateTimeRangeErrorKind};
14
15impl FileTime {
16    /// Returns [MS-DOS date and time] which represents the same date and time
17    /// as this `FileTime`. This date and time is used as the timestamp such as
18    /// [FAT], [exFAT] or [ZIP] file format.
19    ///
20    /// This method returns a `(date, time, resolution, offset)` tuple if the
21    /// result is [`Ok`].
22    ///
23    /// `date` and `time` represents the local date and time. This date and time
24    /// has no notion of time zone. The resolution of MS-DOS date and time is 2
25    /// seconds, but additional [finer resolution] (10 ms units) can be
26    /// provided. `resolution` represents this additional finer resolution.
27    ///
28    /// When the `offset` parameter is [`Some`], converts `date` and `time` from
29    /// UTC to the local date and time in the provided time zone and returns it
30    /// with the [UTC offset]. When the `offset` parameter is [`None`] or is not
31    /// a multiple of 15 minute intervals, returns the UTC date and time as a
32    /// date and time and [`None`] as the UTC offset.
33    ///
34    /// <div class="warning">
35    ///
36    /// Note that exFAT supports `resolution` for creation and last modified
37    /// times, and the `offset` return value for these times and last access
38    /// time, but other file systems and file formats may not support these. For
39    /// example, the built-in timestamp of ZIP used for last modified time only
40    /// records `date` and `time`, not `resolution` and the `offset` return
41    /// value.
42    ///
43    /// </div>
44    ///
45    /// # Errors
46    ///
47    /// Returns [`Err`] if the resulting date and time is out of range for
48    /// MS-DOS date and time.
49    ///
50    /// # Panics
51    ///
52    /// Panics if the `offset` parameter is out of range for the [OffsetFromUtc
53    /// field].
54    ///
55    /// # Examples
56    ///
57    /// ```
58    /// # use nt_time::{FileTime, time::macros::offset};
59    /// #
60    /// // `1980-01-01 00:00:00 UTC`.
61    /// assert_eq!(
62    ///     FileTime::new(119_600_064_000_000_000).to_dos_date_time(None),
63    ///     Ok((0x0021, u16::MIN, u8::MIN, None))
64    /// );
65    /// // `2107-12-31 23:59:59 UTC`.
66    /// assert_eq!(
67    ///     FileTime::new(159_992_927_990_000_000).to_dos_date_time(None),
68    ///     Ok((0xff9f, 0xbf7d, 100, None))
69    /// );
70    ///
71    /// // Before `1980-01-01 00:00:00 UTC`.
72    /// assert!(
73    ///     FileTime::new(119_600_063_990_000_000)
74    ///         .to_dos_date_time(None)
75    ///         .is_err()
76    /// );
77    /// // After `2107-12-31 23:59:59.990000000 UTC`.
78    /// assert!(
79    ///     FileTime::new(159_992_928_000_000_000)
80    ///         .to_dos_date_time(None)
81    ///         .is_err()
82    /// );
83    ///
84    /// // From `2002-11-27 03:25:00 UTC` to `2002-11-26 19:25:00 -08:00`.
85    /// assert_eq!(
86    ///     FileTime::new(126_828_411_000_000_000).to_dos_date_time(Some(offset!(-08:00))),
87    ///     Ok((0x2d7a, 0x9b20, u8::MIN, Some(offset!(-08:00))))
88    /// );
89    /// ```
90    ///
91    /// When the UTC offset is not a multiple of 15 minute intervals, returns
92    /// the UTC date and time:
93    ///
94    /// ```
95    /// # use nt_time::{FileTime, time::macros::offset};
96    /// #
97    /// // `2002-11-27 03:25:00 UTC`.
98    /// assert_eq!(
99    ///     FileTime::new(126_828_411_000_000_000).to_dos_date_time(Some(offset!(-08:01))),
100    ///     Ok((0x2d7b, 0x1b20, u8::MIN, None))
101    /// );
102    /// // `2002-11-27 03:25:00 UTC`.
103    /// assert_eq!(
104    ///     FileTime::new(126_828_411_000_000_000).to_dos_date_time(Some(offset!(-08:14))),
105    ///     Ok((0x2d7b, 0x1b20, u8::MIN, None))
106    /// );
107    ///
108    /// // From `2002-11-27 03:25:00 UTC` to `2002-11-26 19:10:00 -08:15`.
109    /// assert_eq!(
110    ///     FileTime::new(126_828_411_000_000_000).to_dos_date_time(Some(offset!(-08:15))),
111    ///     Ok((0x2d7a, 0x9940, u8::MIN, Some(offset!(-08:15))))
112    /// );
113    /// ```
114    ///
115    /// [MS-DOS date and time]: https://learn.microsoft.com/en-us/windows/win32/sysinfo/ms-dos-date-and-time
116    /// [FAT]: https://en.wikipedia.org/wiki/File_Allocation_Table
117    /// [exFAT]: https://en.wikipedia.org/wiki/ExFAT
118    /// [ZIP]: https://en.wikipedia.org/wiki/ZIP_(file_format)
119    /// [finer resolution]: https://learn.microsoft.com/en-us/windows/win32/fileio/exfat-specification#749-10msincrement-fields
120    /// [UTC offset]: https://learn.microsoft.com/en-us/windows/win32/fileio/exfat-specification#7410-utcoffset-fields
121    /// [OffsetFromUtc field]: https://learn.microsoft.com/en-us/windows/win32/fileio/exfat-specification#74101-offsetfromutc-field
122    pub fn to_dos_date_time(
123        self,
124        offset: Option<UtcOffset>,
125    ) -> Result<(u16, u16, u8, Option<UtcOffset>), DosDateTimeRangeError> {
126        let offset = offset.filter(|o| o.whole_seconds() % 900 == 0);
127        if let Some(o) = offset {
128            // The UTC offset must be in the range of a 7-bit signed integer.
129            assert!((offset!(-16:00)..=offset!(+15:45)).contains(&o));
130        }
131
132        let dt = OffsetDateTime::try_from(self)
133            .ok()
134            .and_then(|dt| dt.checked_to_offset(offset.unwrap_or(UtcOffset::UTC)))
135            .ok_or(DosDateTimeRangeErrorKind::Overflow)?;
136
137        match dt.year() {
138            ..=1979 => Err(DosDateTimeRangeErrorKind::Negative.into()),
139            2108.. => Err(DosDateTimeRangeErrorKind::Overflow.into()),
140            year => {
141                let (year, month, day) = (
142                    u16::try_from(year - 1980).expect("year should be in the range of `u16`"),
143                    u16::from(u8::from(dt.month())),
144                    u16::from(dt.day()),
145                );
146                let date = (year << 9) | (month << 5) | day;
147
148                let (hour, minute, second) = (dt.hour(), dt.minute(), dt.second() / 2);
149                let time = (u16::from(hour) << 11) | (u16::from(minute) << 5) | u16::from(second);
150
151                let resolution = ((dt.time()
152                    - Time::from_hms(hour, minute, second * 2)
153                        .expect("the MS-DOS time should be in the range of `Time`"))
154                .whole_milliseconds()
155                    / 10)
156                    .try_into()
157                    .expect("resolution should be in the range of `u8`");
158                debug_assert!(resolution <= 199);
159
160                Ok((date, time, resolution, offset))
161            }
162        }
163    }
164
165    /// Creates a `FileTime` with the given [MS-DOS date and time]. This date
166    /// and time is used as the timestamp such as [FAT], [exFAT] or [ZIP] file
167    /// format.
168    ///
169    /// When `resolution` is [`Some`], additional [finer resolution] (10 ms
170    /// units) is added to `time`.
171    ///
172    /// When `offset` is [`Some`], converts `date` and `time` from the local
173    /// date and time in the provided time zone to UTC. When `offset` is
174    /// [`None`] or is not a multiple of 15 minute intervals, assumes the
175    /// provided date and time is in UTC.
176    ///
177    /// <div class="warning">
178    ///
179    /// Note that exFAT supports `resolution` for creation and last modified
180    /// times, and `offset` for these times and last access time, but other file
181    /// systems and file formats may not support these. For example, the
182    /// built-in timestamp of ZIP used for last modified time only records
183    /// `date` and `time`, not `resolution` and `offset`.
184    ///
185    /// </div>
186    ///
187    /// # Errors
188    ///
189    /// Returns [`Err`] if `date` or `time` is an invalid date and time.
190    ///
191    /// # Panics
192    ///
193    /// Panics if any of the following are true:
194    ///
195    /// - `resolution` is greater than 199.
196    /// - `offset` is out of range for the [OffsetFromUtc field].
197    ///
198    /// # Examples
199    ///
200    /// ```
201    /// # use nt_time::{FileTime, time::macros::offset};
202    /// #
203    /// // `1980-01-01 00:00:00 UTC`.
204    /// assert_eq!(
205    ///     FileTime::from_dos_date_time(0x0021, u16::MIN, None, None),
206    ///     Ok(FileTime::new(119_600_064_000_000_000))
207    /// );
208    /// // `2107-12-31 23:59:59 UTC`.
209    /// assert_eq!(
210    ///     FileTime::from_dos_date_time(0xff9f, 0xbf7d, Some(100), None),
211    ///     Ok(FileTime::new(159_992_927_990_000_000))
212    /// );
213    ///
214    /// // From `2002-11-26 19:25:00 -08:00` to `2002-11-27 03:25:00 UTC`.
215    /// assert_eq!(
216    ///     FileTime::from_dos_date_time(0x2d7a, 0x9b20, None, Some(offset!(-08:00))),
217    ///     Ok(FileTime::new(126_828_411_000_000_000))
218    /// );
219    ///
220    /// // The Day field is 0.
221    /// assert!(FileTime::from_dos_date_time(0x0020, u16::MIN, None, None).is_err());
222    /// // The DoubleSeconds field is 30.
223    /// assert!(FileTime::from_dos_date_time(0x0021, 0x001e, None, None).is_err());
224    /// ```
225    ///
226    /// When the [UTC offset] is not a multiple of 15 minute intervals, assumes
227    /// the provided date and time is in UTC:
228    ///
229    /// ```
230    /// # use nt_time::{FileTime, time::macros::offset};
231    /// #
232    /// // From `2002-11-26 19:25:00 -08:01` to `2002-11-26 19:25:00 UTC`.
233    /// assert_eq!(
234    ///     FileTime::from_dos_date_time(0x2d7a, 0x9b20, None, Some(offset!(-08:01))),
235    ///     Ok(FileTime::new(126_828_123_000_000_000))
236    /// );
237    /// // From `2002-11-26 19:25:00 -08:14` to `2002-11-26 19:25:00 UTC`.
238    /// assert_eq!(
239    ///     FileTime::from_dos_date_time(0x2d7a, 0x9b20, None, Some(offset!(-08:14))),
240    ///     Ok(FileTime::new(126_828_123_000_000_000))
241    /// );
242    ///
243    /// // From `2002-11-26 19:25:00 -08:15` to `2002-11-27 03:40:00 UTC`.
244    /// assert_eq!(
245    ///     FileTime::from_dos_date_time(0x2d7a, 0x9b20, None, Some(offset!(-08:15))),
246    ///     Ok(FileTime::new(126_828_420_000_000_000))
247    /// );
248    /// ```
249    ///
250    /// Additional finer resolution must be in the range 0 to 199:
251    ///
252    /// ```should_panic
253    /// # use nt_time::FileTime;
254    /// #
255    /// let _ = FileTime::from_dos_date_time(0x0021, u16::MIN, Some(200), None);
256    /// ```
257    ///
258    /// [MS-DOS date and time]: https://learn.microsoft.com/en-us/windows/win32/sysinfo/ms-dos-date-and-time
259    /// [FAT]: https://en.wikipedia.org/wiki/File_Allocation_Table
260    /// [exFAT]: https://en.wikipedia.org/wiki/ExFAT
261    /// [ZIP]: https://en.wikipedia.org/wiki/ZIP_(file_format)
262    /// [finer resolution]: https://learn.microsoft.com/en-us/windows/win32/fileio/exfat-specification#749-10msincrement-fields
263    /// [OffsetFromUtc field]: https://learn.microsoft.com/en-us/windows/win32/fileio/exfat-specification#74101-offsetfromutc-field
264    /// [UTC offset]: https://learn.microsoft.com/en-us/windows/win32/fileio/exfat-specification#7410-utcoffset-fields
265    pub fn from_dos_date_time(
266        date: u16,
267        time: u16,
268        resolution: Option<u8>,
269        offset: Option<UtcOffset>,
270    ) -> Result<Self, ComponentRange> {
271        use core::time::Duration;
272
273        if let Some(res) = resolution {
274            assert!(res <= 199);
275        }
276        let resolution = resolution.map_or(Duration::ZERO, |res| {
277            Duration::from_millis(u64::from(res) * 10)
278        });
279
280        let offset = offset.filter(|o| o.whole_seconds() % 900 == 0);
281        if let Some(o) = offset {
282            // The UTC offset must be in the range of a 7-bit signed integer.
283            assert!((offset!(-16:00)..=offset!(+15:45)).contains(&o));
284        }
285
286        let (year, month, day) = (
287            (1980 + (date >> 9)).into(),
288            u8::try_from((date >> 5) & 0x0f)
289                .expect("month should be in the range of `u8`")
290                .try_into()?,
291            (date & 0x1f)
292                .try_into()
293                .expect("day should be in the range of `u8`"),
294        );
295        let date = Date::from_calendar_date(year, month, day)?;
296
297        let (hour, minute, second) = (
298            (time >> 11)
299                .try_into()
300                .expect("hour should be in the range of `u8`"),
301            ((time >> 5) & 0x3f)
302                .try_into()
303                .expect("minute should be in the range of `u8`"),
304            ((time & 0x1f) * 2)
305                .try_into()
306                .expect("second should be in the range of `u8`"),
307        );
308        let time = Time::from_hms(hour, minute, second)? + resolution;
309
310        let ft = OffsetDateTime::new_in_offset(date, time, offset.unwrap_or(UtcOffset::UTC))
311            .try_into()
312            .expect("MS-DOS date and time should be in the range of `FileTime`");
313        Ok(ft)
314    }
315}
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320
321    #[test]
322    fn to_dos_date_time_before_dos_date_time_epoch() {
323        // `1979-12-31 23:59:58 UTC`.
324        assert_eq!(
325            FileTime::new(119_600_063_980_000_000)
326                .to_dos_date_time(None)
327                .unwrap_err(),
328            DosDateTimeRangeErrorKind::Negative.into()
329        );
330        // `1979-12-31 23:59:59 UTC`.
331        assert_eq!(
332            FileTime::new(119_600_063_990_000_000)
333                .to_dos_date_time(None)
334                .unwrap_err(),
335            DosDateTimeRangeErrorKind::Negative.into()
336        );
337        // `1980-01-01 00:59:58 UTC`.
338        assert_eq!(
339            FileTime::new(119_600_099_980_000_000)
340                .to_dos_date_time(Some(offset!(-01:00)))
341                .unwrap_err(),
342            DosDateTimeRangeErrorKind::Negative.into()
343        );
344        // `1980-01-01 00:59:59 UTC`.
345        assert_eq!(
346            FileTime::new(119_600_099_990_000_000)
347                .to_dos_date_time(Some(offset!(-01:00)))
348                .unwrap_err(),
349            DosDateTimeRangeErrorKind::Negative.into()
350        );
351    }
352
353    #[cfg(feature = "std")]
354    #[test_strategy::proptest]
355    fn to_dos_date_time_before_dos_date_time_epoch_roundtrip(
356        #[strategy(..=119_600_063_980_000_000_u64)] ft: u64,
357    ) {
358        use proptest::prop_assert_eq;
359
360        prop_assert_eq!(
361            FileTime::new(ft).to_dos_date_time(None).unwrap_err(),
362            DosDateTimeRangeErrorKind::Negative.into()
363        );
364    }
365
366    #[test]
367    fn to_dos_date_time() {
368        // `1980-01-01 00:00:00 UTC`.
369        assert_eq!(
370            FileTime::new(119_600_064_000_000_000)
371                .to_dos_date_time(None)
372                .unwrap(),
373            (0x0021, u16::MIN, u8::MIN, None)
374        );
375        // `1980-01-01 00:00:01 UTC`.
376        assert_eq!(
377            FileTime::new(119_600_064_010_000_000)
378                .to_dos_date_time(None)
379                .unwrap(),
380            (0x0021, u16::MIN, 100, None)
381        );
382        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
383        //
384        // `2018-11-17 10:38:30 UTC`.
385        assert_eq!(
386            FileTime::new(131_869_247_100_000_000)
387                .to_dos_date_time(None)
388                .unwrap(),
389            (0x4d71, 0x54cf, u8::MIN, None)
390        );
391        // `2107-12-31 23:59:58 UTC`.
392        assert_eq!(
393            FileTime::new(159_992_927_980_000_000)
394                .to_dos_date_time(None)
395                .unwrap(),
396            (0xff9f, 0xbf7d, u8::MIN, None)
397        );
398        // `2107-12-31 23:59:59 UTC`.
399        assert_eq!(
400            FileTime::new(159_992_927_990_000_000)
401                .to_dos_date_time(None)
402                .unwrap(),
403            (0xff9f, 0xbf7d, 100, None)
404        );
405
406        // `1980-01-01 00:00:00.010000000 UTC`.
407        assert_eq!(
408            FileTime::new(119_600_064_000_100_000)
409                .to_dos_date_time(None)
410                .unwrap(),
411            (0x0021, u16::MIN, 1, None)
412        );
413        // `1980-01-01 00:00:00.100000000 UTC`.
414        assert_eq!(
415            FileTime::new(119_600_064_001_000_000)
416                .to_dos_date_time(None)
417                .unwrap(),
418            (0x0021, u16::MIN, 10, None)
419        );
420        // `1980-01-01 00:00:01.990000000 UTC`.
421        assert_eq!(
422            FileTime::new(119_600_064_019_900_000)
423                .to_dos_date_time(None)
424                .unwrap(),
425            (0x0021, u16::MIN, 199, None)
426        );
427        // `1980-01-01 00:00:02 UTC`.
428        assert_eq!(
429            FileTime::new(119_600_064_020_000_000)
430                .to_dos_date_time(None)
431                .unwrap(),
432            (0x0021, 0x0001, u8::MIN, None)
433        );
434
435        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
436        //
437        // From `2002-11-27 03:25:00 UTC` to `2002-11-26 19:25:00 -08:00`.
438        assert_eq!(
439            FileTime::new(126_828_411_000_000_000)
440                .to_dos_date_time(Some(offset!(-08:00)))
441                .unwrap(),
442            (0x2d7a, 0x9b20, u8::MIN, Some(offset!(-08:00)))
443        );
444        // From `2002-11-27 03:25:00 UTC` to `2002-11-26 19:25:00 -08:00`.
445        assert_eq!(
446            FileTime::new(126_828_411_000_000_000)
447                .to_dos_date_time(Some(offset!(-08)))
448                .unwrap(),
449            (0x2d7a, 0x9b20, u8::MIN, Some(offset!(-08)))
450        );
451        // `2002-11-27 03:25:00 UTC`.
452        //
453        // When the UTC offset is not a multiple of 15 minute intervals, returns the UTC
454        // date and time.
455        assert_eq!(
456            FileTime::new(126_828_411_000_000_000)
457                .to_dos_date_time(Some(offset!(-08:00:01)))
458                .unwrap(),
459            (0x2d7b, 0x1b20, u8::MIN, None)
460        );
461        // `2002-11-27 03:25:00 UTC`.
462        //
463        // When the UTC offset is not a multiple of 15 minute intervals, returns the UTC
464        // date and time.
465        assert_eq!(
466            FileTime::new(126_828_411_000_000_000)
467                .to_dos_date_time(Some(offset!(-08:01)))
468                .unwrap(),
469            (0x2d7b, 0x1b20, u8::MIN, None)
470        );
471        // `2002-11-27 03:25:00 UTC`.
472        //
473        // When the UTC offset is not a multiple of 15 minute intervals, returns the UTC
474        // date and time.
475        assert_eq!(
476            FileTime::new(126_828_411_000_000_000)
477                .to_dos_date_time(Some(offset!(-08:14)))
478                .unwrap(),
479            (0x2d7b, 0x1b20, u8::MIN, None)
480        );
481        // `2002-11-27 03:25:00 UTC`.
482        //
483        // When the UTC offset is not a multiple of 15 minute intervals, returns the UTC
484        // date and time.
485        assert_eq!(
486            FileTime::new(126_828_411_000_000_000)
487                .to_dos_date_time(Some(offset!(-08:14:59)))
488                .unwrap(),
489            (0x2d7b, 0x1b20, u8::MIN, None)
490        );
491        // From `2002-11-27 03:25:00 UTC` to `2002-11-26 19:10:00 -08:15`.
492        assert_eq!(
493            FileTime::new(126_828_411_000_000_000)
494                .to_dos_date_time(Some(offset!(-08:15)))
495                .unwrap(),
496            (0x2d7a, 0x9940, u8::MIN, Some(offset!(-08:15)))
497        );
498        // `2002-11-27 03:25:00 UTC`.
499        assert_eq!(
500            FileTime::new(126_828_411_000_000_000)
501                .to_dos_date_time(Some(UtcOffset::UTC))
502                .unwrap(),
503            (0x2d7b, 0x1b20, u8::MIN, Some(UtcOffset::UTC))
504        );
505        // `2002-11-27 03:25:00 UTC`.
506        assert_eq!(
507            FileTime::new(126_828_411_000_000_000)
508                .to_dos_date_time(Some(offset!(+00:00)))
509                .unwrap(),
510            (0x2d7b, 0x1b20, u8::MIN, Some(UtcOffset::UTC))
511        );
512
513        // From `1980-01-01 00:00:00 UTC` to `1980-01-01 15:45:00 +15:45`.
514        assert_eq!(
515            FileTime::new(119_600_064_000_000_000)
516                .to_dos_date_time(Some(offset!(+15:45)))
517                .unwrap(),
518            (0x0021, 0x7da0, u8::MIN, Some(offset!(+15:45)))
519        );
520        // From `2107-12-31 23:59:58 UTC` to `2107-12-31 07:59:58 -16:00`.
521        assert_eq!(
522            FileTime::new(159_992_927_980_000_000)
523                .to_dos_date_time(Some(offset!(-16:00)))
524                .unwrap(),
525            (0xff9f, 0x3f7d, u8::MIN, Some(offset!(-16:00)))
526        );
527    }
528
529    #[cfg(feature = "std")]
530    #[test_strategy::proptest]
531    fn to_dos_date_time_roundtrip(
532        #[strategy(119_600_064_000_000_000..=159_992_927_980_000_000_u64)] ft: u64,
533    ) {
534        use proptest::prop_assert;
535
536        prop_assert!(FileTime::new(ft).to_dos_date_time(None).is_ok());
537    }
538
539    #[test]
540    fn to_dos_date_time_with_too_big_date_time() {
541        // `2108-01-01 00:00:00 UTC`.
542        assert_eq!(
543            FileTime::new(159_992_928_000_000_000)
544                .to_dos_date_time(None)
545                .unwrap_err(),
546            DosDateTimeRangeErrorKind::Overflow.into()
547        );
548        // `2107-12-31 23:00:00 UTC`.
549        assert_eq!(
550            FileTime::new(159_992_892_000_000_000)
551                .to_dos_date_time(Some(offset!(+01:00)))
552                .unwrap_err(),
553            DosDateTimeRangeErrorKind::Overflow.into()
554        );
555    }
556
557    #[cfg(feature = "std")]
558    #[test_strategy::proptest]
559    fn to_dos_date_time_with_too_big_date_time_roundtrip(
560        #[strategy(159_992_928_000_000_000_u64..)] ft: u64,
561    ) {
562        use proptest::prop_assert_eq;
563
564        prop_assert_eq!(
565            FileTime::new(ft).to_dos_date_time(None).unwrap_err(),
566            DosDateTimeRangeErrorKind::Overflow.into()
567        );
568    }
569
570    #[test]
571    #[should_panic]
572    fn to_dos_date_time_with_invalid_positive_offset() {
573        // From `1980-01-01 00:00:00 UTC` to `1980-01-01 16:00:00 +16:00`.
574        let _ = FileTime::new(119_600_064_000_000_000).to_dos_date_time(Some(offset!(+16:00)));
575    }
576
577    #[test]
578    #[should_panic]
579    fn to_dos_date_time_with_invalid_negative_offset() {
580        // From `2107-12-31 23:59:58 UTC` to `2107-12-31 07:44:58 -16:15`.
581        let _ = FileTime::new(159_992_927_980_000_000).to_dos_date_time(Some(offset!(-16:15)));
582    }
583
584    #[test]
585    fn from_dos_date_time() {
586        // `1980-01-01 00:00:00 UTC`.
587        assert_eq!(
588            FileTime::from_dos_date_time(0x0021, u16::MIN, None, None).unwrap(),
589            FileTime::new(119_600_064_000_000_000)
590        );
591        // `1980-01-01 00:00:01 UTC`.
592        assert_eq!(
593            FileTime::from_dos_date_time(0x0021, u16::MIN, Some(100), None).unwrap(),
594            FileTime::new(119_600_064_010_000_000)
595        );
596        // <https://github.com/zip-rs/zip/blob/v0.6.4/src/types.rs#L553-L569>.
597        //
598        // `2018-11-17 10:38:30 UTC`.
599        assert_eq!(
600            FileTime::from_dos_date_time(0x4d71, 0x54cf, None, None).unwrap(),
601            FileTime::new(131_869_247_100_000_000)
602        );
603        // `2107-12-31 23:59:58 UTC`.
604        assert_eq!(
605            FileTime::from_dos_date_time(0xff9f, 0xbf7d, None, None).unwrap(),
606            FileTime::new(159_992_927_980_000_000)
607        );
608        // `2107-12-31 23:59:59 UTC`.
609        assert_eq!(
610            FileTime::from_dos_date_time(0xff9f, 0xbf7d, Some(100), None).unwrap(),
611            FileTime::new(159_992_927_990_000_000)
612        );
613
614        // `1980-01-01 00:00:00.010000000 UTC`.
615        assert_eq!(
616            FileTime::from_dos_date_time(0x0021, u16::MIN, Some(1), None).unwrap(),
617            FileTime::new(119_600_064_000_100_000)
618        );
619        // `1980-01-01 00:00:00.100000000 UTC`.
620        assert_eq!(
621            FileTime::from_dos_date_time(0x0021, u16::MIN, Some(10), None).unwrap(),
622            FileTime::new(119_600_064_001_000_000)
623        );
624        // `1980-01-01 00:00:01.990000000 UTC`.
625        assert_eq!(
626            FileTime::from_dos_date_time(0x0021, u16::MIN, Some(199), None).unwrap(),
627            FileTime::new(119_600_064_019_900_000)
628        );
629
630        // <https://devblogs.microsoft.com/oldnewthing/20030905-02/?p=42653>.
631        //
632        // From `2002-11-26 19:25:00 -08:00` to `2002-11-27 03:25:00 UTC`.
633        assert_eq!(
634            FileTime::from_dos_date_time(0x2d7a, 0x9b20, None, Some(offset!(-08:00))).unwrap(),
635            FileTime::new(126_828_411_000_000_000)
636        );
637        // From `2002-11-26 19:25:00 -08:00` to `2002-11-27 03:25:00 UTC`.
638        assert_eq!(
639            FileTime::from_dos_date_time(0x2d7a, 0x9b20, None, Some(offset!(-08))).unwrap(),
640            FileTime::new(126_828_411_000_000_000)
641        );
642        // From `2002-11-26 19:25:00 -08:00:01` to `2002-11-26 19:25:00 UTC`.
643        //
644        // When the UTC offset is not a multiple of 15 minute intervals, assumes the
645        // provided date and time is in UTC.
646        assert_eq!(
647            FileTime::from_dos_date_time(0x2d7a, 0x9b20, None, Some(offset!(-08:00:01))).unwrap(),
648            FileTime::new(126_828_123_000_000_000)
649        );
650        // From `2002-11-26 19:25:00 -08:01` to `2002-11-26 19:25:00 UTC`.
651        //
652        // When the UTC offset is not a multiple of 15 minute intervals, assumes the
653        // provided date and time is in UTC.
654        assert_eq!(
655            FileTime::from_dos_date_time(0x2d7a, 0x9b20, None, Some(offset!(-08:01))).unwrap(),
656            FileTime::new(126_828_123_000_000_000)
657        );
658        // From `2002-11-26 19:25:00 -08:14` to `2002-11-26 19:25:00 UTC`.
659        //
660        // When the UTC offset is not a multiple of 15 minute intervals, assumes the
661        // provided date and time is in UTC.
662        assert_eq!(
663            FileTime::from_dos_date_time(0x2d7a, 0x9b20, None, Some(offset!(-08:14))).unwrap(),
664            FileTime::new(126_828_123_000_000_000)
665        );
666        // From `2002-11-26 19:25:00 -08:14:59` to `2002-11-26 19:25:00 UTC`.
667        //
668        // When the UTC offset is not a multiple of 15 minute intervals, assumes the
669        // provided date and time is in UTC.
670        assert_eq!(
671            FileTime::from_dos_date_time(0x2d7a, 0x9b20, None, Some(offset!(-08:14:59))).unwrap(),
672            FileTime::new(126_828_123_000_000_000)
673        );
674        // From `2002-11-26 19:25:00 -08:15` to `2002-11-27 03:40:00 UTC`.
675        assert_eq!(
676            FileTime::from_dos_date_time(0x2d7a, 0x9b20, None, Some(offset!(-08:15))).unwrap(),
677            FileTime::new(126_828_420_000_000_000)
678        );
679        // `2002-11-26 19:25:00 UTC`.
680        assert_eq!(
681            FileTime::from_dos_date_time(0x2d7a, 0x9b20, None, Some(UtcOffset::UTC)).unwrap(),
682            FileTime::new(126_828_123_000_000_000)
683        );
684        // `2002-11-26 19:25:00 UTC`.
685        assert_eq!(
686            FileTime::from_dos_date_time(0x2d7a, 0x9b20, None, Some(offset!(+00:00))).unwrap(),
687            FileTime::new(126_828_123_000_000_000)
688        );
689
690        // From `2107-12-31 23:59:58 +15:45` to `2107-12-31 08:14:58 UTC`.
691        assert_eq!(
692            FileTime::from_dos_date_time(0xff9f, 0xbf7d, None, Some(offset!(+15:45))).unwrap(),
693            FileTime::new(159_992_360_980_000_000)
694        );
695        // From `1980-01-01 00:00:00 -16:00` to `1980-01-01 16:00:00 UTC`.
696        assert_eq!(
697            FileTime::from_dos_date_time(0x0021, u16::MIN, None, Some(offset!(-16:00))).unwrap(),
698            FileTime::new(119_600_640_000_000_000)
699        );
700    }
701
702    #[cfg(feature = "std")]
703    #[test_strategy::proptest]
704    fn from_dos_date_time_roundtrip(
705        #[strategy(1980..=2107_u16)] year: u16,
706        #[strategy(1..=12_u8)] month: u8,
707        #[strategy(1..=31_u8)] day: u8,
708        #[strategy(..=23_u8)] hour: u8,
709        #[strategy(..=59_u8)] minute: u8,
710        #[strategy(..=58_u8)] second: u8,
711    ) {
712        use proptest::{prop_assert, prop_assume};
713
714        prop_assume!(Date::from_calendar_date(year.into(), month.try_into().unwrap(), day).is_ok());
715        prop_assume!(Time::from_hms(hour, minute, second).is_ok());
716
717        let date = u16::from(day) + (u16::from(month) << 5) + ((year - 1980) << 9);
718        let time = u16::from(second / 2) + (u16::from(minute) << 5) + (u16::from(hour) << 11);
719        prop_assert!(FileTime::from_dos_date_time(date, time, None, None).is_ok());
720    }
721
722    #[test]
723    fn from_dos_date_time_with_invalid_date_time() {
724        // The Day field is 0.
725        assert!(FileTime::from_dos_date_time(0x0020, u16::MIN, None, None).is_err());
726        // The Day field is 30, which is after the last day of February.
727        assert!(FileTime::from_dos_date_time(0x005e, u16::MIN, None, None).is_err());
728        // The Month field is 0.
729        assert!(FileTime::from_dos_date_time(0x0001, u16::MIN, None, None).is_err());
730        // The Month field is 13.
731        assert!(FileTime::from_dos_date_time(0x01a1, u16::MIN, None, None).is_err());
732
733        // The DoubleSeconds field is 30.
734        assert!(FileTime::from_dos_date_time(0x0021, 0x001e, None, None).is_err());
735        // The Minute field is 60.
736        assert!(FileTime::from_dos_date_time(0x0021, 0x0780, None, None).is_err());
737        // The Hour field is 24.
738        assert!(FileTime::from_dos_date_time(0x0021, 0xc000, None, None).is_err());
739    }
740
741    #[test]
742    #[should_panic]
743    fn from_dos_date_time_with_invalid_resolution() {
744        let _ = FileTime::from_dos_date_time(0x0021, u16::MIN, Some(200), None);
745    }
746
747    #[test]
748    #[should_panic]
749    fn from_dos_date_time_with_invalid_positive_offset() {
750        // From `2107-12-31 23:59:58 +16:00` to `2107-12-31 07:59:58 UTC`.
751        let _ = FileTime::from_dos_date_time(0xff9f, 0xbf7d, None, Some(offset!(+16:00)));
752    }
753
754    #[test]
755    #[should_panic]
756    fn from_dos_date_time_with_invalid_negative_offset() {
757        // From `1980-01-01 00:00:00 -16:15` to `1980-01-01 16:15:00 UTC`.
758        let _ = FileTime::from_dos_date_time(0x0021, u16::MIN, None, Some(offset!(-16:15)));
759    }
760}