mod archive;
mod compatibility;
mod namespaces;
mod ooxml;
mod relationships;
mod structure;
mod styles;
use std::collections::HashSet;
use std::fmt;
pub use archive::{DocumentParts, DocxLimits, extract_document_parts, extract_document_xml};
pub use ooxml::{
PackageParagraphId, ParagraphStructure, RevisionProjectionStatus, RevisionUnsupportedReason,
RevisionView, TextFormattingSpan, TextMaterialization, TextStyle, is_semantic_highlight_color,
};
pub use structure::{
BookmarkFact, DocumentStructureFacts, InternalReferenceFact, InternalReferenceRole,
NumberingHierarchyFact, ParagraphIndentation, ParagraphIndentationFact, SpanCoverage,
StructuralFactSet, StructuralFactUnknownReason, StructuralSpan,
};
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct InternalParagraphId(String);
impl InternalParagraphId {
pub fn new(value: impl Into<String>) -> Result<Self, ProjectionError> {
let value = value.into();
if value.is_empty() || value.len() > 128 {
return Err(ProjectionError::InvalidInternalParagraphId);
}
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Clone, Copy, Debug)]
pub struct ParagraphIdentityFacts<'a> {
pub ordinal: usize,
pub package_paragraph_id: Option<PackageParagraphId>,
pub text: &'a str,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ProjectedParagraph {
pub id: InternalParagraphId,
pub ordinal: usize,
pub package_paragraph_id: Option<PackageParagraphId>,
pub text: String,
pub formatting: Vec<TextFormattingSpan>,
pub structure: Option<ParagraphStructure>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DocumentProjection {
pub paragraphs: Vec<ProjectedParagraph>,
pub revision_status: RevisionProjectionStatus,
pub structural_facts: DocumentStructureFacts,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ProjectionOptions {
pub revision_view: RevisionView,
pub text_materialization: TextMaterialization,
}
impl Default for ProjectionOptions {
fn default() -> Self {
Self {
revision_view: RevisionView::Current,
text_materialization: TextMaterialization::WordHost,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ProjectionError {
ArchiveTooLarge,
InvalidArchive,
TooManyArchiveEntries,
InvalidPackageRelationships,
PackageRelationshipsTooLarge,
MissingDocumentXml,
DuplicateDocumentXml,
DuplicateStylesXml,
EncryptedDocumentXml,
UnsupportedCompression(u16),
DocumentXmlTooLarge,
SuspiciousCompressionRatio,
InvalidDocumentXmlEntry,
DocumentXmlIntegrity,
StylesXmlTooLarge,
InvalidStylesXmlEntry,
StylesXmlIntegrity,
InvalidDocumentXml,
InvalidStylesXml,
MissingDocumentBody,
TooManyParagraphs,
TooManyStructuralFacts,
InvalidInternalParagraphId,
DuplicateInternalParagraphId,
}
impl fmt::Display for ProjectionError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let message = match self {
Self::ArchiveTooLarge => "DOCX archive exceeds the configured size limit",
Self::InvalidArchive => "DOCX archive is invalid",
Self::TooManyArchiveEntries => "DOCX archive has too many entries",
Self::InvalidPackageRelationships => "DOCX archive has invalid package relationships",
Self::PackageRelationshipsTooLarge => {
"DOCX package relationships exceed the configured size limit"
}
Self::MissingDocumentXml => "DOCX archive has no main document part",
Self::DuplicateDocumentXml => "DOCX archive has duplicate main document parts",
Self::DuplicateStylesXml => "DOCX archive has duplicate word/styles.xml entries",
Self::EncryptedDocumentXml => "DOCX main document part is encrypted",
Self::UnsupportedCompression(_) => {
"selected DOCX package part uses unsupported compression"
}
Self::DocumentXmlTooLarge => {
"DOCX main document part exceeds the configured size limit"
}
Self::SuspiciousCompressionRatio => {
"selected DOCX package part exceeds the compression-ratio limit"
}
Self::InvalidDocumentXmlEntry => "DOCX main document part has an invalid ZIP entry",
Self::DocumentXmlIntegrity => "DOCX main document part failed size or CRC validation",
Self::StylesXmlTooLarge => "word/styles.xml exceeds the configured size limit",
Self::InvalidStylesXmlEntry => "word/styles.xml has an invalid ZIP entry",
Self::StylesXmlIntegrity => "word/styles.xml failed size or CRC validation",
Self::InvalidDocumentXml => "DOCX main document part is invalid XML",
Self::InvalidStylesXml => "word/styles.xml is invalid XML",
Self::MissingDocumentBody => "DOCX main document part has no document body",
Self::TooManyParagraphs => "DOCX main document part has too many paragraphs",
Self::TooManyStructuralFacts => {
"DOCX main document part produces too many structural facts"
}
Self::InvalidInternalParagraphId => "application paragraph ID is invalid",
Self::DuplicateInternalParagraphId => "application paragraph IDs are not unique",
};
formatter.write_str(message)
}
}
impl std::error::Error for ProjectionError {}
pub fn project_docx<F>(
bytes: &[u8],
limits: DocxLimits,
allocate_id: F,
) -> Result<DocumentProjection, ProjectionError>
where
F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
{
project_docx_with_options(bytes, limits, ProjectionOptions::default(), allocate_id)
}
pub fn project_docx_with_options<F>(
bytes: &[u8],
limits: DocxLimits,
options: ProjectionOptions,
allocate_id: F,
) -> Result<DocumentProjection, ProjectionError>
where
F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
{
let parts = extract_document_parts(bytes, limits)?;
let styles = parts.styles_xml.as_deref().map_or(
Err(StructuralFactUnknownReason::StylesPartUnavailable),
|styles| {
styles::parse_styles(styles).map_err(|_| StructuralFactUnknownReason::UnsupportedStyles)
},
);
project_document_xml_with_limit(
&parts.document_xml,
limits.maximum_paragraphs,
limits.maximum_structural_facts,
options,
styles.as_ref().map_err(|reason| *reason),
allocate_id,
)
}
pub fn project_document_xml<F>(
xml: &[u8],
allocate_id: F,
) -> Result<DocumentProjection, ProjectionError>
where
F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
{
project_document_xml_with_options(xml, ProjectionOptions::default(), allocate_id)
}
pub fn project_document_xml_with_options<F>(
xml: &[u8],
options: ProjectionOptions,
allocate_id: F,
) -> Result<DocumentProjection, ProjectionError>
where
F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
{
project_document_xml_with_limit(
xml,
DocxLimits::default().maximum_paragraphs,
DocxLimits::default().maximum_structural_facts,
options,
Err(StructuralFactUnknownReason::DocumentPartOnly),
allocate_id,
)
}
fn project_document_xml_with_limit<F>(
xml: &[u8],
maximum_paragraphs: usize,
maximum_structural_facts: usize,
options: ProjectionOptions,
styles: Result<&structure::StyleSheet, StructuralFactUnknownReason>,
mut allocate_id: F,
) -> Result<DocumentProjection, ProjectionError>
where
F: FnMut(ParagraphIdentityFacts<'_>) -> Result<InternalParagraphId, ProjectionError>,
{
let projected = ooxml::project_document_xml(
xml,
maximum_paragraphs,
options.revision_view,
options.text_materialization,
)?;
let mut seen_ids = HashSet::with_capacity(projected.paragraphs.len());
let mut ids = Vec::with_capacity(projected.paragraphs.len());
for paragraph in &projected.paragraphs {
let facts = ParagraphIdentityFacts {
ordinal: paragraph.ordinal,
package_paragraph_id: paragraph.package_paragraph_id,
text: ¶graph.text,
};
let id = allocate_id(facts)?;
if !seen_ids.insert(id.clone()) {
return Err(ProjectionError::DuplicateInternalParagraphId);
}
ids.push(id);
}
let texts = projected
.paragraphs
.iter()
.map(|paragraph| paragraph.text.as_str())
.collect::<Vec<_>>();
let properties = projected
.paragraphs
.iter()
.map(|paragraph| paragraph.properties.clone())
.collect::<Vec<_>>();
let structural_facts = structure::materialize_structure(
structure::RawStructureInput {
paragraph_texts: &texts,
properties: &properties,
bookmarks: projected.bookmarks.as_deref().map_err(|reason| *reason),
references: projected.references.as_deref().map_err(|reason| *reason),
},
styles,
maximum_structural_facts,
)?;
let mut paragraphs = Vec::with_capacity(projected.paragraphs.len());
for (id, paragraph) in ids.into_iter().zip(projected.paragraphs) {
paragraphs.push(ProjectedParagraph {
id,
ordinal: paragraph.ordinal,
package_paragraph_id: paragraph.package_paragraph_id,
text: paragraph.text,
formatting: paragraph.formatting,
structure: paragraph.structure,
});
}
Ok(DocumentProjection {
paragraphs,
revision_status: projected.revision_status,
structural_facts,
})
}