1mod archive;
8mod compatibility;
9mod namespaces;
10mod ooxml;
11mod relationships;
12mod structure;
13mod styles;
14
15use std::collections::HashSet;
16use std::fmt;
17
18pub use archive::{DocumentParts, DocxLimits, extract_document_parts, extract_document_xml};
19pub use ooxml::{
20 PackageParagraphId, ParagraphStructure, RevisionProjectionStatus, RevisionUnsupportedReason,
21 RevisionView, TextFormattingSpan, TextMaterialization, TextStyle, is_semantic_highlight_color,
22};
23pub use structure::{
24 BookmarkFact, DocumentStructureFacts, InternalReferenceFact, InternalReferenceRole,
25 NumberingHierarchyFact, ParagraphIndentation, ParagraphIndentationFact, SpanCoverage,
26 StructuralFactSet, StructuralFactUnknownReason, StructuralSpan,
27};
28
29#[derive(Clone, Debug, Eq, Hash, PartialEq)]
30pub struct InternalParagraphId(String);
31
32impl InternalParagraphId {
33 pub fn new(value: impl Into<String>) -> Result<Self, ProjectionError> {
40 let value = value.into();
41 if value.is_empty() || value.len() > 128 {
42 return Err(ProjectionError::InvalidInternalParagraphId);
43 }
44 Ok(Self(value))
45 }
46
47 #[must_use]
48 pub fn as_str(&self) -> &str {
49 &self.0
50 }
51}
52
53#[derive(Clone, Copy, Debug)]
54pub struct ParagraphIdentityFacts<'a> {
55 pub ordinal: usize,
56 pub package_paragraph_id: Option<PackageParagraphId>,
57 pub text: &'a str,
58}
59
60#[derive(Clone, Debug, Eq, PartialEq)]
61pub struct ProjectedParagraph {
62 pub id: InternalParagraphId,
63 pub ordinal: usize,
64 pub package_paragraph_id: Option<PackageParagraphId>,
65 pub text: String,
66 pub formatting: Vec<TextFormattingSpan>,
67 pub structure: Option<ParagraphStructure>,
68}
69
70#[derive(Clone, Debug, Eq, PartialEq)]
71pub struct DocumentProjection {
72 pub paragraphs: Vec<ProjectedParagraph>,
73 pub revision_status: RevisionProjectionStatus,
74 pub structural_facts: DocumentStructureFacts,
75}
76
77#[derive(Clone, Copy, Debug, Eq, PartialEq)]
78pub struct ProjectionOptions {
79 pub revision_view: RevisionView,
80 pub text_materialization: TextMaterialization,
81}
82
83impl Default for ProjectionOptions {
84 fn default() -> Self {
85 Self {
86 revision_view: RevisionView::Current,
87 text_materialization: TextMaterialization::WordHost,
88 }
89 }
90}
91
92#[derive(Clone, Debug, Eq, PartialEq)]
93pub enum ProjectionError {
94 ArchiveTooLarge,
95 InvalidArchive,
96 TooManyArchiveEntries,
97 InvalidPackageRelationships,
98 PackageRelationshipsTooLarge,
99 MissingDocumentXml,
100 DuplicateDocumentXml,
101 DuplicateStylesXml,
102 EncryptedDocumentXml,
103 UnsupportedCompression(u16),
104 DocumentXmlTooLarge,
105 SuspiciousCompressionRatio,
106 InvalidDocumentXmlEntry,
107 DocumentXmlIntegrity,
108 StylesXmlTooLarge,
109 InvalidStylesXmlEntry,
110 StylesXmlIntegrity,
111 InvalidDocumentXml,
112 InvalidStylesXml,
113 MissingDocumentBody,
114 TooManyParagraphs,
115 TooManyStructuralFacts,
116 InvalidInternalParagraphId,
117 DuplicateInternalParagraphId,
118}
119
120impl fmt::Display for ProjectionError {
121 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
122 let message = match self {
123 Self::ArchiveTooLarge => "DOCX archive exceeds the configured size limit",
124 Self::InvalidArchive => "DOCX archive is invalid",
125 Self::TooManyArchiveEntries => "DOCX archive has too many entries",
126 Self::InvalidPackageRelationships => "DOCX archive has invalid package relationships",
127 Self::PackageRelationshipsTooLarge => {
128 "DOCX package relationships exceed the configured size limit"
129 }
130 Self::MissingDocumentXml => "DOCX archive has no main document part",
131 Self::DuplicateDocumentXml => "DOCX archive has duplicate main document parts",
132 Self::DuplicateStylesXml => "DOCX archive has duplicate word/styles.xml entries",
133 Self::EncryptedDocumentXml => "DOCX main document part is encrypted",
134 Self::UnsupportedCompression(_) => {
135 "selected DOCX package part uses unsupported compression"
136 }
137 Self::DocumentXmlTooLarge => {
138 "DOCX main document part exceeds the configured size limit"
139 }
140 Self::SuspiciousCompressionRatio => {
141 "selected DOCX package part exceeds the compression-ratio limit"
142 }
143 Self::InvalidDocumentXmlEntry => "DOCX main document part has an invalid ZIP entry",
144 Self::DocumentXmlIntegrity => "DOCX main document part failed size or CRC validation",
145 Self::StylesXmlTooLarge => "word/styles.xml exceeds the configured size limit",
146 Self::InvalidStylesXmlEntry => "word/styles.xml has an invalid ZIP entry",
147 Self::StylesXmlIntegrity => "word/styles.xml failed size or CRC validation",
148 Self::InvalidDocumentXml => "DOCX main document part is invalid XML",
149 Self::InvalidStylesXml => "word/styles.xml is invalid XML",
150 Self::MissingDocumentBody => "DOCX main document part has no document body",
151 Self::TooManyParagraphs => "DOCX main document part has too many paragraphs",
152 Self::TooManyStructuralFacts => {
153 "DOCX main document part produces too many structural facts"
154 }
155 Self::InvalidInternalParagraphId => "application paragraph ID is invalid",
156 Self::DuplicateInternalParagraphId => "application paragraph IDs are not unique",
157 };
158 formatter.write_str(message)
159 }
160}
161
162impl std::error::Error for ProjectionError {}
163
164pub fn project_docx<F>(
171 bytes: &[u8],
172 limits: DocxLimits,
173 allocate_id: F,
174) -> Result<DocumentProjection, ProjectionError>
175where
176 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
177{
178 project_docx_with_options(bytes, limits, ProjectionOptions::default(), allocate_id)
179}
180
181pub fn project_docx_with_options<F>(
188 bytes: &[u8],
189 limits: DocxLimits,
190 options: ProjectionOptions,
191 allocate_id: F,
192) -> Result<DocumentProjection, ProjectionError>
193where
194 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
195{
196 let parts = extract_document_parts(bytes, limits)?;
197 let styles = parts.styles_xml.as_deref().map_or(
198 Err(StructuralFactUnknownReason::StylesPartUnavailable),
199 |styles| {
200 styles::parse_styles(styles).map_err(|_| StructuralFactUnknownReason::UnsupportedStyles)
201 },
202 );
203 project_document_xml_with_limit(
204 &parts.document_xml,
205 limits.maximum_paragraphs,
206 limits.maximum_structural_facts,
207 options,
208 styles.as_ref().map_err(|reason| *reason),
209 allocate_id,
210 )
211}
212
213pub fn project_document_xml<F>(
220 xml: &[u8],
221 allocate_id: F,
222) -> Result<DocumentProjection, ProjectionError>
223where
224 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
225{
226 project_document_xml_with_options(xml, ProjectionOptions::default(), allocate_id)
227}
228
229pub fn project_document_xml_with_options<F>(
236 xml: &[u8],
237 options: ProjectionOptions,
238 allocate_id: F,
239) -> Result<DocumentProjection, ProjectionError>
240where
241 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
242{
243 project_document_xml_with_limit(
244 xml,
245 DocxLimits::default().maximum_paragraphs,
246 DocxLimits::default().maximum_structural_facts,
247 options,
248 Err(StructuralFactUnknownReason::DocumentPartOnly),
249 allocate_id,
250 )
251}
252
253fn project_document_xml_with_limit<F>(
254 xml: &[u8],
255 maximum_paragraphs: usize,
256 maximum_structural_facts: usize,
257 options: ProjectionOptions,
258 styles: Result<&structure::StyleSheet, StructuralFactUnknownReason>,
259 mut allocate_id: F,
260) -> Result<DocumentProjection, ProjectionError>
261where
262 F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
263{
264 let projected = ooxml::project_document_xml(
265 xml,
266 maximum_paragraphs,
267 options.revision_view,
268 options.text_materialization,
269 )?;
270 let mut seen_ids = HashSet::with_capacity(projected.paragraphs.len());
271 let mut ids = Vec::with_capacity(projected.paragraphs.len());
272 for paragraph in &projected.paragraphs {
273 let facts = ParagraphIdentityFacts {
274 ordinal: paragraph.ordinal,
275 package_paragraph_id: paragraph.package_paragraph_id,
276 text: ¶graph.text,
277 };
278 let id = allocate_id(facts)?;
279 if !seen_ids.insert(id.clone()) {
280 return Err(ProjectionError::DuplicateInternalParagraphId);
281 }
282 ids.push(id);
283 }
284 let texts = projected
285 .paragraphs
286 .iter()
287 .map(|paragraph| paragraph.text.as_str())
288 .collect::<Vec<_>>();
289 let properties = projected
290 .paragraphs
291 .iter()
292 .map(|paragraph| paragraph.properties.clone())
293 .collect::<Vec<_>>();
294 let structural_facts = structure::materialize_structure(
295 structure::RawStructureInput {
296 paragraph_texts: &texts,
297 properties: &properties,
298 bookmarks: projected.bookmarks.as_deref().map_err(|reason| *reason),
299 references: projected.references.as_deref().map_err(|reason| *reason),
300 },
301 styles,
302 maximum_structural_facts,
303 )?;
304 let mut paragraphs = Vec::with_capacity(projected.paragraphs.len());
305 for (id, paragraph) in ids.into_iter().zip(projected.paragraphs) {
306 paragraphs.push(ProjectedParagraph {
307 id,
308 ordinal: paragraph.ordinal,
309 package_paragraph_id: paragraph.package_paragraph_id,
310 text: paragraph.text,
311 formatting: paragraph.formatting,
312 structure: paragraph.structure,
313 });
314 }
315 Ok(DocumentProjection {
316 paragraphs,
317 revision_status: projected.revision_status,
318 structural_facts,
319 })
320}