Skip to main content

stella_docx_kernel/projection/
archive.rs

1use miniz_oxide::inflate::{TINFLStatus, decompress_to_vec_with_limit};
2use rawzip::{CompressionMethod, ZipArchive, ZipSliceArchive, crc32};
3
4use crate::ProjectionError;
5use crate::projection::relationships::{
6    InvalidReviewRelationships, ReviewPartPaths, document_relationship_paths,
7    document_relationships_path, main_document_path,
8};
9use crate::projection::review::{ReviewFactLimits, ReviewFactUnknownReason};
10
11const DOCUMENT_XML_PATH: &[u8] = b"word/document.xml";
12const ROOT_RELATIONSHIPS_PATH: &[u8] = b"_rels/.rels";
13const MAXIMUM_ROOT_RELATIONSHIPS_BYTES: usize = 1024 * 1024;
14const MAXIMUM_DOCUMENT_RELATIONSHIPS_BYTES: usize = 1024 * 1024;
15
16#[derive(Clone, Copy, Debug, Eq, PartialEq)]
17pub struct DocxLimits {
18    pub maximum_archive_bytes: usize,
19    pub maximum_document_xml_bytes: usize,
20    pub maximum_styles_xml_bytes: usize,
21    pub maximum_styles: usize,
22    pub maximum_numbering_xml_bytes: usize,
23    pub maximum_numbering_items: usize,
24    pub maximum_entries: u64,
25    pub maximum_compression_ratio: u64,
26    pub maximum_paragraphs: usize,
27    pub maximum_structural_facts: usize,
28}
29
30impl Default for DocxLimits {
31    fn default() -> Self {
32        Self {
33            maximum_archive_bytes: 64 * 1024 * 1024,
34            maximum_document_xml_bytes: 32 * 1024 * 1024,
35            maximum_styles_xml_bytes: 8 * 1024 * 1024,
36            maximum_styles: 4_079,
37            maximum_numbering_xml_bytes: 8 * 1024 * 1024,
38            maximum_numbering_items: 100_000,
39            maximum_entries: 4096,
40            maximum_compression_ratio: 200,
41            maximum_paragraphs: 250_000,
42            maximum_structural_facts: 1_000_000,
43        }
44    }
45}
46
47#[derive(Clone, Copy)]
48struct XmlEntry {
49    wayfinder: rawzip::ZipArchiveEntryWayfinder,
50    method: CompressionMethod,
51    compressed_size: u64,
52    uncompressed_size: u64,
53    crc32: u32,
54    encrypted: bool,
55}
56
57struct PackageEntry {
58    path: Vec<u8>,
59    xml: XmlEntry,
60}
61
62struct EntryExtractionErrors {
63    invalid_entry: ProjectionError,
64    too_large: ProjectionError,
65    integrity: ProjectionError,
66}
67
68struct RelatedPartExtraction<'a> {
69    path: Option<&'a [u8]>,
70    maximum_bytes: usize,
71    duplicate: ProjectionError,
72    invalid_entry: ProjectionError,
73    too_large: ProjectionError,
74    errors: EntryExtractionErrors,
75}
76
77impl EntryExtractionErrors {
78    const DOCUMENT_XML: Self = Self {
79        invalid_entry: ProjectionError::InvalidDocumentXmlEntry,
80        too_large: ProjectionError::DocumentXmlTooLarge,
81        integrity: ProjectionError::DocumentXmlIntegrity,
82    };
83    const PACKAGE_RELATIONSHIPS: Self = Self {
84        invalid_entry: ProjectionError::InvalidPackageRelationships,
85        too_large: ProjectionError::PackageRelationshipsTooLarge,
86        integrity: ProjectionError::InvalidPackageRelationships,
87    };
88    const STYLES_XML: Self = Self {
89        invalid_entry: ProjectionError::InvalidStylesXmlEntry,
90        too_large: ProjectionError::StylesXmlTooLarge,
91        integrity: ProjectionError::StylesXmlIntegrity,
92    };
93    const NUMBERING_XML: Self = Self {
94        invalid_entry: ProjectionError::InvalidNumberingXmlEntry,
95        too_large: ProjectionError::NumberingXmlTooLarge,
96        integrity: ProjectionError::NumberingXmlIntegrity,
97    };
98}
99
100#[derive(Clone, Debug, Eq, PartialEq)]
101pub struct DocumentParts {
102    pub document_xml: Vec<u8>,
103    pub styles_xml: Option<Vec<u8>>,
104    pub numbering_xml: Option<Vec<u8>>,
105}
106
107pub(super) struct ProjectionPackageParts {
108    pub document: Vec<u8>,
109    pub styles: Option<Vec<u8>>,
110    pub numbering: Option<Vec<u8>>,
111    pub comments: Result<Option<Vec<u8>>, ReviewFactUnknownReason>,
112    pub comments_extended: Result<Option<Vec<u8>>, ReviewFactUnknownReason>,
113}
114
115struct RequiredPackageParts {
116    document: Vec<u8>,
117    review_paths: Result<ReviewPartPaths, InvalidReviewRelationships>,
118    styles: Option<Vec<u8>>,
119    numbering: Option<Vec<u8>>,
120}
121
122struct ReviewPackageParts {
123    comments: Result<Option<Vec<u8>>, ReviewFactUnknownReason>,
124    comments_extended: Result<Option<Vec<u8>>, ReviewFactUnknownReason>,
125}
126
127/// Extracts the relationship-resolved main document part from a bounded DOCX package.
128///
129/// # Errors
130///
131/// Returns [`ProjectionError`] when the package is invalid, unsupported, or
132/// exceeds the supplied resource limits.
133pub fn extract_document_xml(bytes: &[u8], limits: DocxLimits) -> Result<Vec<u8>, ProjectionError> {
134    extract_document_parts(bytes, limits).map(|parts| parts.document_xml)
135}
136
137/// Extracts the main document and its relationship-selected paragraph-style and
138/// numbering parts from one bounded scan of the ZIP directory.
139///
140/// # Errors
141///
142/// Returns [`ProjectionError`] when the package is invalid, unsupported, or
143/// exceeds the supplied resource limits.
144pub fn extract_document_parts(
145    bytes: &[u8],
146    limits: DocxLimits,
147) -> Result<DocumentParts, ProjectionError> {
148    extract_required_parts(bytes, limits)
149}
150
151pub(super) fn extract_projection_parts(
152    bytes: &[u8],
153    limits: DocxLimits,
154    review_limits: ReviewFactLimits,
155) -> Result<ProjectionPackageParts, ProjectionError> {
156    if bytes.len() > limits.maximum_archive_bytes {
157        return Err(ProjectionError::ArchiveTooLarge);
158    }
159    let archive = ZipArchive::from_slice(bytes).map_err(|_| ProjectionError::InvalidArchive)?;
160    let package_entries = scan_package_entries(&archive, limits)?;
161    let required = extract_required_parts_from_archive(&archive, &package_entries, limits)?;
162    let review = extract_review_parts(
163        &archive,
164        &package_entries,
165        required.review_paths,
166        review_limits,
167        limits,
168    );
169    Ok(ProjectionPackageParts {
170        document: required.document,
171        styles: required.styles,
172        numbering: required.numbering,
173        comments: review.comments,
174        comments_extended: review.comments_extended,
175    })
176}
177
178fn extract_required_parts(
179    bytes: &[u8],
180    limits: DocxLimits,
181) -> Result<DocumentParts, ProjectionError> {
182    if bytes.len() > limits.maximum_archive_bytes {
183        return Err(ProjectionError::ArchiveTooLarge);
184    }
185    let archive = ZipArchive::from_slice(bytes).map_err(|_| ProjectionError::InvalidArchive)?;
186    let package_entries = scan_package_entries(&archive, limits)?;
187
188    let required = extract_required_parts_from_archive(&archive, &package_entries, limits)?;
189    Ok(DocumentParts {
190        document_xml: required.document,
191        styles_xml: required.styles,
192        numbering_xml: required.numbering,
193    })
194}
195
196fn extract_required_parts_from_archive(
197    archive: &ZipSliceArchive<&[u8]>,
198    package_entries: &[PackageEntry],
199    limits: DocxLimits,
200) -> Result<RequiredPackageParts, ProjectionError> {
201    let document_path = resolve_document_path(archive, package_entries, limits)?;
202    let document = unique_entry(
203        package_entries,
204        &document_path,
205        ProjectionError::DuplicateDocumentXml,
206    )?
207    .ok_or(ProjectionError::MissingDocumentXml)?;
208    validate_selected_entry(
209        document,
210        limits.maximum_document_xml_bytes,
211        ProjectionError::DocumentXmlTooLarge,
212        ProjectionError::EncryptedDocumentXml,
213        limits,
214    )?;
215    let document_xml = extract_entry(
216        archive,
217        document,
218        &document_path,
219        limits.maximum_document_xml_bytes,
220        EntryExtractionErrors::DOCUMENT_XML,
221    )?;
222    let relationships_path = document_relationships_path(&document_path)?;
223    let document_relationships = unique_entry(
224        package_entries,
225        &relationships_path,
226        ProjectionError::InvalidPackageRelationships,
227    )?
228    .map(|relationships| {
229        validate_selected_entry(
230            relationships,
231            MAXIMUM_DOCUMENT_RELATIONSHIPS_BYTES,
232            ProjectionError::PackageRelationshipsTooLarge,
233            ProjectionError::InvalidPackageRelationships,
234            limits,
235        )?;
236        extract_entry(
237            archive,
238            relationships,
239            &relationships_path,
240            MAXIMUM_DOCUMENT_RELATIONSHIPS_BYTES,
241            EntryExtractionErrors::PACKAGE_RELATIONSHIPS,
242        )
243    })
244    .transpose()?;
245    let related_paths = document_relationships
246        .as_deref()
247        .map(|relationships| document_relationship_paths(relationships, &document_path))
248        .transpose()?
249        .unwrap_or_default();
250    let styles_xml = extract_related_part(
251        archive,
252        package_entries,
253        limits,
254        RelatedPartExtraction {
255            path: related_paths.styles.as_deref(),
256            maximum_bytes: limits.maximum_styles_xml_bytes,
257            duplicate: ProjectionError::DuplicateStylesXml,
258            invalid_entry: ProjectionError::InvalidStylesXmlEntry,
259            too_large: ProjectionError::StylesXmlTooLarge,
260            errors: EntryExtractionErrors::STYLES_XML,
261        },
262    )?;
263    let numbering_xml = extract_related_part(
264        archive,
265        package_entries,
266        limits,
267        RelatedPartExtraction {
268            path: related_paths.numbering.as_deref(),
269            maximum_bytes: limits.maximum_numbering_xml_bytes,
270            duplicate: ProjectionError::DuplicateNumberingXml,
271            invalid_entry: ProjectionError::InvalidNumberingXmlEntry,
272            too_large: ProjectionError::NumberingXmlTooLarge,
273            errors: EntryExtractionErrors::NUMBERING_XML,
274        },
275    )?;
276    Ok(RequiredPackageParts {
277        document: document_xml,
278        review_paths: related_paths.review,
279        styles: styles_xml,
280        numbering: numbering_xml,
281    })
282}
283
284fn extract_related_part(
285    archive: &ZipSliceArchive<&[u8]>,
286    entries: &[PackageEntry],
287    limits: DocxLimits,
288    extraction: RelatedPartExtraction<'_>,
289) -> Result<Option<Vec<u8>>, ProjectionError> {
290    let Some(path) = extraction.path else {
291        return Ok(None);
292    };
293    let selected = unique_entry(entries, path, extraction.duplicate)?
294        .ok_or_else(|| extraction.invalid_entry.clone())?;
295    validate_selected_entry(
296        selected,
297        extraction.maximum_bytes,
298        extraction.too_large,
299        extraction.invalid_entry,
300        limits,
301    )?;
302    extract_entry(
303        archive,
304        selected,
305        path,
306        extraction.maximum_bytes,
307        extraction.errors,
308    )
309    .map(Some)
310}
311
312fn extract_review_parts(
313    archive: &ZipSliceArchive<&[u8]>,
314    entries: &[PackageEntry],
315    paths: Result<ReviewPartPaths, InvalidReviewRelationships>,
316    review_limits: ReviewFactLimits,
317    limits: DocxLimits,
318) -> ReviewPackageParts {
319    let Ok(paths) = paths else {
320        return ReviewPackageParts {
321            comments: Err(ReviewFactUnknownReason::InvalidComments),
322            comments_extended: Err(ReviewFactUnknownReason::InvalidCommentsExtended),
323        };
324    };
325    let comments = paths.comments.map_or(Ok(None), |path| {
326        extract_optional_review_entry(
327            archive,
328            entries,
329            &path,
330            review_limits.maximum_comments_xml_bytes,
331            ReviewFactUnknownReason::InvalidComments,
332            limits,
333        )
334        .and_then(|value| {
335            value
336                .ok_or(ReviewFactUnknownReason::InvalidComments)
337                .map(Some)
338        })
339    });
340    let comments_extended = paths.comments_extended.map_or(Ok(None), |path| {
341        extract_optional_review_entry(
342            archive,
343            entries,
344            &path,
345            review_limits.maximum_comments_extended_xml_bytes,
346            ReviewFactUnknownReason::InvalidCommentsExtended,
347            limits,
348        )
349        .and_then(|value| {
350            value
351                .ok_or(ReviewFactUnknownReason::InvalidCommentsExtended)
352                .map(Some)
353        })
354    });
355    ReviewPackageParts {
356        comments,
357        comments_extended,
358    }
359}
360
361fn extract_optional_review_entry(
362    archive: &ZipSliceArchive<&[u8]>,
363    entries: &[PackageEntry],
364    path: &[u8],
365    maximum_bytes: usize,
366    invalid: ReviewFactUnknownReason,
367    limits: DocxLimits,
368) -> Result<Option<Vec<u8>>, ReviewFactUnknownReason> {
369    let selected =
370        unique_entry(entries, path, ProjectionError::InvalidArchive).map_err(|_| invalid)?;
371    let Some(selected) = selected else {
372        return Ok(None);
373    };
374    let validation = validate_selected_entry(
375        selected,
376        maximum_bytes,
377        ProjectionError::DocumentXmlTooLarge,
378        ProjectionError::InvalidDocumentXmlEntry,
379        limits,
380    );
381    validation.map_err(|error| match error {
382        ProjectionError::DocumentXmlTooLarge | ProjectionError::SuspiciousCompressionRatio => {
383            ReviewFactUnknownReason::ResourceLimit
384        }
385        _ => invalid,
386    })?;
387    let extracted = extract_entry(
388        archive,
389        selected,
390        path,
391        maximum_bytes,
392        EntryExtractionErrors::DOCUMENT_XML,
393    );
394    extracted.map(Some).map_err(|error| match error {
395        ProjectionError::DocumentXmlTooLarge | ProjectionError::SuspiciousCompressionRatio => {
396            ReviewFactUnknownReason::ResourceLimit
397        }
398        _ => invalid,
399    })
400}
401
402fn scan_package_entries(
403    archive: &ZipSliceArchive<&[u8]>,
404    limits: DocxLimits,
405) -> Result<Vec<PackageEntry>, ProjectionError> {
406    if archive.entries_hint() > limits.maximum_entries {
407        return Err(ProjectionError::TooManyArchiveEntries);
408    }
409
410    let mut package_entries = Vec::new();
411    let mut entries = archive.entries();
412    let mut entry_count = 0_u64;
413    while let Some(entry) = entries
414        .next_entry()
415        .map_err(|_| ProjectionError::InvalidArchive)?
416    {
417        entry_count = entry_count
418            .checked_add(1)
419            .ok_or(ProjectionError::TooManyArchiveEntries)?;
420        if entry_count > limits.maximum_entries {
421            return Err(ProjectionError::TooManyArchiveEntries);
422        }
423        package_entries.push(PackageEntry {
424            path: entry.file_path().as_ref().to_vec(),
425            xml: XmlEntry {
426                wayfinder: entry.wayfinder(),
427                method: entry.compression_method(),
428                compressed_size: entry.compressed_size_hint(),
429                uncompressed_size: entry.uncompressed_size_hint(),
430                crc32: entry.crc32(),
431                encrypted: entry.flags().is_encrypted(),
432            },
433        });
434    }
435    Ok(package_entries)
436}
437
438fn resolve_document_path(
439    archive: &ZipSliceArchive<&[u8]>,
440    package_entries: &[PackageEntry],
441    limits: DocxLimits,
442) -> Result<Vec<u8>, ProjectionError> {
443    if let Some(relationships) = unique_entry(
444        package_entries,
445        ROOT_RELATIONSHIPS_PATH,
446        ProjectionError::InvalidPackageRelationships,
447    )? {
448        validate_selected_entry(
449            relationships,
450            MAXIMUM_ROOT_RELATIONSHIPS_BYTES,
451            ProjectionError::PackageRelationshipsTooLarge,
452            ProjectionError::InvalidPackageRelationships,
453            limits,
454        )?;
455        let relationships_xml = extract_entry(
456            archive,
457            relationships,
458            ROOT_RELATIONSHIPS_PATH,
459            MAXIMUM_ROOT_RELATIONSHIPS_BYTES,
460            EntryExtractionErrors::PACKAGE_RELATIONSHIPS,
461        )?;
462        main_document_path(&relationships_xml)
463    } else {
464        Ok(DOCUMENT_XML_PATH.to_vec())
465    }
466}
467
468fn unique_entry(
469    entries: &[PackageEntry],
470    path: &[u8],
471    duplicate: ProjectionError,
472) -> Result<Option<XmlEntry>, ProjectionError> {
473    let mut matches = entries
474        .iter()
475        .filter(|entry| entry.path == path)
476        .map(|entry| entry.xml);
477    let first = matches.next();
478    if matches.next().is_some() {
479        return Err(duplicate);
480    }
481    Ok(first)
482}
483
484fn validate_selected_entry(
485    entry: XmlEntry,
486    maximum_bytes: usize,
487    too_large: ProjectionError,
488    encrypted: ProjectionError,
489    limits: DocxLimits,
490) -> Result<(), ProjectionError> {
491    if entry.encrypted {
492        return Err(encrypted);
493    }
494    if entry.method != CompressionMethod::STORE && entry.method != CompressionMethod::DEFLATE {
495        return Err(ProjectionError::UnsupportedCompression(
496            entry.method.as_u16(),
497        ));
498    }
499    validate_declared_sizes(
500        entry.compressed_size,
501        entry.uncompressed_size,
502        maximum_bytes,
503        too_large,
504        limits,
505    )
506}
507
508fn extract_entry(
509    archive: &ZipSliceArchive<&[u8]>,
510    entry: XmlEntry,
511    expected_path: &[u8],
512    maximum_bytes: usize,
513    errors: EntryExtractionErrors,
514) -> Result<Vec<u8>, ProjectionError> {
515    let local = archive
516        .get_entry(entry.wayfinder)
517        .map_err(|_| errors.invalid_entry.clone())?;
518    let local_header = local.local_header();
519    if local_header.compression_method() != entry.method
520        || local_header.file_path().as_ref() != expected_path
521        || local_header.flags().is_encrypted()
522    {
523        return Err(errors.invalid_entry);
524    }
525    let output = match entry.method {
526        CompressionMethod::STORE => local.data().to_vec(),
527        CompressionMethod::DEFLATE => decompress_to_vec_with_limit(local.data(), maximum_bytes)
528            .map_err(|error| {
529                if error.status == TINFLStatus::HasMoreOutput {
530                    errors.too_large.clone()
531                } else {
532                    errors.invalid_entry.clone()
533                }
534            })?,
535        _ => {
536            return Err(ProjectionError::UnsupportedCompression(
537                entry.method.as_u16(),
538            ));
539        }
540    };
541    let expected_size = usize::try_from(entry.uncompressed_size).map_err(|_| errors.too_large)?;
542    if output.len() != expected_size || crc32(&output) != entry.crc32 {
543        return Err(errors.integrity);
544    }
545    if u64::try_from(local.data().len()).ok() != Some(entry.compressed_size) {
546        return Err(errors.integrity);
547    }
548    Ok(output)
549}
550
551fn validate_declared_sizes(
552    compressed_size: u64,
553    uncompressed_size: u64,
554    maximum_bytes: usize,
555    too_large: ProjectionError,
556    limits: DocxLimits,
557) -> Result<(), ProjectionError> {
558    let uncompressed_size_as_usize =
559        usize::try_from(uncompressed_size).map_err(|_| too_large.clone())?;
560    if uncompressed_size_as_usize > maximum_bytes {
561        return Err(too_large);
562    }
563    if uncompressed_size == 0 {
564        return Ok(());
565    }
566    if compressed_size == 0
567        || uncompressed_size > compressed_size.saturating_mul(limits.maximum_compression_ratio)
568    {
569        return Err(ProjectionError::SuspiciousCompressionRatio);
570    }
571    Ok(())
572}