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 | |
| 45 | pub mod read; |
| 46 | pub mod write; |
| 47 | |
| 48 | use oxedyne_fe2o3_core::prelude::*; |
| 49 | |
| 50 | use 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. |
| 55 | pub 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)] |
| 62 | pub 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 | |
| 68 | impl 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`]. |
| 92 | pub const EPOCH_DATE: u16 = 0x0021; |
| 93 | |
| 94 | /// Where a member's bytes come from when the archive is written. |
| 95 | #[derive(Clone, Debug)] |
| 96 | pub 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)] |
| 113 | pub 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 | |
| 123 | impl 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)] |
| 166 | pub 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 | |
| 174 | impl 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. |
| 331 | fn 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 | |
| 346 | pub(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 | |
| 355 | pub(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 | |
| 364 | pub(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 | } |