1mod archive;
8mod compatibility;
9mod namespaces;
10mod numbering;
11mod ooxml;
12mod relationships;
13mod review;
14mod structure;
15mod styles;
16
17use std::collections::{HashMap, HashSet};
18use std::fmt;
19
20pub use archive::{DocumentParts, DocxLimits, extract_document_parts, extract_document_xml};
21pub use ooxml::{
22 PackageParagraphId, ParagraphStructure, RevisionProjectionStatus, RevisionUnsupportedReason,
23 RevisionView, TextFormattingSpan, TextMaterialization, TextStyle,
24};
25pub use review::{
26 AttributedComment, AttributedRevision, CommentContent, DocumentReviewFacts, ReviewDetail,
27 ReviewFactLimits, ReviewFactSet, ReviewFactUnknownReason, ReviewPoint, ReviewSpan,
28 RevisionContent, RevisionFactKind,
29};
30pub use structure::{
31 BookmarkFact, DocumentStructureFacts, InternalReferenceFact, InternalReferenceRole,
32 NumberingHierarchyFact, ParagraphIndentation, ParagraphIndentationFact,
33 ParagraphOutlineLevelFact, SpanCoverage, StructuralFactSet, StructuralFactUnknownReason,
34 StructuralSpan,
35};
36pub use styles::is_semantic_highlight_color;
37
38#[derive(Clone, Debug, Eq, Hash, PartialEq)]
39pub struct InternalParagraphId(String);
40
41impl InternalParagraphId {
42 pub fn new(value: impl Into<String>) -> Result<Self, ProjectionError> {
49 let value = value.into();
50 if value.is_empty() || value.len() > 128 {
51 return Err(ProjectionError::InvalidInternalParagraphId);
52 }
53 Ok(Self(value))
54 }
55
56 #[must_use]
57 pub fn as_str(&self) -> &str {
58 &self.0
59 }
60}
61
62#[derive(Clone, Copy, Debug)]
63pub struct ParagraphIdentityFacts<'a> {
64 pub ordinal: usize,
65 pub package_paragraph_id: Option<PackageParagraphId>,
66 pub text: &'a str,
67}
68
69#[derive(Clone, Debug, Eq, PartialEq)]
70pub struct ProjectedParagraph {
71 pub id: InternalParagraphId,
72 pub ordinal: usize,
73 pub package_paragraph_id: Option<PackageParagraphId>,
74 pub style_id: Option<String>,
75 pub text: String,
76 pub formatting: Vec<TextFormattingSpan>,
77 pub structure: Option<ParagraphStructure>,
78}
79
80#[derive(Clone, Copy, Debug, Eq, PartialEq)]
81pub enum FormattingUnknownReason {
82 DocumentPartOnly,
83 StylesPartUnavailable,
84 UnsupportedStyles,
85}
86
87#[derive(Clone, Copy, Debug, Eq, PartialEq)]
88pub enum FormattingProjectionStatus {
89 Complete,
90 Incomplete(FormattingUnknownReason),
91}
92
93impl Default for FormattingProjectionStatus {
94 fn default() -> Self {
95 Self::Incomplete(FormattingUnknownReason::DocumentPartOnly)
96 }
97}
98
99#[derive(Clone, Debug, Eq, PartialEq)]
100pub struct DocumentProjection {
101 pub paragraphs: Vec<ProjectedParagraph>,
102 pub formatting_status: FormattingProjectionStatus,
103 pub revision_status: RevisionProjectionStatus,
104 pub structural_facts: DocumentStructureFacts,
105}
106
107#[derive(Clone, Debug, Eq, PartialEq)]
108pub struct DocumentPackageProjection {
109 pub document: DocumentProjection,
110 pub review_facts: DocumentReviewFacts,
111}
112
113#[derive(Clone, Copy, Debug, Eq, PartialEq)]
114pub struct ProjectionOptions {
115 pub revision_view: RevisionView,
116 pub text_materialization: TextMaterialization,
117}
118
119impl Default for ProjectionOptions {
120 fn default() -> Self {
121 Self {
122 revision_view: RevisionView::Current,
123 text_materialization: TextMaterialization::WordHost,
124 }
125 }
126}
127
128#[derive(Clone, Debug, Eq, PartialEq)]
129pub enum ProjectionError {
130 ArchiveTooLarge,
131 InvalidArchive,
132 TooManyArchiveEntries,
133 InvalidPackageRelationships,
134 PackageRelationshipsTooLarge,
135 MissingDocumentXml,
136 DuplicateDocumentXml,
137 DuplicateStylesXml,
138 DuplicateNumberingXml,
139 EncryptedDocumentXml,
140 UnsupportedCompression(u16),
141 DocumentXmlTooLarge,
142 SuspiciousCompressionRatio,
143 InvalidDocumentXmlEntry,
144 DocumentXmlIntegrity,
145 StylesXmlTooLarge,
146 InvalidStylesXmlEntry,
147 StylesXmlIntegrity,
148 NumberingXmlTooLarge,
149 InvalidNumberingXmlEntry,
150 NumberingXmlIntegrity,
151 InvalidDocumentXml,
152 InvalidStylesXml,
153 InvalidNumberingXml,
154 MissingDocumentBody,
155 TooManyParagraphs,
156 TooManyStructuralFacts,
157 TooManyNumberingItems,
158 InvalidInternalParagraphId,
159 DuplicateInternalParagraphId,
160}
161
162impl fmt::Display for ProjectionError {
163 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
164 let message = match self {
165 Self::ArchiveTooLarge => "DOCX archive exceeds the configured size limit",
166 Self::InvalidArchive => "DOCX archive is invalid",
167 Self::TooManyArchiveEntries => "DOCX archive has too many entries",
168 Self::InvalidPackageRelationships => "DOCX archive has invalid package relationships",
169 Self::PackageRelationshipsTooLarge => {
170 "DOCX package relationships exceed the configured size limit"
171 }
172 Self::MissingDocumentXml => "DOCX archive has no main document part",
173 Self::DuplicateDocumentXml => "DOCX archive has duplicate main document parts",
174 Self::DuplicateStylesXml => "DOCX archive has duplicate entries for its styles part",
175 Self::DuplicateNumberingXml => {
176 "DOCX archive has duplicate entries for its numbering part"
177 }
178 Self::EncryptedDocumentXml => "DOCX main document part is encrypted",
179 Self::UnsupportedCompression(_) => {
180 "selected DOCX package part uses unsupported compression"
181 }
182 Self::DocumentXmlTooLarge => {
183 "DOCX main document part exceeds the configured size limit"
184 }
185 Self::SuspiciousCompressionRatio => {
186 "selected DOCX package part exceeds the compression-ratio limit"
187 }
188 Self::InvalidDocumentXmlEntry => "DOCX main document part has an invalid ZIP entry",
189 Self::DocumentXmlIntegrity => "DOCX main document part failed size or CRC validation",
190 Self::StylesXmlTooLarge => "DOCX styles part exceeds the configured size limit",
191 Self::InvalidStylesXmlEntry => "DOCX styles part has an invalid ZIP entry",
192 Self::StylesXmlIntegrity => "DOCX styles part failed size or CRC validation",
193 Self::NumberingXmlTooLarge => "DOCX numbering part exceeds the configured size limit",
194 Self::InvalidNumberingXmlEntry => "DOCX numbering part has an invalid ZIP entry",
195 Self::NumberingXmlIntegrity => "DOCX numbering part failed size or CRC validation",
196 Self::InvalidDocumentXml => "DOCX main document part is invalid XML",
197 Self::InvalidStylesXml => "DOCX styles part is invalid XML",
198 Self::InvalidNumberingXml => "DOCX numbering part is invalid XML",
199 Self::MissingDocumentBody => "DOCX main document part has no document body",
200 Self::TooManyParagraphs => "DOCX main document part has too many paragraphs",
201 Self::TooManyStructuralFacts => {
202 "DOCX main document part produces too many structural facts"
203 }
204 Self::TooManyNumberingItems => "DOCX numbering part exceeds the configured item limit",
205 Self::InvalidInternalParagraphId => "application paragraph ID is invalid",
206 Self::DuplicateInternalParagraphId => "application paragraph IDs are not unique",
207 };
208 formatter.write_str(message)
209 }
210}
211
212impl std::error::Error for ProjectionError {}
213
214pub fn project_docx<F>(
221 bytes: &[u8],
222 limits: DocxLimits,
223 allocate_id: F,
224) -> Result<DocumentProjection, ProjectionError>
225where
226 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
227{
228 project_docx_with_options(bytes, limits, ProjectionOptions::default(), allocate_id)
229}
230
231pub fn project_docx_with_options<F>(
238 bytes: &[u8],
239 limits: DocxLimits,
240 options: ProjectionOptions,
241 allocate_id: F,
242) -> Result<DocumentProjection, ProjectionError>
243where
244 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
245{
246 let parts = extract_document_parts(bytes, limits)?;
247 let styles = parts.styles_xml.as_deref().map_or(
248 Err(StructuralFactUnknownReason::StylesPartUnavailable),
249 |styles| {
250 styles::parse_styles(styles, limits.maximum_styles)
251 .map_err(|_| StructuralFactUnknownReason::UnsupportedStyles)
252 },
253 );
254 let numbering = parse_optional_numbering(
255 parts.numbering_xml.as_deref(),
256 limits.maximum_numbering_items,
257 )?;
258 project_document_xml_with_limit(
259 &parts.document_xml,
260 limits.maximum_paragraphs,
261 limits.maximum_structural_facts,
262 options,
263 ProjectionDependencies {
264 styles: styles.as_ref().map_err(|reason| *reason),
265 numbering: numbering
266 .as_ref()
267 .ok_or(StructuralFactUnknownReason::UnsupportedNumbering),
268 },
269 allocate_id,
270 )
271}
272
273pub fn project_docx_with_review_facts<F>(
285 bytes: &[u8],
286 limits: DocxLimits,
287 review_limits: ReviewFactLimits,
288 options: ProjectionOptions,
289 allocate_id: F,
290) -> Result<DocumentPackageProjection, ProjectionError>
291where
292 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
293{
294 let parts = archive::extract_projection_parts(bytes, limits, review_limits)?;
295 let styles = parts.styles.as_deref().map_or(
296 Err(StructuralFactUnknownReason::StylesPartUnavailable),
297 |styles| {
298 styles::parse_styles(styles, limits.maximum_styles)
299 .map_err(|_| StructuralFactUnknownReason::UnsupportedStyles)
300 },
301 );
302 let numbering =
303 parse_optional_numbering(parts.numbering.as_deref(), limits.maximum_numbering_items)?;
304 let ProjectedDocumentWithReview {
305 document,
306 revisions,
307 comment_anchors,
308 } = project_document_xml_with_limit_and_review(
309 &parts.document,
310 limits.maximum_paragraphs,
311 limits.maximum_structural_facts,
312 options,
313 ProjectionDependencies {
314 styles: styles.as_ref().map_err(|reason| *reason),
315 numbering: numbering
316 .as_ref()
317 .ok_or(StructuralFactUnknownReason::UnsupportedNumbering),
318 },
319 Some(ooxml::ReviewProjectionLimits {
320 maximum_facts: review_limits.maximum_facts_per_family,
321 maximum_detail_bytes: review_limits.maximum_review_detail_bytes,
322 }),
323 allocate_id,
324 )?;
325 let review_facts = review::project_review_facts(
326 revisions.unwrap_or(ReviewFactSet::Unknown(
327 ReviewFactUnknownReason::InvalidDocument,
328 )),
329 comment_anchors.as_ref(),
330 &document,
331 parts.comments,
332 parts.comments_extended,
333 review_limits,
334 options.text_materialization,
335 );
336 Ok(DocumentPackageProjection {
337 document,
338 review_facts,
339 })
340}
341
342fn parse_optional_numbering(
343 xml: Option<&[u8]>,
344 maximum_items: usize,
345) -> Result<Option<numbering::NumberingCatalog>, ProjectionError> {
346 let Some(xml) = xml else {
347 return Ok(None);
348 };
349 match numbering::parse_numbering(xml, maximum_items) {
350 Ok(catalog) => Ok(Some(catalog)),
351 Err(ProjectionError::TooManyNumberingItems) => Err(ProjectionError::TooManyNumberingItems),
352 Err(_) => Ok(None),
353 }
354}
355
356pub fn project_document_xml<F>(
363 xml: &[u8],
364 allocate_id: F,
365) -> Result<DocumentProjection, ProjectionError>
366where
367 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
368{
369 project_document_xml_with_options(xml, ProjectionOptions::default(), allocate_id)
370}
371
372pub fn project_document_xml_with_options<F>(
379 xml: &[u8],
380 options: ProjectionOptions,
381 allocate_id: F,
382) -> Result<DocumentProjection, ProjectionError>
383where
384 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
385{
386 project_document_xml_with_limit(
387 xml,
388 DocxLimits::default().maximum_paragraphs,
389 DocxLimits::default().maximum_structural_facts,
390 options,
391 ProjectionDependencies {
392 styles: Err(StructuralFactUnknownReason::DocumentPartOnly),
393 numbering: Err(StructuralFactUnknownReason::DocumentPartOnly),
394 },
395 allocate_id,
396 )
397}
398
399fn project_document_xml_with_limit<F>(
400 xml: &[u8],
401 maximum_paragraphs: usize,
402 maximum_structural_facts: usize,
403 options: ProjectionOptions,
404 dependencies: ProjectionDependencies<'_>,
405 allocate_id: F,
406) -> Result<DocumentProjection, ProjectionError>
407where
408 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
409{
410 project_document_xml_with_limit_and_review(
411 xml,
412 maximum_paragraphs,
413 maximum_structural_facts,
414 options,
415 dependencies,
416 None,
417 allocate_id,
418 )
419 .map(|projection| projection.document)
420}
421
422struct ProjectedDocumentWithReview {
423 document: DocumentProjection,
424 revisions: Option<ReviewFactSet<AttributedRevision>>,
425 comment_anchors: Option<HashMap<String, ReviewSpan>>,
426}
427
428#[derive(Clone, Copy)]
429struct ProjectionDependencies<'a> {
430 styles: Result<&'a structure::StyleSheet, StructuralFactUnknownReason>,
431 numbering: Result<&'a numbering::NumberingCatalog, StructuralFactUnknownReason>,
432}
433
434fn project_document_xml_with_limit_and_review<F>(
435 xml: &[u8],
436 maximum_paragraphs: usize,
437 maximum_structural_facts: usize,
438 options: ProjectionOptions,
439 dependencies: ProjectionDependencies<'_>,
440 review_limits: Option<ooxml::ReviewProjectionLimits>,
441 mut allocate_id: F,
442) -> Result<ProjectedDocumentWithReview, ProjectionError>
443where
444 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
445{
446 let projected = ooxml::project_document_xml(
447 xml,
448 maximum_paragraphs,
449 options.revision_view,
450 options.text_materialization,
451 dependencies.styles.map_err(formatting_unknown_reason),
452 review_limits,
453 )?;
454 let review_revisions = projected.review_revisions;
455 let review_comment_anchors = review_limits
456 .is_some()
457 .then_some(projected.review_comment_anchors);
458 let mut seen_ids = HashSet::with_capacity(projected.paragraphs.len());
459 let mut ids = Vec::with_capacity(projected.paragraphs.len());
460 for paragraph in &projected.paragraphs {
461 let facts = ParagraphIdentityFacts {
462 ordinal: paragraph.ordinal,
463 package_paragraph_id: paragraph.package_paragraph_id,
464 text: ¶graph.text,
465 };
466 let id = allocate_id(facts)?;
467 if !seen_ids.insert(id.clone()) {
468 return Err(ProjectionError::DuplicateInternalParagraphId);
469 }
470 ids.push(id);
471 }
472 let texts = projected
473 .paragraphs
474 .iter()
475 .map(|paragraph| paragraph.text.as_str())
476 .collect::<Vec<_>>();
477 let properties = projected
478 .paragraphs
479 .iter()
480 .map(|paragraph| ¶graph.properties)
481 .collect::<Vec<_>>();
482 let structural_facts = structure::materialize_structure(
483 structure::RawStructureInput {
484 paragraph_texts: &texts,
485 properties: &properties,
486 bookmarks: projected.bookmarks.as_deref().map_err(|reason| *reason),
487 references: projected.references.as_deref().map_err(|reason| *reason),
488 },
489 dependencies.styles,
490 dependencies.numbering,
491 maximum_structural_facts,
492 )?;
493 let mut paragraphs = Vec::with_capacity(projected.paragraphs.len());
494 for (id, paragraph) in ids.into_iter().zip(projected.paragraphs) {
495 paragraphs.push(ProjectedParagraph {
496 id,
497 ordinal: paragraph.ordinal,
498 package_paragraph_id: paragraph.package_paragraph_id,
499 style_id: paragraph.properties.style_id,
500 text: paragraph.text,
501 formatting: paragraph.formatting,
502 structure: paragraph.structure,
503 });
504 }
505 Ok(ProjectedDocumentWithReview {
506 document: DocumentProjection {
507 paragraphs,
508 formatting_status: projected.formatting_status,
509 revision_status: projected.revision_status,
510 structural_facts,
511 },
512 revisions: review_revisions,
513 comment_anchors: review_comment_anchors,
514 })
515}
516
517const fn formatting_unknown_reason(reason: StructuralFactUnknownReason) -> FormattingUnknownReason {
518 match reason {
519 StructuralFactUnknownReason::DocumentPartOnly => FormattingUnknownReason::DocumentPartOnly,
520 StructuralFactUnknownReason::StylesPartUnavailable => {
521 FormattingUnknownReason::StylesPartUnavailable
522 }
523 StructuralFactUnknownReason::UnsupportedStyles
524 | StructuralFactUnknownReason::UnsupportedNumbering
525 | StructuralFactUnknownReason::IncompleteBookmarkRanges
526 | StructuralFactUnknownReason::UnsupportedInternalReferences => {
527 FormattingUnknownReason::UnsupportedStyles
528 }
529 }
530}