rc_zip/parse/
eocd.rs

1use std::borrow::Cow;
2
3use ownable::{traits as ownable_traits, IntoOwned, ToOwned};
4use tracing::trace;
5use winnow::{
6    binary::{le_u16, le_u32, le_u64, length_take},
7    seq,
8    token::literal,
9    PResult, Parser, Partial,
10};
11
12use crate::error::{Error, FormatError};
13
14/// 4.3.16  End of central directory record:
15#[derive(Debug, ToOwned, IntoOwned, Clone)]
16pub struct EndOfCentralDirectoryRecord<'a> {
17    /// number of this disk
18    pub disk_nbr: u16,
19
20    /// number of the disk with the start of the central directory
21    pub dir_disk_nbr: u16,
22
23    /// total number of entries in the central directory on this disk
24    pub dir_records_this_disk: u16,
25
26    /// total number of entries in the central directory
27    pub directory_records: u16,
28
29    /// size of the central directory
30    pub directory_size: u32,
31
32    /// offset of start of central directory with respect to the starting disk number
33    pub directory_offset: u32,
34
35    /// .ZIP file comment
36    pub comment: Cow<'a, [u8]>,
37}
38
39impl<'a> EndOfCentralDirectoryRecord<'a> {
40    /// Does not include comment size & comment data
41    const MIN_LENGTH: usize = 20;
42    const SIGNATURE: &'static str = "PK\x05\x06";
43
44    /// Find the end of central directory record in a block of data
45    pub fn find_in_block(b: &'a [u8]) -> Option<Located<Self>> {
46        for i in (0..(b.len().saturating_sub(Self::MIN_LENGTH + 1))).rev() {
47            let mut input = Partial::new(&b[i..]);
48            if let Ok(directory) = Self::parser.parse_next(&mut input) {
49                return Some(Located {
50                    offset: i as u64,
51                    inner: directory,
52                });
53            }
54        }
55        None
56    }
57
58    /// Parser for the end of central directory record
59    pub fn parser(i: &mut Partial<&'a [u8]>) -> PResult<Self> {
60        let _ = literal(Self::SIGNATURE).parse_next(i)?;
61        seq! {Self {
62            disk_nbr: le_u16,
63            dir_disk_nbr: le_u16,
64            dir_records_this_disk: le_u16,
65            directory_records: le_u16,
66            directory_size: le_u32,
67            directory_offset: le_u32,
68            comment: length_take(le_u16).map(Cow::Borrowed),
69        }}
70        .parse_next(i)
71    }
72}
73
74/// 4.3.15 Zip64 end of central directory locator
75#[derive(Debug)]
76pub struct EndOfCentralDirectory64Locator {
77    /// number of the disk with the start of the zip64 end of central directory
78    pub dir_disk_number: u32,
79    /// relative offset of the zip64 end of central directory record
80    pub directory_offset: u64,
81    /// total number of disks
82    pub total_disks: u32,
83}
84
85impl EndOfCentralDirectory64Locator {
86    /// Length of the locator
87    pub const LENGTH: usize = 20;
88    const SIGNATURE: &'static str = "PK\x06\x07";
89
90    /// Parser for the zip64 end of central directory locator
91    pub fn parser(i: &mut Partial<&'_ [u8]>) -> PResult<Self> {
92        _ = literal(Self::SIGNATURE).parse_next(i)?;
93        seq! {Self {
94            dir_disk_number: le_u32,
95            directory_offset: le_u64,
96            total_disks: le_u32,
97        }}
98        .parse_next(i)
99    }
100}
101
102/// 4.3.14  Zip64 end of central directory record
103#[derive(Debug, Clone, ToOwned, IntoOwned)]
104pub struct EndOfCentralDirectory64Record {
105    /// size of zip64 end of central directory record
106    pub record_size: u64,
107
108    /// version made by
109    pub creator_version: u16,
110
111    /// version needed to extract
112    pub reader_version: u16,
113
114    /// number of this disk
115    pub disk_nbr: u32,
116
117    /// number of the disk with the start of the central directory
118    pub dir_disk_nbr: u32,
119
120    /// total number of entries in the central directory on this disk
121    pub dir_records_this_disk: u64,
122
123    /// total number of entries in the central directory
124    pub directory_records: u64,
125
126    /// size of the central directory
127    pub directory_size: u64,
128
129    /// offset of the start of central directory with respect to the
130    /// starting disk number
131    pub directory_offset: u64,
132}
133
134impl EndOfCentralDirectory64Record {
135    const SIGNATURE: &'static str = "PK\x06\x06";
136
137    /// Parser for the zip64 end of central directory record
138    pub fn parser(i: &mut Partial<&'_ [u8]>) -> PResult<Self> {
139        _ = literal(Self::SIGNATURE).parse_next(i)?;
140        seq! {Self {
141            record_size: le_u64,
142            creator_version: le_u16,
143            reader_version: le_u16,
144            disk_nbr: le_u32,
145            dir_disk_nbr: le_u32,
146            dir_records_this_disk: le_u64,
147            directory_records: le_u64,
148            directory_size: le_u64,
149            directory_offset: le_u64,
150        }}
151        .parse_next(i)
152    }
153}
154
155/// A zip structure and its location in the input file
156#[derive(Debug, Clone)]
157pub struct Located<T> {
158    /// Absolute by offset from the start of the file
159    pub offset: u64,
160
161    /// The structure itself
162    pub inner: T,
163}
164
165impl<T> ownable_traits::ToOwned for Located<T>
166where
167    T: ownable_traits::ToOwned,
168{
169    type Owned = Located<T::Owned>;
170
171    fn to_owned(&self) -> Self::Owned {
172        Located {
173            offset: self.offset,
174            inner: self.inner.to_owned(),
175        }
176    }
177}
178
179impl<T> ownable_traits::IntoOwned for Located<T>
180where
181    T: ownable_traits::IntoOwned,
182{
183    type Owned = Located<T::Owned>;
184
185    fn into_owned(self) -> Self::Owned {
186        Located {
187            offset: self.offset,
188            inner: self.inner.into_owned(),
189        }
190    }
191}
192
193/// Coalesces zip and zip64 "end of central directory" record info
194#[derive(ToOwned, IntoOwned)]
195pub struct EndOfCentralDirectory<'a> {
196    /// The end of central directory record
197    pub dir: Located<EndOfCentralDirectoryRecord<'a>>,
198
199    /// The zip64 end of central directory record
200    pub dir64: Option<Located<EndOfCentralDirectory64Record>>,
201
202    /// Zip files may be prepended by arbitrary data, this is how much
203    /// data is at the beginning of the file that isn't part of the zip
204    pub global_offset: i64,
205}
206
207impl<'a> EndOfCentralDirectory<'a> {
208    pub(crate) fn new(
209        size: u64,
210        dir: Located<EndOfCentralDirectoryRecord<'a>>,
211        dir64: Option<Located<EndOfCentralDirectory64Record>>,
212    ) -> Result<Self, Error> {
213        let mut res = Self {
214            dir,
215            dir64,
216            global_offset: 0,
217        };
218
219        //
220        // Pure .zip files look like this:
221        // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
222        //                     <------directory_size----->
223        // [ Data 1 ][ Data 2 ][    Central directory    ][ ??? ]
224        // ^                   ^                          ^
225        // 0                   directory_offset           directory_end_offset
226        // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
227        //
228        // But there exist some valid zip archives with padding at the beginning, like so:
229        // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
230        // <--global_offset->                    <------directory_size----->
231        // [    Padding     ][ Data 1 ][ Data 2 ][    Central directory    ][ ??? ]
232        // ^                 ^                   ^                         ^
233        // 0                 global_offset       computed_directory_offset directory_end_offset
234        // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
235        //
236        // (e.g. https://www.icculus.org/mojosetup/ installers are ELF binaries with a .zip file appended)
237        //
238        // `directory_end_offset` is found by scanning the file (so it accounts for padding), but
239        // `directory_offset` is found by reading a data structure (so it does not account for padding).
240        // If we just trusted `directory_offset`, we'd be reading the central directory at the wrong place:
241        // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
242        //                                       <------directory_size----->
243        // [    Padding     ][ Data 1 ][ Data 2 ][    Central directory    ][ ??? ]
244        // ^                   ^                                           ^
245        // 0                   directory_offset - woops!                   directory_end_offset
246        // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
247
248        let computed_directory_offset = res
249            .located_directory_offset()
250            .checked_sub(res.directory_size())
251            .ok_or(FormatError::DirectoryOffsetPointsOutsideFile)?;
252
253        // did we find a valid offset?
254        if (0..size).contains(&computed_directory_offset) {
255            // that's different from the recorded one?
256            if computed_directory_offset != res.directory_offset() {
257                // then assume the whole file is offset
258                res.global_offset =
259                    computed_directory_offset as i64 - res.directory_offset() as i64;
260                res.set_directory_offset(computed_directory_offset);
261            }
262        }
263
264        // make sure directory_offset points to somewhere in our file
265        trace!(
266            "directory offset = {}, valid range = 0..{}",
267            res.directory_offset(),
268            size
269        );
270        if !(0..size).contains(&res.directory_offset()) {
271            return Err(FormatError::DirectoryOffsetPointsOutsideFile.into());
272        }
273
274        Ok(res)
275    }
276
277    #[inline]
278    pub(crate) fn located_directory_offset(&self) -> u64 {
279        match self.dir64.as_ref() {
280            Some(d64) => d64.offset,
281            None => self.dir.offset,
282        }
283    }
284
285    #[inline]
286    pub(crate) fn directory_offset(&self) -> u64 {
287        match self.dir64.as_ref() {
288            Some(d64) => d64.inner.directory_offset,
289            None => self.dir.inner.directory_offset as u64,
290        }
291    }
292
293    #[inline]
294    pub(crate) fn directory_size(&self) -> u64 {
295        match self.dir64.as_ref() {
296            Some(d64) => d64.inner.directory_size,
297            None => self.dir.inner.directory_size as u64,
298        }
299    }
300
301    #[inline]
302    pub(crate) fn set_directory_offset(&mut self, offset: u64) {
303        match self.dir64.as_mut() {
304            Some(d64) => d64.inner.directory_offset = offset,
305            None => self.dir.inner.directory_offset = offset as u32,
306        };
307    }
308
309    #[inline]
310    pub(crate) fn directory_records(&self) -> u64 {
311        match self.dir64.as_ref() {
312            Some(d64) => d64.inner.directory_records,
313            None => self.dir.inner.directory_records as u64,
314        }
315    }
316
317    #[inline]
318    pub(crate) fn comment(&self) -> &[u8] {
319        &self.dir.inner.comment
320    }
321}