1use 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#[derive(Copy, Clone, Debug, PartialEq)]
27pub struct Zip64Sizes {
28 pub(crate) uncompressed_size: u64,
29 pub(crate) compressed_size: u64,
30}
31
32#[derive(Copy, Clone, Debug, PartialEq)]
34pub struct Zip64ExtendedInformation {
35 pub(crate) sizes: Option<Zip64Sizes>,
36 pub(crate) header_start: Option<u64>,
37 }
41
42impl Zip64ExtendedInformation {
43 pub(crate) const MAGIC: UsedExtraField = UsedExtraField::Zip64ExtendedInfo;
44
45 pub(crate) fn local_header(
48 is_large_file: bool,
49 uncompressed_size: u64,
50 compressed_size: u64,
51 ) -> Option<Self> {
52 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 if size == 0 {
97 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 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 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 #[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 #[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 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 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 assert!(by_uncompressed.is_some_and(|c| c.sizes.is_some()));
284 }
285}