Skip to main content

zip/extra_fields/
extended_timestamp.rs

1//! Extended Timestamp extra field
2//! Defined in <https://libzip.org/specifications/extrafld.txt>
3
4use crate::result::invalid;
5use crate::result::{ZipError, ZipResult};
6use crate::unstable::LittleEndianReadExt;
7use core::mem;
8use std::io::Read;
9
10/// `ExtendedTimestamp` Flags
11#[rustfmt::skip]
12#[repr(u8)]
13#[derive(Debug, Copy, Clone, PartialEq, Eq)]
14enum ExtendedTimestampFlags {
15    /// modification time is present
16    Modified = 0b0000_0001_u8,
17    /// access time is present
18    Accessed = 0b0000_0010_u8,
19    /// creation time is present
20    Created  = 0b0000_0100_u8,
21    // others bytes reversed
22}
23
24impl ExtendedTimestampFlags {
25    #[inline(always)]
26    fn matching(flags: u8, matching_flag: Self) -> bool {
27        flags & u8::from(matching_flag) != 0
28    }
29}
30
31impl From<ExtendedTimestampFlags> for u8 {
32    fn from(extended_timestamp: ExtendedTimestampFlags) -> u8 {
33        extended_timestamp as u8
34    }
35}
36
37/// Extended timestamp, as described in <https://libzip.org/specifications/extrafld.txt>
38#[derive(Debug, Clone, PartialEq)]
39pub struct ExtendedTimestamp {
40    modified: Option<u32>,
41    accessed: Option<u32>,
42    created: Option<u32>,
43}
44
45impl ExtendedTimestamp {
46    const MAX_LENGTH: u16 = (mem::size_of::<u8>()
47        + mem::size_of::<u32>()
48        + mem::size_of::<u32>()
49        + mem::size_of::<u32>()) as u16;
50    /// creates an extended timestamp struct by reading the required bytes from the reader.
51    ///
52    /// This method assumes that the length has already been read, therefore
53    /// it must be passed as an argument
54    ///
55    /// # Errors
56    ///
57    /// Fails if the len is empty or a read fails
58    ///
59    pub fn try_from_reader<R: Read>(
60        reader: &mut R,
61        len: u16,
62        is_local_header: bool,
63    ) -> ZipResult<Self> {
64        if len == 0 {
65            return Err(invalid!("Extended timestamp field is empty"));
66        }
67        let mut flags = [0u8];
68        reader.read_exact(&mut flags)?;
69        let bytes_to_read = (len as usize)
70            .checked_sub(flags.len())
71            .ok_or_else(|| invalid!("Extended timestamp field too short for flags"))?;
72
73        if is_local_header {
74            let flags = flags[0];
75            return Self::parse_local_header(reader, len, flags, bytes_to_read);
76        }
77        // the flags field refer to the local header
78        Self::parse_central_header(reader, bytes_to_read)
79    }
80
81    /// parse the local header
82    fn parse_local_header<R: Read>(
83        reader: &mut R,
84        len: u16,
85        flags: u8,
86        mut bytes_to_read: usize,
87    ) -> ZipResult<Self> {
88        // the `flags` field refers to the local headers and might not correspond
89        // to the len field. If the length field is 1+4, we assume that only
90        // the modification time has been set
91
92        // > Those times that are present will appear in the order indicated, but
93        // > any combination of times may be omitted.  (Creation time may be
94        // > present without access time, for example.)  TSize should equal
95        // > (1 + 4*(number of set bits in Flags)), as the block is currently
96        // > defined.
97        if len != 5 && u32::from(len) != 1 + 4 * flags.count_ones() {
98            return Err(ZipError::Io(std::io::Error::other(format!(
99                "flags and len don't match in extended timestamp field len={len} flags={flags:08b}"
100            ))));
101        }
102
103        let modified =
104            if (ExtendedTimestampFlags::matching(flags, ExtendedTimestampFlags::Modified)
105                && bytes_to_read >= mem::size_of::<u32>())
106                || len == Self::MAX_LENGTH
107            {
108                bytes_to_read = bytes_to_read.checked_sub(mem::size_of::<u32>()).ok_or_else(||
109                    invalid!(
110                        "Extended timestamp field too short for mod_time len={} flags={flags:08b}",
111                        len
112                    ),
113                )?;
114                Some(reader.read_u32_le()?)
115            } else {
116                None
117            };
118
119        let accessed =
120            if (ExtendedTimestampFlags::matching(flags, ExtendedTimestampFlags::Accessed)
121                && bytes_to_read >= mem::size_of::<u32>())
122                || len == Self::MAX_LENGTH
123            {
124                bytes_to_read = bytes_to_read.checked_sub(mem::size_of::<u32>()).ok_or_else(||
125                    invalid!(
126                        "Extended timestamp field too short for ac_time len={} flags={flags:08b}",
127                        len
128                    ),
129                )?;
130                Some(reader.read_u32_le()?)
131            } else {
132                None
133            };
134
135        let created =
136            if (ExtendedTimestampFlags::matching(flags, ExtendedTimestampFlags::Created)
137                && bytes_to_read >= mem::size_of::<u32>())
138                || len == Self::MAX_LENGTH
139            {
140                bytes_to_read = bytes_to_read
141                .checked_sub(mem::size_of::<u32>())
142                .ok_or_else(|| invalid!(
143                    "Extended timestamp field too short for cr_time len={} flags={flags:08b}",
144                    len
145                ))?;
146                Some(reader.read_u32_le()?)
147            } else {
148                None
149            };
150
151        if bytes_to_read > 0 {
152            // ignore undocumented bytes
153            let n = std::io::copy(&mut reader.take(bytes_to_read as u64), &mut std::io::sink())?;
154
155            if n != bytes_to_read as u64 {
156                return Err(invalid!(
157                    "unexpected EOF in local header of Extended Timestamp extra field"
158                ));
159            }
160        }
161
162        Ok(Self {
163            modified,
164            accessed,
165            created,
166        })
167    }
168
169    /// parse the central header
170    fn parse_central_header<R: Read>(reader: &mut R, mut bytes_to_read: usize) -> ZipResult<Self> {
171        let modified = if bytes_to_read >= mem::size_of::<u32>() {
172            bytes_to_read -= mem::size_of::<u32>();
173            Some(reader.read_u32_le()?)
174        } else {
175            None
176        };
177        if bytes_to_read > 0 {
178            // ignore undocumented bytes
179            let n = std::io::copy(&mut reader.take(bytes_to_read as u64), &mut std::io::sink())?;
180
181            if n != bytes_to_read as u64 {
182                return Err(invalid!(
183                    "unexpected EOF in central header of Extended Timestamp extra field"
184                ));
185            }
186        }
187        Ok(Self {
188            modified,
189            accessed: None,
190            created: None,
191        })
192    }
193
194    /// returns the last modification timestamp, if defined, as UNIX epoch seconds
195    #[must_use]
196    pub fn mod_time(&self) -> Option<u32> {
197        self.modified
198    }
199
200    /// returns the last access timestamp, if defined, as UNIX epoch seconds
201    #[must_use]
202    pub fn ac_time(&self) -> Option<u32> {
203        self.accessed
204    }
205
206    /// returns the creation timestamp, if defined, as UNIX epoch seconds
207    #[must_use]
208    pub fn cr_time(&self) -> Option<u32> {
209        self.created
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::ExtendedTimestamp;
216
217    use std::io::Cursor;
218
219    /// Ensure we don't panic or read garbage data if the field body is empty
220    #[test]
221    pub fn test_bad_extended_timestamp() {
222        use crate::ZipArchive;
223
224        assert!(
225            ZipArchive::new(Cursor::new(include_bytes!(
226                "../../tests/data/extended_timestamp_bad.zip"
227            )))
228            .is_err()
229        );
230    }
231
232    /// Ensure that a truncated extended timestamp (len too short for flags)
233    /// returns an error instead of panicking from a subtraction overflow.
234    #[test]
235    fn test_extended_timestamp_overflow() {
236        let tests_args = [
237            (vec![0b0000_0001_u8, 0x00, 0x00, 0x00], 0), // len is 0
238            (vec![0b0000_0001_u8, 0x00, 0x00, 0x00], 2), // len is 2 (not enough)
239            (vec![0b0000_0010_u8, 0x00, 0x00, 0x00], 3), // len is 3 (not enough)
240            (vec![0b0000_0100_u8, 0x00, 0x00, 0x00], 4), // len is 4 (not enough)
241            // len is 8 (not enough)
242            (
243                vec![
244                    0b0000_0011_u8,
245                    0x00,
246                    0x00,
247                    0x00,
248                    0x00,
249                    0x00,
250                    0x00,
251                    0x00,
252                    0x00,
253                ],
254                8,
255            ),
256            // len is too long and missing 4 bytes
257            (vec![0b0000_0011_u8, 0x00, 0x00, 0x00, 0x00], 8),
258            (vec![0b0000_0000_u8], 5), // too long len
259        ];
260        for (data, len) in tests_args {
261            let mut cursor = Cursor::new(data);
262            let result = ExtendedTimestamp::try_from_reader(&mut cursor, len, true);
263            assert!(result.is_err());
264        }
265    }
266
267    #[test]
268    fn check_extended_timestamp_value() {
269        let mut cursor = Cursor::new(&[0b0000_0001_u8, 0x00, 0x00, 0x00, 0x01]);
270        let result = ExtendedTimestamp::try_from_reader(&mut cursor, 5, true).unwrap();
271        assert_eq!(result.mod_time(), Some(1 << 24));
272        assert_eq!(result.ac_time(), None);
273        assert_eq!(result.cr_time(), None);
274
275        let mut cursor = Cursor::new(&[0b0000_0010_u8, 0x00, 0x00, 0x00, 0x02]);
276        let result = ExtendedTimestamp::try_from_reader(&mut cursor, 5, true).unwrap();
277        assert_eq!(result.mod_time(), None);
278        assert_eq!(result.ac_time(), Some(2 << 24));
279        assert_eq!(result.cr_time(), None);
280
281        let mut cursor = Cursor::new(&[0b0000_0100_u8, 0x00, 0x00, 0x00, 0x03]);
282        let result = ExtendedTimestamp::try_from_reader(&mut cursor, 5, true).unwrap();
283        assert_eq!(result.mod_time(), None);
284        assert_eq!(result.ac_time(), None);
285        assert_eq!(result.cr_time(), Some(3 << 24));
286
287        let mut cursor = Cursor::new(&[
288            0b0000_0011_u8,
289            0x00,
290            0x00,
291            0x00,
292            0x01,
293            0x00,
294            0x00,
295            0x00,
296            0x02,
297        ]);
298        let result = ExtendedTimestamp::try_from_reader(&mut cursor, 9, true).unwrap();
299        assert_eq!(result.mod_time(), Some(1 << 24));
300        assert_eq!(result.ac_time(), Some(2 << 24));
301        assert_eq!(result.cr_time(), None);
302
303        let mut cursor = Cursor::new(&[
304            0b0000_0111_u8,
305            0x00,
306            0x00,
307            0x00,
308            0x01,
309            0x00,
310            0x00,
311            0x00,
312            0x02,
313            0x00,
314            0x00,
315            0x00,
316            0x03,
317        ]);
318        let result = ExtendedTimestamp::try_from_reader(&mut cursor, 13, true).unwrap();
319        assert_eq!(result.mod_time(), Some(1 << 24));
320        assert_eq!(result.ac_time(), Some(2 << 24));
321        assert_eq!(result.cr_time(), Some(3 << 24));
322    }
323
324    #[test]
325    fn test_extended_timestamp() {
326        // in the central header
327        let mut cursor = Cursor::new(&[0b0000_0111_u8, 0x00, 0x00, 0x00, 0x01]);
328        let result = ExtendedTimestamp::try_from_reader(&mut cursor, 5, true).unwrap();
329        assert_eq!(result.mod_time(), Some(1 << 24));
330        assert_eq!(result.ac_time(), None);
331        assert_eq!(result.cr_time(), None);
332
333        // in the local header
334        let mut cursor = Cursor::new(&[
335            0b0000_0111_u8,
336            0x00,
337            0x00,
338            0x00,
339            0x01,
340            0x00,
341            0x00,
342            0x00,
343            0x02,
344            0x00,
345            0x00,
346            0x00,
347            0x03,
348        ]);
349        let result = ExtendedTimestamp::try_from_reader(&mut cursor, 13, true).unwrap();
350        assert_eq!(result.mod_time(), Some(1 << 24));
351        assert_eq!(result.ac_time(), Some(2 << 24));
352        assert_eq!(result.cr_time(), Some(3 << 24));
353    }
354
355    #[test]
356    fn test_extended_timestamp_in_central_header() {
357        let mut cursor = Cursor::new(&[0b0000_0111_u8]);
358        let result = ExtendedTimestamp::try_from_reader(&mut cursor, 1, false).unwrap();
359        assert_eq!(result.mod_time(), None);
360        assert_eq!(result.ac_time(), None);
361        assert_eq!(result.cr_time(), None);
362
363        let mut cursor = Cursor::new(&[0b0000_0100_u8, 0x00, 0xF1, 0x53, 0x65]);
364        let result = ExtendedTimestamp::try_from_reader(&mut cursor, 5, false).unwrap();
365        assert_eq!(result.mod_time(), Some(1700000000));
366        assert_eq!(result.ac_time(), None);
367        assert_eq!(result.cr_time(), None);
368
369        // a writer that copies the local field into the central directory
370        let data = [
371            0b0000_0011_u8,
372            0x00,
373            0xF1,
374            0x53,
375            0x65,
376            0x01,
377            0xF1,
378            0x53,
379            0x65,
380        ];
381        let mut cursor = Cursor::new(&data);
382        let result = ExtendedTimestamp::try_from_reader(&mut cursor, 9, false).unwrap();
383        assert_eq!(result.mod_time(), Some(1700000000));
384        assert_eq!(cursor.position(), 9);
385    }
386}