oxedyne/fe2o3/fe2o3_file/src/zip/write.rs
7.3 KiB, 7 runs
created by r1870400018:22544, 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 | //! Writing an archive back out, copying everything nobody touched. |
| 2 | //! |
| 3 | //! A held member is written by copying the bytes it was read from -- header, data, descriptor and |
| 4 | //! directory entry alike -- so a member this never understood comes out exactly as it went in. Only a |
| 5 | //! member the caller replaced is built afresh, and only its directory entry is written rather than |
| 6 | //! copied. |
| 7 | //! |
| 8 | //! The one field a copied directory entry has patched is the offset of the member's local header, |
| 9 | //! because inserting or removing a member moves everything after it. Where nothing moved, nothing is |
| 10 | //! patched, and the bytes out are the bytes in. |
| 11 | //! |
| 12 | //! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\ |
| 13 | //! Anthropic Claude |
| 14 | |
| 15 | use crate::zip::{ |
| 16 | Body, |
| 17 | Member, |
| 18 | Method, |
| 19 | Zip, |
| 20 | u32le, |
| 21 | }; |
| 22 | |
| 23 | use oxedyne_fe2o3_core::prelude::*; |
| 24 | |
| 25 | /// A central directory entry. |
| 26 | const SIG_CEN: u32 = 0x0201_4b50; |
| 27 | /// A local file header. |
| 28 | const SIG_LOC: u32 = 0x0403_4b50; |
| 29 | /// The end of central directory record. |
| 30 | const SIG_EOCD: u32 = 0x0605_4b50; |
| 31 | |
| 32 | /// The version that reads a DEFLATE member: 2.0. |
| 33 | const VERSION: u16 = 20; |
| 34 | /// Bit 11 of the general purpose flag: the name is UTF-8. |
| 35 | const FLAG_UTF8: u16 = 0x0800; |
| 36 | |
| 37 | impl Zip { |
| 38 | |
| 39 | /// An archive read and written straight back gives the bytes it was read from. That is a property |
| 40 | /// callers should check rather than take on trust: it is what says this build understood the file |
| 41 | /// well enough to be handed somebody's document. |
| 42 | pub fn write(&self) -> Outcome<Vec<u8>> { |
| 43 | if self.zip64 { |
| 44 | return Err(err!( |
| 45 | "This archive needed the ZIP64 records to be read -- it holds more than 65,535 \ |
| 46 | members, or more than 4 GB. ZIP64 is read here and not written, so writing it back \ |
| 47 | would produce a file that is not the one that was opened. The archive is refused \ |
| 48 | rather than damaged."; Unimplemented, Excessive)); |
| 49 | } |
| 50 | let mut out = Vec::with_capacity(self.src.len() + 4096); |
| 51 | let mut at = Vec::with_capacity(self.members.len()); |
| 52 | for m in &self.members { |
| 53 | at.push(out.len() as u64); |
| 54 | match &m.body { |
| 55 | Body::Held { whole, .. } => { |
| 56 | let bytes = res!(self.src.get(whole.clone()).ok_or_else(|| err!( |
| 57 | "'{}' addresses bytes {}..{} of an archive of {} bytes.", |
| 58 | m.name, whole.start, whole.end, self.src.len(); Bug, Range))); |
| 59 | out.extend_from_slice(bytes); |
| 60 | } |
| 61 | Body::Fresh { data, stamp } => res!(fresh(&mut out, m, data, *stamp)), |
| 62 | } |
| 63 | } |
| 64 | let cd_at = out.len(); |
| 65 | for (i, m) in self.members.iter().enumerate() { |
| 66 | let off = at[i]; |
| 67 | if off > u32::MAX as u64 { |
| 68 | return Err(err!( |
| 69 | "'{}' would sit at byte {}, past the 4 GB an ordinary ZIP directory can \ |
| 70 | address. ZIP64 is read here and not written.", m.name, off; |
| 71 | Unimplemented, Excessive)); |
| 72 | } |
| 73 | match &m.body { |
| 74 | Body::Held { cen, .. } => { |
| 75 | let bytes = res!(self.src.get(cen.clone()).ok_or_else(|| err!( |
| 76 | "'{}' addresses directory bytes {}..{} of an archive of {} bytes.", |
| 77 | m.name, cen.start, cen.end, self.src.len(); Bug, Range))); |
| 78 | let from = out.len(); |
| 79 | out.extend_from_slice(bytes); |
| 80 | // The only field that can have moved. Every other byte of the entry is the |
| 81 | // archive's own. |
| 82 | let was = res!(u32le(bytes, 42)); |
| 83 | if was as u64 != off { |
| 84 | let k = from + 42; |
| 85 | out[k..k + 4].copy_from_slice(&(off as u32).to_le_bytes()); |
| 86 | } |
| 87 | } |
| 88 | Body::Fresh { data, stamp } => res!(cen_fresh(&mut out, m, data, *stamp, off as u32)), |
| 89 | } |
| 90 | } |
| 91 | let cd_len = out.len() - cd_at; |
| 92 | if self.members.len() > 0xFFFF { |
| 93 | return Err(err!( |
| 94 | "The archive holds {} members, past the 65,535 an ordinary ZIP directory can \ |
| 95 | count. ZIP64 is read here and not written.", self.members.len(); |
| 96 | Unimplemented, Excessive)); |
| 97 | } |
| 98 | let n = self.members.len() as u16; |
| 99 | out.extend_from_slice(&SIG_EOCD.to_le_bytes()); |
| 100 | out.extend_from_slice(&0u16.to_le_bytes()); // This disk. |
| 101 | out.extend_from_slice(&0u16.to_le_bytes()); // The disk the directory starts on. |
| 102 | out.extend_from_slice(&n.to_le_bytes()); |
| 103 | out.extend_from_slice(&n.to_le_bytes()); |
| 104 | out.extend_from_slice(&(cd_len as u32).to_le_bytes()); |
| 105 | out.extend_from_slice(&(cd_at as u32).to_le_bytes()); |
| 106 | out.extend_from_slice(&(self.comment.len() as u16).to_le_bytes()); |
| 107 | out.extend_from_slice(&self.comment); |
| 108 | Ok(out) |
| 109 | } |
| 110 | } |
| 111 | |
| 112 | /// Writes a fresh member's local header and its bytes. |
| 113 | fn fresh( |
| 114 | out: &mut Vec<u8>, |
| 115 | m: &Member, |
| 116 | data: &[u8], |
| 117 | stamp: (u16, u16), |
| 118 | ) |
| 119 | -> Outcome<()> |
| 120 | { |
| 121 | let body = res!(pack(m, data)); |
| 122 | let name = m.name.as_bytes(); |
| 123 | out.extend_from_slice(&SIG_LOC.to_le_bytes()); |
| 124 | out.extend_from_slice(&VERSION.to_le_bytes()); |
| 125 | out.extend_from_slice(&flags(&m.name).to_le_bytes()); |
| 126 | out.extend_from_slice(&m.method.code().to_le_bytes()); |
| 127 | out.extend_from_slice(&stamp.0.to_le_bytes()); |
| 128 | out.extend_from_slice(&stamp.1.to_le_bytes()); |
| 129 | out.extend_from_slice(&m.crc.to_le_bytes()); |
| 130 | out.extend_from_slice(&(body.len() as u32).to_le_bytes()); |
| 131 | out.extend_from_slice(&(data.len() as u32).to_le_bytes()); |
| 132 | out.extend_from_slice(&(name.len() as u16).to_le_bytes()); |
| 133 | out.extend_from_slice(&0u16.to_le_bytes()); // No extra field. |
| 134 | out.extend_from_slice(name); |
| 135 | out.extend_from_slice(&body); |
| 136 | Ok(()) |
| 137 | } |
| 138 | |
| 139 | /// Writes a fresh member's central directory entry. |
| 140 | fn cen_fresh( |
| 141 | out: &mut Vec<u8>, |
| 142 | m: &Member, |
| 143 | data: &[u8], |
| 144 | stamp: (u16, u16), |
| 145 | off: u32, |
| 146 | ) |
| 147 | -> Outcome<()> |
| 148 | { |
| 149 | let body = res!(pack(m, data)); |
| 150 | let name = m.name.as_bytes(); |
| 151 | out.extend_from_slice(&SIG_CEN.to_le_bytes()); |
| 152 | out.extend_from_slice(&VERSION.to_le_bytes()); // Made by version 2.0, host MS-DOS. |
| 153 | out.extend_from_slice(&VERSION.to_le_bytes()); |
| 154 | out.extend_from_slice(&flags(&m.name).to_le_bytes()); |
| 155 | out.extend_from_slice(&m.method.code().to_le_bytes()); |
| 156 | out.extend_from_slice(&stamp.0.to_le_bytes()); |
| 157 | out.extend_from_slice(&stamp.1.to_le_bytes()); |
| 158 | out.extend_from_slice(&m.crc.to_le_bytes()); |
| 159 | out.extend_from_slice(&(body.len() as u32).to_le_bytes()); |
| 160 | out.extend_from_slice(&(data.len() as u32).to_le_bytes()); |
| 161 | out.extend_from_slice(&(name.len() as u16).to_le_bytes()); |
| 162 | out.extend_from_slice(&0u16.to_le_bytes()); // No extra field. |
| 163 | out.extend_from_slice(&0u16.to_le_bytes()); // No comment. |
| 164 | out.extend_from_slice(&0u16.to_le_bytes()); // Disk zero. |
| 165 | out.extend_from_slice(&0u16.to_le_bytes()); // Internal attributes. |
| 166 | out.extend_from_slice(&0u32.to_le_bytes()); // External attributes. |
| 167 | out.extend_from_slice(&off.to_le_bytes()); |
| 168 | out.extend_from_slice(name); |
| 169 | Ok(()) |
| 170 | } |
| 171 | |
| 172 | /// A fresh member's bytes as the archive will hold them. |
| 173 | /// |
| 174 | /// Called twice for the same member -- once for the local header and once for the directory entry, |
| 175 | /// which must agree about the compressed size -- so it is deterministic by construction rather than |
| 176 | /// by a cached size that could go stale. |
| 177 | fn pack(m: &Member, data: &[u8]) -> Outcome<Vec<u8>> { |
| 178 | match m.method { |
| 179 | Method::Store => Ok(data.to_vec()), |
| 180 | Method::Deflate => { |
| 181 | use std::io::Write; |
| 182 | let mut w = flate2::write::DeflateEncoder::new( |
| 183 | Vec::new(), |
| 184 | flate2::Compression::default(), |
| 185 | ); |
| 186 | res!(w.write_all(data), IO); |
| 187 | Ok(res!(w.finish(), IO)) |
| 188 | } |
| 189 | Method::Other(c) => Err(err!( |
| 190 | "'{}' was given to the archive under compression method {}, which this does not \ |
| 191 | write.", m.name, c; Unimplemented)), |
| 192 | } |
| 193 | } |
| 194 | |
| 195 | /// Bit 11 is set only where the name needs it. Setting it on an ASCII name would be legal and would |
| 196 | /// still change the bytes of every archive written here, for nothing. |
| 197 | fn flags(name: &str) -> u16 { |
| 198 | match name.is_ascii() { |
| 199 | true => 0, |
| 200 | false => FLAG_UTF8, |
| 201 | } |
| 202 | } |