1mod archive;
8mod compatibility;
9mod namespaces;
10mod ooxml;
11mod relationships;
12mod review;
13mod structure;
14mod styles;
15
16use std::collections::{HashMap, HashSet};
17use std::fmt;
18
19pub use archive::{DocumentParts, DocxLimits, extract_document_parts, extract_document_xml};
20pub use ooxml::{
21 PackageParagraphId, ParagraphStructure, RevisionProjectionStatus, RevisionUnsupportedReason,
22 RevisionView, TextFormattingSpan, TextMaterialization, TextStyle, is_semantic_highlight_color,
23};
24pub use review::{
25 AttributedComment, AttributedRevision, CommentContent, DocumentReviewFacts, ReviewDetail,
26 ReviewFactLimits, ReviewFactSet, ReviewFactUnknownReason, ReviewPoint, ReviewSpan,
27 RevisionContent, RevisionFactKind,
28};
29pub use structure::{
30 BookmarkFact, DocumentStructureFacts, InternalReferenceFact, InternalReferenceRole,
31 NumberingHierarchyFact, ParagraphIndentation, ParagraphIndentationFact,
32 ParagraphOutlineLevelFact, SpanCoverage, StructuralFactSet, StructuralFactUnknownReason,
33 StructuralSpan,
34};
35
36#[derive(Clone, Debug, Eq, Hash, PartialEq)]
37pub struct InternalParagraphId(String);
38
39impl InternalParagraphId {
40 pub fn new(value: impl Into<String>) -> Result<Self, ProjectionError> {
47 let value = value.into();
48 if value.is_empty() || value.len() > 128 {
49 return Err(ProjectionError::InvalidInternalParagraphId);
50 }
51 Ok(Self(value))
52 }
53
54 #[must_use]
55 pub fn as_str(&self) -> &str {
56 &self.0
57 }
58}
59
60#[derive(Clone, Copy, Debug)]
61pub struct ParagraphIdentityFacts<'a> {
62 pub ordinal: usize,
63 pub package_paragraph_id: Option<PackageParagraphId>,
64 pub text: &'a str,
65}
66
67#[derive(Clone, Debug, Eq, PartialEq)]
68pub struct ProjectedParagraph {
69 pub id: InternalParagraphId,
70 pub ordinal: usize,
71 pub package_paragraph_id: Option<PackageParagraphId>,
72 pub style_id: Option<String>,
73 pub text: String,
74 pub formatting: Vec<TextFormattingSpan>,
75 pub structure: Option<ParagraphStructure>,
76}
77
78#[derive(Clone, Debug, Eq, PartialEq)]
79pub struct DocumentProjection {
80 pub paragraphs: Vec<ProjectedParagraph>,
81 pub revision_status: RevisionProjectionStatus,
82 pub structural_facts: DocumentStructureFacts,
83}
84
85#[derive(Clone, Debug, Eq, PartialEq)]
86pub struct DocumentPackageProjection {
87 pub document: DocumentProjection,
88 pub review_facts: DocumentReviewFacts,
89}
90
91#[derive(Clone, Copy, Debug, Eq, PartialEq)]
92pub struct ProjectionOptions {
93 pub revision_view: RevisionView,
94 pub text_materialization: TextMaterialization,
95}
96
97impl Default for ProjectionOptions {
98 fn default() -> Self {
99 Self {
100 revision_view: RevisionView::Current,
101 text_materialization: TextMaterialization::WordHost,
102 }
103 }
104}
105
106#[derive(Clone, Debug, Eq, PartialEq)]
107pub enum ProjectionError {
108 ArchiveTooLarge,
109 InvalidArchive,
110 TooManyArchiveEntries,
111 InvalidPackageRelationships,
112 PackageRelationshipsTooLarge,
113 MissingDocumentXml,
114 DuplicateDocumentXml,
115 DuplicateStylesXml,
116 EncryptedDocumentXml,
117 UnsupportedCompression(u16),
118 DocumentXmlTooLarge,
119 SuspiciousCompressionRatio,
120 InvalidDocumentXmlEntry,
121 DocumentXmlIntegrity,
122 StylesXmlTooLarge,
123 InvalidStylesXmlEntry,
124 StylesXmlIntegrity,
125 InvalidDocumentXml,
126 InvalidStylesXml,
127 MissingDocumentBody,
128 TooManyParagraphs,
129 TooManyStructuralFacts,
130 InvalidInternalParagraphId,
131 DuplicateInternalParagraphId,
132}
133
134impl fmt::Display for ProjectionError {
135 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
136 let message = match self {
137 Self::ArchiveTooLarge => "DOCX archive exceeds the configured size limit",
138 Self::InvalidArchive => "DOCX archive is invalid",
139 Self::TooManyArchiveEntries => "DOCX archive has too many entries",
140 Self::InvalidPackageRelationships => "DOCX archive has invalid package relationships",
141 Self::PackageRelationshipsTooLarge => {
142 "DOCX package relationships exceed the configured size limit"
143 }
144 Self::MissingDocumentXml => "DOCX archive has no main document part",
145 Self::DuplicateDocumentXml => "DOCX archive has duplicate main document parts",
146 Self::DuplicateStylesXml => "DOCX archive has duplicate word/styles.xml entries",
147 Self::EncryptedDocumentXml => "DOCX main document part is encrypted",
148 Self::UnsupportedCompression(_) => {
149 "selected DOCX package part uses unsupported compression"
150 }
151 Self::DocumentXmlTooLarge => {
152 "DOCX main document part exceeds the configured size limit"
153 }
154 Self::SuspiciousCompressionRatio => {
155 "selected DOCX package part exceeds the compression-ratio limit"
156 }
157 Self::InvalidDocumentXmlEntry => "DOCX main document part has an invalid ZIP entry",
158 Self::DocumentXmlIntegrity => "DOCX main document part failed size or CRC validation",
159 Self::StylesXmlTooLarge => "word/styles.xml exceeds the configured size limit",
160 Self::InvalidStylesXmlEntry => "word/styles.xml has an invalid ZIP entry",
161 Self::StylesXmlIntegrity => "word/styles.xml failed size or CRC validation",
162 Self::InvalidDocumentXml => "DOCX main document part is invalid XML",
163 Self::InvalidStylesXml => "word/styles.xml is invalid XML",
164 Self::MissingDocumentBody => "DOCX main document part has no document body",
165 Self::TooManyParagraphs => "DOCX main document part has too many paragraphs",
166 Self::TooManyStructuralFacts => {
167 "DOCX main document part produces too many structural facts"
168 }
169 Self::InvalidInternalParagraphId => "application paragraph ID is invalid",
170 Self::DuplicateInternalParagraphId => "application paragraph IDs are not unique",
171 };
172 formatter.write_str(message)
173 }
174}
175
176impl std::error::Error for ProjectionError {}
177
178pub fn project_docx<F>(
185 bytes: &[u8],
186 limits: DocxLimits,
187 allocate_id: F,
188) -> Result<DocumentProjection, ProjectionError>
189where
190 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
191{
192 project_docx_with_options(bytes, limits, ProjectionOptions::default(), allocate_id)
193}
194
195pub fn project_docx_with_options<F>(
202 bytes: &[u8],
203 limits: DocxLimits,
204 options: ProjectionOptions,
205 allocate_id: F,
206) -> Result<DocumentProjection, ProjectionError>
207where
208 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
209{
210 let parts = extract_document_parts(bytes, limits)?;
211 let styles = parts.styles_xml.as_deref().map_or(
212 Err(StructuralFactUnknownReason::StylesPartUnavailable),
213 |styles| {
214 styles::parse_styles(styles).map_err(|_| StructuralFactUnknownReason::UnsupportedStyles)
215 },
216 );
217 project_document_xml_with_limit(
218 &parts.document_xml,
219 limits.maximum_paragraphs,
220 limits.maximum_structural_facts,
221 options,
222 styles.as_ref().map_err(|reason| *reason),
223 allocate_id,
224 )
225}
226
227pub fn project_docx_with_review_facts<F>(
239 bytes: &[u8],
240 limits: DocxLimits,
241 review_limits: ReviewFactLimits,
242 options: ProjectionOptions,
243 allocate_id: F,
244) -> Result<DocumentPackageProjection, ProjectionError>
245where
246 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
247{
248 let parts = archive::extract_projection_parts(bytes, limits, review_limits)?;
249 let styles = parts.styles.as_deref().map_or(
250 Err(StructuralFactUnknownReason::StylesPartUnavailable),
251 |styles| {
252 styles::parse_styles(styles).map_err(|_| StructuralFactUnknownReason::UnsupportedStyles)
253 },
254 );
255 let ProjectedDocumentWithReview {
256 document,
257 revisions,
258 comment_anchors,
259 } = project_document_xml_with_limit_and_review(
260 &parts.document,
261 limits.maximum_paragraphs,
262 limits.maximum_structural_facts,
263 options,
264 styles.as_ref().map_err(|reason| *reason),
265 Some(ooxml::ReviewProjectionLimits {
266 maximum_facts: review_limits.maximum_facts_per_family,
267 maximum_detail_bytes: review_limits.maximum_review_detail_bytes,
268 }),
269 allocate_id,
270 )?;
271 let review_facts = review::project_review_facts(
272 revisions.unwrap_or(ReviewFactSet::Unknown(
273 ReviewFactUnknownReason::InvalidDocument,
274 )),
275 comment_anchors.as_ref(),
276 &document,
277 parts.comments,
278 parts.comments_extended,
279 review_limits,
280 options.text_materialization,
281 );
282 Ok(DocumentPackageProjection {
283 document,
284 review_facts,
285 })
286}
287
288pub fn project_document_xml<F>(
295 xml: &[u8],
296 allocate_id: F,
297) -> Result<DocumentProjection, ProjectionError>
298where
299 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
300{
301 project_document_xml_with_options(xml, ProjectionOptions::default(), allocate_id)
302}
303
304pub fn project_document_xml_with_options<F>(
311 xml: &[u8],
312 options: ProjectionOptions,
313 allocate_id: F,
314) -> Result<DocumentProjection, ProjectionError>
315where
316 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
317{
318 project_document_xml_with_limit(
319 xml,
320 DocxLimits::default().maximum_paragraphs,
321 DocxLimits::default().maximum_structural_facts,
322 options,
323 Err(StructuralFactUnknownReason::DocumentPartOnly),
324 allocate_id,
325 )
326}
327
328fn project_document_xml_with_limit<F>(
329 xml: &[u8],
330 maximum_paragraphs: usize,
331 maximum_structural_facts: usize,
332 options: ProjectionOptions,
333 styles: Result<&structure::StyleSheet, StructuralFactUnknownReason>,
334 allocate_id: F,
335) -> Result<DocumentProjection, ProjectionError>
336where
337 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
338{
339 project_document_xml_with_limit_and_review(
340 xml,
341 maximum_paragraphs,
342 maximum_structural_facts,
343 options,
344 styles,
345 None,
346 allocate_id,
347 )
348 .map(|projection| projection.document)
349}
350
351struct ProjectedDocumentWithReview {
352 document: DocumentProjection,
353 revisions: Option<ReviewFactSet<AttributedRevision>>,
354 comment_anchors: Option<HashMap<String, ReviewSpan>>,
355}
356
357fn project_document_xml_with_limit_and_review<F>(
358 xml: &[u8],
359 maximum_paragraphs: usize,
360 maximum_structural_facts: usize,
361 options: ProjectionOptions,
362 styles: Result<&structure::StyleSheet, StructuralFactUnknownReason>,
363 review_limits: Option<ooxml::ReviewProjectionLimits>,
364 mut allocate_id: F,
365) -> Result<ProjectedDocumentWithReview, ProjectionError>
366where
367 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
368{
369 let projected = ooxml::project_document_xml(
370 xml,
371 maximum_paragraphs,
372 options.revision_view,
373 options.text_materialization,
374 review_limits,
375 )?;
376 let review_revisions = projected.review_revisions;
377 let review_comment_anchors = review_limits
378 .is_some()
379 .then_some(projected.review_comment_anchors);
380 let mut seen_ids = HashSet::with_capacity(projected.paragraphs.len());
381 let mut ids = Vec::with_capacity(projected.paragraphs.len());
382 for paragraph in &projected.paragraphs {
383 let facts = ParagraphIdentityFacts {
384 ordinal: paragraph.ordinal,
385 package_paragraph_id: paragraph.package_paragraph_id,
386 text: ¶graph.text,
387 };
388 let id = allocate_id(facts)?;
389 if !seen_ids.insert(id.clone()) {
390 return Err(ProjectionError::DuplicateInternalParagraphId);
391 }
392 ids.push(id);
393 }
394 let texts = projected
395 .paragraphs
396 .iter()
397 .map(|paragraph| paragraph.text.as_str())
398 .collect::<Vec<_>>();
399 let properties = projected
400 .paragraphs
401 .iter()
402 .map(|paragraph| ¶graph.properties)
403 .collect::<Vec<_>>();
404 let structural_facts = structure::materialize_structure(
405 structure::RawStructureInput {
406 paragraph_texts: &texts,
407 properties: &properties,
408 bookmarks: projected.bookmarks.as_deref().map_err(|reason| *reason),
409 references: projected.references.as_deref().map_err(|reason| *reason),
410 },
411 styles,
412 maximum_structural_facts,
413 )?;
414 let mut paragraphs = Vec::with_capacity(projected.paragraphs.len());
415 for (id, paragraph) in ids.into_iter().zip(projected.paragraphs) {
416 paragraphs.push(ProjectedParagraph {
417 id,
418 ordinal: paragraph.ordinal,
419 package_paragraph_id: paragraph.package_paragraph_id,
420 style_id: paragraph.properties.style_id,
421 text: paragraph.text,
422 formatting: paragraph.formatting,
423 structure: paragraph.structure,
424 });
425 }
426 Ok(ProjectedDocumentWithReview {
427 document: DocumentProjection {
428 paragraphs,
429 revision_status: projected.revision_status,
430 structural_facts,
431 },
432 revisions: review_revisions,
433 comment_anchors: review_comment_anchors,
434 })
435}