Skip to main content

extend_docx/
zip.rs

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, &central_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, &central_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}