rc_zip/parse/
central_directory_file_header.rs

1use std::borrow::Cow;
2
3use ownable::{IntoOwned, ToOwned};
4use tracing::trace;
5use winnow::{
6    binary::{le_u16, le_u32},
7    prelude::PResult,
8    token::{literal, take},
9    Parser, Partial,
10};
11
12use crate::{
13    encoding::detect_utf8,
14    encoding::Encoding,
15    error::{Error, FormatError},
16    parse::{
17        zero_datetime, Entry, ExtraField, ExtraFieldSettings, HostSystem, Mode, MsdosMode,
18        MsdosTimestamp, UnixMode, Version,
19    },
20};
21
22use super::Method;
23
24/// 4.3.12 Central directory structure: File header
25#[derive(IntoOwned, ToOwned)]
26pub struct CentralDirectoryFileHeader<'a> {
27    /// version made by
28    pub creator_version: Version,
29
30    /// version needed to extract
31    pub reader_version: Version,
32
33    /// general purpose bit flag
34    pub flags: u16,
35
36    /// compression method
37    pub method: Method,
38
39    /// last mod file datetime
40    pub modified: MsdosTimestamp,
41
42    /// crc32 hash
43    pub crc32: u32,
44
45    /// compressed size
46    pub compressed_size: u32,
47
48    /// uncompressed size
49    pub uncompressed_size: u32,
50
51    /// disk number start
52    pub disk_nbr_start: u16,
53
54    /// internal file attributes
55    pub internal_attrs: u16,
56
57    /// external file attributes
58    pub external_attrs: u32,
59
60    /// relative offset of local header
61    pub header_offset: u32,
62
63    /// name field
64    pub name: Cow<'a, [u8]>,
65
66    /// extra field
67    pub extra: Cow<'a, [u8]>,
68
69    /// comment field
70    pub comment: Cow<'a, [u8]>,
71}
72
73impl<'a> CentralDirectoryFileHeader<'a> {
74    const SIGNATURE: &'static str = "PK\x01\x02";
75
76    /// Parser for the central directory file header
77    pub fn parser(i: &mut Partial<&'a [u8]>) -> PResult<Self> {
78        _ = literal(Self::SIGNATURE).parse_next(i)?;
79        let creator_version = Version::parser.parse_next(i)?;
80        let reader_version = Version::parser.parse_next(i)?;
81        let flags = le_u16.parse_next(i)?;
82        let method = Method::parser.parse_next(i)?;
83        let modified = MsdosTimestamp::parser.parse_next(i)?;
84        let crc32 = le_u32.parse_next(i)?;
85        let compressed_size = le_u32.parse_next(i)?;
86        let uncompressed_size = le_u32.parse_next(i)?;
87        let name_len = le_u16.parse_next(i)?;
88        let extra_len = le_u16.parse_next(i)?;
89        let comment_len = le_u16.parse_next(i)?;
90        let disk_nbr_start = le_u16.parse_next(i)?;
91        let internal_attrs = le_u16.parse_next(i)?;
92        let external_attrs = le_u32.parse_next(i)?;
93        let header_offset = le_u32.parse_next(i)?;
94
95        let name = take(name_len).parse_next(i)?;
96        let extra = take(extra_len).parse_next(i)?;
97        let comment = take(comment_len).parse_next(i)?;
98
99        Ok(Self {
100            creator_version,
101            reader_version,
102            flags,
103            method,
104            modified,
105            crc32,
106            compressed_size,
107            uncompressed_size,
108            disk_nbr_start,
109            internal_attrs,
110            external_attrs,
111            header_offset,
112            name: Cow::Borrowed(name),
113            extra: Cow::Borrowed(extra),
114            comment: Cow::Borrowed(comment),
115        })
116    }
117}
118
119impl CentralDirectoryFileHeader<'_> {
120    /// Returns true if the name or comment is not valid UTF-8
121    pub fn is_non_utf8(&self) -> bool {
122        let (valid1, require1) = detect_utf8(&self.name[..]);
123        let (valid2, require2) = detect_utf8(&self.comment[..]);
124        if !valid1 || !valid2 {
125            // definitely not utf-8
126            return true;
127        }
128
129        if !require1 && !require2 {
130            // name and comment only use single-byte runes that overlap with UTF-8
131            return false;
132        }
133
134        // Might be UTF-8, might be some other encoding; preserve existing flag.
135        // Some ZIP writers use UTF-8 encoding without setting the UTF-8 flag.
136        // Since it is impossible to always distinguish valid UTF-8 from some
137        // other encoding (e.g., GBK or Shift-JIS), we trust the flag.
138        self.flags & 0x800 == 0
139    }
140
141    /// Converts the directory header into a entry: this involves
142    /// parsing the extra fields and converting the timestamps.
143    pub fn as_entry(&self, encoding: Encoding, global_offset: u64) -> Result<Entry, Error> {
144        let mut entry = Entry {
145            name: encoding.decode(&self.name[..])?,
146            method: self.method,
147            comment: encoding.decode(&self.comment[..])?,
148            modified: self.modified.to_datetime().unwrap_or_else(zero_datetime),
149            created: None,
150            accessed: None,
151            header_offset: (self.header_offset as u64)
152                .checked_add(global_offset)
153                .ok_or(FormatError::InvalidHeaderOffset)?,
154            reader_version: self.reader_version,
155            flags: self.flags,
156            uid: None,
157            gid: None,
158            crc32: self.crc32,
159            compressed_size: self.compressed_size as _,
160            uncompressed_size: self.uncompressed_size as _,
161            mode: Mode(0),
162        };
163
164        entry.mode = match self.creator_version.host_system {
165            HostSystem::Unix | HostSystem::Osx => UnixMode(self.external_attrs >> 16).into(),
166            HostSystem::WindowsNtfs | HostSystem::Vfat | HostSystem::MsDos => {
167                MsdosMode(self.external_attrs).into()
168            }
169            _ => Mode(0),
170        };
171        if entry.name.ends_with('/') {
172            // believe it or not, this is straight from the APPNOTE
173            entry.mode |= Mode::DIR
174        };
175
176        let settings = ExtraFieldSettings {
177            uncompressed_size_u32: self.uncompressed_size,
178            compressed_size_u32: self.compressed_size,
179            header_offset_u32: self.header_offset,
180        };
181
182        let mut slice = Partial::new(&self.extra[..]);
183        while !slice.is_empty() {
184            match ExtraField::mk_parser(settings).parse_next(&mut slice) {
185                Ok(ef) => {
186                    entry.set_extra_field(&ef);
187                }
188                Err(e) => {
189                    trace!("extra field error: {:#?}", e);
190                    return Err(FormatError::InvalidExtraField.into());
191                }
192            }
193        }
194
195        Ok(entry)
196    }
197}