// ECMA-376 Part 3 Markup Compatibility processing for strict XML trees.
// This entry point deliberately uses an empty markup configuration (there are
// no application-defined extension elements) and a caller-supplied application
// namespace configuration. OPC Relationships parts use exactly that model.
///|
const MCE_NAMESPACE_URI : String = "http://schemas.openxmlformats.org/markup-compatibility/2006"
///|
const MCE_ALTERNATE_CONTENT : String = "{http://schemas.openxmlformats.org/markup-compatibility/2006}AlternateContent"
///|
const MCE_CHOICE : String = "{http://schemas.openxmlformats.org/markup-compatibility/2006}Choice"
///|
const MCE_FALLBACK : String = "{http://schemas.openxmlformats.org/markup-compatibility/2006}Fallback"
///|
const MCE_IGNORABLE : String = "{http://schemas.openxmlformats.org/markup-compatibility/2006}Ignorable"
///|
const MCE_PROCESS_CONTENT : String = "{http://schemas.openxmlformats.org/markup-compatibility/2006}ProcessContent"
///|
// PreserveElements and PreserveAttributes were compatibility-rule attributes
// in the first edition of ECMA-376. Later editions removed them, but existing
// Office packages can still contain the preservation hints.
const MCE_PRESERVE_ELEMENTS : String = "{http://schemas.openxmlformats.org/markup-compatibility/2006}PreserveElements"
///|
const MCE_PRESERVE_ATTRIBUTES : String = "{http://schemas.openxmlformats.org/markup-compatibility/2006}PreserveAttributes"
///|
const MCE_MUST_UNDERSTAND : String = "{http://schemas.openxmlformats.org/markup-compatibility/2006}MustUnderstand"
///|
const MCE_XML_BASE : String = "{http://www.w3.org/XML/1998/namespace}base"
///|
const MCE_XML_LANG : String = "{http://www.w3.org/XML/1998/namespace}lang"
///|
const MCE_XML_SPACE : String = "{http://www.w3.org/XML/1998/namespace}space"
///|
priv struct MceNamespaceChange {
prefix : String
previous : String?
}
///|
priv struct MceProcessAddition {
key : String
wildcard : Bool
}
///|
priv struct MceElementScope {
namespace_changes : Array[MceNamespaceChange]
ignorable_additions : Array[String]
process_additions : Array[MceProcessAddition]
must_understand_satisfied : Bool
}
///|
priv struct MceProcessor {
budget : XmlReadBudget
application_namespaces : @sorted_map.SortedMap[String, Bool]
namespaces : @sorted_map.SortedMap[String, String]
ignorable_namespaces : @sorted_map.SortedMap[String, Int]
process_exact_names : @sorted_map.SortedMap[String, Int]
process_wildcard_namespaces : @sorted_map.SortedMap[String, Int]
}
///|
fn mce_mismatch(message : String) -> Unit raise DocxError {
raise InvalidXml(message="markup compatibility mismatch: " + message)
}
///|
fn mce_xml_whitespace_only(value : String) -> Bool {
for character in value {
if character != ' ' &&
character != '\t' &&
character != '\n' &&
character != '\r' {
return false
}
}
true
}
///|
fn mce_namespace_from_expanded_name(
name : String,
budget : XmlReadBudget,
) -> String? raise DocxError {
if !name.has_prefix("{") {
return None
}
match name.rev_find("}") {
Some(close) if close > 0 => {
budget.charge_derived_chars(close - 1)
Some(name[1:close].to_owned())
}
_ => None
}
}
///|
// Use the final Clark-name separator rather than a string prefix: namespace
// names may themselves contain `}`, and must not impersonate `xmlns`.
fn mce_expanded_namespace_is(name : String, namespace_uri : String) -> Bool {
if !name.has_prefix("{") {
return false
}
match name.rev_find("}") {
Some(close) if close > 0 => name[1:close] == namespace_uri
_ => false
}
}
///|
fn mce_split_tokens(
value : String,
budget : XmlReadBudget,
) -> Array[String] raise DocxError {
let tokens : Array[String] = []
let mut index = 0
while index < value.length() {
while index < value.length() {
let character = match value.get_char(index) {
Some(character) => character
None => raise InvalidXml(message="invalid UTF-16 in MCE attribute")
}
if character != ' ' &&
character != '\t' &&
character != '\n' &&
character != '\r' {
break
}
index = index + character.utf16_len()
}
if index == value.length() {
break
}
let start = index
while index < value.length() {
let character = match value.get_char(index) {
Some(character) => character
None => raise InvalidXml(message="invalid UTF-16 in MCE attribute")
}
if character == ' ' ||
character == '\t' ||
character == '\n' ||
character == '\r' {
break
}
index = index + character.utf16_len()
}
budget.charge_namespace_bindings(1)
budget.charge_derived_chars(index - start)
tokens.push(value[start:index].to_owned())
}
tokens
}
///|
fn mce_expanded_name(
namespace_uri : String,
local_name : String,
budget : XmlReadBudget,
) -> String raise DocxError {
let length = xml_derived_name_length(
namespace_uri.length(),
local_name.length(),
2,
)
budget.charge_derived_chars(length)
let output = StringBuilder()
output.write_char('{')
output.write_string(namespace_uri)
output.write_char('}')
output.write_string(local_name)
output.to_string()
}
///|
fn MceProcessor::application_understands(
self : MceProcessor,
namespace_uri : String,
) -> Bool {
self.application_namespaces.contains(namespace_uri)
}
///|
fn increment_mce_count(
counts : @sorted_map.SortedMap[String, Int],
key : String,
) -> Unit {
match counts.get(key) {
Some(count) => counts[key] = count + 1
None => counts[key] = 1
}
}
///|
fn decrement_mce_count(
counts : @sorted_map.SortedMap[String, Int],
key : String,
) -> Unit {
match counts.get(key) {
Some(count) if count > 1 => counts[key] = count - 1
Some(_) => counts.remove(key)
None => ()
}
}
///|
fn MceProcessor::namespace_is_ignorable(
self : MceProcessor,
namespace_uri : String,
) -> Bool {
self.ignorable_namespaces.contains(namespace_uri)
}
///|
fn MceProcessor::element_matches_process_content(
self : MceProcessor,
element_name : String,
namespace_uri : String,
) -> Bool {
self.process_exact_names.contains(element_name) ||
self.process_wildcard_namespaces.contains(namespace_uri)
}
///|
fn MceProcessor::resolve_prefix(
self : MceProcessor,
prefix : String,
directive : String,
) -> String raise DocxError {
if !is_xml_ncname(prefix) {
mce_mismatch(directive + " contains an invalid namespace prefix")
}
match self.namespaces.get(prefix) {
Some(namespace_uri) if namespace_uri != MCE_NAMESPACE_URI => namespace_uri
Some(_) => {
mce_mismatch(directive + " cannot name the MCE namespace")
""
}
None => {
mce_mismatch(directive + " contains an unbound namespace prefix")
""
}
}
}
///|
fn MceProcessor::apply_namespace_declarations(
self : MceProcessor,
element : XmlElement,
) -> Array[MceNamespaceChange] raise DocxError {
let changes : Array[MceNamespaceChange] = []
for name, namespace_uri in element.attributes {
guard mce_expanded_namespace_is(name, XMLNS_NAMESPACE_URI) else { continue }
guard name.rev_find("}") is Some(close) else {
raise InvalidXml(message="invalid retained XML namespace declaration")
}
let local_start = close + 1
self.budget.charge_derived_chars(name.length() - local_start)
let declaration_name = name[local_start:].to_owned()
let prefix = if declaration_name == "xmlns" { "" } else { declaration_name }
self.budget.charge_namespace_bindings(2)
changes.push({ prefix, previous: self.namespaces.get(prefix) })
self.namespaces[prefix] = namespace_uri
}
changes
}
///|
fn MceProcessor::add_ignorable_namespaces(
self : MceProcessor,
value : String,
) -> Array[String] raise DocxError {
let additions : Array[String] = []
for prefix in mce_split_tokens(value, self.budget) {
let namespace_uri = self.resolve_prefix(prefix, "Ignorable")
self.budget.charge_namespace_bindings(1)
increment_mce_count(self.ignorable_namespaces, namespace_uri)
additions.push(namespace_uri)
}
additions
}
///|
fn MceProcessor::add_process_content_names(
self : MceProcessor,
value : String,
) -> Array[MceProcessAddition] raise DocxError {
let additions : Array[MceProcessAddition] = []
for token in mce_split_tokens(value, self.budget) {
let (namespace_uri, local_name) = self.resolve_mce_name_pair(
token, "ProcessContent",
)
if !self.namespace_is_ignorable(namespace_uri) {
mce_mismatch("ProcessContent names a namespace that is not ignorable")
}
self.budget.charge_namespace_bindings(1)
if local_name == "*" {
increment_mce_count(self.process_wildcard_namespaces, namespace_uri)
additions.push({ key: namespace_uri, wildcard: true })
} else {
let expanded = mce_expanded_name(namespace_uri, local_name, self.budget)
increment_mce_count(self.process_exact_names, expanded)
additions.push({ key: expanded, wildcard: false })
}
}
additions
}
///|
fn MceProcessor::resolve_mce_name_pair(
self : MceProcessor,
token : String,
directive : String,
) -> (String, String) raise DocxError {
guard token.find(":") is Some(colon) &&
colon > 0 &&
colon + 1 < token.length() &&
!token[colon + 1:].contains(":") else {
mce_mismatch(directive + " contains an invalid name pair")
("", "")
}
self.budget.charge_derived_chars(token.length() - 1)
let prefix = token[:colon].to_owned()
let local_name = token[colon + 1:].to_owned()
if local_name != "*" && !is_xml_ncname(local_name) {
mce_mismatch(directive + " contains an invalid local name")
}
(self.resolve_prefix(prefix, directive), local_name)
}
///|
fn MceProcessor::validate_legacy_preservation_names(
self : MceProcessor,
value : String,
directive : String,
same_element_ignorable_namespaces : ArrayView[String],
) -> Unit raise DocxError {
for token in mce_split_tokens(value, self.budget) {
let (namespace_uri, _local_name) = self.resolve_mce_name_pair(
token, directive,
)
if !same_element_ignorable_namespaces.any(candidate => {
candidate == namespace_uri
}) {
mce_mismatch(
directive +
" names a namespace that is not declared ignorable on the same element",
)
}
self.budget.charge_namespace_bindings(1)
}
}
///|
fn MceProcessor::must_understand_is_satisfied(
self : MceProcessor,
value : String,
) -> Bool raise DocxError {
let mut satisfied = true
for prefix in mce_split_tokens(value, self.budget) {
let namespace_uri = self.resolve_prefix(prefix, "MustUnderstand")
if !self.application_understands(namespace_uri) {
satisfied = false
}
}
satisfied
}
///|
fn MceProcessor::enter_element(
self : MceProcessor,
element : XmlElement,
) -> MceElementScope raise DocxError {
self.budget.charge_namespace_bindings(1)
let namespace_changes = self.apply_namespace_declarations(element)
let ignorable_additions = match element.attributes.get(MCE_IGNORABLE) {
Some(value) => self.add_ignorable_namespaces(value)
None => []
}
let process_additions = match element.attributes.get(MCE_PROCESS_CONTENT) {
Some(value) => self.add_process_content_names(value)
None => []
}
// These legacy attributes are preservation hints for markup editors, not
// semantic application content. Validate their scoped contracts here; the
// raw tree returned by the paired-root API remains the preservation view,
// while filter_attributes removes the hints from the effective view.
match element.attributes.get(MCE_PRESERVE_ELEMENTS) {
Some(value) =>
self.validate_legacy_preservation_names(
value, "PreserveElements", ignorable_additions,
)
None => ()
}
match element.attributes.get(MCE_PRESERVE_ATTRIBUTES) {
Some(value) =>
self.validate_legacy_preservation_names(
value, "PreserveAttributes", ignorable_additions,
)
None => ()
}
let must_understand_satisfied = match
element.attributes.get(MCE_MUST_UNDERSTAND) {
Some(value) => self.must_understand_is_satisfied(value)
None => true
}
{
namespace_changes,
ignorable_additions,
process_additions,
must_understand_satisfied,
}
}
///|
fn MceProcessor::leave_element(
self : MceProcessor,
scope : MceElementScope,
) -> Unit {
for offset in 0.. self.namespaces[change.prefix] = previous
None => self.namespaces.remove(change.prefix)
}
}
}
///|
fn mce_require_must_understand(scope : MceElementScope) -> Unit raise DocxError {
if !scope.must_understand_satisfied {
mce_mismatch("MustUnderstand names an unsupported namespace")
}
}
///|
fn MceProcessor::qualified_attribute_allowed_on_mce_element(
self : MceProcessor,
name : String,
) -> Bool raise DocxError {
guard mce_namespace_from_expanded_name(name, self.budget)
is Some(namespace_uri) else {
return false
}
namespace_uri != XML_NAMESPACE_URI &&
(
namespace_uri == MCE_NAMESPACE_URI ||
self.namespace_is_ignorable(namespace_uri)
)
}
///|
fn MceProcessor::validate_alternate_content_attributes(
self : MceProcessor,
element : XmlElement,
) -> Unit raise DocxError {
for name, _ in element.attributes {
if mce_expanded_namespace_is(name, XMLNS_NAMESPACE_URI) {
continue
}
if !self.qualified_attribute_allowed_on_mce_element(name) {
mce_mismatch("AlternateContent has a disallowed attribute")
}
}
}
///|
fn MceProcessor::validate_choice_attributes(
self : MceProcessor,
element : XmlElement,
) -> String raise DocxError {
let mut requires : String? = None
for name, value in element.attributes {
if mce_expanded_namespace_is(name, XMLNS_NAMESPACE_URI) {
continue
}
if name == "Requires" {
requires = Some(value)
} else if !self.qualified_attribute_allowed_on_mce_element(name) {
mce_mismatch("Choice has a disallowed attribute")
}
}
match requires {
Some(value) => value
None => {
mce_mismatch("Choice is missing Requires")
""
}
}
}
///|
fn MceProcessor::validate_fallback_attributes(
self : MceProcessor,
element : XmlElement,
) -> Unit raise DocxError {
for name, _ in element.attributes {
if mce_expanded_namespace_is(name, XMLNS_NAMESPACE_URI) {
continue
}
if !self.qualified_attribute_allowed_on_mce_element(name) {
mce_mismatch("Fallback has a disallowed attribute")
}
}
}
///|
fn MceProcessor::choice_is_supported(
self : MceProcessor,
requires : String,
) -> Bool raise DocxError {
let prefixes = mce_split_tokens(requires, self.budget)
if prefixes.is_empty() {
mce_mismatch("Choice Requires must contain at least one prefix")
}
let mut supported = true
for prefix in prefixes {
let namespace_uri = self.resolve_prefix(prefix, "Choice Requires")
if !self.application_understands(namespace_uri) {
supported = false
}
}
supported
}
///|
fn MceProcessor::filter_attributes(
self : MceProcessor,
element : XmlElement,
) -> @sorted_map.SortedMap[String, String] raise DocxError {
let output : @sorted_map.SortedMap[String, String] = SortedMap([])
for name, value in element.attributes {
self.budget.charge_namespace_bindings(1)
if mce_expanded_namespace_is(name, XMLNS_NAMESPACE_URI) {
continue
}
match mce_namespace_from_expanded_name(name, self.budget) {
Some(namespace_uri) if namespace_uri == MCE_NAMESPACE_URI &&
(
name == MCE_IGNORABLE ||
name == MCE_PROCESS_CONTENT ||
name == MCE_PRESERVE_ELEMENTS ||
name == MCE_PRESERVE_ATTRIBUTES ||
name == MCE_MUST_UNDERSTAND
) => continue
Some(namespace_uri) if self.namespace_is_ignorable(namespace_uri) &&
!self.application_understands(namespace_uri) => continue
_ => output[name] = value
}
}
output
}
///|
fn MceProcessor::process_children(
self : MceProcessor,
children : Array[XmlNode],
) -> Array[XmlNode] raise DocxError {
let output : Array[XmlNode] = []
for child in children {
self.budget.charge_namespace_bindings(1)
match child {
XmlText(value) => output.push(XmlText(value))
XmlElement(element) => output.append(self.process_element(element))
}
}
output
}
///|
fn MceProcessor::foreign_alternate_content_child_is_ignored(
self : MceProcessor,
element : XmlElement,
) -> Bool raise DocxError {
let scope = self.enter_element(element)
let ignored = match
mce_namespace_from_expanded_name(element.name, self.budget) {
Some(namespace_uri) =>
self.namespace_is_ignorable(namespace_uri) &&
!self.application_understands(namespace_uri) &&
!self.element_matches_process_content(element.name, namespace_uri)
None => false
}
self.leave_element(scope)
ignored
}
///|
fn MceProcessor::process_choice(
self : MceProcessor,
element : XmlElement,
select_if_supported : Bool,
) -> (Bool, Array[XmlNode]) raise DocxError {
let scope = self.enter_element(element)
let requires = self.validate_choice_attributes(element)
let supported = self.choice_is_supported(requires)
let selected = select_if_supported && supported
let output = if selected {
mce_require_must_understand(scope)
self.process_children(element.children)
} else {
[]
}
self.leave_element(scope)
(selected, output)
}
///|
fn MceProcessor::process_fallback(
self : MceProcessor,
element : XmlElement,
selected : Bool,
) -> Array[XmlNode] raise DocxError {
let scope = self.enter_element(element)
self.validate_fallback_attributes(element)
let output = if selected {
mce_require_must_understand(scope)
self.process_children(element.children)
} else {
[]
}
self.leave_element(scope)
output
}
///|
fn MceProcessor::process_alternate_content(
self : MceProcessor,
element : XmlElement,
scope : MceElementScope,
) -> Array[XmlNode] raise DocxError {
self.validate_alternate_content_attributes(element)
mce_require_must_understand(scope)
let mut choices = 0
let mut saw_fallback = false
let mut selected = false
let selected_content : Array[XmlNode] = []
for child in element.children {
match child {
XmlText(value) =>
if !mce_xml_whitespace_only(value) {
mce_mismatch("AlternateContent contains non-whitespace text")
}
XmlElement(child_element) =>
if child_element.name == MCE_CHOICE {
if saw_fallback {
mce_mismatch("Choice follows Fallback in AlternateContent")
}
choices = choices + 1
let (choice_selected, content) = self.process_choice(
child_element,
!selected,
)
if choice_selected {
selected = true
selected_content.append(content)
}
} else if child_element.name == MCE_FALLBACK {
if saw_fallback {
mce_mismatch("AlternateContent has multiple Fallback elements")
}
saw_fallback = true
if !selected {
selected = true
selected_content.append(self.process_fallback(child_element, true))
} else {
ignore(self.process_fallback(child_element, false))
}
} else if mce_namespace_from_expanded_name(
child_element.name,
self.budget,
) ==
Some(MCE_NAMESPACE_URI) {
mce_mismatch("AlternateContent has an unexpected MCE child")
} else if !self.foreign_alternate_content_child_is_ignored(
child_element,
) {
mce_mismatch("AlternateContent has a non-ignorable foreign child")
}
}
}
if choices == 0 {
mce_mismatch("AlternateContent has no Choice element")
}
selected_content
}
///|
fn MceProcessor::process_element(
self : MceProcessor,
element : XmlElement,
) -> Array[XmlNode] raise DocxError {
let scope = self.enter_element(element)
if element.name == MCE_ALTERNATE_CONTENT {
let output = self.process_alternate_content(element, scope)
self.leave_element(scope)
return output
}
match mce_namespace_from_expanded_name(element.name, self.budget) {
Some(namespace_uri) if namespace_uri == MCE_NAMESPACE_URI =>
mce_mismatch("Choice, Fallback, or unknown MCE element is misplaced")
Some(namespace_uri) if self.namespace_is_ignorable(namespace_uri) &&
!self.application_understands(namespace_uri) => {
if self.element_matches_process_content(element.name, namespace_uri) {
if element.attributes.contains(MCE_XML_BASE) ||
element.attributes.contains(MCE_XML_LANG) ||
element.attributes.contains(MCE_XML_SPACE) {
mce_mismatch(
"an unwrapped ProcessContent element has xml:base, xml:lang, or xml:space",
)
}
mce_require_must_understand(scope)
let output = self.process_children(element.children)
self.leave_element(scope)
return output
}
self.leave_element(scope)
return []
}
_ => ()
}
mce_require_must_understand(scope)
let attributes = self.filter_attributes(element)
let children = self.process_children(element.children)
let output_element = XmlElement::{ name: element.name, attributes, children }
self.leave_element(scope)
[XmlElement(output_element)]
}
///|
fn process_markup_compatibility(
root : XmlElement,
budget : XmlReadBudget,
application_namespaces : Array[String],
) -> XmlElement raise DocxError {
let understood : @sorted_map.SortedMap[String, Bool] = SortedMap([])
for namespace_uri in application_namespaces {
budget.charge_namespace_bindings(1)
understood[namespace_uri] = true
}
let processor = MceProcessor::{
budget,
application_namespaces: understood,
namespaces: SortedMap([("xml", XML_NAMESPACE_URI)]),
ignorable_namespaces: SortedMap([]),
process_exact_names: SortedMap([]),
process_wildcard_namespaces: SortedMap([]),
}
let output = processor.process_element(root)
let mut document_root : XmlElement? = None
for node in output {
match node {
XmlText(value) =>
if !mce_xml_whitespace_only(value) {
mce_mismatch("processing did not produce a single document element")
}
XmlElement(element) =>
if document_root is Some(_) {
mce_mismatch("processing produced multiple document elements")
} else {
document_root = Some(element)
}
}
}
match document_root {
Some(element) => element
None => {
mce_mismatch("processing produced no document element")
xml_element("")
}
}
}
///|
fn read_xml_string_strict_mce_encoded_roots_with_mode(
source : String,
source_encoding : XmlSourceEncoding,
budget : XmlReadBudget,
application_namespaces : Array[String],
source_already_charged : Bool,
) -> (XmlElement, XmlElement) raise DocxError {
let root = read_xml_string_with_mode(
source,
Map([]),
true,
Some(budget),
source_already_charged,
Some(source_encoding),
true,
)
let effective_root = process_markup_compatibility(
root, budget, application_namespaces,
)
(root, effective_root)
}
///|
fn read_xml_string_strict_mce_encoded_with_mode(
source : String,
source_encoding : XmlSourceEncoding,
budget : XmlReadBudget,
application_namespaces : Array[String],
source_already_charged : Bool,
) -> XmlElement raise DocxError {
let (_, effective_root) = read_xml_string_strict_mce_encoded_roots_with_mode(
source, source_encoding, budget, application_namespaces, source_already_charged,
)
effective_root
}
///|
/// Strictly parses an already decoded XML string, enforces the declared byte
/// encoding, preserves namespace scope long enough to apply ECMA-376 Part 3
/// Markup Compatibility processing, and charges the caller-owned cumulative
/// budget. String input is charged in UTF-16 storage units before parsing.
/// The markup configuration is empty; callers provide the namespaces their
/// application understands.
pub fn read_xml_string_strict_mce_encoded_limited(
source : String,
source_encoding : XmlSourceEncoding,
budget : XmlReadBudget,
application_namespaces : Array[String],
) -> XmlElement raise DocxError {
read_xml_string_strict_mce_encoded_with_mode(
source, source_encoding, budget, application_namespaces, false,
)
}
///|
/// Strictly parses UTF-8 XML, preserves namespace scope long enough to apply
/// ECMA-376 Part 3 Markup Compatibility processing, and returns the processed
/// single-root tree. The markup configuration is empty; callers provide the
/// namespaces their application understands.
pub fn read_xml_bytes_strict_mce_limited(
source : BytesView,
budget : XmlReadBudget,
application_namespaces : Array[String],
) -> XmlElement raise DocxError {
budget.charge_source(source.length())
let text = @utf8.decode(source, ignore_bom=true) catch {
_ => raise InvalidXml(message="XML source is not valid UTF-8")
}
read_xml_string_strict_mce_encoded_with_mode(
text,
Utf8,
budget,
application_namespaces,
true,
)
}
///|
/// Strictly parses UTF-8 XML and returns both the raw document tree and the
/// single root selected by Markup Compatibility. Mutation callers inspect the
/// raw tree before changing preserved bytes, while semantic readers consume
/// the effective tree. Both projections come from one bounded parse.
pub fn read_xml_bytes_strict_mce_with_roots_limited(
source : BytesView,
budget : XmlReadBudget,
application_namespaces : Array[String],
) -> (XmlElement, XmlElement) raise DocxError {
budget.charge_source(source.length())
let text = @utf8.decode(source, ignore_bom=true) catch {
_ => raise InvalidXml(message="XML source is not valid UTF-8")
}
read_xml_string_strict_mce_encoded_roots_with_mode(
text,
Utf8,
budget,
application_namespaces,
true,
)
}