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
427fn 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
435fn concat_bytes(chunks: &[&[u8]]) -> Vec<u8> {
436 let total_length: usize = chunks.iter().map(|chunk| chunk.len()).sum();
437 let mut output = Vec::with_capacity(total_length);
438 for chunk in chunks {
439 output.extend_from_slice(chunk);
440 }
441 output
442}
443
444pub fn package_to_bytes(pkg: &OoxmlPackage) -> Result<Vec<u8>, String> {
445 let mut entries: Vec<(String, Vec<u8>)> = pkg
446 .parts
447 .values()
448 .map(|part| (part.name.clone(), part.content.as_bytes().to_vec()))
449 .chain(
450 pkg.binary_assets
451 .iter()
452 .map(|(name, data)| (name.clone(), data.clone())),
453 )
454 .collect();
455
456 entries.sort_by(|left, right| left.0.cmp(&right.0));
457
458 if entries.is_empty() {
459 return Err("Cannot create DOCX ZIP from an empty package".to_string());
460 }
461
462 if entries.len() > 0xffff {
463 return Err("Too many ZIP entries for non-ZIP64 writer".to_string());
464 }
465
466 let mut local_chunks: Vec<Vec<u8>> = Vec::new();
467 let mut central_chunks: Vec<Vec<u8>> = Vec::new();
468 let mut offset = 0u32;
469
470 for (name, payload) in &entries {
471 let name_bytes = name.as_bytes();
472 let crc = crc32(payload);
473
474 let mut local_header = vec![0u8; 30 + name_bytes.len() + payload.len()];
475 write_u32_le(&mut local_header, 0, ZIP_LOCAL_FILE_HEADER_SIGNATURE);
476 write_u16_le(&mut local_header, 4, 20);
477 write_u16_le(&mut local_header, 6, UTF8_FLAG);
478 write_u16_le(&mut local_header, 8, STORE_COMPRESSION);
479 write_u16_le(&mut local_header, 10, 0);
480 write_u16_le(&mut local_header, 12, 0);
481 write_u32_le(&mut local_header, 14, crc);
482 write_u32_le(&mut local_header, 18, payload.len() as u32);
483 write_u32_le(&mut local_header, 22, payload.len() as u32);
484 write_u16_le(&mut local_header, 26, name_bytes.len() as u16);
485 write_u16_le(&mut local_header, 28, 0);
486 local_header[30..30 + name_bytes.len()].copy_from_slice(name_bytes);
487 local_header[30 + name_bytes.len()..].copy_from_slice(payload);
488 local_chunks.push(local_header.clone());
489
490 let mut central_header = vec![0u8; 46 + name_bytes.len()];
491 write_u32_le(&mut central_header, 0, ZIP_CENTRAL_DIRECTORY_SIGNATURE);
492 write_u16_le(&mut central_header, 4, 20);
493 write_u16_le(&mut central_header, 6, 20);
494 write_u16_le(&mut central_header, 8, UTF8_FLAG);
495 write_u16_le(&mut central_header, 10, STORE_COMPRESSION);
496 write_u16_le(&mut central_header, 12, 0);
497 write_u16_le(&mut central_header, 14, 0);
498 write_u32_le(&mut central_header, 16, crc);
499 write_u32_le(&mut central_header, 20, payload.len() as u32);
500 write_u32_le(&mut central_header, 24, payload.len() as u32);
501 write_u16_le(&mut central_header, 28, name_bytes.len() as u16);
502 write_u16_le(&mut central_header, 30, 0);
503 write_u16_le(&mut central_header, 32, 0);
504 write_u16_le(&mut central_header, 34, 0);
505 write_u16_le(&mut central_header, 36, 0);
506 write_u32_le(&mut central_header, 38, 0);
507 write_u32_le(&mut central_header, 42, offset);
508 central_header[46..].copy_from_slice(name_bytes);
509 central_chunks.push(central_header);
510
511 offset += local_header.len() as u32;
512 }
513
514 let central_directory_offset = offset;
515 let central_directory: Vec<u8> = central_chunks.into_iter().flatten().collect();
516 let local_file_data: Vec<u8> = local_chunks.into_iter().flatten().collect();
517
518 let mut eocd = vec![0u8; 22];
519 write_u32_le(&mut eocd, 0, ZIP_EOCD_SIGNATURE);
520 write_u16_le(&mut eocd, 4, 0);
521 write_u16_le(&mut eocd, 6, 0);
522 write_u16_le(&mut eocd, 8, entries.len() as u16);
523 write_u16_le(&mut eocd, 10, entries.len() as u16);
524 write_u32_le(&mut eocd, 12, central_directory.len() as u32);
525 write_u32_le(&mut eocd, 16, central_directory_offset);
526 write_u16_le(&mut eocd, 20, 0);
527
528 Ok(concat_bytes(&[&local_file_data, ¢ral_directory, &eocd]))
529}
530
531pub fn create_minimal_docx_package(document_xml: Option<&str>) -> OoxmlPackage {
532 let document_xml = document_xml.unwrap_or(DEFAULT_DOCUMENT_XML);
533
534 let mut parts = HashMap::new();
535 parts.insert(
536 "[Content_Types].xml".to_string(),
537 OoxmlPart {
538 name: "[Content_Types].xml".to_string(),
539 content: DEFAULT_CONTENT_TYPES_XML.to_string(),
540 },
541 );
542 parts.insert(
543 "_rels/.rels".to_string(),
544 OoxmlPart {
545 name: "_rels/.rels".to_string(),
546 content: DEFAULT_ROOT_RELS_XML.to_string(),
547 },
548 );
549 parts.insert(
550 "word/document.xml".to_string(),
551 OoxmlPart {
552 name: "word/document.xml".to_string(),
553 content: document_xml.to_string(),
554 },
555 );
556 parts.insert(
557 "word/_rels/document.xml.rels".to_string(),
558 OoxmlPart {
559 name: "word/_rels/document.xml.rels".to_string(),
560 content: DEFAULT_DOCUMENT_RELS_XML.to_string(),
561 },
562 );
563
564 OoxmlPackage {
565 parts,
566 binary_assets: HashMap::new(),
567 warnings: Vec::new(),
568 }
569}
570
571#[cfg(test)]
572mod tests {
573 use super::*;
574 use crate::package::get_part;
575
576 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>"#;
577
578 struct ZipSourceEntry<'a> {
579 name: &'a str,
580 content: &'a str,
581 deflate: bool,
582 }
583
584 fn create_zip(entries: &[ZipSourceEntry<'_>]) -> Vec<u8> {
585 let mut local_chunks: Vec<Vec<u8>> = Vec::new();
586 let mut central_chunks: Vec<Vec<u8>> = Vec::new();
587 let mut offset = 0u32;
588
589 for entry in entries {
590 let name_bytes = entry.name.as_bytes();
591 let uncompressed = entry.content.as_bytes();
592 let compression_method = if entry.deflate {
593 DEFLATE_COMPRESSION
594 } else {
595 STORE_COMPRESSION
596 };
597 let compressed = if entry.deflate {
598 miniz_oxide::deflate::compress_to_vec(uncompressed, 6)
599 } else {
600 uncompressed.to_vec()
601 };
602 let crc = crc32(uncompressed);
603
604 let mut local = vec![0u8; 30 + name_bytes.len() + compressed.len()];
605 write_u32_le(&mut local, 0, ZIP_LOCAL_FILE_HEADER_SIGNATURE);
606 write_u16_le(&mut local, 4, 20);
607 write_u16_le(&mut local, 6, UTF8_FLAG);
608 write_u16_le(&mut local, 8, compression_method);
609 write_u16_le(&mut local, 10, 0);
610 write_u16_le(&mut local, 12, 0);
611 write_u32_le(&mut local, 14, crc);
612 write_u32_le(&mut local, 18, compressed.len() as u32);
613 write_u32_le(&mut local, 22, uncompressed.len() as u32);
614 write_u16_le(&mut local, 26, name_bytes.len() as u16);
615 write_u16_le(&mut local, 28, 0);
616 local[30..30 + name_bytes.len()].copy_from_slice(name_bytes);
617 local[30 + name_bytes.len()..].copy_from_slice(&compressed);
618 local_chunks.push(local.clone());
619
620 let mut central = vec![0u8; 46 + name_bytes.len()];
621 write_u32_le(&mut central, 0, ZIP_CENTRAL_DIRECTORY_SIGNATURE);
622 write_u16_le(&mut central, 4, 20);
623 write_u16_le(&mut central, 6, 20);
624 write_u16_le(&mut central, 8, UTF8_FLAG);
625 write_u16_le(&mut central, 10, compression_method);
626 write_u16_le(&mut central, 12, 0);
627 write_u16_le(&mut central, 14, 0);
628 write_u32_le(&mut central, 16, crc);
629 write_u32_le(&mut central, 20, compressed.len() as u32);
630 write_u32_le(&mut central, 24, uncompressed.len() as u32);
631 write_u16_le(&mut central, 28, name_bytes.len() as u16);
632 write_u16_le(&mut central, 30, 0);
633 write_u16_le(&mut central, 32, 0);
634 write_u16_le(&mut central, 34, 0);
635 write_u16_le(&mut central, 36, 0);
636 write_u32_le(&mut central, 38, 0);
637 write_u32_le(&mut central, 42, offset);
638 central[46..].copy_from_slice(name_bytes);
639 central_chunks.push(central);
640
641 offset += local.len() as u32;
642 }
643
644 let local_data: Vec<u8> = local_chunks.into_iter().flatten().collect();
645 let central_data: Vec<u8> = central_chunks.into_iter().flatten().collect();
646
647 let mut eocd = vec![0u8; 22];
648 write_u32_le(&mut eocd, 0, ZIP_EOCD_SIGNATURE);
649 write_u16_le(&mut eocd, 4, 0);
650 write_u16_le(&mut eocd, 6, 0);
651 write_u16_le(&mut eocd, 8, entries.len() as u16);
652 write_u16_le(&mut eocd, 10, entries.len() as u16);
653 write_u32_le(&mut eocd, 12, central_data.len() as u32);
654 write_u32_le(&mut eocd, 16, local_data.len() as u32);
655 write_u16_le(&mut eocd, 20, 0);
656
657 concat_bytes(&[&local_data, ¢ral_data, &eocd])
658 }
659
660 #[test]
661 fn throws_on_empty_input() {
662 let error = parse_docx(&[]).unwrap_err();
663 assert!(error.contains("cannot be empty"));
664 }
665
666 #[test]
667 fn parses_stored_zip_entries() {
668 let zip = create_zip(&[
669 ZipSourceEntry {
670 name: "[Content_Types].xml",
671 content: "<Types/>",
672 deflate: false,
673 },
674 ZipSourceEntry {
675 name: "word/document.xml",
676 content: DOCUMENT_XML,
677 deflate: false,
678 },
679 ]);
680 let pkg = parse_docx(&zip).expect("parse stored zip");
681
682 assert!(get_part(&pkg, "word/document.xml")
683 .expect("document part")
684 .content
685 .contains("Hello DOCX"));
686 }
687
688 #[test]
689 fn parses_deflated_zip_entries() {
690 let zip = create_zip(&[
691 ZipSourceEntry {
692 name: "[Content_Types].xml",
693 content: "<Types/>",
694 deflate: true,
695 },
696 ZipSourceEntry {
697 name: "word/document.xml",
698 content: DOCUMENT_XML,
699 deflate: true,
700 },
701 ]);
702 let pkg = parse_docx(&zip).expect("parse deflated zip");
703
704 assert!(get_part(&pkg, "word/document.xml")
705 .expect("document part")
706 .content
707 .contains("Hello DOCX"));
708 }
709
710 #[test]
711 fn writes_and_reparses_docx_zip_archives() {
712 let source = create_minimal_docx_package(Some(DOCUMENT_XML));
713 let zip = package_to_bytes(&source).expect("package to bytes");
714 let reparsed = parse_docx(&zip).expect("reparse docx");
715
716 assert!(reparsed
717 .parts
718 .get("word/document.xml")
719 .expect("document part")
720 .content
721 .contains("Hello DOCX"));
722 }
723 #[cfg(not(target_arch = "wasm32"))]
724 #[test]
725 fn parallel_entries_preserve_payloads_duplicates_and_error_order() {
726 let mut state = 123456789u32;
727 let payload: String = (0..512 * 1024).map(|_| {
728 state ^= state << 13;
729 state ^= state >> 17;
730 state ^= state << 5;
731 char::from(32 + (state % 95) as u8)
732 }).collect();
733 let mut zip = create_zip(&[
734 ZipSourceEntry { name: "word/document.xml", content: DOCUMENT_XML, deflate: false },
735 ZipSourceEntry { name: "payload/a.bin", content: &payload, deflate: true },
736 ZipSourceEntry { name: "payload/b.bin", content: &payload, deflate: true },
737 ZipSourceEntry { name: "payload/a.bin", content: "replacement", deflate: true },
738 ZipSourceEntry { name: "payload/", content: "", deflate: false },
739 ]);
740 let entries = parse_central_directory(&zip).unwrap();
741 let mut inflater = ZipInflater::default();
742 let expected: Vec<_> = entries.iter().map(|entry| {
743 (!entry.name.ends_with('/')).then(|| extract_entry_data(&zip, entry, &mut inflater))
744 }).collect();
745 assert_eq!(parallel_entry_data(&zip, &entries).unwrap(), expected);
746 let parsed = parse_docx(&zip).unwrap();
747 assert_eq!(parsed.binary_assets["payload/a.bin"], b"replacement");
748 assert_eq!(parsed.binary_assets["payload/b.bin"], payload.as_bytes());
749 assert!(!parsed.binary_assets.contains_key("payload/"));
750 let main = entries[0].local_header_offset as usize;
751 let content = main + 30 + read_u16_le(&zip, main + 26) as usize;
752 zip[content] = 0xff;
753 let later = entries[2].local_header_offset as usize;
754 zip[later..later + 4].fill(0);
755 assert!(parse_docx(&zip).unwrap_err().starts_with("Invalid or unsupported XML encoding:"));
756 }
757
758}