rc_zip/parse/
extra_field.rs

1use std::borrow::Cow;
2
3use ownable::{IntoOwned, ToOwned};
4use winnow::{
5    binary::{le_u16, le_u32, le_u64, le_u8, length_take},
6    combinator::{opt, preceded, repeat_till},
7    error::{ErrMode, ErrorKind, ParserError, StrContext},
8    seq,
9    token::{literal, take},
10    PResult, Parser, Partial,
11};
12
13use crate::parse::NtfsTimestamp;
14
15/// 4.4.28 extra field: (Variable)
16pub(crate) struct ExtraFieldRecord<'a> {
17    pub(crate) tag: u16,
18    pub(crate) payload: &'a [u8],
19}
20
21impl<'a> ExtraFieldRecord<'a> {
22    pub(crate) fn parser(i: &mut Partial<&'a [u8]>) -> PResult<Self> {
23        seq! {Self {
24            tag: le_u16,
25            payload: length_take(le_u16),
26        }}
27        .parse_next(i)
28    }
29}
30
31/// Useful because zip64 extended information extra field has fixed order *but*
32/// optional fields. From the appnote:
33///
34/// If one of the size or offset fields in the Local or Central directory record
35/// is too small to hold the required data, a Zip64 extended information record
36/// is created. The order of the fields in the zip64 extended information record
37/// is fixed, but the fields MUST only appear if the corresponding Local or
38/// Central directory record field is set to 0xFFFF or 0xFFFFFFFF.
39#[derive(Debug, Clone, Copy)]
40pub struct ExtraFieldSettings {
41    /// The uncompressed size field read from a local or central directory record
42    /// If this is 0xFFFF_FFFF, then the zip64 extra field uncompressed size
43    /// field will be present.
44    pub uncompressed_size_u32: u32,
45
46    /// The compressed size field read from a local or central directory record
47    /// If this is 0xFFFF_FFFF, then the zip64 extra field compressed size
48    /// field will be present.
49    pub compressed_size_u32: u32,
50
51    /// The header offset field read from a central directory record (or zero
52    /// for local directory records). If this is 0xFFFF_FFFF, then the zip64
53    /// extra field header offset field will be present.
54    pub header_offset_u32: u32,
55}
56
57/// Information stored in the central directory header `extra` field
58///
59/// This typically contains timestamps, file sizes and offsets, file mode, uid/gid, etc.
60///
61/// See `extrafld.txt` in this crate's source distribution.
62#[derive(Clone)]
63pub enum ExtraField<'a> {
64    /// Zip64 extended information extra field
65    Zip64(ExtraZip64Field),
66    /// Extended timestamp
67    Timestamp(ExtraTimestampField),
68    /// UNIX & Info-Zip UNIX
69    Unix(ExtraUnixField<'a>),
70    /// New UNIX extra field
71    NewUnix(ExtraNewUnixField),
72    /// NTFS (Win9x/WinNT FileTimes)
73    Ntfs(ExtraNtfsField),
74    /// Unknown extra field, with tag
75    Unknown {
76        /// tag of the extra field
77        tag: u16,
78    },
79}
80
81impl<'a> ExtraField<'a> {
82    /// Make a parser for extra fields, given the settings for the zip64 extra
83    /// field (which depend on whether the u32 values are 0xFFFF_FFFF or not)
84    pub fn mk_parser(
85        settings: ExtraFieldSettings,
86    ) -> impl FnMut(&mut Partial<&'a [u8]>) -> PResult<Self> {
87        move |i| {
88            use ExtraField as EF;
89            let rec = ExtraFieldRecord::parser.parse_next(i)?;
90            let payload = &mut Partial::new(rec.payload);
91
92            let variant = match rec.tag {
93                ExtraZip64Field::TAG => opt(ExtraZip64Field::mk_parser(settings).map(EF::Zip64))
94                    .context(StrContext::Label("zip64"))
95                    .parse_next(payload)?,
96                ExtraTimestampField::TAG => opt(ExtraTimestampField::parser.map(EF::Timestamp))
97                    .context(StrContext::Label("timestamp"))
98                    .parse_next(payload)?,
99                ExtraNtfsField::TAG => {
100                    opt(ExtraNtfsField::parser.map(EF::Ntfs)).parse_next(payload)?
101                }
102                ExtraUnixField::TAG | ExtraUnixField::TAG_INFOZIP => {
103                    opt(ExtraUnixField::parser.map(EF::Unix)).parse_next(payload)?
104                }
105                ExtraNewUnixField::TAG => {
106                    opt(ExtraNewUnixField::parser.map(EF::NewUnix)).parse_next(payload)?
107                }
108                _ => None,
109            }
110            .unwrap_or(EF::Unknown { tag: rec.tag });
111
112            Ok(variant)
113        }
114    }
115}
116
117/// 4.5.3 -Zip64 Extended Information Extra Field (0x0001)
118#[derive(Clone, Default)]
119pub struct ExtraZip64Field {
120    /// 64-bit uncompressed size
121    pub uncompressed_size: u64,
122
123    /// 64-bit compressed size
124    pub compressed_size: u64,
125
126    /// 64-bit header offset
127    pub header_offset: u64,
128
129    /// 32-bit disk start number
130    pub disk_start: Option<u32>,
131}
132
133impl ExtraZip64Field {
134    const TAG: u16 = 0x0001;
135
136    pub(crate) fn mk_parser(
137        settings: ExtraFieldSettings,
138    ) -> impl FnMut(&mut Partial<&'_ [u8]>) -> PResult<Self> {
139        move |i| {
140            let uncompressed_size = if settings.uncompressed_size_u32 == 0xFFFF_FFFF {
141                le_u64.parse_next(i)?
142            } else {
143                settings.uncompressed_size_u32 as u64
144            };
145            let compressed_size = if settings.compressed_size_u32 == 0xFFFF_FFFF {
146                le_u64.parse_next(i)?
147            } else {
148                settings.compressed_size_u32 as u64
149            };
150            let header_offset = if settings.header_offset_u32 == 0xFFFF_FFFF {
151                le_u64.parse_next(i)?
152            } else {
153                settings.header_offset_u32 as u64
154            };
155            let disk_start = opt(le_u32.complete_err()).parse_next(i)?;
156
157            Ok(Self {
158                uncompressed_size,
159                compressed_size,
160                header_offset,
161                disk_start,
162            })
163        }
164    }
165}
166
167/// Extended timestamp extra field
168#[derive(Clone)]
169pub struct ExtraTimestampField {
170    /// number of seconds since epoch
171    pub mtime: u32,
172}
173
174impl ExtraTimestampField {
175    const TAG: u16 = 0x5455;
176
177    fn parser(i: &mut Partial<&'_ [u8]>) -> PResult<Self> {
178        preceded(
179            // 1 byte of flags, if bit 0 is set, modification time is present
180            le_u8.verify(|x| x & 0b1 != 0),
181            seq! {Self { mtime: le_u32 }},
182        )
183        .parse_next(i)
184    }
185}
186
187/// 4.5.7 -UNIX Extra Field (0x000d):
188#[derive(Clone, ToOwned, IntoOwned)]
189pub struct ExtraUnixField<'a> {
190    /// file last access time
191    pub atime: u32,
192    /// file last modification time
193    pub mtime: u32,
194    /// file user id
195    pub uid: u16,
196    /// file group id
197    pub gid: u16,
198    /// variable length data field
199    pub data: Cow<'a, [u8]>,
200}
201
202impl<'a> ExtraUnixField<'a> {
203    const TAG: u16 = 0x000d;
204    const TAG_INFOZIP: u16 = 0x5855;
205
206    fn parser(i: &mut Partial<&'a [u8]>) -> PResult<Self> {
207        let t_size = le_u16.parse_next(i)?;
208
209        // t_size includes the size of the atime .. gid fields, totalling 12 bytes.
210        let t_size = t_size
211            .checked_sub(12)
212            .ok_or(ErrMode::from_error_kind(i, ErrorKind::Verify))?;
213
214        seq! {Self {
215            atime: le_u32,
216            mtime: le_u32,
217            uid: le_u16,
218            gid: le_u16,
219            data: take(t_size).map(Cow::Borrowed),
220        }}
221        .parse_next(i)
222    }
223}
224
225/// Info-ZIP New Unix Extra Field:
226/// ====================================
227///
228/// Currently stores Unix UIDs/GIDs up to 32 bits.
229/// (Last Revision 20080509)
230///
231/// ```text
232/// Value         Size        Description
233/// -----         ----        -----------
234/// 0x7875        Short       tag for this extra block type ("ux")
235/// TSize         Short       total data size for this block
236/// Version       1 byte      version of this extra field, currently 1
237/// UIDSize       1 byte      Size of UID field
238/// UID           Variable    UID for this entry
239/// GIDSize       1 byte      Size of GID field
240/// GID           Variable    GID for this entry
241/// ```
242#[derive(Clone)]
243pub struct ExtraNewUnixField {
244    /// file user id
245    pub uid: u64,
246
247    /// file group id
248    pub gid: u64,
249}
250
251impl ExtraNewUnixField {
252    const TAG: u16 = 0x7875;
253
254    fn parser(i: &mut Partial<&'_ [u8]>) -> PResult<Self> {
255        let _ = literal("\x01").parse_next(i)?;
256        seq! {Self {
257            uid: Self::parse_variable_length_integer,
258            gid: Self::parse_variable_length_integer,
259        }}
260        .parse_next(i)
261    }
262
263    fn parse_variable_length_integer(i: &mut Partial<&'_ [u8]>) -> PResult<u64> {
264        let slice = length_take(le_u8).parse_next(i)?;
265        if let Some(u) = match slice.len() {
266            1 => Some(le_u8.parse_peek(slice)?.1 as u64),
267            2 => Some(le_u16.parse_peek(slice)?.1 as u64),
268            4 => Some(le_u32.parse_peek(slice)?.1 as u64),
269            8 => Some(le_u64.parse_peek(slice)?.1),
270            _ => None,
271        } {
272            Ok(u)
273        } else {
274            Err(ErrMode::from_error_kind(i, ErrorKind::Alt))
275        }
276    }
277}
278
279/// 4.5.5 -NTFS Extra Field (0x000a):
280#[derive(Clone)]
281pub struct ExtraNtfsField {
282    /// NTFS attributes
283    pub attrs: Vec<NtfsAttr>,
284}
285
286impl ExtraNtfsField {
287    const TAG: u16 = 0x000a;
288
289    fn parser(i: &mut Partial<&'_ [u8]>) -> PResult<Self> {
290        let _ = take(4_usize).parse_next(i)?; // reserved (unused)
291        seq! {Self {
292            // from the winnow docs:
293            //   Parsers like repeat do not know when an eof is from insufficient
294            //   data or the end of the stream, causing them to always report
295            //   Incomplete.
296            // using repeat_till with eof combinator to work around this:
297            attrs: repeat_till(0.., NtfsAttr::parser, winnow::combinator::eof).map(|x| x.0),
298        }}
299        .parse_next(i)
300    }
301}
302
303/// NTFS attribute for zip entries (mostly timestamps)
304#[derive(Clone)]
305pub enum NtfsAttr {
306    /// NTFS attribute 1, which contains modified/accessed/created timestamps
307    Attr1(NtfsAttr1),
308
309    /// Unknown NTFS attribute
310    Unknown {
311        /// tag of the attribute
312        tag: u16,
313    },
314}
315
316impl NtfsAttr {
317    fn parser(i: &mut Partial<&'_ [u8]>) -> PResult<Self> {
318        let tag = le_u16.parse_next(i)?;
319        let payload = length_take(le_u16).parse_next(i)?;
320
321        match tag {
322            0x0001 => NtfsAttr1::parser
323                .parse_peek(Partial::new(payload))
324                .map(|(_, attr)| NtfsAttr::Attr1(attr)),
325            _ => Ok(NtfsAttr::Unknown { tag }),
326        }
327    }
328}
329
330/// NTFS attribute 1, which contains modified/accessed/created timestamps
331#[derive(Clone)]
332pub struct NtfsAttr1 {
333    /// modified time
334    pub mtime: NtfsTimestamp,
335
336    /// accessed time
337    pub atime: NtfsTimestamp,
338
339    /// created time
340    pub ctime: NtfsTimestamp,
341}
342
343impl NtfsAttr1 {
344    fn parser(i: &mut Partial<&'_ [u8]>) -> PResult<Self> {
345        seq! {Self {
346            mtime: NtfsTimestamp::parser,
347            atime: NtfsTimestamp::parser,
348            ctime: NtfsTimestamp::parser,
349        }}
350        .parse_next(i)
351    }
352}