Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_file/src/zip/mod.rs

12.8 KiB, 95 runs

created by r1870400018:22540, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! An archive is a filesystem in a file, which is why this sits here rather than anywhere else.
2//!
3//! A ZIP archive held wholly in memory over `&[u8]`, with one property the ordinary archive library
4//! does not offer and which everything above this depends on: **a member nobody touched is written
5//! back byte for byte**. Not re-compressed to the same content -- copied. Its stored DEFLATE stream,
6//! its local header, its extra fields, its data descriptor and its central directory entry are all
7//! the bytes that were read.
8//!
9//! # Why that matters more than it sounds
10//!
11//! The thing above this reads and edits Office documents, which are ZIPs of XML. A `.docx` a
12//! colleague sent carries parts nothing here understands -- a theme, custom XML, content controls,
13//! tracked changes, a signature. Anything that parsed the archive into a model and wrote the model
14//! back would lose every one of them, silently, and the person who found out would be the colleague.
15//! So the archive is held whole and only what is touched is rebuilt.
16//!
17//! The property is checkable, and callers should check it: read an archive, write it straight back
18//! out, and the bytes are identical. [`Zip::is_pristine`] says whether anything was touched at all.
19//!
20//! # Target-neutral
21//!
22//! Nothing here reaches the filesystem. It takes bytes and returns bytes, so the same code runs in a
23//! browser, where a `.docx` arrives from a file picker and never has a path.
24//!
25//! # What it does not do
26//!
27//! ZIP64 is read but not written: an archive that needed the ZIP64 records to be read is refused on
28//! write, by name, rather than written back wrong. Encryption is refused. Multi-disk archives are
29//! refused. DEFLATE itself is `flate2`'s, and is not hand-rolled here.
30//!
31//! # Usage
32//!
33//! ```ignore
34//! use oxedyne_fe2o3_file::zip::{Method, Zip};
35//!
36//! let mut zip = res!(Zip::read(bytes));
37//! let xml = res!(zip.text("word/document.xml"));
38//! zip.set("word/document.xml", edited.into_bytes(), Method::Deflate);
39//! let out = res!(zip.write()); // Every other member is the bytes it was.
40//! ```
41//!
42//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
43//! Anthropic Claude
44
45pub mod read;
46pub mod write;
47
48use oxedyne_fe2o3_core::prelude::*;
49
50use std::ops::Range;
51
52// A `.xlsx` part inflates to many times its compressed size as a matter of course, so the ceiling
53// has to be generous; a hostile archive inflates to whatever it likes, so there has to be one. A
54// caller with its own idea of the ceiling uses Zip::content_capped and says what it is.
55pub const MAX_INFLATE: u64 = 256 * 1024 * 1024;
56
57/// How a member's bytes are held in the archive.
58///
59/// The numeric code is kept for a method this does not decode, so an archive holding one still reads
60/// its directory, still names the member, and still copies it through untouched.
61#[derive(Clone, Copy, Debug, PartialEq)]
62pub enum Method {
63 Store, // the bytes in the archive are the content
64 Deflate, // what all but the smallest members of an Office document use
65 Other(u16), // a method this does not decode, by its code
66}
67
68impl Method {
69
70 pub fn code(&self) -> u16 {
71 match self {
72 Self::Store => 0,
73 Self::Deflate => 8,
74 Self::Other(c) => *c,
75 }
76 }
77
78 pub fn of(code: u16) -> Self {
79 match code {
80 0 => Self::Store,
81 8 => Self::Deflate,
82 c => Self::Other(c),
83 }
84 }
85}
86
87/// The DOS date of 1 January 1980, the earliest a ZIP can express.
88///
89/// A fresh member is stamped with this rather than with the clock, so writing the same archive twice
90/// gives the same bytes. A build that has to be reproducible cannot have the time of day in it, and a
91/// caller who wants a real timestamp sets one with [`Member::stamp`].
92pub const EPOCH_DATE: u16 = 0x0021;
93
94/// Where a member's bytes come from when the archive is written.
95#[derive(Clone, Debug)]
96pub enum Body {
97 // Read from an archive and not since changed, as byte ranges into the source. Writing it copies
98 // `whole` verbatim, which is what makes an untouched member survive a round trip exactly.
99 Held {
100 whole: Range<usize>, // local header, data, any data descriptor
101 data: Range<usize>, // the compressed bytes alone
102 cen: Range<usize>, // its entry in the central directory
103 },
104 // Given to this archive, and so written afresh from its uncompressed bytes.
105 Fresh {
106 data: Vec<u8>, // uncompressed
107 stamp: (u16, u16), // DOS time and date to write
108 },
109}
110
111/// One member of an archive.
112#[derive(Clone, Debug)]
113pub struct Member {
114 pub name: String, // path within the archive, with forward slashes
115 pub method: Method,
116 pub crc: u32, // of the uncompressed content
117 pub size: u64, // uncompressed
118 pub csize: u64, // as the archive holds them
119 pub flags: u16, // general purpose bit flag, as recorded
120 pub body: Body,
121}
122
123impl Member {
124
125 /// Is bit 0 of the general purpose flag set?
126 pub fn is_encrypted(&self) -> bool {
127 self.flags & 1 != 0
128 }
129
130 /// A directory entry is a trailing slash and no content.
131 pub fn is_dir(&self) -> bool {
132 self.name.ends_with('/') && self.size == 0
133 }
134
135 /// Stamps a fresh member with a DOS time and date. A held member's stamp is in bytes that are
136 /// copied, so setting one would be a lie about what will be written.
137 pub fn stamp(&mut self, time: u16, date: u16) {
138 if let Body::Fresh { stamp, .. } = &mut self.body {
139 *stamp = (time, date);
140 }
141 }
142
143 /// Are the member's bytes the ones it was read with?
144 pub fn is_held(&self) -> bool {
145 matches!(self.body, Body::Held { .. })
146 }
147
148 /// The member's bytes exactly as the archive holds them, compressed and not decoded. For a member
149 /// the caller supplied, its content, which has not been compressed yet.
150 ///
151 /// What a check that an untouched member was *copied* rather than rebuilt compares. Two members
152 /// can hold the same content and different bytes -- another compression level gives another
153 /// stream -- and it is the bytes a colleague's reader parses.
154 pub fn raw<'a>(&'a self, zip: &'a Zip) -> Outcome<&'a [u8]> {
155 match &self.body {
156 Body::Held { data, .. } => Ok(res!(zip.src.get(data.clone()).ok_or_else(|| err!(
157 "'{}' addresses bytes {}..{} of an archive of {} bytes.",
158 self.name, data.start, data.end, zip.src.len(); Bug, Range)))),
159 Body::Fresh { data, .. } => Ok(data),
160 }
161 }
162}
163
164/// A ZIP archive held in memory, with the bytes it was read from.
165#[derive(Clone, Debug, Default)]
166pub struct Zip {
167 pub(crate) src: Vec<u8>, // what every held member addresses into
168 pub(crate) members: Vec<Member>, // in the order they occupy the archive
169 pub(crate) comment: Vec<u8>, // carried by the end record
170 pub(crate) zip64: bool, // reading needed the ZIP64 records
171 pub(crate) touched: bool, // a member added, replaced or removed
172}
173
174impl Zip {
175
176 pub fn new() -> Self {
177 Self::default()
178 }
179
180 pub fn len(&self) -> usize {
181 self.members.len()
182 }
183
184 pub fn is_empty(&self) -> bool {
185 self.members.is_empty()
186 }
187
188 /// Is every member still the bytes it was read with, so that writing reproduces the source?
189 pub fn is_pristine(&self) -> bool {
190 !self.touched
191 }
192
193 /// In the order they occupy the archive.
194 pub fn members(&self) -> &[Member] {
195 &self.members
196 }
197
198 /// Empty where the archive was built rather than read.
199 pub fn source(&self) -> &[u8] {
200 &self.src
201 }
202
203 /// In archive order.
204 pub fn names(&self) -> Vec<&str> {
205 self.members.iter().map(|m| m.name.as_str()).collect()
206 }
207
208 pub fn index_of(&self, name: &str) -> Option<usize> {
209 self.members.iter().position(|m| m.name == name)
210 }
211
212 pub fn has(&self, name: &str) -> bool {
213 self.index_of(name).is_some()
214 }
215
216 pub fn member(&self, name: &str) -> Option<&Member> {
217 self.index_of(name).and_then(|i| self.members.get(i))
218 }
219
220 /// Refuses a member inflating past the 256 MiB [`MAX_INFLATE`] ceiling.
221 pub fn content(&self, name: &str) -> Outcome<Vec<u8>> {
222 self.content_capped(name, MAX_INFLATE)
223 }
224
225 /// The declared size is checked against the ceiling before a byte is inflated, so a member that
226 /// claims to be enormous costs nothing to refuse, and the inflated length is checked again after,
227 /// so a member that lied about its size is refused too.
228 pub fn content_capped(&self, name: &str, cap: u64) -> Outcome<Vec<u8>> {
229 let i = res!(self.index_of(name).ok_or_else(|| err!(
230 "The archive holds no member named '{}'.", name; Missing)));
231 self.content_at(i, cap)
232 }
233
234 pub fn content_at(&self, i: usize, cap: u64) -> Outcome<Vec<u8>> {
235 let m = res!(self.members.get(i).ok_or_else(|| err!(
236 "The archive has {} members, so there is none at index {}.", self.members.len(), i;
237 Missing, Range)));
238 if m.is_encrypted() {
239 return Err(err!(
240 "'{}' is encrypted. This reads no encrypted archive, so its content is not \
241 available; the member is still copied through untouched.", m.name;
242 Invalid, Input, Unimplemented));
243 }
244 if m.size > cap {
245 return Err(err!(
246 "'{}' says it holds {} bytes, over the {} byte ceiling for reading one.",
247 m.name, m.size, cap; Excessive, Size));
248 }
249 let raw = match &m.body {
250 Body::Held { data, .. } => res!(self.src.get(data.clone()).ok_or_else(|| err!(
251 "'{}' addresses bytes {}..{} of an archive of {} bytes.",
252 m.name, data.start, data.end, self.src.len(); Bug, Range))),
253 Body::Fresh { data, .. } => return Ok(data.clone()),
254 };
255 let out = match m.method {
256 Method::Store => raw.to_vec(),
257 Method::Deflate => res!(inflate(raw, cap, &m.name)),
258 Method::Other(c) => return Err(err!(
259 "'{}' is held by compression method {}, which this does not decode. The member \
260 is still copied through untouched.", m.name, c; Unimplemented)),
261 };
262 if out.len() as u64 > cap {
263 return Err(err!(
264 "'{}' inflated to more than the {} byte ceiling for reading one.", m.name, cap;
265 Excessive, Size));
266 }
267 let mut crc = flate2::Crc::new();
268 crc.update(&out);
269 if crc.sum() != m.crc {
270 return Err(err!(
271 "'{}' does not match its own checksum: the directory says {:08x} and the bytes \
272 give {:08x}. The archive is damaged.", m.name, m.crc, crc.sum();
273 Invalid, Data, Mismatch));
274 }
275 Ok(out)
276 }
277
278 /// The member must be UTF-8.
279 pub fn text(&self, name: &str) -> Outcome<String> {
280 let bytes = res!(self.content(name));
281 Ok(res!(String::from_utf8(bytes), Decode, String))
282 }
283
284 /// Replacing puts the new member where the old one was, so a caller editing one part of a
285 /// document does not reorder the archive.
286 pub fn set(&mut self, name: &str, data: Vec<u8>, method: Method) {
287 let mut crc = flate2::Crc::new();
288 crc.update(&data);
289 let m = Member {
290 name: name.to_string(),
291 method,
292 crc: crc.sum(),
293 size: data.len() as u64,
294 csize: 0, // Not known until it is written.
295 flags: 0,
296 body: Body::Fresh { data, stamp: (0, EPOCH_DATE) },
297 };
298 self.touched = true;
299 match self.index_of(name) {
300 Some(i) => self.members[i] = m,
301 None => self.members.push(m),
302 }
303 }
304
305 /// OpenDocument needs this and needs it stored rather than compressed: a reader identifies an
306 /// `.odt` by finding `mimetype` first in the archive and uncompressed, and one written anywhere
307 /// else is a file that opens as a plain ZIP.
308 pub fn set_first(&mut self, name: &str, data: Vec<u8>, method: Method) {
309 self.set(name, data, method);
310 if let Some(i) = self.index_of(name) {
311 let m = self.members.remove(i);
312 self.members.insert(0, m);
313 }
314 }
315
316 /// Says whether there was one to remove.
317 pub fn remove(&mut self, name: &str) -> bool {
318 match self.index_of(name) {
319 Some(i) => {
320 self.members.remove(i);
321 self.touched = true;
322 true
323 }
324 None => false,
325 }
326 }
327}
328
329/// The ceiling is enforced as it goes rather than after, because a member that claims a small size
330/// and inflates without end would otherwise take the machine down before anything checked it.
331fn inflate(raw: &[u8], cap: u64, name: &str) -> Outcome<Vec<u8>> {
332 use std::io::Read;
333 let mut out = Vec::new();
334 // One byte over the ceiling is enough to tell a member at the ceiling from one past it.
335 let lim = cap.saturating_add(1);
336 let mut r = flate2::read::DeflateDecoder::new(raw).take(lim);
337 res!(r.read_to_end(&mut out), IO, Decode);
338 if out.len() as u64 > cap {
339 return Err(err!(
340 "'{}' inflates to more than the {} byte ceiling for reading one.", name, cap;
341 Excessive, Size));
342 }
343 Ok(out)
344}
345
346pub(crate) fn u16le(b: &[u8], i: usize) -> Outcome<u16> {
347 match b.get(i..i + 2) {
348 Some(s) => Ok(u16::from_le_bytes([s[0], s[1]])),
349 None => Err(err!(
350 "An archive of {} bytes has no 16-bit field at offset {}.", b.len(), i;
351 Invalid, Input, Range)),
352 }
353}
354
355pub(crate) fn u32le(b: &[u8], i: usize) -> Outcome<u32> {
356 match b.get(i..i + 4) {
357 Some(s) => Ok(u32::from_le_bytes([s[0], s[1], s[2], s[3]])),
358 None => Err(err!(
359 "An archive of {} bytes has no 32-bit field at offset {}.", b.len(), i;
360 Invalid, Input, Range)),
361 }
362}
363
364pub(crate) fn u64le(b: &[u8], i: usize) -> Outcome<u64> {
365 match b.get(i..i + 8) {
366 Some(s) => Ok(u64::from_le_bytes([
367 s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7],
368 ])),
369 None => Err(err!(
370 "An archive of {} bytes has no 64-bit field at offset {}.", b.len(), i;
371 Invalid, Input, Range)),
372 }
373}