///|
/// Diagnostic emitted while reading or converting a document.
pub(all) enum Message {
Warning(String)
Error(String)
} derive(Debug, Eq)
///|
/// Creates a warning diagnostic.
pub fn warning(message : String) -> Message {
Warning(message)
}
///|
/// Creates an error diagnostic.
pub fn error(message : String) -> Message {
Error(message)
}
///|
/// A machine-readable XML work or allocation guard. Keeping this distinct
/// from diagnostic text prevents document-controlled names and identifiers
/// from forging resource-limit status at higher API layers.
pub(all) enum DocxXmlResourceLimit {
DocxXmlSourceUnits
DocxXmlTokens
DocxXmlTokenLength
DocxXmlMaterializedCharacters
DocxXmlNestingDepth
} derive(Debug, Eq)
///|
/// Stable human-readable text retained for CLI and validation diagnostics.
pub fn DocxXmlResourceLimit::message(self : DocxXmlResourceLimit) -> String {
match self {
DocxXmlSourceUnits => "XML source budget exceeded"
DocxXmlTokens => "XML token budget exceeded"
DocxXmlTokenLength => "XML token length budget exceeded"
DocxXmlMaterializedCharacters =>
"XML materialized character budget exceeded"
DocxXmlNestingDepth => "XML nesting too deep"
}
}
///|
/// Errors raised for invalid archives, invalid XML, missing parts, or unsupported input.
pub(all) suberror DocxError {
InvalidZip(message~ : String)
InvalidXml(message~ : String)
MissingPart(message~ : String)
Unsupported(message~ : String)
ResourceLimit(limit~ : DocxXmlResourceLimit, message~ : String)
// A package-WRITE resource ceiling (output/entries/uncompressed bytes) was
// exceeded during bounded serialization. Distinct from ResourceLimit, which
// is an XML-READ/parser guard. `kind` is machine-readable; office maps this
// to office.docx.resource_limit with limit/actual.
WriteResourceLimit(
kind~ : String,
limit~ : Int64,
actual~ : Int64,
message~ : String
)
} derive(Debug, Eq)
///|
/// Constructs a typed XML resource error with its stable default diagnostic.
pub fn docx_xml_resource_limit_error(limit : DocxXmlResourceLimit) -> DocxError {
ResourceLimit(limit~, message=limit.message())
}
///|
/// Adds trusted operation context while preserving the machine discriminator.
pub fn contextual_docx_xml_resource_limit_error(
limit : DocxXmlResourceLimit,
message : String,
) -> DocxError {
ResourceLimit(limit~, message~)
}
///|
/// Returns the typed XML resource discriminator, if this error represents a
/// bounded parser/scanner guard rather than malformed document content.
pub fn DocxError::xml_resource_limit(self : DocxError) -> DocxXmlResourceLimit? {
match self {
ResourceLimit(limit~, ..) => Some(limit)
_ => None
}
}
///|
/// Returns the stable caller-facing message for every DOCX error variant.
pub fn DocxError::description(self : DocxError) -> String {
match self {
InvalidZip(message~)
| InvalidXml(message~)
| MissingPart(message~)
| Unsupported(message~) => message
ResourceLimit(message~, ..) => message
WriteResourceLimit(message~, ..) => message
}
}
///|
/// Converted value together with accumulated diagnostics.
pub(all) struct ConversionResult {
value : String
messages : Array[Message]
} derive(Debug, Eq)
///|
/// Creates a successful conversion result with no diagnostics.
pub fn success(value : String) -> ConversionResult {
{ value, messages: [] }
}
///|
/// Removes duplicate diagnostics while preserving order.
pub fn dedupe_messages(messages : Array[Message]) -> Array[Message] {
let result : Array[Message] = []
// Diagnostic text can contain attacker-controlled XML names. Ordered sets
// keep deduplication independent of runtime string-hash collisions while
// `result` continues to preserve first-seen order.
let warning_texts : @sorted_set.SortedSet[String] = SortedSet([])
let error_texts : @sorted_set.SortedSet[String] = SortedSet([])
for message in messages {
match message {
Warning(value) =>
if !warning_texts.contains(value) {
warning_texts.add(value)
result.push(message)
}
Error(value) =>
if !error_texts.contains(value) {
error_texts.add(value)
result.push(message)
}
}
}
result
}
///|
/// Maps the converted value while preserving diagnostics.
pub fn ConversionResult::map(
self : ConversionResult,
f : (String) -> String raise?,
) -> ConversionResult raise? {
{ value: f(self.value), messages: self.messages }
}
///|
/// Flat-maps the converted value while preserving diagnostics.
pub fn ConversionResult::flat_map(
self : ConversionResult,
f : (String) -> ConversionResult raise?,
) -> ConversionResult raise? {
let next = f(self.value)
let messages = []
messages.append(self.messages)
messages.append(next.messages)
{ value: next.value, messages: dedupe_messages(messages) }
}
///|
/// Combines two conversion results.
pub fn ConversionResult::combine(
self : ConversionResult,
other : ConversionResult,
) -> ConversionResult {
combine_results([self, other])
}
///|
/// Combines conversion results, preserving values and diagnostics.
pub fn combine_results(results : Array[ConversionResult]) -> ConversionResult {
let value = StringBuilder()
let messages : Array[Message] = []
for result in results {
value.write_string(result.value)
messages.append(result.messages)
}
{ value: value.to_string(), messages: dedupe_messages(messages) }
}