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