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}