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
127pub 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
137pub 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}