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