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, is_semantic_highlight_color,
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};
36
37#[derive(Clone, Debug, Eq, Hash, PartialEq)]
38pub struct InternalParagraphId(String);
39
40impl InternalParagraphId {
41 pub fn new(value: impl Into<String>) -> Result<Self, ProjectionError> {
48 let value = value.into();
49 if value.is_empty() || value.len() > 128 {
50 return Err(ProjectionError::InvalidInternalParagraphId);
51 }
52 Ok(Self(value))
53 }
54
55 #[must_use]
56 pub fn as_str(&self) -> &str {
57 &self.0
58 }
59}
60
61#[derive(Clone, Copy, Debug)]
62pub struct ParagraphIdentityFacts<'a> {
63 pub ordinal: usize,
64 pub package_paragraph_id: Option<PackageParagraphId>,
65 pub text: &'a str,
66}
67
68#[derive(Clone, Debug, Eq, PartialEq)]
69pub struct ProjectedParagraph {
70 pub id: InternalParagraphId,
71 pub ordinal: usize,
72 pub package_paragraph_id: Option<PackageParagraphId>,
73 pub style_id: Option<String>,
74 pub text: String,
75 pub formatting: Vec<TextFormattingSpan>,
76 pub structure: Option<ParagraphStructure>,
77}
78
79#[derive(Clone, Debug, Eq, PartialEq)]
80pub struct DocumentProjection {
81 pub paragraphs: Vec<ProjectedParagraph>,
82 pub revision_status: RevisionProjectionStatus,
83 pub structural_facts: DocumentStructureFacts,
84}
85
86#[derive(Clone, Debug, Eq, PartialEq)]
87pub struct DocumentPackageProjection {
88 pub document: DocumentProjection,
89 pub review_facts: DocumentReviewFacts,
90}
91
92#[derive(Clone, Copy, Debug, Eq, PartialEq)]
93pub struct ProjectionOptions {
94 pub revision_view: RevisionView,
95 pub text_materialization: TextMaterialization,
96}
97
98impl Default for ProjectionOptions {
99 fn default() -> Self {
100 Self {
101 revision_view: RevisionView::Current,
102 text_materialization: TextMaterialization::WordHost,
103 }
104 }
105}
106
107#[derive(Clone, Debug, Eq, PartialEq)]
108pub enum ProjectionError {
109 ArchiveTooLarge,
110 InvalidArchive,
111 TooManyArchiveEntries,
112 InvalidPackageRelationships,
113 PackageRelationshipsTooLarge,
114 MissingDocumentXml,
115 DuplicateDocumentXml,
116 DuplicateStylesXml,
117 DuplicateNumberingXml,
118 EncryptedDocumentXml,
119 UnsupportedCompression(u16),
120 DocumentXmlTooLarge,
121 SuspiciousCompressionRatio,
122 InvalidDocumentXmlEntry,
123 DocumentXmlIntegrity,
124 StylesXmlTooLarge,
125 InvalidStylesXmlEntry,
126 StylesXmlIntegrity,
127 NumberingXmlTooLarge,
128 InvalidNumberingXmlEntry,
129 NumberingXmlIntegrity,
130 InvalidDocumentXml,
131 InvalidStylesXml,
132 InvalidNumberingXml,
133 MissingDocumentBody,
134 TooManyParagraphs,
135 TooManyStructuralFacts,
136 TooManyNumberingItems,
137 InvalidInternalParagraphId,
138 DuplicateInternalParagraphId,
139}
140
141impl fmt::Display for ProjectionError {
142 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
143 let message = match self {
144 Self::ArchiveTooLarge => "DOCX archive exceeds the configured size limit",
145 Self::InvalidArchive => "DOCX archive is invalid",
146 Self::TooManyArchiveEntries => "DOCX archive has too many entries",
147 Self::InvalidPackageRelationships => "DOCX archive has invalid package relationships",
148 Self::PackageRelationshipsTooLarge => {
149 "DOCX package relationships exceed the configured size limit"
150 }
151 Self::MissingDocumentXml => "DOCX archive has no main document part",
152 Self::DuplicateDocumentXml => "DOCX archive has duplicate main document parts",
153 Self::DuplicateStylesXml => "DOCX archive has duplicate word/styles.xml entries",
154 Self::DuplicateNumberingXml => "DOCX archive has duplicate word/numbering.xml entries",
155 Self::EncryptedDocumentXml => "DOCX main document part is encrypted",
156 Self::UnsupportedCompression(_) => {
157 "selected DOCX package part uses unsupported compression"
158 }
159 Self::DocumentXmlTooLarge => {
160 "DOCX main document part exceeds the configured size limit"
161 }
162 Self::SuspiciousCompressionRatio => {
163 "selected DOCX package part exceeds the compression-ratio limit"
164 }
165 Self::InvalidDocumentXmlEntry => "DOCX main document part has an invalid ZIP entry",
166 Self::DocumentXmlIntegrity => "DOCX main document part failed size or CRC validation",
167 Self::StylesXmlTooLarge => "word/styles.xml exceeds the configured size limit",
168 Self::InvalidStylesXmlEntry => "word/styles.xml has an invalid ZIP entry",
169 Self::StylesXmlIntegrity => "word/styles.xml failed size or CRC validation",
170 Self::NumberingXmlTooLarge => "word/numbering.xml exceeds the configured size limit",
171 Self::InvalidNumberingXmlEntry => "word/numbering.xml has an invalid ZIP entry",
172 Self::NumberingXmlIntegrity => "word/numbering.xml failed size or CRC validation",
173 Self::InvalidDocumentXml => "DOCX main document part is invalid XML",
174 Self::InvalidStylesXml => "word/styles.xml is invalid XML",
175 Self::InvalidNumberingXml => "word/numbering.xml is invalid XML",
176 Self::MissingDocumentBody => "DOCX main document part has no document body",
177 Self::TooManyParagraphs => "DOCX main document part has too many paragraphs",
178 Self::TooManyStructuralFacts => {
179 "DOCX main document part produces too many structural facts"
180 }
181 Self::TooManyNumberingItems => "word/numbering.xml exceeds the configured item limit",
182 Self::InvalidInternalParagraphId => "application paragraph ID is invalid",
183 Self::DuplicateInternalParagraphId => "application paragraph IDs are not unique",
184 };
185 formatter.write_str(message)
186 }
187}
188
189impl std::error::Error for ProjectionError {}
190
191pub fn project_docx<F>(
198 bytes: &[u8],
199 limits: DocxLimits,
200 allocate_id: F,
201) -> Result<DocumentProjection, ProjectionError>
202where
203 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
204{
205 project_docx_with_options(bytes, limits, ProjectionOptions::default(), allocate_id)
206}
207
208pub fn project_docx_with_options<F>(
215 bytes: &[u8],
216 limits: DocxLimits,
217 options: ProjectionOptions,
218 allocate_id: F,
219) -> Result<DocumentProjection, ProjectionError>
220where
221 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
222{
223 let parts = extract_document_parts(bytes, limits)?;
224 let styles = parts.styles_xml.as_deref().map_or(
225 Err(StructuralFactUnknownReason::StylesPartUnavailable),
226 |styles| {
227 styles::parse_styles(styles).map_err(|_| StructuralFactUnknownReason::UnsupportedStyles)
228 },
229 );
230 let numbering = parse_optional_numbering(
231 parts.numbering_xml.as_deref(),
232 limits.maximum_numbering_items,
233 )?;
234 project_document_xml_with_limit(
235 &parts.document_xml,
236 limits.maximum_paragraphs,
237 limits.maximum_structural_facts,
238 options,
239 ProjectionDependencies {
240 styles: styles.as_ref().map_err(|reason| *reason),
241 numbering: numbering
242 .as_ref()
243 .ok_or(StructuralFactUnknownReason::UnsupportedNumbering),
244 },
245 allocate_id,
246 )
247}
248
249pub fn project_docx_with_review_facts<F>(
261 bytes: &[u8],
262 limits: DocxLimits,
263 review_limits: ReviewFactLimits,
264 options: ProjectionOptions,
265 allocate_id: F,
266) -> Result<DocumentPackageProjection, ProjectionError>
267where
268 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
269{
270 let parts = archive::extract_projection_parts(bytes, limits, review_limits)?;
271 let styles = parts.styles.as_deref().map_or(
272 Err(StructuralFactUnknownReason::StylesPartUnavailable),
273 |styles| {
274 styles::parse_styles(styles).map_err(|_| StructuralFactUnknownReason::UnsupportedStyles)
275 },
276 );
277 let numbering =
278 parse_optional_numbering(parts.numbering.as_deref(), limits.maximum_numbering_items)?;
279 let ProjectedDocumentWithReview {
280 document,
281 revisions,
282 comment_anchors,
283 } = project_document_xml_with_limit_and_review(
284 &parts.document,
285 limits.maximum_paragraphs,
286 limits.maximum_structural_facts,
287 options,
288 ProjectionDependencies {
289 styles: styles.as_ref().map_err(|reason| *reason),
290 numbering: numbering
291 .as_ref()
292 .ok_or(StructuralFactUnknownReason::UnsupportedNumbering),
293 },
294 Some(ooxml::ReviewProjectionLimits {
295 maximum_facts: review_limits.maximum_facts_per_family,
296 maximum_detail_bytes: review_limits.maximum_review_detail_bytes,
297 }),
298 allocate_id,
299 )?;
300 let review_facts = review::project_review_facts(
301 revisions.unwrap_or(ReviewFactSet::Unknown(
302 ReviewFactUnknownReason::InvalidDocument,
303 )),
304 comment_anchors.as_ref(),
305 &document,
306 parts.comments,
307 parts.comments_extended,
308 review_limits,
309 options.text_materialization,
310 );
311 Ok(DocumentPackageProjection {
312 document,
313 review_facts,
314 })
315}
316
317fn parse_optional_numbering(
318 xml: Option<&[u8]>,
319 maximum_items: usize,
320) -> Result<Option<numbering::NumberingCatalog>, ProjectionError> {
321 let Some(xml) = xml else {
322 return Ok(None);
323 };
324 match numbering::parse_numbering(xml, maximum_items) {
325 Ok(catalog) => Ok(Some(catalog)),
326 Err(ProjectionError::TooManyNumberingItems) => Err(ProjectionError::TooManyNumberingItems),
327 Err(_) => Ok(None),
328 }
329}
330
331pub fn project_document_xml<F>(
338 xml: &[u8],
339 allocate_id: F,
340) -> Result<DocumentProjection, ProjectionError>
341where
342 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
343{
344 project_document_xml_with_options(xml, ProjectionOptions::default(), allocate_id)
345}
346
347pub fn project_document_xml_with_options<F>(
354 xml: &[u8],
355 options: ProjectionOptions,
356 allocate_id: F,
357) -> Result<DocumentProjection, ProjectionError>
358where
359 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
360{
361 project_document_xml_with_limit(
362 xml,
363 DocxLimits::default().maximum_paragraphs,
364 DocxLimits::default().maximum_structural_facts,
365 options,
366 ProjectionDependencies {
367 styles: Err(StructuralFactUnknownReason::DocumentPartOnly),
368 numbering: Err(StructuralFactUnknownReason::DocumentPartOnly),
369 },
370 allocate_id,
371 )
372}
373
374fn project_document_xml_with_limit<F>(
375 xml: &[u8],
376 maximum_paragraphs: usize,
377 maximum_structural_facts: usize,
378 options: ProjectionOptions,
379 dependencies: ProjectionDependencies<'_>,
380 allocate_id: F,
381) -> Result<DocumentProjection, ProjectionError>
382where
383 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
384{
385 project_document_xml_with_limit_and_review(
386 xml,
387 maximum_paragraphs,
388 maximum_structural_facts,
389 options,
390 dependencies,
391 None,
392 allocate_id,
393 )
394 .map(|projection| projection.document)
395}
396
397struct ProjectedDocumentWithReview {
398 document: DocumentProjection,
399 revisions: Option<ReviewFactSet<AttributedRevision>>,
400 comment_anchors: Option<HashMap<String, ReviewSpan>>,
401}
402
403#[derive(Clone, Copy)]
404struct ProjectionDependencies<'a> {
405 styles: Result<&'a structure::StyleSheet, StructuralFactUnknownReason>,
406 numbering: Result<&'a numbering::NumberingCatalog, StructuralFactUnknownReason>,
407}
408
409fn project_document_xml_with_limit_and_review<F>(
410 xml: &[u8],
411 maximum_paragraphs: usize,
412 maximum_structural_facts: usize,
413 options: ProjectionOptions,
414 dependencies: ProjectionDependencies<'_>,
415 review_limits: Option<ooxml::ReviewProjectionLimits>,
416 mut allocate_id: F,
417) -> Result<ProjectedDocumentWithReview, ProjectionError>
418where
419 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
420{
421 let projected = ooxml::project_document_xml(
422 xml,
423 maximum_paragraphs,
424 options.revision_view,
425 options.text_materialization,
426 review_limits,
427 )?;
428 let review_revisions = projected.review_revisions;
429 let review_comment_anchors = review_limits
430 .is_some()
431 .then_some(projected.review_comment_anchors);
432 let mut seen_ids = HashSet::with_capacity(projected.paragraphs.len());
433 let mut ids = Vec::with_capacity(projected.paragraphs.len());
434 for paragraph in &projected.paragraphs {
435 let facts = ParagraphIdentityFacts {
436 ordinal: paragraph.ordinal,
437 package_paragraph_id: paragraph.package_paragraph_id,
438 text: ¶graph.text,
439 };
440 let id = allocate_id(facts)?;
441 if !seen_ids.insert(id.clone()) {
442 return Err(ProjectionError::DuplicateInternalParagraphId);
443 }
444 ids.push(id);
445 }
446 let texts = projected
447 .paragraphs
448 .iter()
449 .map(|paragraph| paragraph.text.as_str())
450 .collect::<Vec<_>>();
451 let properties = projected
452 .paragraphs
453 .iter()
454 .map(|paragraph| ¶graph.properties)
455 .collect::<Vec<_>>();
456 let structural_facts = structure::materialize_structure(
457 structure::RawStructureInput {
458 paragraph_texts: &texts,
459 properties: &properties,
460 bookmarks: projected.bookmarks.as_deref().map_err(|reason| *reason),
461 references: projected.references.as_deref().map_err(|reason| *reason),
462 },
463 dependencies.styles,
464 dependencies.numbering,
465 maximum_structural_facts,
466 )?;
467 let mut paragraphs = Vec::with_capacity(projected.paragraphs.len());
468 for (id, paragraph) in ids.into_iter().zip(projected.paragraphs) {
469 paragraphs.push(ProjectedParagraph {
470 id,
471 ordinal: paragraph.ordinal,
472 package_paragraph_id: paragraph.package_paragraph_id,
473 style_id: paragraph.properties.style_id,
474 text: paragraph.text,
475 formatting: paragraph.formatting,
476 structure: paragraph.structure,
477 });
478 }
479 Ok(ProjectedDocumentWithReview {
480 document: DocumentProjection {
481 paragraphs,
482 revision_status: projected.revision_status,
483 structural_facts,
484 },
485 revisions: review_revisions,
486 comment_anchors: review_comment_anchors,
487 })
488}