Skip to main content

zip/extra_fields/
zip64_extended_information.rs

1//! 4.5.3 -Zip64 Extended Information Extra Field (0x0001)
2//!
3//! | Value                  | Size    | Description                                  |
4//! | ---------------------- | ------- | -------------------------------------------- |
5//! | `0x0001`               | 2 bytes | Tag for this "extra" block type              |
6//! | Size                   | 2 bytes | Size of this "extra" block                   |
7//! | Original Size          | 8 bytes | Original uncompressed file size              |
8//! | Compressed Size        | 8 bytes | Size of compressed data                      |
9//! | Relative Header Offset | 8 bytes | Offset of local header record                |
10//! | Disk Start Number      | 4 bytes | Number of the disk on which this file starts |
11//!
12
13use core::mem;
14use std::io::{ErrorKind, Read, Write, copy, sink};
15
16use crate::unstable::LittleEndianReadExt;
17use crate::{
18    ZIP64_BYTES_THR,
19    extra_fields::UsedExtraField,
20    result::{ZipResult, invalid},
21};
22
23/// Zip64 Sizes
24/// This entry in the Local header MUST include BOTH original
25/// and compressed file size fields.
26#[derive(Copy, Clone, Debug, PartialEq)]
27pub struct Zip64Sizes {
28    pub(crate) uncompressed_size: u64,
29    pub(crate) compressed_size: u64,
30}
31
32/// Zip64 extended information extra field
33#[derive(Copy, Clone, Debug, PartialEq)]
34pub struct Zip64ExtendedInformation {
35    pub(crate) sizes: Option<Zip64Sizes>,
36    pub(crate) header_start: Option<u64>,
37    // TODO: (unsupported for now)
38    // Disk Start Number  4 bytes    Number of the disk on which this file starts
39    // disk_start: Option<u32>
40}
41
42impl Zip64ExtendedInformation {
43    pub(crate) const MAGIC: UsedExtraField = UsedExtraField::Zip64ExtendedInfo;
44
45    /// This entry in the Local header MUST include BOTH original and compressed file size fields
46    /// If the user is using `is_large_file` when the file is not large we force the zip64 extra field
47    pub(crate) fn local_header(
48        is_large_file: bool,
49        uncompressed_size: u64,
50        compressed_size: u64,
51    ) -> Option<Self> {
52        // here - we force if `is_large_file` is `true`
53        let should_add_size = is_large_file
54            || uncompressed_size >= ZIP64_BYTES_THR
55            || compressed_size >= ZIP64_BYTES_THR;
56        if !should_add_size {
57            return None;
58        }
59        Some(Self {
60            sizes: Some(Zip64Sizes {
61                uncompressed_size,
62                compressed_size,
63            }),
64            header_start: None,
65        })
66    }
67
68    pub(crate) fn central_header(
69        is_large_file: bool,
70        uncompressed_size: u64,
71        compressed_size: u64,
72        header_start: u64,
73    ) -> Option<Self> {
74        let mut size: u16 = 0;
75        let sizes = if is_large_file
76            || uncompressed_size >= ZIP64_BYTES_THR
77            || compressed_size >= ZIP64_BYTES_THR
78        {
79            size += mem::size_of::<u64>() as u16 + mem::size_of::<u64>() as u16;
80            Some(Zip64Sizes {
81                uncompressed_size,
82                compressed_size,
83            })
84        } else {
85            None
86        };
87        let header_start = if header_start != 0 && header_start >= ZIP64_BYTES_THR {
88            size += mem::size_of::<u64>() as u16;
89            Some(header_start)
90        } else {
91            None
92        };
93        // TODO: (unsupported for now)
94        // Disk Start Number  4 bytes    Number of the disk on which this file starts
95
96        if size == 0 {
97            // no info added, return early
98            return None;
99        }
100
101        Some(Self {
102            sizes,
103            header_start,
104        })
105    }
106
107    pub(crate) fn full_size(&self, is_local_header: bool) -> usize {
108        mem::size_of::<UsedExtraField>() + mem::size_of::<u16>() + self.size(is_local_header)
109    }
110
111    pub(crate) fn size(&self, is_local_header: bool) -> usize {
112        let mut size = 0;
113        if self.sizes.is_some() {
114            size += mem::size_of::<u64>() + mem::size_of::<u64>();
115        }
116        if !is_local_header && self.header_start.is_some() {
117            size += mem::size_of::<u64>();
118        }
119        size
120    }
121
122    /// Serialize the block
123    pub fn write<T: Write>(&self, writer: &mut T, is_local_header: bool) -> ZipResult<()> {
124        writer.write_all(&Self::MAGIC.to_le_bytes())?;
125        let size = self.size(is_local_header) as u16;
126        writer.write_all(&size.to_le_bytes())?;
127        if let Some(Zip64Sizes {
128            uncompressed_size,
129            compressed_size,
130        }) = self.sizes
131        {
132            writer.write_all(&u64::to_le_bytes(uncompressed_size))?;
133            writer.write_all(&u64::to_le_bytes(compressed_size))?;
134        }
135
136        // the local header does not contains the header start
137        if !is_local_header && let Some(header_start) = self.header_start {
138            writer.write_all(&u64::to_le_bytes(header_start))?;
139        }
140        Ok(())
141    }
142
143    /// Reads the value for one field, and keeps it only if the entry asked for it.
144    ///
145    /// Whether the value is *read* is decided by the block's length, so that the reader stays in
146    /// step with writers that emit more fields than they need to. Whether it is *kept* is decided
147    /// by `is_zip64`, which says whether the matching field in the entry held the sentinel.
148    #[inline]
149    fn read_field<R: Read>(
150        reader: &mut R,
151        len: u16,
152        consumed_len: &mut usize,
153        is_zip64: bool,
154    ) -> ZipResult<Option<u64>> {
155        if len < 24 && !is_zip64 {
156            return Ok(None);
157        }
158        let value = match reader.read_u64_le() {
159            Ok(value) => value,
160            Err(e) if e.kind() == ErrorKind::UnexpectedEof => {
161                return Err(invalid!("ZIP64 extra field truncated"));
162            }
163            Err(e) => return Err(e.into()),
164        };
165        *consumed_len += mem::size_of::<u64>();
166        Ok(is_zip64.then_some(value))
167    }
168
169    /// Reads the block, keeping only the values the entry actually asked for.
170    ///
171    /// Per APPNOTE 4.5.3 a value belongs in this block only when the matching field in the entry
172    /// itself holds the 0xFFFFFFFF sentinel, which means "too large to store here, the real value
173    /// is in the ZIP64 block". Writers exist that emit a full length block anyway, and at least
174    /// one emits a malformed one, so a value that no sentinel asked for is dropped rather than
175    /// read over the entry's own perfectly good field. Such a value can only repeat what the entry
176    /// already said, so dropping it costs nothing when the block is well formed, and it is the
177    /// only way a malformed block can be told apart from a meaningful one.
178    ///
179    /// The `None` fields this leaves behind are the same `None` the writer uses for "this entry
180    /// has nothing to record here", so a block that was ignored on the way in is not written back
181    /// out on the way through.
182    ///
183    /// `entry_header_start` is `None` for a local header, which has no relative offset field for
184    /// the block to override in the first place.
185    #[inline]
186    pub(crate) fn parse<R: Read>(
187        reader: &mut R,
188        len: u16,
189        entry_uncompressed_size: u32,
190        entry_compressed_size: u32,
191        entry_header_start: Option<u32>,
192    ) -> ZipResult<Self> {
193        let mut consumed_len = 0;
194
195        let uncompressed_size = Self::read_field(
196            reader,
197            len,
198            &mut consumed_len,
199            u64::from(entry_uncompressed_size) == ZIP64_BYTES_THR,
200        )?;
201        let compressed_size = Self::read_field(
202            reader,
203            len,
204            &mut consumed_len,
205            u64::from(entry_compressed_size) == ZIP64_BYTES_THR,
206        )?;
207        let header_start = Self::read_field(
208            reader,
209            len,
210            &mut consumed_len,
211            entry_header_start.is_some_and(|start| u64::from(start) == ZIP64_BYTES_THR),
212        )?;
213
214        // The two sizes travel together, so one sentinel brings both along. The field that had no
215        // sentinel keeps the entry's own value, which is what it already held.
216        let sizes = if uncompressed_size.is_some() || compressed_size.is_some() {
217            Some(Zip64Sizes {
218                uncompressed_size: uncompressed_size
219                    .unwrap_or_else(|| entry_uncompressed_size.into()),
220                compressed_size: compressed_size.unwrap_or_else(|| entry_compressed_size.into()),
221            })
222        } else {
223            None
224        };
225
226        let Some(leftover_len) = (len as usize).checked_sub(consumed_len) else {
227            return Err(invalid!("ZIP64 extra-data field is the wrong length"));
228        };
229        let mut limited = reader.take(leftover_len as u64);
230        if let Err(e) = copy(&mut limited, &mut sink()) {
231            if e.kind() == ErrorKind::UnexpectedEof {
232                return Err(invalid!("ZIP64 extra field truncated"));
233            }
234            return Err(e.into());
235        }
236
237        Ok(Self {
238            sizes,
239            header_start,
240        })
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    #[test]
249    fn local_and_central_agree_at_the_threshold() {
250        // The two constructors decide the same question, so they must agree for
251        // every input; ZIP64_BYTES_THR itself is the only value where they did not.
252        for size in [ZIP64_BYTES_THR - 1, ZIP64_BYTES_THR, ZIP64_BYTES_THR + 1] {
253            for (uncompressed, compressed) in [(0, size), (size, 0)] {
254                let local = Zip64ExtendedInformation::local_header(false, uncompressed, compressed);
255                let central =
256                    Zip64ExtendedInformation::central_header(false, uncompressed, compressed, 0);
257                let want = size >= ZIP64_BYTES_THR;
258                assert_eq!(
259                    local.is_some(),
260                    want,
261                    "local_header({uncompressed}, {compressed})"
262                );
263                assert_eq!(
264                    central.is_some_and(|c| c.sizes.is_some()),
265                    want,
266                    "central_header({uncompressed}, {compressed})"
267                );
268            }
269        }
270    }
271
272    #[test]
273    fn central_header_treats_both_sizes_alike() {
274        let by_uncompressed =
275            Zip64ExtendedInformation::central_header(false, ZIP64_BYTES_THR, 0, 0);
276        let by_compressed = Zip64ExtendedInformation::central_header(false, 0, ZIP64_BYTES_THR, 0);
277        assert_eq!(
278            by_uncompressed.is_some(),
279            by_compressed.is_some(),
280            "the two size terms disagree at the threshold",
281        );
282        // A size of exactly ZIP64_BYTES_THR does not fit a u32 field, so both must ask for ZIP64.
283        assert!(by_uncompressed.is_some_and(|c| c.sizes.is_some()));
284    }
285}