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}