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, PartialEq)]
84pub enum FormattingUnknownReason {
85 DocumentPartOnly,
86 StylesPartUnavailable,
87 UnsupportedStyles,
88}
89
90#[derive(Clone, Copy, Debug, Eq, PartialEq)]
91pub enum FormattingProjectionStatus {
92 Complete,
93 Incomplete(FormattingUnknownReason),
94}
95
96impl Default for FormattingProjectionStatus {
97 fn default() -> Self {
98 Self::Incomplete(FormattingUnknownReason::DocumentPartOnly)
99 }
100}
101
102#[derive(Clone, Debug, Eq, PartialEq)]
103pub struct DocumentProjection {
104 pub paragraphs: Vec<ProjectedParagraph>,
105 pub formatting_status: FormattingProjectionStatus,
106 pub revision_status: RevisionProjectionStatus,
107 pub structural_facts: DocumentStructureFacts,
108}
109
110#[derive(Clone, Debug, Eq, PartialEq)]
111pub struct DocumentPackageProjection {
112 pub document: DocumentProjection,
113 pub review_facts: DocumentReviewFacts,
114}
115
116#[derive(Clone, Copy, Debug, Eq, PartialEq)]
117pub struct ProjectionOptions {
118 pub revision_view: RevisionView,
119 pub text_materialization: TextMaterialization,
120}
121
122impl Default for ProjectionOptions {
123 fn default() -> Self {
124 Self {
125 revision_view: RevisionView::Current,
126 text_materialization: TextMaterialization::WordHost,
127 }
128 }
129}
130
131#[derive(Clone, Debug, Eq, PartialEq)]
132pub enum ProjectionError {
133 ArchiveTooLarge,
134 InvalidArchive,
135 InvalidFlatOpcPackage,
136 TooManyArchiveEntries,
137 InvalidPackageRelationships,
138 PackageRelationshipsTooLarge,
139 MissingDocumentXml,
140 DuplicateDocumentXml,
141 DuplicateStylesXml,
142 DuplicateNumberingXml,
143 EncryptedDocumentXml,
144 UnsupportedCompression(u16),
145 DocumentXmlTooLarge,
146 SuspiciousCompressionRatio,
147 InvalidDocumentXmlEntry,
148 DocumentXmlIntegrity,
149 StylesXmlTooLarge,
150 InvalidStylesXmlEntry,
151 StylesXmlIntegrity,
152 NumberingXmlTooLarge,
153 InvalidNumberingXmlEntry,
154 NumberingXmlIntegrity,
155 InvalidDocumentXml,
156 InvalidStylesXml,
157 InvalidNumberingXml,
158 MissingDocumentBody,
159 TooManyParagraphs,
160 TooManyStructuralFacts,
161 TooManyNumberingItems,
162 InvalidInternalParagraphId,
163 DuplicateInternalParagraphId,
164}
165
166impl fmt::Display for ProjectionError {
167 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
168 let message = match self {
169 Self::ArchiveTooLarge => "DOCX archive exceeds the configured size limit",
170 Self::InvalidFlatOpcPackage => "Flat OPC package has invalid structure",
171 Self::InvalidArchive => "DOCX archive is invalid",
172 Self::TooManyArchiveEntries => "DOCX archive has too many entries",
173 Self::InvalidPackageRelationships => "DOCX archive has invalid package relationships",
174 Self::PackageRelationshipsTooLarge => {
175 "DOCX package relationships exceed the configured size limit"
176 }
177 Self::MissingDocumentXml => "DOCX archive has no main document part",
178 Self::DuplicateDocumentXml => "DOCX archive has duplicate main document parts",
179 Self::DuplicateStylesXml => "DOCX archive has duplicate entries for its styles part",
180 Self::DuplicateNumberingXml => {
181 "DOCX archive has duplicate entries for its numbering part"
182 }
183 Self::EncryptedDocumentXml => "DOCX main document part is encrypted",
184 Self::UnsupportedCompression(_) => {
185 "selected DOCX package part uses unsupported compression"
186 }
187 Self::DocumentXmlTooLarge => {
188 "DOCX main document part exceeds the configured size limit"
189 }
190 Self::SuspiciousCompressionRatio => {
191 "selected DOCX package part exceeds the compression-ratio limit"
192 }
193 Self::InvalidDocumentXmlEntry => "DOCX main document part has an invalid ZIP entry",
194 Self::DocumentXmlIntegrity => "DOCX main document part failed size or CRC validation",
195 Self::StylesXmlTooLarge => "DOCX styles part exceeds the configured size limit",
196 Self::InvalidStylesXmlEntry => "DOCX styles part has an invalid ZIP entry",
197 Self::StylesXmlIntegrity => "DOCX styles part failed size or CRC validation",
198 Self::NumberingXmlTooLarge => "DOCX numbering part exceeds the configured size limit",
199 Self::InvalidNumberingXmlEntry => "DOCX numbering part has an invalid ZIP entry",
200 Self::NumberingXmlIntegrity => "DOCX numbering part failed size or CRC validation",
201 Self::InvalidDocumentXml => "DOCX main document part is invalid XML",
202 Self::InvalidStylesXml => "DOCX styles part is invalid XML",
203 Self::InvalidNumberingXml => "DOCX numbering part is invalid XML",
204 Self::MissingDocumentBody => "DOCX main document part has no document body",
205 Self::TooManyParagraphs => "DOCX main document part has too many paragraphs",
206 Self::TooManyStructuralFacts => {
207 "DOCX main document part produces too many structural facts"
208 }
209 Self::TooManyNumberingItems => "DOCX numbering part exceeds the configured item limit",
210 Self::InvalidInternalParagraphId => "application paragraph ID is invalid",
211 Self::DuplicateInternalParagraphId => "application paragraph IDs are not unique",
212 };
213 formatter.write_str(message)
214 }
215}
216
217impl std::error::Error for ProjectionError {}
218
219pub fn project_docx<F>(
226 bytes: &[u8],
227 limits: DocxLimits,
228 allocate_id: F,
229) -> Result<DocumentProjection, ProjectionError>
230where
231 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
232{
233 project_docx_with_options(bytes, limits, ProjectionOptions::default(), allocate_id)
234}
235
236pub fn project_docx_with_options<F>(
243 bytes: &[u8],
244 limits: DocxLimits,
245 options: ProjectionOptions,
246 allocate_id: F,
247) -> Result<DocumentProjection, ProjectionError>
248where
249 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
250{
251 let parts = extract_document_parts(bytes, limits)?;
252 project_parts(&parts, limits, options, allocate_id)
253}
254
255#[inline(never)]
257fn project_parts<F>(
258 parts: &DocumentParts,
259 limits: DocxLimits,
260 options: ProjectionOptions,
261 allocate_id: F,
262) -> Result<DocumentProjection, ProjectionError>
263where
264 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
265{
266 let styles = parts.styles_xml.as_deref().map_or(
267 Err(StructuralFactUnknownReason::StylesPartUnavailable),
268 |styles| {
269 styles::parse_styles(styles, limits.maximum_styles)
270 .map_err(|_| StructuralFactUnknownReason::UnsupportedStyles)
271 },
272 );
273 let numbering = parse_optional_numbering(
274 parts.numbering_xml.as_deref(),
275 limits.maximum_numbering_items,
276 )?;
277 project_document_xml_with_limit(
278 &parts.document_xml,
279 limits.maximum_paragraphs,
280 limits.maximum_structural_facts,
281 options,
282 ProjectionDependencies {
283 styles: styles.as_ref().map_err(|reason| *reason),
284 numbering: numbering
285 .as_ref()
286 .ok_or(StructuralFactUnknownReason::UnsupportedNumbering),
287 },
288 allocate_id,
289 )
290}
291
292pub fn project_docx_with_review_facts<F>(
304 bytes: &[u8],
305 limits: DocxLimits,
306 review_limits: ReviewFactLimits,
307 options: ProjectionOptions,
308 allocate_id: F,
309) -> Result<DocumentPackageProjection, ProjectionError>
310where
311 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
312{
313 let parts = archive::extract_projection_parts(bytes, limits, review_limits)?;
314 let styles = parts.styles.as_deref().map_or(
315 Err(StructuralFactUnknownReason::StylesPartUnavailable),
316 |styles| {
317 styles::parse_styles(styles, limits.maximum_styles)
318 .map_err(|_| StructuralFactUnknownReason::UnsupportedStyles)
319 },
320 );
321 let numbering =
322 parse_optional_numbering(parts.numbering.as_deref(), limits.maximum_numbering_items)?;
323 let ProjectedDocumentWithReview {
324 document,
325 revisions,
326 comment_anchors,
327 } = project_document_xml_with_limit_and_review(
328 &parts.document,
329 limits.maximum_paragraphs,
330 limits.maximum_structural_facts,
331 options,
332 ProjectionDependencies {
333 styles: styles.as_ref().map_err(|reason| *reason),
334 numbering: numbering
335 .as_ref()
336 .ok_or(StructuralFactUnknownReason::UnsupportedNumbering),
337 },
338 Some(ooxml::ReviewProjectionLimits {
339 maximum_facts: review_limits.maximum_facts_per_family,
340 maximum_detail_bytes: review_limits.maximum_review_detail_bytes,
341 }),
342 allocate_id,
343 )?;
344 let review_facts = review::project_review_facts(
345 revisions.unwrap_or(ReviewFactSet::Unknown(
346 ReviewFactUnknownReason::InvalidDocument,
347 )),
348 comment_anchors.as_ref(),
349 &document,
350 parts.comments,
351 parts.comments_extended,
352 review_limits,
353 options.text_materialization,
354 );
355 Ok(DocumentPackageProjection {
356 document,
357 review_facts,
358 })
359}
360
361fn parse_optional_numbering(
362 xml: Option<&[u8]>,
363 maximum_items: usize,
364) -> Result<Option<numbering::NumberingCatalog>, ProjectionError> {
365 let Some(xml) = xml else {
366 return Ok(None);
367 };
368 match numbering::parse_numbering(xml, maximum_items) {
369 Ok(catalog) => Ok(Some(catalog)),
370 Err(ProjectionError::TooManyNumberingItems) => Err(ProjectionError::TooManyNumberingItems),
371 Err(_) => Ok(None),
372 }
373}
374
375pub fn project_main_document_xml<F>(
384 xml: &[u8],
385 limits: DocxLimits,
386 allocate_id: F,
387) -> Result<DocumentProjection, ProjectionError>
388where
389 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
390{
391 if xml.len() > limits.maximum_archive_bytes {
392 return Err(ProjectionError::ArchiveTooLarge);
393 }
394 match flat_opc::input_kind(xml)? {
395 flat_opc::XmlInputKind::Document => {
396 if xml.len() > limits.maximum_document_xml_bytes {
397 return Err(ProjectionError::DocumentXmlTooLarge);
398 }
399 project_document_xml_with_limit(
400 xml,
401 limits.maximum_paragraphs,
402 limits.maximum_structural_facts,
403 ProjectionOptions::default(),
404 ProjectionDependencies {
405 styles: Err(StructuralFactUnknownReason::DocumentPartOnly),
406 numbering: Err(StructuralFactUnknownReason::DocumentPartOnly),
407 },
408 allocate_id,
409 )
410 }
411 flat_opc::XmlInputKind::Package => project_parts(
412 &flat_opc::extract_parts(xml, limits)?,
413 limits,
414 ProjectionOptions::default(),
415 allocate_id,
416 ),
417 }
418}
419
420pub fn project_paragraph_fragment<F>(
432 xml: &[u8],
433 mut limits: DocxLimits,
434 allocate_id: F,
435) -> Result<DocumentProjection, ProjectionError>
436where
437 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
438{
439 limits.maximum_paragraphs = limits.maximum_paragraphs.min(1);
440 let mut projection = project_parts(
441 &flat_opc::extract_parts(xml, limits)?,
442 limits,
443 ProjectionOptions::default(),
444 allocate_id,
445 )?;
446 let [paragraph] = projection.paragraphs.as_mut_slice() else {
447 return Err(ProjectionError::InvalidDocumentXml);
448 };
449 paragraph.structure = None;
450 projection.structural_facts = DocumentStructureFacts {
451 indentation: StructuralFactSet::Unknown(StructuralFactUnknownReason::ParagraphFragment),
452 numbering_hierarchy: StructuralFactSet::Unknown(
453 StructuralFactUnknownReason::ParagraphFragment,
454 ),
455 bookmarks: StructuralFactSet::Unknown(StructuralFactUnknownReason::ParagraphFragment),
456 internal_references: StructuralFactSet::Unknown(
457 StructuralFactUnknownReason::ParagraphFragment,
458 ),
459 outline_levels: StructuralFactSet::Unknown(StructuralFactUnknownReason::ParagraphFragment),
460 };
461 Ok(projection)
462}
463
464pub fn project_document_xml<F>(
471 xml: &[u8],
472 allocate_id: F,
473) -> Result<DocumentProjection, ProjectionError>
474where
475 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
476{
477 project_document_xml_with_options(xml, ProjectionOptions::default(), allocate_id)
478}
479
480pub fn project_document_xml_with_options<F>(
487 xml: &[u8],
488 options: ProjectionOptions,
489 allocate_id: F,
490) -> Result<DocumentProjection, ProjectionError>
491where
492 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
493{
494 project_document_xml_with_limit(
495 xml,
496 DocxLimits::default().maximum_paragraphs,
497 DocxLimits::default().maximum_structural_facts,
498 options,
499 ProjectionDependencies {
500 styles: Err(StructuralFactUnknownReason::DocumentPartOnly),
501 numbering: Err(StructuralFactUnknownReason::DocumentPartOnly),
502 },
503 allocate_id,
504 )
505}
506
507fn project_document_xml_with_limit<F>(
508 xml: &[u8],
509 maximum_paragraphs: usize,
510 maximum_structural_facts: usize,
511 options: ProjectionOptions,
512 dependencies: ProjectionDependencies<'_>,
513 allocate_id: F,
514) -> Result<DocumentProjection, ProjectionError>
515where
516 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
517{
518 project_document_xml_with_limit_and_review(
519 xml,
520 maximum_paragraphs,
521 maximum_structural_facts,
522 options,
523 dependencies,
524 None,
525 allocate_id,
526 )
527 .map(|projection| projection.document)
528}
529
530struct ProjectedDocumentWithReview {
531 document: DocumentProjection,
532 revisions: Option<ReviewFactSet<AttributedRevision>>,
533 comment_anchors: Option<HashMap<String, ReviewSpan>>,
534}
535
536#[derive(Clone, Copy)]
537struct ProjectionDependencies<'a> {
538 styles: Result<&'a structure::StyleSheet, StructuralFactUnknownReason>,
539 numbering: Result<&'a numbering::NumberingCatalog, StructuralFactUnknownReason>,
540}
541
542fn project_document_xml_with_limit_and_review<F>(
543 xml: &[u8],
544 maximum_paragraphs: usize,
545 maximum_structural_facts: usize,
546 options: ProjectionOptions,
547 dependencies: ProjectionDependencies<'_>,
548 review_limits: Option<ooxml::ReviewProjectionLimits>,
549 mut allocate_id: F,
550) -> Result<ProjectedDocumentWithReview, ProjectionError>
551where
552 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
553{
554 let projected = ooxml::project_document_xml(
555 xml,
556 maximum_paragraphs,
557 options.revision_view,
558 options.text_materialization,
559 dependencies.styles.map_err(formatting_unknown_reason),
560 review_limits,
561 )?;
562 let review_revisions = projected.review_revisions;
563 let review_comment_anchors = review_limits
564 .is_some()
565 .then_some(projected.review_comment_anchors);
566 let mut seen_ids = HashSet::with_capacity(projected.paragraphs.len());
567 let mut ids = Vec::with_capacity(projected.paragraphs.len());
568 for paragraph in &projected.paragraphs {
569 let facts = ParagraphIdentityFacts {
570 ordinal: paragraph.ordinal,
571 package_paragraph_id: paragraph.package_paragraph_id,
572 text: ¶graph.text,
573 };
574 let id = allocate_id(facts)?;
575 if !seen_ids.insert(id.clone()) {
576 return Err(ProjectionError::DuplicateInternalParagraphId);
577 }
578 ids.push(id);
579 }
580 let texts = projected
581 .paragraphs
582 .iter()
583 .map(|paragraph| paragraph.text.as_str())
584 .collect::<Vec<_>>();
585 let properties = projected
586 .paragraphs
587 .iter()
588 .map(|paragraph| ¶graph.properties)
589 .collect::<Vec<_>>();
590 let structural_facts = structure::materialize_structure(
591 structure::RawStructureInput {
592 paragraph_texts: &texts,
593 properties: &properties,
594 bookmarks: projected.bookmarks.as_deref().map_err(|reason| *reason),
595 references: projected.references.as_deref().map_err(|reason| *reason),
596 },
597 dependencies.styles,
598 dependencies.numbering,
599 maximum_structural_facts,
600 )?;
601 let mut paragraphs = Vec::with_capacity(projected.paragraphs.len());
602 for (id, paragraph) in ids.into_iter().zip(projected.paragraphs) {
603 let alignment =
604 structure::resolve_paragraph_alignment(dependencies.styles, ¶graph.properties);
605 paragraphs.push(ProjectedParagraph {
606 id,
607 ordinal: paragraph.ordinal,
608 package_paragraph_id: paragraph.package_paragraph_id,
609 style_id: paragraph.properties.style_id,
610 text: paragraph.text,
611 formatting: paragraph.formatting,
612 structure: paragraph.structure,
613 alignment,
614 });
615 }
616 Ok(ProjectedDocumentWithReview {
617 document: DocumentProjection {
618 paragraphs,
619 formatting_status: projected.formatting_status,
620 revision_status: projected.revision_status,
621 structural_facts,
622 },
623 revisions: review_revisions,
624 comment_anchors: review_comment_anchors,
625 })
626}
627
628const fn formatting_unknown_reason(reason: StructuralFactUnknownReason) -> FormattingUnknownReason {
629 match reason {
630 StructuralFactUnknownReason::DocumentPartOnly
631 | StructuralFactUnknownReason::ParagraphFragment => {
632 FormattingUnknownReason::DocumentPartOnly
633 }
634 StructuralFactUnknownReason::StylesPartUnavailable => {
635 FormattingUnknownReason::StylesPartUnavailable
636 }
637 StructuralFactUnknownReason::UnsupportedStyles
638 | StructuralFactUnknownReason::UnsupportedNumbering
639 | StructuralFactUnknownReason::IncompleteBookmarkRanges
640 | StructuralFactUnknownReason::UnsupportedInternalReferences => {
641 FormattingUnknownReason::UnsupportedStyles
642 }
643 }
644}