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