Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_file/tests/zip.rs

8.2 KiB, 10 runs

created by r1870400018:22552, 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//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
2//! Anthropic Claude
3
4use oxedyne_fe2o3_file::zip::{
5 Method,
6 Zip,
7};
8
9use oxedyne_fe2o3_core::{
10 prelude::*,
11 test::test_it,
12};
13
14// An archive written by Info-ZIP, holding a deflated member, a stored member and one in a
15// subdirectory. It carries extra fields in both its local headers and its directory entries, which
16// is the part a rebuilt archive loses and a copied one keeps.
17const FOREIGN: &[u8] = include_bytes!("data/foreign.zip");
18
19// A `.docx` written by LibreOffice: ten members, a foreign writer, and the shape every Office
20// document has. Nothing here reads WordprocessingML; this is an archive from somebody else's
21// implementation, which is the only kind worth testing a round trip against.
22const DOCX: &[u8] = include_bytes!("data/loffice.docx");
23
24pub fn test_zip(filter: &'static str) -> Outcome<()> {
25
26 res!(test_it(filter, &["A foreign archive survives a round trip byte for byte 000", "all", "zip"], || {
27 // The property everything above this rests on. Not "opens afterwards" and not "holds the same
28 // content" -- the same bytes, so a member nobody understood is a member nobody damaged.
29 for (what, src) in [("the Info-ZIP archive", FOREIGN), ("the LibreOffice .docx", DOCX)] {
30 let zip = res!(Zip::read(src.to_vec()));
31 assert!(zip.is_pristine(), "{} was read and nothing was touched", what);
32 let out = res!(zip.write());
33 assert_eq!(out.len(), src.len(), "{} changed length on a round trip", what);
34 match out == src {
35 true => {}
36 false => {
37 let at = out.iter().zip(src).position(|(a, b)| a != b);
38 panic!("{} differs at byte {:?} after a round trip", what, at);
39 }
40 }
41 }
42 Ok(())
43 }));
44
45 res!(test_it(filter, &["A member's content is what was put in it 001", "all", "zip"], || {
46 let zip = res!(Zip::read(FOREIGN.to_vec()));
47 assert_eq!(zip.len(), 3);
48 assert_eq!(zip.names(), vec!["alpha.txt", "tiny.bin", "sub/beta.txt"]);
49 // The deflated one.
50 let alpha = res!(zip.text("alpha.txt"));
51 assert!(alpha.starts_with("hello hello"), "got {:?}", alpha);
52 assert_eq!(alpha.len(), 72);
53 assert_eq!(res!(zip.member("alpha.txt").ok_or_else(|| err!("no alpha"; Missing))).method,
54 Method::Deflate);
55 // The stored one, which must not go near the inflater.
56 assert_eq!(res!(zip.content("tiny.bin")), b"x".to_vec());
57 assert_eq!(res!(zip.member("tiny.bin").ok_or_else(|| err!("no tiny"; Missing))).method,
58 Method::Store);
59 // And one with a path in its name.
60 assert_eq!(res!(zip.text("sub/beta.txt")), "The quick brown fox jumps over the lazy dog.\n");
61 Ok(())
62 }));
63
64 res!(test_it(filter, &["An unknown member is named rather than lost 002", "all", "zip"], || {
65 // A `.docx` holds ten parts and this crate understands none of them. It still names all ten,
66 // which is what lets the layer above decide what to parse and what to copy.
67 let zip = res!(Zip::read(DOCX.to_vec()));
68 assert_eq!(zip.len(), 10);
69 assert!(zip.has("word/document.xml"));
70 assert!(zip.has("[Content_Types].xml"));
71 assert!(zip.has("word/theme/theme1.xml"), "the part an editor most often loses");
72 let doc = res!(zip.text("word/document.xml"));
73 assert!(doc.contains("<w:body>"), "the document part is XML");
74 Ok(())
75 }));
76
77 res!(test_it(filter, &["Replacing one member leaves the others byte for byte 003", "all", "zip"], || {
78 // The second property: an edit reaches the part it names and no other. Checked against the
79 // bytes in the archive rather than against the content, because two members can hold the same
80 // content and different bytes, and it is the bytes a colleague's reader will parse.
81 let before = res!(Zip::read(DOCX.to_vec()));
82 let mut after = res!(Zip::read(DOCX.to_vec()));
83 let edited = "<?xml version=\"1.0\"?><w:document/>";
84 after.set("word/document.xml", edited.as_bytes().to_vec(), Method::Deflate);
85 assert!(!after.is_pristine(), "the archive was touched");
86 let out = res!(after.write());
87 assert_ne!(out, DOCX.to_vec(), "the edit reached the file");
88 let back = res!(Zip::read(out));
89 assert_eq!(back.len(), before.len(), "no member was added or lost");
90 assert_eq!(back.names(), before.names(), "no member moved");
91 let mut differ = Vec::new();
92 for m in before.members() {
93 let was = res!(before.content(&m.name));
94 let now = res!(back.content(&m.name));
95 if was != now {
96 differ.push(m.name.clone());
97 }
98 }
99 assert_eq!(differ, vec!["word/document.xml".to_string()],
100 "exactly one member changed");
101 assert_eq!(res!(back.text("word/document.xml")), edited);
102 // And the untouched members are the same bytes, not merely the same content.
103 for m in before.members() {
104 if m.name == "word/document.xml" {
105 continue;
106 }
107 let a = res!(m.raw(&before));
108 let b = res!(res!(back.member(&m.name).ok_or_else(|| err!(
109 "'{}' went missing", m.name; Missing))).raw(&back));
110 assert_eq!(a, b, "'{}' was re-compressed rather than copied", m.name);
111 }
112 Ok(())
113 }));
114
115 res!(test_it(filter, &["An archive built from nothing reads back 004", "all", "zip"], || {
116 let mut zip = Zip::new();
117 zip.set("mimetype", b"application/vnd.oasis.opendocument.text".to_vec(), Method::Store);
118 zip.set("a/b.txt", b"some content that is long enough to be worth deflating, twice over, \
119 some content that is long enough to be worth deflating".to_vec(), Method::Deflate);
120 zip.set("empty.txt", Vec::new(), Method::Deflate);
121 let out = res!(zip.write());
122 let back = res!(Zip::read(out.clone()));
123 assert_eq!(back.names(), vec!["mimetype", "a/b.txt", "empty.txt"]);
124 assert_eq!(res!(back.text("mimetype")), "application/vnd.oasis.opendocument.text");
125 assert_eq!(res!(back.content("empty.txt")), Vec::<u8>::new());
126 assert!(res!(back.text("a/b.txt")).starts_with("some content"));
127 // Written twice, the same bytes: there is no clock in the file, so a build that has to be
128 // reproducible stays reproducible.
129 let again = res!(zip.write());
130 assert_eq!(out, again, "the writer put the time of day in the archive");
131 Ok(())
132 }));
133
134 res!(test_it(filter, &["A member may be put at the head of the archive 005", "all", "zip"], || {
135 // OpenDocument requires it: `mimetype` first and stored, or a reader calls the file a ZIP.
136 let mut zip = Zip::new();
137 zip.set("content.xml", b"<x/>".to_vec(), Method::Deflate);
138 zip.set_first("mimetype", b"application/vnd.oasis.opendocument.text".to_vec(), Method::Store);
139 assert_eq!(zip.names(), vec!["mimetype", "content.xml"]);
140 let out = res!(zip.write());
141 // Stored and first means the media type sits at a known offset in the raw bytes, which is how
142 // every reader identifies the file.
143 assert_eq!(&out[30..38], b"mimetype");
144 assert_eq!(&out[38..77], b"application/vnd.oasis.opendocument.text");
145 Ok(())
146 }));
147
148 res!(test_it(filter, &["Damage is refused rather than guessed at 006", "all", "zip"], || {
149 // Not an archive at all.
150 assert!(Zip::read(b"not an archive, not even close".to_vec()).is_err());
151 // The directory cut off, which is what a half-downloaded file looks like.
152 let cut = FOREIGN[..FOREIGN.len() - 40].to_vec();
153 assert!(Zip::read(cut).is_err(), "a truncated archive is refused");
154 // A member whose bytes no longer hash to what the directory says.
155 let mut bent = FOREIGN.to_vec();
156 // The first member's deflated data begins after its local header, name and extra field.
157 let nlen = u16::from_le_bytes([bent[26], bent[27]]) as usize;
158 let elen = u16::from_le_bytes([bent[28], bent[29]]) as usize;
159 let at = 30 + nlen + elen;
160 bent[at] ^= 0xFF;
161 let zip = res!(Zip::read(bent));
162 assert!(zip.content("alpha.txt").is_err(), "a damaged member is refused, not returned");
163 Ok(())
164 }));
165
166 res!(test_it(filter, &["A member is not inflated past a stated ceiling 007", "all", "zip"], || {
167 let zip = res!(Zip::read(FOREIGN.to_vec()));
168 assert!(zip.content_capped("alpha.txt", 71).is_err(), "72 bytes is over a 71 byte ceiling");
169 assert_eq!(res!(zip.content_capped("alpha.txt", 72)).len(), 72);
170 Ok(())
171 }));
172
173 res!(test_it(filter, &["A member that is not there is said to not be there 008", "all", "zip"], || {
174 let mut zip = res!(Zip::read(FOREIGN.to_vec()));
175 assert!(!zip.has("nothing.txt"));
176 let e = zip.content("nothing.txt");
177 assert!(e.is_err());
178 assert!(!zip.remove("nothing.txt"));
179 assert!(zip.is_pristine(), "removing nothing touches nothing");
180 Ok(())
181 }));
182
183 Ok(())
184}