11use alloc:: { string:: String , vec:: Vec } ;
2+ use core:: fmt;
23
34use bytes:: { BufMut , BytesMut } ;
45use crc32fast:: Hasher as Crc32Hasher ;
@@ -22,6 +23,19 @@ const VERSION_MADE_BY: u16 = (3 << 8) | 0x2D;
2223
2324// ── Date / time ──────────────────────────────────────────────────────────────
2425
26+ /// Error returned when [`MsDosDateTime::new`] is given an invalid date or time.
27+ #[ derive( Debug ) ]
28+ #[ non_exhaustive]
29+ pub struct InvalidMsDosDateTime ;
30+
31+ impl fmt:: Display for InvalidMsDosDateTime {
32+ fn fmt ( & self , f : & mut fmt:: Formatter < ' _ > ) -> fmt:: Result {
33+ write ! ( f, "invalid MS-DOS date/time" )
34+ }
35+ }
36+
37+ impl core:: error:: Error for InvalidMsDosDateTime { }
38+
2539/// MS-DOS date and time as stored in ZIP file headers.
2640///
2741/// The default value is all zeros, which most extractors display as
@@ -37,39 +51,67 @@ pub struct MsDosDateTime {
3751impl MsDosDateTime {
3852 /// Create from individual calendar components.
3953 ///
40- /// Values are packed as-is with no validation. Out-of-range inputs
41- /// will produce a garbage timestamp but will not panic.
54+ /// Returns `None` if any field is out of the valid MS-DOS range.
55+ /// The day is validated against the actual number of days in the given
56+ /// month and year (including leap years for February).
4257 ///
4358 /// * `year` - 1980..=2107
4459 /// * `month` - 1..=12
45- /// * `day` - 1..=31
60+ /// * `day` - 1..=days-in-month
4661 /// * `hour` - 0..=23
4762 /// * `minute` - 0..=59
48- /// * `second` - 0..=58 (rounded down to even)
63+ /// * `second` - 0..=59 (truncated to even)
4964 #[ must_use]
50- pub const fn new ( year : u16 , month : u16 , day : u16 , hour : u16 , minute : u16 , second : u16 ) -> Self {
51- Self {
65+ pub const fn new (
66+ year : u16 ,
67+ month : u16 ,
68+ day : u16 ,
69+ hour : u16 ,
70+ minute : u16 ,
71+ second : u16 ,
72+ ) -> Option < Self > {
73+ if year < 1980
74+ || year > 2107
75+ || month < 1
76+ || month > 12
77+ || day < 1
78+ || day > days_in_month ( year, month)
79+ || second > 59
80+ {
81+ return None ;
82+ }
83+ Some ( Self {
5284 time : ( hour << 11 ) | ( minute << 5 ) | ( second / 2 ) ,
5385 date : ( ( year - 1980 ) << 9 ) | ( month << 5 ) | day,
54- }
86+ } )
87+ }
88+ }
89+
90+ /// Return the number of days in the given month and year.
91+ ///
92+ /// Returns `0` for months outside 1..=12.
93+ const fn days_in_month ( year : u16 , month : u16 ) -> u16 {
94+ match month {
95+ 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31 ,
96+ 4 | 6 | 9 | 11 => 30 ,
97+ 2 if year. is_multiple_of ( 4 ) && !year. is_multiple_of ( 100 ) || year. is_multiple_of ( 400 ) => 29 ,
98+ 2 => 28 ,
99+ _ => 0 ,
55100 }
56101}
57102
58103#[ cfg( feature = "jiff" ) ]
59- impl From < jiff:: civil:: DateTime > for MsDosDateTime {
60- #[ expect(
61- clippy:: cast_sign_loss,
62- reason = "jiff date/time components are non-negative"
63- ) ]
64- fn from ( dt : jiff:: civil:: DateTime ) -> Self {
65- Self :: new (
66- dt. year ( ) as u16 ,
67- dt. month ( ) as u16 ,
68- dt. day ( ) as u16 ,
69- dt. hour ( ) as u16 ,
70- dt. minute ( ) as u16 ,
71- dt. second ( ) as u16 ,
72- )
104+ impl TryFrom < jiff:: civil:: DateTime > for MsDosDateTime {
105+ type Error = InvalidMsDosDateTime ;
106+
107+ fn try_from ( dt : jiff:: civil:: DateTime ) -> Result < Self , Self :: Error > {
108+ let year: u16 = dt. year ( ) . try_into ( ) . map_err ( |_| InvalidMsDosDateTime ) ?;
109+ let month: u16 = dt. month ( ) . try_into ( ) . map_err ( |_| InvalidMsDosDateTime ) ?;
110+ let day: u16 = dt. day ( ) . try_into ( ) . map_err ( |_| InvalidMsDosDateTime ) ?;
111+ let hour: u16 = dt. hour ( ) . try_into ( ) . map_err ( |_| InvalidMsDosDateTime ) ?;
112+ let minute: u16 = dt. minute ( ) . try_into ( ) . map_err ( |_| InvalidMsDosDateTime ) ?;
113+ let second: u16 = dt. second ( ) . try_into ( ) . map_err ( |_| InvalidMsDosDateTime ) ?;
114+ Self :: new ( year, month, day, hour, minute, second) . ok_or ( InvalidMsDosDateTime )
73115 }
74116}
75117
0 commit comments