1use std::collections::HashMap;
2use std::sync::LazyLock;
3
4use miniz_oxide::inflate::decompress_to_vec_with_limit;
5
6use crate::package::{OoxmlPackage, OoxmlPart};
7
8const ZIP_LOCAL_FILE_HEADER_SIGNATURE: u32 = 0x0403_4b50;
9const ZIP_CENTRAL_DIRECTORY_SIGNATURE: u32 = 0x0201_4b50;
10const ZIP_EOCD_SIGNATURE: u32 = 0x0605_4b50;
11const UTF8_FLAG: u16 = 0x0800;
12const STORE_COMPRESSION: u16 = 0;
13const DEFLATE_COMPRESSION: u16 = 8;
14
15const DEFAULT_DOCUMENT_XML: &str = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
16<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
17 <w:body>
18 <w:p><w:r><w:t/></w:r></w:p>
19 <w:sectPr>
20 <w:pgSz w:w="12240" w:h="15840"/>
21 <w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440" w:header="708" w:footer="708" w:gutter="0"/>
22 </w:sectPr>
23 </w:body>
24</w:document>"#;
25
26const DEFAULT_CONTENT_TYPES_XML: &str = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
27<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
28 <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
29 <Default Extension="xml" ContentType="application/xml"/>
30 <Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
31</Types>"#;
32
33const DEFAULT_ROOT_RELS_XML: &str = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
34<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
35 <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
36</Relationships>"#;
37
38const DEFAULT_DOCUMENT_RELS_XML: &str = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
39<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"/>"#;
40
41struct ZipEntry {
42 name: String,
43 compression_method: u16,
44 compressed_size: u32,
45 uncompressed_size: u32,
46 local_header_offset: u32,
47}
48
49fn read_u16_le(bytes: &[u8], offset: usize) -> u16 {
50 u16::from_le_bytes([bytes[offset], bytes[offset + 1]])
51}
52
53fn read_u32_le(bytes: &[u8], offset: usize) -> u32 {
54 u32::from_le_bytes([
55 bytes[offset],
56 bytes[offset + 1],
57 bytes[offset + 2],
58 bytes[offset + 3],
59 ])
60}
61
62fn write_u16_le(out: &mut [u8], offset: usize, value: u16) {
63 out[offset..offset + 2].copy_from_slice(&value.to_le_bytes());
64}
65
66fn write_u32_le(out: &mut [u8], offset: usize, value: u32) {
67 out[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
68}
69
70fn find_eocd_offset(bytes: &[u8]) -> Result<usize, String> {
71 const MINIMUM_LENGTH: usize = 22;
72 if bytes.len() < MINIMUM_LENGTH {
73 return Err("Invalid DOCX ZIP: too small to contain EOCD".to_string());
74 }
75
76 let max_comment_length = 0xffff;
77 let search_start = bytes
78 .len()
79 .saturating_sub(MINIMUM_LENGTH + max_comment_length);
80
81 let mut index = bytes.len().saturating_sub(MINIMUM_LENGTH);
82 while index >= search_start {
83 if read_u32_le(bytes, index) == ZIP_EOCD_SIGNATURE {
84 return Ok(index);
85 }
86 if index == 0 {
87 break;
88 }
89 index -= 1;
90 }
91
92 Err("Invalid DOCX ZIP: end of central directory not found".to_string())
93}
94
95fn parse_central_directory(bytes: &[u8]) -> Result<Vec<ZipEntry>, String> {
96 let eocd_offset = find_eocd_offset(bytes)?;
97 let total_entries = read_u16_le(bytes, eocd_offset + 10) as usize;
98 let central_directory_offset = read_u32_le(bytes, eocd_offset + 16) as usize;
99
100 let mut entries = Vec::with_capacity(total_entries);
101 let mut cursor = central_directory_offset;
102
103 for _ in 0..total_entries {
104 if cursor.checked_add(46).is_none_or(|end| end > bytes.len())
105 || read_u32_le(bytes, cursor) != ZIP_CENTRAL_DIRECTORY_SIGNATURE
106 {
107 return Err("Invalid DOCX ZIP: malformed central directory".to_string());
108 }
109
110 let compression_method = read_u16_le(bytes, cursor + 10);
111 let compressed_size = read_u32_le(bytes, cursor + 20);
112 let uncompressed_size = read_u32_le(bytes, cursor + 24);
113 let file_name_length = read_u16_le(bytes, cursor + 28) as usize;
114 let extra_field_length = read_u16_le(bytes, cursor + 30) as usize;
115 let file_comment_length = read_u16_le(bytes, cursor + 32) as usize;
116 let local_header_offset = read_u32_le(bytes, cursor + 42);
117
118 let file_name_start = cursor + 46;
119 let file_name_end = file_name_start + file_name_length;
120 if file_name_end
121 .checked_add(extra_field_length + file_comment_length)
122 .is_none_or(|end| end > bytes.len())
123 {
124 return Err("Invalid DOCX ZIP: truncated directory entry".into());
125 }
126 let name = String::from_utf8_lossy(&bytes[file_name_start..file_name_end]).into_owned();
127
128 entries.push(ZipEntry {
129 name,
130 compression_method,
131 compressed_size,
132 uncompressed_size,
133 local_header_offset,
134 });
135
136 cursor = file_name_end + extra_field_length + file_comment_length;
137 }
138
139 Ok(entries)
140}
141
142fn inflate_raw(data: &[u8]) -> Result<Vec<u8>, String> {
143 decompress_to_vec_with_limit(data, usize::MAX)
144 .map_err(|error| format!("Failed to inflate DOCX ZIP entry: {error:?}"))
145}
146
147#[derive(Default)]
148struct ZipInflater {
149 #[cfg(all(feature = "native-deflate", not(target_arch = "wasm32")))]
150 native: libdeflater::Decompressor,
151}
152
153impl ZipInflater {
154 fn decode(&mut self, compressed: &[u8], output: &mut [u8]) -> Option<usize> {
155 #[cfg(all(feature = "native-deflate", not(target_arch = "wasm32")))]
156 if let Ok(length) = self.native.deflate_decompress(compressed, output) {
157 return Some(length);
158 }
159 let (decoded, status) = zlib_rs::decompress_slice(
160 output,
161 compressed,
162 zlib_rs::InflateConfig { window_bits: -15 },
163 );
164 (status == zlib_rs::ReturnCode::Ok).then_some(decoded.len())
165 }
166}
167
168fn extract_entry_data(
169 bytes: &[u8],
170 entry: &ZipEntry,
171 inflater: &mut ZipInflater,
172) -> Result<Vec<u8>, String> {
173 let local_header_offset = entry.local_header_offset as usize;
174
175 if local_header_offset
176 .checked_add(30)
177 .is_none_or(|end| end > bytes.len())
178 || read_u32_le(bytes, local_header_offset) != ZIP_LOCAL_FILE_HEADER_SIGNATURE
179 {
180 return Err(format!(
181 "Invalid DOCX ZIP: bad local header for {}",
182 entry.name
183 ));
184 }
185
186 let file_name_length = read_u16_le(bytes, local_header_offset + 26) as usize;
187 let extra_field_length = read_u16_le(bytes, local_header_offset + 28) as usize;
188 let payload_offset = local_header_offset + 30 + file_name_length + extra_field_length;
189 let payload_end = payload_offset
190 .checked_add(entry.compressed_size as usize)
191 .ok_or("Invalid DOCX ZIP: entry size overflow")?;
192 let compressed_data = bytes
193 .get(payload_offset..payload_end)
194 .ok_or("Invalid DOCX ZIP: truncated entry")?;
195
196 if entry.compression_method == STORE_COMPRESSION {
197 return Ok(compressed_data.to_vec());
198 }
199
200 if entry.compression_method == DEFLATE_COMPRESSION {
201 let expected = entry.uncompressed_size as usize;
202 if expected <= 64 * 1024 * 1024
203 && expected <= compressed_data.len().saturating_mul(512).max(1024 * 1024)
204 {
205 let mut output = vec![0; expected];
206 if let Some(length) = inflater.decode(compressed_data, &mut output) {
207 output.truncate(length);
208 return Ok(output);
209 }
210 }
211 return inflate_raw(compressed_data);
212 }
213
214 Err(format!(
215 "Unsupported DOCX ZIP compression method {} for {}",
216 entry.compression_method, entry.name
217 ))
218}
219
220pub fn is_text_part(part_name: &str) -> bool {
221 if part_name == "[Content_Types].xml" {
222 return true;
223 }
224
225 match part_name.rsplit_once('.') {
226 Some((_, extension)) => {
227 extension.eq_ignore_ascii_case("xml")
228 || extension.eq_ignore_ascii_case("rels")
229 || extension.eq_ignore_ascii_case("txt")
230 }
231 None => false,
232 }
233}
234
235fn decode_xml_part(bytes: Vec<u8>) -> Result<String, String> {
236 let big_endian = bytes.starts_with(&[0xfe, 0xff]) || bytes.starts_with(&[0, b'<', 0, b'?']);
237 let little_endian = bytes.starts_with(&[0xff, 0xfe]) || bytes.starts_with(&[b'<', 0, b'?', 0]);
238 if big_endian || little_endian {
239 let skip = if bytes.starts_with(&[0xfe, 0xff]) || bytes.starts_with(&[0xff, 0xfe]) {
240 2
241 } else {
242 0
243 };
244 let body = &bytes[skip..];
245 if body.len() % 2 != 0 {
246 return Err("Invalid UTF-16 XML byte length".into());
247 }
248 let units = body
249 .chunks_exact(2)
250 .map(|pair| {
251 if big_endian {
252 u16::from_be_bytes([pair[0], pair[1]])
253 } else {
254 u16::from_le_bytes([pair[0], pair[1]])
255 }
256 })
257 .collect::<Vec<_>>();
258 let xml =
259 String::from_utf16(&units).map_err(|error| format!("Invalid UTF-16 XML: {error}"))?;
260 static ENCODING: std::sync::LazyLock<regex::Regex> = std::sync::LazyLock::new(|| {
261 regex::Regex::new(
262 r#"(?i)encoding[\x09-\x0D\x20]*=[\x09-\x0D\x20]*["']UTF-16(?:LE|BE)?["']"#,
263 )
264 .unwrap()
265 });
266 let encoding = &*ENCODING;
267 return Ok(encoding.replace(&xml, "encoding=\"UTF-8\"").into_owned());
268 }
269 String::from_utf8(bytes)
270 .map_err(|error| format!("Invalid or unsupported XML encoding: {error}"))
271}
272
273#[cfg(not(target_arch = "wasm32"))]
274fn extract_entry_group(
275 bytes: &[u8],
276 entries: &[ZipEntry],
277 group: &[usize],
278) -> Vec<(usize, Result<Vec<u8>, String>)> {
279 let mut inflater = ZipInflater::default();
280 group.iter().map(|&index| {
281 (index, extract_entry_data(bytes, &entries[index], &mut inflater))
282 }).collect()
283}
284
285#[cfg(not(target_arch = "wasm32"))]
286#[cold]
287fn parallel_entry_data(
288 bytes: &[u8],
289 entries: &[ZipEntry],
290) -> Option<Vec<Option<Result<Vec<u8>, String>>>> {
291 if bytes.len() < 16 * 1024
292 || entries.iter().map(|entry| u64::from(entry.uncompressed_size)).sum::<u64>() < 64 * 1024
293 || entries.iter().filter(|entry| {
294 !entry.name.ends_with('/')
295 && entry.compression_method == DEFLATE_COMPRESSION
296 && entry.uncompressed_size >= 16 * 1024
297 }).take(2).count() < 2
298 {
299 return None;
300 }
301 let pool = crate::parse::worker_pool()?;
302 let mut jobs: Vec<_> = entries.iter().enumerate()
303 .filter_map(|(index, entry)| (!entry.name.ends_with('/')).then_some(index))
304 .collect();
305 jobs.sort_unstable_by_key(|&index| std::cmp::Reverse(entries[index].uncompressed_size));
306 let mut groups: [Vec<usize>; 4] = std::array::from_fn(|_| Vec::new());
307 let mut weights = [0u64; 4];
308 for index in jobs {
309 let group = (0..4).min_by_key(|&group| weights[group]).unwrap();
310 weights[group] += u64::from(entries[index].uncompressed_size);
311 groups[group].push(index);
312 }
313 let mut outputs: [Option<Vec<(usize, Result<Vec<u8>, String>)>>; 4] = std::array::from_fn(|_| None);
314 let (caller, workers) = outputs.split_first_mut().unwrap();
315 pool.in_place_scope(|scope| {
316 for (slot, group) in workers.iter_mut().zip(&groups[1..]) {
317 scope.spawn(move |_| *slot = Some(extract_entry_group(bytes, entries, group)));
318 }
319 *caller = Some(extract_entry_group(bytes, entries, &groups[0]));
320 });
321 let mut result: Vec<_> = (0..entries.len()).map(|_| None).collect();
322 for group in outputs.into_iter().flatten() {
323 for (index, value) in group {
324 result[index] = Some(value);
325 }
326 }
327 Some(result)
328}
329
330#[cfg(not(target_arch = "wasm32"))]
331#[cold]
332fn package_from_parallel_entries(
333 entries: Vec<ZipEntry>,
334 results: Vec<Option<Result<Vec<u8>, String>>>,
335) -> Result<OoxmlPackage, String> {
336 let mut parts = HashMap::new();
337 let mut binary_assets = HashMap::new();
338 for (entry, data) in entries.into_iter().zip(results) {
339 if entry.name.ends_with('/') {
340 continue;
341 }
342 let data = data.expect("scheduled ZIP entry")?;
343 if is_text_part(&entry.name) {
344 parts.insert(entry.name.clone(), OoxmlPart {
345 name: entry.name,
346 content: decode_xml_part(data)?,
347 });
348 } else {
349 binary_assets.insert(entry.name, data);
350 }
351 }
352 let package = OoxmlPackage { parts, binary_assets, warnings: Vec::new() };
353 let main_part = crate::package::main_document_part_name(&package);
354 if !package.parts.contains_key(&main_part) {
355 return Err(format!("Invalid DOCX: missing {main_part}"));
356 }
357 Ok(package)
358}
359
360pub fn parse_docx(bytes: &[u8]) -> Result<OoxmlPackage, String> {
361 if bytes.is_empty() {
362 return Err("DOCX input cannot be empty".to_string());
363 }
364
365 let entries = parse_central_directory(bytes)?;
366 #[cfg(not(target_arch = "wasm32"))]
367 if bytes.len() >= 16 * 1024 {
368 if let Some(parallel) = parallel_entry_data(bytes, &entries) {
369 return package_from_parallel_entries(entries, parallel);
370 }
371 }
372 let mut parts = HashMap::new();
373 let mut binary_assets = HashMap::new();
374 let mut inflater = ZipInflater::default();
375
376 for entry in entries {
377 if entry.name.ends_with('/') {
378 continue;
379 }
380
381 let file_bytes = extract_entry_data(bytes, &entry, &mut inflater)?;
382
383 if is_text_part(&entry.name) {
384 parts.insert(
385 entry.name.clone(),
386 OoxmlPart {
387 name: entry.name,
388 content: decode_xml_part(file_bytes)?,
389 },
390 );
391 continue;
392 }
393
394 binary_assets.insert(entry.name, file_bytes);
395 }
396
397 let package = OoxmlPackage {
398 parts,
399 binary_assets,
400 warnings: Vec::new(),
401 };
402 let main_part = crate::package::main_document_part_name(&package);
403 if !package.parts.contains_key(&main_part) {
404 return Err(format!("Invalid DOCX: missing {main_part}"));
405 }
406 Ok(package)
407}
408
409fn create_crc32_table() -> [u32; 256] {
410 let mut table = [0u32; 256];
411 for index in 0..256 {
412 let mut value = index as u32;
413 for _ in 0..8 {
414 value = if value & 1 == 1 {
415 (value >> 1) ^ 0xedb8_8320
416 } else {
417 value >> 1
418 };
419 }
420 table[index] = value;
421 }
422 table
423}
424
425static CRC32_TABLE: LazyLock<[u32; 256]> = LazyLock::new(create_crc32_table);
426
427pub(crate) fn crc32(bytes: &[u8]) -> u32 {
428 let mut value = 0xffff_ffffu32;
429 for byte in bytes {
430 value = (value >> 8) ^ CRC32_TABLE[((value ^ u32::from(*byte)) & 0xff) as usize];
431 }
432 value ^ 0xffff_ffff
433}
434
435#[cfg(test)]
436fn concat_bytes(chunks: &[&[u8]]) -> Vec<u8> {
437 let total_length: usize = chunks.iter().map(|chunk| chunk.len()).sum();
438 let mut output = Vec::with_capacity(total_length);
439 for chunk in chunks {
440 output.extend_from_slice(chunk);
441 }
442 output
443}
444
445const PRECOMPRESSED_EXTENSIONS: &[&str] = &[
447 "7z", "avi", "avif", "docm", "docx", "gif", "gz", "hdp", "heic", "heif", "jpe", "jpeg", "jpg",
448 "jxr", "m4a", "m4v", "mov", "mp3", "mp4", "png", "pptm", "pptx", "wdp", "webp", "wma", "wmv",
449 "xlsm", "xlsx", "zip",
450];
451
452fn is_precompressed(name: &str) -> bool {
453 name.rsplit_once('.').is_some_and(|(_, extension)| {
454 PRECOMPRESSED_EXTENSIONS
455 .iter()
456 .any(|known| known.eq_ignore_ascii_case(extension))
457 })
458}
459
460fn zip_u32(value: usize, what: &str) -> Result<u32, String> {
461 u32::try_from(value).map_err(|_| format!("{what} exceeds the 4 GiB non-ZIP64 limit"))
462}
463
464pub fn package_to_bytes(pkg: &OoxmlPackage) -> Result<Vec<u8>, String> {
470 let mut entries: Vec<(&str, &[u8])> = pkg
471 .parts
472 .values()
473 .map(|part| (part.name.as_str(), part.content.as_bytes()))
474 .chain(
475 pkg.binary_assets
476 .iter()
477 .map(|(name, data)| (name.as_str(), data.as_slice())),
478 )
479 .collect();
480
481 entries.sort_by(|left, right| left.0.cmp(right.0));
482
483 if entries.is_empty() {
484 return Err("Cannot create DOCX ZIP from an empty package".to_string());
485 }
486
487 if entries.len() > 0xffff {
488 return Err("Too many ZIP entries for non-ZIP64 writer".to_string());
489 }
490
491 let stored_media: usize = entries
492 .iter()
493 .filter(|(name, _)| is_precompressed(name))
494 .map(|(name, payload)| 30 + name.len() + payload.len())
495 .sum();
496 let mut output = Vec::with_capacity(stored_media + 64 * 1024);
497 let mut central_directory = Vec::with_capacity(entries.iter().map(|(name, _)| 46 + name.len()).sum());
498
499 for (name, payload) in &entries {
500 let name_bytes = name.as_bytes();
501 let crc = crc32(payload);
502 let offset = zip_u32(output.len(), "ZIP archive")?;
503 let uncompressed_size = zip_u32(payload.len(), name)?;
504
505 let deflated = (!is_precompressed(name) && !payload.is_empty())
506 .then(|| miniz_oxide::deflate::compress_to_vec(payload, 6))
507 .filter(|deflated| deflated.len() < payload.len());
508 let (method, data): (u16, &[u8]) = match &deflated {
509 Some(deflated) => (DEFLATE_COMPRESSION, deflated),
510 None => (STORE_COMPRESSION, payload),
511 };
512 let compressed_size = zip_u32(data.len(), name)?;
513
514 let mut local_header = [0u8; 30];
515 write_u32_le(&mut local_header, 0, ZIP_LOCAL_FILE_HEADER_SIGNATURE);
516 write_u16_le(&mut local_header, 4, 20);
517 write_u16_le(&mut local_header, 6, UTF8_FLAG);
518 write_u16_le(&mut local_header, 8, method);
519 write_u32_le(&mut local_header, 14, crc);
520 write_u32_le(&mut local_header, 18, compressed_size);
521 write_u32_le(&mut local_header, 22, uncompressed_size);
522 write_u16_le(&mut local_header, 26, name_bytes.len() as u16);
523 output.extend_from_slice(&local_header);
524 output.extend_from_slice(name_bytes);
525 output.extend_from_slice(data);
526 drop(deflated);
527
528 let mut central_header = [0u8; 46];
529 write_u32_le(&mut central_header, 0, ZIP_CENTRAL_DIRECTORY_SIGNATURE);
530 write_u16_le(&mut central_header, 4, 20);
531 write_u16_le(&mut central_header, 6, 20);
532 write_u16_le(&mut central_header, 8, UTF8_FLAG);
533 write_u16_le(&mut central_header, 10, method);
534 write_u32_le(&mut central_header, 16, crc);
535 write_u32_le(&mut central_header, 20, compressed_size);
536 write_u32_le(&mut central_header, 24, uncompressed_size);
537 write_u16_le(&mut central_header, 28, name_bytes.len() as u16);
538 write_u32_le(&mut central_header, 42, offset);
539 central_directory.extend_from_slice(¢ral_header);
540 central_directory.extend_from_slice(name_bytes);
541 }
542
543 let central_directory_offset = zip_u32(output.len(), "ZIP archive")?;
544 let central_directory_size = zip_u32(central_directory.len(), "ZIP central directory")?;
545 output.extend_from_slice(¢ral_directory);
546 drop(central_directory);
547
548 let mut eocd = [0u8; 22];
549 write_u32_le(&mut eocd, 0, ZIP_EOCD_SIGNATURE);
550 write_u16_le(&mut eocd, 8, entries.len() as u16);
551 write_u16_le(&mut eocd, 10, entries.len() as u16);
552 write_u32_le(&mut eocd, 12, central_directory_size);
553 write_u32_le(&mut eocd, 16, central_directory_offset);
554 output.extend_from_slice(&eocd);
555 Ok(output)
556}
557
558pub fn create_minimal_docx_package(document_xml: Option<&str>) -> OoxmlPackage {
559 let document_xml = document_xml.unwrap_or(DEFAULT_DOCUMENT_XML);
560
561 let mut parts = HashMap::new();
562 parts.insert(
563 "[Content_Types].xml".to_string(),
564 OoxmlPart {
565 name: "[Content_Types].xml".to_string(),
566 content: DEFAULT_CONTENT_TYPES_XML.to_string(),
567 },
568 );
569 parts.insert(
570 "_rels/.rels".to_string(),
571 OoxmlPart {
572 name: "_rels/.rels".to_string(),
573 content: DEFAULT_ROOT_RELS_XML.to_string(),
574 },
575 );
576 parts.insert(
577 "word/document.xml".to_string(),
578 OoxmlPart {
579 name: "word/document.xml".to_string(),
580 content: document_xml.to_string(),
581 },
582 );
583 parts.insert(
584 "word/_rels/document.xml.rels".to_string(),
585 OoxmlPart {
586 name: "word/_rels/document.xml.rels".to_string(),
587 content: DEFAULT_DOCUMENT_RELS_XML.to_string(),
588 },
589 );
590
591 OoxmlPackage {
592 parts,
593 binary_assets: HashMap::new(),
594 warnings: Vec::new(),
595 }
596}
597
598#[cfg(test)]
599mod tests {
600 use super::*;
601 use crate::package::get_part;
602
603 const DOCUMENT_XML: &str = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body><w:p><w:r><w:t>Hello DOCX</w:t></w:r></w:p></w:body></w:document>"#;
604
605 struct ZipSourceEntry<'a> {
606 name: &'a str,
607 content: &'a str,
608 deflate: bool,
609 }
610
611 fn create_zip(entries: &[ZipSourceEntry<'_>]) -> Vec<u8> {
612 let mut local_chunks: Vec<Vec<u8>> = Vec::new();
613 let mut central_chunks: Vec<Vec<u8>> = Vec::new();
614 let mut offset = 0u32;
615
616 for entry in entries {
617 let name_bytes = entry.name.as_bytes();
618 let uncompressed = entry.content.as_bytes();
619 let compression_method = if entry.deflate {
620 DEFLATE_COMPRESSION
621 } else {
622 STORE_COMPRESSION
623 };
624 let compressed = if entry.deflate {
625 miniz_oxide::deflate::compress_to_vec(uncompressed, 6)
626 } else {
627 uncompressed.to_vec()
628 };
629 let crc = crc32(uncompressed);
630
631 let mut local = vec![0u8; 30 + name_bytes.len() + compressed.len()];
632 write_u32_le(&mut local, 0, ZIP_LOCAL_FILE_HEADER_SIGNATURE);
633 write_u16_le(&mut local, 4, 20);
634 write_u16_le(&mut local, 6, UTF8_FLAG);
635 write_u16_le(&mut local, 8, compression_method);
636 write_u16_le(&mut local, 10, 0);
637 write_u16_le(&mut local, 12, 0);
638 write_u32_le(&mut local, 14, crc);
639 write_u32_le(&mut local, 18, compressed.len() as u32);
640 write_u32_le(&mut local, 22, uncompressed.len() as u32);
641 write_u16_le(&mut local, 26, name_bytes.len() as u16);
642 write_u16_le(&mut local, 28, 0);
643 local[30..30 + name_bytes.len()].copy_from_slice(name_bytes);
644 local[30 + name_bytes.len()..].copy_from_slice(&compressed);
645 local_chunks.push(local.clone());
646
647 let mut central = vec![0u8; 46 + name_bytes.len()];
648 write_u32_le(&mut central, 0, ZIP_CENTRAL_DIRECTORY_SIGNATURE);
649 write_u16_le(&mut central, 4, 20);
650 write_u16_le(&mut central, 6, 20);
651 write_u16_le(&mut central, 8, UTF8_FLAG);
652 write_u16_le(&mut central, 10, compression_method);
653 write_u16_le(&mut central, 12, 0);
654 write_u16_le(&mut central, 14, 0);
655 write_u32_le(&mut central, 16, crc);
656 write_u32_le(&mut central, 20, compressed.len() as u32);
657 write_u32_le(&mut central, 24, uncompressed.len() as u32);
658 write_u16_le(&mut central, 28, name_bytes.len() as u16);
659 write_u16_le(&mut central, 30, 0);
660 write_u16_le(&mut central, 32, 0);
661 write_u16_le(&mut central, 34, 0);
662 write_u16_le(&mut central, 36, 0);
663 write_u32_le(&mut central, 38, 0);
664 write_u32_le(&mut central, 42, offset);
665 central[46..].copy_from_slice(name_bytes);
666 central_chunks.push(central);
667
668 offset += local.len() as u32;
669 }
670
671 let local_data: Vec<u8> = local_chunks.into_iter().flatten().collect();
672 let central_data: Vec<u8> = central_chunks.into_iter().flatten().collect();
673
674 let mut eocd = vec![0u8; 22];
675 write_u32_le(&mut eocd, 0, ZIP_EOCD_SIGNATURE);
676 write_u16_le(&mut eocd, 4, 0);
677 write_u16_le(&mut eocd, 6, 0);
678 write_u16_le(&mut eocd, 8, entries.len() as u16);
679 write_u16_le(&mut eocd, 10, entries.len() as u16);
680 write_u32_le(&mut eocd, 12, central_data.len() as u32);
681 write_u32_le(&mut eocd, 16, local_data.len() as u32);
682 write_u16_le(&mut eocd, 20, 0);
683
684 concat_bytes(&[&local_data, ¢ral_data, &eocd])
685 }
686
687 #[test]
688 fn throws_on_empty_input() {
689 let error = parse_docx(&[]).unwrap_err();
690 assert!(error.contains("cannot be empty"));
691 }
692
693 #[test]
694 fn parses_stored_zip_entries() {
695 let zip = create_zip(&[
696 ZipSourceEntry {
697 name: "[Content_Types].xml",
698 content: "<Types/>",
699 deflate: false,
700 },
701 ZipSourceEntry {
702 name: "word/document.xml",
703 content: DOCUMENT_XML,
704 deflate: false,
705 },
706 ]);
707 let pkg = parse_docx(&zip).expect("parse stored zip");
708
709 assert!(get_part(&pkg, "word/document.xml")
710 .expect("document part")
711 .content
712 .contains("Hello DOCX"));
713 }
714
715 #[test]
716 fn parses_deflated_zip_entries() {
717 let zip = create_zip(&[
718 ZipSourceEntry {
719 name: "[Content_Types].xml",
720 content: "<Types/>",
721 deflate: true,
722 },
723 ZipSourceEntry {
724 name: "word/document.xml",
725 content: DOCUMENT_XML,
726 deflate: true,
727 },
728 ]);
729 let pkg = parse_docx(&zip).expect("parse deflated zip");
730
731 assert!(get_part(&pkg, "word/document.xml")
732 .expect("document part")
733 .content
734 .contains("Hello DOCX"));
735 }
736
737 #[test]
738 fn writes_and_reparses_docx_zip_archives() {
739 let source = create_minimal_docx_package(Some(DOCUMENT_XML));
740 let zip = package_to_bytes(&source).expect("package to bytes");
741 let reparsed = parse_docx(&zip).expect("reparse docx");
742
743 assert!(reparsed
744 .parts
745 .get("word/document.xml")
746 .expect("document part")
747 .content
748 .contains("Hello DOCX"));
749 }
750 #[test]
751 fn writes_deflated_xml_and_stored_media_that_reparse_exactly() {
752 let mut source = create_minimal_docx_package(Some(DOCUMENT_XML));
753 let photo: Vec<u8> = (0..200_000u32).map(|value| (value.wrapping_mul(2_654_435_761) >> 24) as u8).collect();
754 let metafile = vec![7u8; 100_000];
755 source.binary_assets.insert("word/media/photo.png".into(), photo.clone());
756 source.binary_assets.insert("word/media/figure.emf".into(), metafile.clone());
757 source.binary_assets.insert("word/media/empty.bin".into(), Vec::new());
758 let zip = package_to_bytes(&source).expect("package to bytes");
759
760 let entries = parse_central_directory(&zip).expect("central directory");
761 let method = |name: &str| entries.iter().find(|entry| entry.name == name).map(|entry| entry.compression_method);
762 assert_eq!(method("word/document.xml"), Some(DEFLATE_COMPRESSION));
763 assert_eq!(method("word/media/figure.emf"), Some(DEFLATE_COMPRESSION));
764 assert_eq!(method("word/media/photo.png"), Some(STORE_COMPRESSION));
765 assert_eq!(method("word/media/empty.bin"), Some(STORE_COMPRESSION));
766 assert!(zip.len() < photo.len() + 20_000, "metafile and XML should be compressed");
767
768 let reparsed = parse_docx(&zip).expect("reparse docx");
769 assert_eq!(reparsed.binary_assets["word/media/photo.png"], photo);
770 assert_eq!(reparsed.binary_assets["word/media/figure.emf"], metafile);
771 assert!(reparsed.binary_assets["word/media/empty.bin"].is_empty());
772 assert_eq!(reparsed.parts["word/document.xml"].content, source.parts["word/document.xml"].content);
773 }
774
775 #[cfg(not(target_arch = "wasm32"))]
776 #[test]
777 fn parallel_entries_preserve_payloads_duplicates_and_error_order() {
778 let mut state = 123456789u32;
779 let payload: String = (0..512 * 1024).map(|_| {
780 state ^= state << 13;
781 state ^= state >> 17;
782 state ^= state << 5;
783 char::from(32 + (state % 95) as u8)
784 }).collect();
785 let mut zip = create_zip(&[
786 ZipSourceEntry { name: "word/document.xml", content: DOCUMENT_XML, deflate: false },
787 ZipSourceEntry { name: "payload/a.bin", content: &payload, deflate: true },
788 ZipSourceEntry { name: "payload/b.bin", content: &payload, deflate: true },
789 ZipSourceEntry { name: "payload/a.bin", content: "replacement", deflate: true },
790 ZipSourceEntry { name: "payload/", content: "", deflate: false },
791 ]);
792 let entries = parse_central_directory(&zip).unwrap();
793 let mut inflater = ZipInflater::default();
794 let expected: Vec<_> = entries.iter().map(|entry| {
795 (!entry.name.ends_with('/')).then(|| extract_entry_data(&zip, entry, &mut inflater))
796 }).collect();
797 assert_eq!(parallel_entry_data(&zip, &entries).unwrap(), expected);
798 let parsed = parse_docx(&zip).unwrap();
799 assert_eq!(parsed.binary_assets["payload/a.bin"], b"replacement");
800 assert_eq!(parsed.binary_assets["payload/b.bin"], payload.as_bytes());
801 assert!(!parsed.binary_assets.contains_key("payload/"));
802 let main = entries[0].local_header_offset as usize;
803 let content = main + 30 + read_u16_le(&zip, main + 26) as usize;
804 zip[content] = 0xff;
805 let later = entries[2].local_header_offset as usize;
806 zip[later..later + 4].fill(0);
807 assert!(parse_docx(&zip).unwrap_err().starts_with("Invalid or unsupported XML encoding:"));
808 }
809
810}