1mod archive;
8mod compatibility;
9mod container;
10mod flat_opc;
11mod namespaces;
12mod numbering;
13mod ooxml;
14mod relationships;
15mod review;
16mod structure;
17mod styles;
18
19use std::collections::{HashMap, HashSet};
20use std::fmt;
21
22pub use archive::{DocumentParts, DocxLimits, extract_document_parts, extract_document_xml};
23pub use container::ParagraphContainer;
24pub use ooxml::{
25 PackageParagraphId, ParagraphStructure, RevisionProjectionStatus, RevisionUnsupportedReason,
26 RevisionView, TextFormattingSpan, TextMaterialization, TextStyle,
27};
28pub use review::{
29 AttributedComment, AttributedRevision, CommentContent, DocumentReviewFacts, ReviewDetail,
30 ReviewFactLimits, ReviewFactSet, ReviewFactUnknownReason, ReviewPoint, ReviewSpan,
31 RevisionContent, RevisionFactKind, RevisionPayload,
32};
33pub use structure::{
34 BookmarkFact, DocumentStructureFacts, InternalReferenceFact, InternalReferenceRole,
35 NumberingHierarchyFact, ParagraphAlignmentFact, ParagraphAlignmentSource,
36 ParagraphAlignmentValue, ParagraphIndentation, ParagraphIndentationFact,
37 ParagraphOutlineLevelFact, SpanCoverage, StructuralFactSet, StructuralFactUnknownReason,
38 StructuralSpan,
39};
40pub use styles::is_semantic_highlight_color;
41
42#[derive(Clone, Debug, Eq, Hash, PartialEq)]
43pub struct InternalParagraphId(String);
44
45impl InternalParagraphId {
46 pub fn new(value: impl Into<String>) -> Result<Self, ProjectionError> {
53 let value = value.into();
54 if value.is_empty() || value.len() > 128 {
55 return Err(ProjectionError::InvalidInternalParagraphId);
56 }
57 Ok(Self(value))
58 }
59
60 #[must_use]
61 pub fn as_str(&self) -> &str {
62 &self.0
63 }
64}
65
66#[derive(Clone, Copy, Debug)]
67pub struct ParagraphIdentityFacts<'a> {
68 pub ordinal: usize,
69 pub package_paragraph_id: Option<PackageParagraphId>,
70 pub text: &'a str,
71}
72
73#[derive(Clone, Debug, Eq, PartialEq)]
74pub struct ProjectedParagraph {
75 pub id: InternalParagraphId,
76 pub ordinal: usize,
77 pub package_paragraph_id: Option<PackageParagraphId>,
78 pub style_id: Option<String>,
79 pub text: String,
80 pub formatting: Vec<TextFormattingSpan>,
81 pub structure: Option<ParagraphStructure>,
82 pub container: ParagraphContainer,
83 pub alignment: Option<ParagraphAlignmentFact>,
84}
85
86#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
87pub enum FormattingUnknownReason {
88 DocumentPartOnly,
89 StylesPartUnavailable,
90 UnsupportedStyles,
91 UnsupportedAlignment,
92}
93
94#[derive(Clone, Copy, Debug, Eq, PartialEq)]
95pub enum FormattingFactStatus {
96 Known,
97 Unknown(FormattingUnknownReason),
98}
99
100#[derive(Clone, Copy, Debug, Eq, PartialEq)]
101pub struct FormattingCompleteness {
102 pub alignment: FormattingFactStatus,
103 pub bold: FormattingFactStatus,
104 pub highlight: FormattingFactStatus,
105 pub superscript: FormattingFactStatus,
106}
107
108impl FormattingCompleteness {
109 fn from_styles(styles: Result<(), FormattingUnknownReason>) -> Self {
110 let status = |family| match family {
111 TextStyle::Highlight => FormattingFactStatus::Known,
113 TextStyle::Bold | TextStyle::Superscript => styles
114 .map_or_else(FormattingFactStatus::Unknown, |()| {
115 FormattingFactStatus::Known
116 }),
117 };
118 Self {
119 alignment: styles.map_or_else(FormattingFactStatus::Unknown, |()| {
120 FormattingFactStatus::Known
121 }),
122 bold: status(TextStyle::Bold),
123 highlight: status(TextStyle::Highlight),
124 superscript: status(TextStyle::Superscript),
125 }
126 }
127
128 fn mark_styles_unread(&mut self) {
129 let unread = Self::from_styles(Err(FormattingUnknownReason::UnsupportedStyles));
130 self.bold = unread.bold;
131 self.superscript = unread.superscript;
132 }
133
134 const fn mark_formatting_unread(&mut self) {
135 let unknown = FormattingFactStatus::Unknown(FormattingUnknownReason::UnsupportedStyles);
136 self.bold = unknown;
137 self.highlight = unknown;
138 self.superscript = unknown;
139 }
140}
141
142impl Default for FormattingCompleteness {
143 fn default() -> Self {
144 Self::from_styles(Err(FormattingUnknownReason::DocumentPartOnly))
145 }
146}
147
148#[derive(Clone, Debug, Eq, PartialEq)]
149pub struct DocumentProjection {
150 pub paragraphs: Vec<ProjectedParagraph>,
151 pub formatting_completeness: FormattingCompleteness,
152 pub revision_status: RevisionProjectionStatus,
153 pub structural_facts: DocumentStructureFacts,
154}
155
156#[derive(Clone, Debug, Eq, PartialEq)]
157pub struct DocumentPackageProjection {
158 pub document: DocumentProjection,
159 pub review_facts: DocumentReviewFacts,
160}
161
162#[derive(Clone, Copy, Debug, Eq, PartialEq)]
163pub struct ProjectionOptions {
164 pub revision_view: RevisionView,
165 pub text_materialization: TextMaterialization,
166}
167
168impl Default for ProjectionOptions {
169 fn default() -> Self {
170 Self {
171 revision_view: RevisionView::Current,
172 text_materialization: TextMaterialization::WordHost,
173 }
174 }
175}
176
177#[derive(Clone, Debug, Eq, PartialEq)]
178pub enum ProjectionError {
179 ArchiveTooLarge,
180 InvalidArchive,
181 InvalidFlatOpcPackage,
182 TooManyArchiveEntries,
183 InvalidPackageRelationships,
184 PackageRelationshipsTooLarge,
185 MissingDocumentXml,
186 DuplicateDocumentXml,
187 DuplicateStylesXml,
188 DuplicateNumberingXml,
189 EncryptedDocumentXml,
190 UnsupportedCompression(u16),
191 DocumentXmlTooLarge,
192 SuspiciousCompressionRatio,
193 InvalidDocumentXmlEntry,
194 DocumentXmlIntegrity,
195 StylesXmlTooLarge,
196 InvalidStylesXmlEntry,
197 StylesXmlIntegrity,
198 NumberingXmlTooLarge,
199 InvalidNumberingXmlEntry,
200 NumberingXmlIntegrity,
201 InvalidDocumentXml,
202 InvalidStylesXml,
203 InvalidNumberingXml,
204 MissingDocumentBody,
205 TooManyParagraphs,
206 TooManyStructuralFacts,
207 TooManyNumberingItems,
208 InvalidInternalParagraphId,
209 DuplicateInternalParagraphId,
210}
211
212impl fmt::Display for ProjectionError {
213 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
214 let message = match self {
215 Self::ArchiveTooLarge => "DOCX archive exceeds the configured size limit",
216 Self::InvalidFlatOpcPackage => "Flat OPC package has invalid structure",
217 Self::InvalidArchive => "DOCX archive is invalid",
218 Self::TooManyArchiveEntries => "DOCX archive has too many entries",
219 Self::InvalidPackageRelationships => "DOCX archive has invalid package relationships",
220 Self::PackageRelationshipsTooLarge => {
221 "DOCX package relationships exceed the configured size limit"
222 }
223 Self::MissingDocumentXml => "DOCX archive has no main document part",
224 Self::DuplicateDocumentXml => "DOCX archive has duplicate main document parts",
225 Self::DuplicateStylesXml => "DOCX archive has duplicate entries for its styles part",
226 Self::DuplicateNumberingXml => {
227 "DOCX archive has duplicate entries for its numbering part"
228 }
229 Self::EncryptedDocumentXml => "DOCX main document part is encrypted",
230 Self::UnsupportedCompression(_) => {
231 "selected DOCX package part uses unsupported compression"
232 }
233 Self::DocumentXmlTooLarge => {
234 "DOCX main document part exceeds the configured size limit"
235 }
236 Self::SuspiciousCompressionRatio => {
237 "selected DOCX package part exceeds the compression-ratio limit"
238 }
239 Self::InvalidDocumentXmlEntry => "DOCX main document part has an invalid ZIP entry",
240 Self::DocumentXmlIntegrity => "DOCX main document part failed size or CRC validation",
241 Self::StylesXmlTooLarge => "DOCX styles part exceeds the configured size limit",
242 Self::InvalidStylesXmlEntry => "DOCX styles part has an invalid ZIP entry",
243 Self::StylesXmlIntegrity => "DOCX styles part failed size or CRC validation",
244 Self::NumberingXmlTooLarge => "DOCX numbering part exceeds the configured size limit",
245 Self::InvalidNumberingXmlEntry => "DOCX numbering part has an invalid ZIP entry",
246 Self::NumberingXmlIntegrity => "DOCX numbering part failed size or CRC validation",
247 Self::InvalidDocumentXml => "DOCX main document part is invalid XML",
248 Self::InvalidStylesXml => "DOCX styles part is invalid XML",
249 Self::InvalidNumberingXml => "DOCX numbering part is invalid XML",
250 Self::MissingDocumentBody => "DOCX main document part has no document body",
251 Self::TooManyParagraphs => "DOCX main document part has too many paragraphs",
252 Self::TooManyStructuralFacts => {
253 "DOCX main document part produces too many structural facts"
254 }
255 Self::TooManyNumberingItems => "DOCX numbering part exceeds the configured item limit",
256 Self::InvalidInternalParagraphId => "application paragraph ID is invalid",
257 Self::DuplicateInternalParagraphId => "application paragraph IDs are not unique",
258 };
259 formatter.write_str(message)
260 }
261}
262
263impl std::error::Error for ProjectionError {}
264
265pub fn project_docx<F>(
272 bytes: &[u8],
273 limits: DocxLimits,
274 allocate_id: F,
275) -> Result<DocumentProjection, ProjectionError>
276where
277 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
278{
279 project_docx_with_options(bytes, limits, ProjectionOptions::default(), allocate_id)
280}
281
282pub fn project_docx_with_options<F>(
289 bytes: &[u8],
290 limits: DocxLimits,
291 options: ProjectionOptions,
292 allocate_id: F,
293) -> Result<DocumentProjection, ProjectionError>
294where
295 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
296{
297 let parts = extract_document_parts(bytes, limits)?;
298 project_parts(&parts, limits, options, allocate_id)
299}
300
301#[inline(never)]
303fn project_parts<F>(
304 parts: &DocumentParts,
305 limits: DocxLimits,
306 options: ProjectionOptions,
307 allocate_id: F,
308) -> Result<DocumentProjection, ProjectionError>
309where
310 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
311{
312 let styles = parts.styles_xml.as_deref().map_or(
313 Err(StructuralFactUnknownReason::StylesPartUnavailable),
314 |styles| {
315 styles::parse_styles(styles, limits.maximum_styles)
316 .map_err(|_| StructuralFactUnknownReason::UnsupportedStyles)
317 },
318 );
319 let numbering = parse_optional_numbering(
320 parts.numbering_xml.as_deref(),
321 limits.maximum_numbering_items,
322 )?;
323 project_document_xml_with_limit(
324 &parts.document_xml,
325 limits.maximum_paragraphs,
326 limits.maximum_structural_facts,
327 options,
328 ProjectionDependencies {
329 styles: styles.as_ref().map_err(|reason| *reason),
330 numbering: numbering
331 .as_ref()
332 .ok_or(StructuralFactUnknownReason::UnsupportedNumbering),
333 },
334 allocate_id,
335 )
336}
337
338pub fn project_docx_with_review_facts<F>(
350 bytes: &[u8],
351 limits: DocxLimits,
352 review_limits: ReviewFactLimits,
353 options: ProjectionOptions,
354 allocate_id: F,
355) -> Result<DocumentPackageProjection, ProjectionError>
356where
357 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
358{
359 let parts = archive::extract_projection_parts(bytes, limits, review_limits)?;
360 let styles = parts.styles.as_deref().map_or(
361 Err(StructuralFactUnknownReason::StylesPartUnavailable),
362 |styles| {
363 styles::parse_styles(styles, limits.maximum_styles)
364 .map_err(|_| StructuralFactUnknownReason::UnsupportedStyles)
365 },
366 );
367 let numbering =
368 parse_optional_numbering(parts.numbering.as_deref(), limits.maximum_numbering_items)?;
369 let ProjectedDocumentWithReview {
370 document,
371 revisions,
372 comment_anchors,
373 } = project_document_xml_with_limit_and_review(
374 &parts.document,
375 limits.maximum_paragraphs,
376 limits.maximum_structural_facts,
377 options,
378 ProjectionDependencies {
379 styles: styles.as_ref().map_err(|reason| *reason),
380 numbering: numbering
381 .as_ref()
382 .ok_or(StructuralFactUnknownReason::UnsupportedNumbering),
383 },
384 Some(ooxml::ReviewProjectionLimits {
385 maximum_facts: review_limits.maximum_facts_per_family,
386 maximum_detail_bytes: review_limits.maximum_review_detail_bytes,
387 }),
388 allocate_id,
389 )?;
390 let review_facts = review::project_review_facts(
391 revisions.unwrap_or(ReviewFactSet::Unknown(
392 ReviewFactUnknownReason::InvalidDocument,
393 )),
394 comment_anchors.as_ref(),
395 &document,
396 parts.comments,
397 parts.comments_extended,
398 review_limits,
399 options.text_materialization,
400 );
401 Ok(DocumentPackageProjection {
402 document,
403 review_facts,
404 })
405}
406
407fn parse_optional_numbering(
408 xml: Option<&[u8]>,
409 maximum_items: usize,
410) -> Result<Option<numbering::NumberingCatalog>, ProjectionError> {
411 let Some(xml) = xml else {
412 return Ok(None);
413 };
414 match numbering::parse_numbering(xml, maximum_items) {
415 Ok(catalog) => Ok(Some(catalog)),
416 Err(ProjectionError::TooManyNumberingItems) => Err(ProjectionError::TooManyNumberingItems),
417 Err(_) => Ok(None),
418 }
419}
420
421pub fn project_main_document_xml<F>(
430 xml: &[u8],
431 limits: DocxLimits,
432 allocate_id: F,
433) -> Result<DocumentProjection, ProjectionError>
434where
435 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
436{
437 if xml.len() > limits.maximum_archive_bytes {
438 return Err(ProjectionError::ArchiveTooLarge);
439 }
440 match flat_opc::input_kind(xml)? {
441 flat_opc::XmlInputKind::Document => {
442 if xml.len() > limits.maximum_document_xml_bytes {
443 return Err(ProjectionError::DocumentXmlTooLarge);
444 }
445 project_document_xml_with_limit(
446 xml,
447 limits.maximum_paragraphs,
448 limits.maximum_structural_facts,
449 ProjectionOptions::default(),
450 ProjectionDependencies {
451 styles: Err(StructuralFactUnknownReason::DocumentPartOnly),
452 numbering: Err(StructuralFactUnknownReason::DocumentPartOnly),
453 },
454 allocate_id,
455 )
456 }
457 flat_opc::XmlInputKind::Package => project_parts(
458 &flat_opc::extract_parts(xml, limits)?,
459 limits,
460 ProjectionOptions::default(),
461 allocate_id,
462 ),
463 }
464}
465
466pub fn project_paragraph_fragment<F>(
478 xml: &[u8],
479 mut limits: DocxLimits,
480 allocate_id: F,
481) -> Result<DocumentProjection, ProjectionError>
482where
483 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
484{
485 limits.maximum_paragraphs = limits.maximum_paragraphs.min(1);
486 let mut projection = project_parts(
487 &flat_opc::extract_parts(xml, limits)?,
488 limits,
489 ProjectionOptions::default(),
490 allocate_id,
491 )?;
492 let [paragraph] = projection.paragraphs.as_mut_slice() else {
493 return Err(ProjectionError::InvalidDocumentXml);
494 };
495 paragraph.structure = None;
496 projection.structural_facts = DocumentStructureFacts {
497 indentation: StructuralFactSet::Unknown(StructuralFactUnknownReason::ParagraphFragment),
498 numbering_hierarchy: StructuralFactSet::Unknown(
499 StructuralFactUnknownReason::ParagraphFragment,
500 ),
501 bookmarks: StructuralFactSet::Unknown(StructuralFactUnknownReason::ParagraphFragment),
502 internal_references: StructuralFactSet::Unknown(
503 StructuralFactUnknownReason::ParagraphFragment,
504 ),
505 outline_levels: StructuralFactSet::Unknown(StructuralFactUnknownReason::ParagraphFragment),
506 };
507 Ok(projection)
508}
509
510pub fn project_document_xml<F>(
517 xml: &[u8],
518 allocate_id: F,
519) -> Result<DocumentProjection, ProjectionError>
520where
521 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
522{
523 project_document_xml_with_options(xml, ProjectionOptions::default(), allocate_id)
524}
525
526pub fn project_document_xml_with_options<F>(
533 xml: &[u8],
534 options: ProjectionOptions,
535 allocate_id: F,
536) -> Result<DocumentProjection, ProjectionError>
537where
538 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
539{
540 project_document_xml_with_limit(
541 xml,
542 DocxLimits::default().maximum_paragraphs,
543 DocxLimits::default().maximum_structural_facts,
544 options,
545 ProjectionDependencies {
546 styles: Err(StructuralFactUnknownReason::DocumentPartOnly),
547 numbering: Err(StructuralFactUnknownReason::DocumentPartOnly),
548 },
549 allocate_id,
550 )
551}
552
553fn project_document_xml_with_limit<F>(
554 xml: &[u8],
555 maximum_paragraphs: usize,
556 maximum_structural_facts: usize,
557 options: ProjectionOptions,
558 dependencies: ProjectionDependencies<'_>,
559 allocate_id: F,
560) -> Result<DocumentProjection, ProjectionError>
561where
562 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
563{
564 project_document_xml_with_limit_and_review(
565 xml,
566 maximum_paragraphs,
567 maximum_structural_facts,
568 options,
569 dependencies,
570 None,
571 allocate_id,
572 )
573 .map(|projection| projection.document)
574}
575
576struct ProjectedDocumentWithReview {
577 document: DocumentProjection,
578 revisions: Option<ReviewFactSet<AttributedRevision>>,
579 comment_anchors: Option<HashMap<String, ReviewSpan>>,
580}
581
582#[derive(Clone, Copy)]
583struct ProjectionDependencies<'a> {
584 styles: Result<&'a structure::StyleSheet, StructuralFactUnknownReason>,
585 numbering: Result<&'a numbering::NumberingCatalog, StructuralFactUnknownReason>,
586}
587
588fn project_document_xml_with_limit_and_review<F>(
589 xml: &[u8],
590 maximum_paragraphs: usize,
591 maximum_structural_facts: usize,
592 options: ProjectionOptions,
593 dependencies: ProjectionDependencies<'_>,
594 review_limits: Option<ooxml::ReviewProjectionLimits>,
595 mut allocate_id: F,
596) -> Result<ProjectedDocumentWithReview, ProjectionError>
597where
598 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
599{
600 let projected = ooxml::project_document_xml(
601 xml,
602 maximum_paragraphs,
603 options.revision_view,
604 options.text_materialization,
605 dependencies.styles.map_err(formatting_unknown_reason),
606 review_limits,
607 )?;
608 let review_revisions = projected.review_revisions;
609 let review_comment_anchors = review_limits
610 .is_some()
611 .then_some(projected.review_comment_anchors);
612 let mut seen_ids = HashSet::with_capacity(projected.paragraphs.len());
613 let mut ids = Vec::with_capacity(projected.paragraphs.len());
614 for paragraph in &projected.paragraphs {
615 let facts = ParagraphIdentityFacts {
616 ordinal: paragraph.ordinal,
617 package_paragraph_id: paragraph.package_paragraph_id,
618 text: ¶graph.text,
619 };
620 let id = allocate_id(facts)?;
621 if !seen_ids.insert(id.clone()) {
622 return Err(ProjectionError::DuplicateInternalParagraphId);
623 }
624 ids.push(id);
625 }
626 let texts = projected
627 .paragraphs
628 .iter()
629 .map(|paragraph| paragraph.text.as_str())
630 .collect::<Vec<_>>();
631 let properties = projected
632 .paragraphs
633 .iter()
634 .map(|paragraph| ¶graph.properties)
635 .collect::<Vec<_>>();
636 let structural_facts = structure::materialize_structure(
637 structure::RawStructureInput {
638 paragraph_texts: &texts,
639 properties: &properties,
640 bookmarks: projected.bookmarks.as_deref().map_err(|reason| *reason),
641 references: projected.references.as_deref().map_err(|reason| *reason),
642 },
643 dependencies.styles,
644 dependencies.numbering,
645 maximum_structural_facts,
646 )?;
647 let mut formatting_completeness = projected.formatting_completeness;
648 formatting_completeness.alignment = FormattingFactStatus::Known;
649 let mut paragraphs = Vec::with_capacity(projected.paragraphs.len());
650 for (id, paragraph) in ids.into_iter().zip(projected.paragraphs) {
651 let alignment =
652 structure::resolve_paragraph_alignment(dependencies.styles, ¶graph.properties);
653 let alignment = match alignment {
654 Ok(fact) => fact,
655 Err(reason) => {
656 formatting_completeness.alignment = FormattingFactStatus::Unknown(reason);
657 None
658 }
659 };
660 paragraphs.push(ProjectedParagraph {
661 id,
662 ordinal: paragraph.ordinal,
663 package_paragraph_id: paragraph.package_paragraph_id,
664 style_id: paragraph.properties.style_id,
665 text: paragraph.text,
666 formatting: paragraph.formatting,
667 structure: paragraph.structure,
668 container: paragraph.container,
669 alignment,
670 });
671 }
672 Ok(ProjectedDocumentWithReview {
673 document: DocumentProjection {
674 paragraphs,
675 formatting_completeness,
676 revision_status: projected.revision_status,
677 structural_facts,
678 },
679 revisions: review_revisions,
680 comment_anchors: review_comment_anchors,
681 })
682}
683
684const fn formatting_unknown_reason(reason: StructuralFactUnknownReason) -> FormattingUnknownReason {
685 match reason {
686 StructuralFactUnknownReason::DocumentPartOnly
687 | StructuralFactUnknownReason::ParagraphFragment => {
688 FormattingUnknownReason::DocumentPartOnly
689 }
690 StructuralFactUnknownReason::StylesPartUnavailable => {
691 FormattingUnknownReason::StylesPartUnavailable
692 }
693 StructuralFactUnknownReason::UnsupportedStyles
694 | StructuralFactUnknownReason::UnsupportedNumbering
695 | StructuralFactUnknownReason::IncompleteBookmarkRanges
696 | StructuralFactUnknownReason::UnsupportedInternalReferences => {
697 FormattingUnknownReason::UnsupportedStyles
698 }
699 }
700}