use std::collections::HashMap;
use oxml_opc::OpcPackage;
use quick_xml::XmlVersion;
use quick_xml::events::Event;
use quick_xml::name::{Namespace, ResolveResult};
use quick_xml::reader::NsReader;
use rdocx_oxml::content_control::{CT_DataBinding, CT_Sdt, SdtContent, SdtType};
use rdocx_oxml::document::{BodyContent, CT_Body};
use rdocx_oxml::namespace::matches_local_name;
use rdocx_oxml::table::{CT_Row, CT_Tbl, CT_Tc, CellContent};
use rdocx_oxml::text::{CT_P, CT_R, CT_Text, RunContent};
use crate::{Document, Error, Result};
const CUSTOM_XML_REL_TYPE: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXml";
const CUSTOM_XML_PROPS_REL_TYPE: &str =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXmlProps";
const CUSTOM_XML_PROPS_NS: &[u8] =
b"http://schemas.openxmlformats.org/officeDocument/2006/customXml";
#[derive(Debug, Clone, Copy)]
pub struct ContentControlRef<'a> {
pub(crate) inner: &'a CT_Sdt,
}
impl ContentControlRef<'_> {
pub fn tag(&self) -> Option<&str> {
self.inner
.properties
.as_ref()
.and_then(|properties| properties.tag.as_deref())
}
pub fn alias(&self) -> Option<&str> {
self.inner
.properties
.as_ref()
.and_then(|properties| properties.alias.as_deref())
}
pub fn id(&self) -> Option<i32> {
self.inner
.properties
.as_ref()
.and_then(|properties| properties.id)
}
pub fn control_type(&self) -> Option<SdtType> {
self.inner
.properties
.as_ref()
.and_then(|properties| properties.control_type)
}
pub fn text(&self) -> String {
let mut text = String::new();
collect_sdt_text(self.inner, &mut text);
text
}
}
#[derive(Clone)]
struct ControlValue {
display: String,
bound: String,
checkbox: Option<(bool, String)>,
full_date: Option<String>,
}
fn prepare_control_value(control: &CT_Sdt, value: &str) -> Result<ControlValue> {
let kind = control
.properties
.as_ref()
.and_then(|properties| properties.control_type);
if kind == Some(SdtType::CheckBox) {
let checked = match value.trim().to_ascii_lowercase().as_str() {
"true" | "1" | "yes" => true,
"false" | "0" | "no" => false,
_ => {
return Err(Error::Other(
"checkbox value must be true or false (also 1/0, yes/no)".to_owned(),
));
}
};
let (glyph, font) = control
.properties
.as_ref()
.unwrap()
.checkbox_state(checked)?;
return Ok(ControlValue {
display: glyph.to_string(),
bound: checked.to_string(),
checkbox: Some((checked, font)),
full_date: None,
});
}
if matches!(kind, Some(SdtType::Picture | SdtType::Group)) {
return Err(Error::Other(
"selected content-control kind does not support a text value".to_owned(),
));
}
let (display, bound, full_date) = if let Some(properties) = &control.properties {
properties.prepare_text_value(value)?
} else {
(value.to_owned(), value.to_owned(), None)
};
Ok(ControlValue {
display,
bound,
checkbox: None,
full_date,
})
}
impl Document {
pub fn content_controls(&self) -> Vec<ContentControlRef<'_>> {
self.document
.body
.content_controls()
.into_iter()
.map(|inner| ContentControlRef { inner })
.collect()
}
pub fn content_controls_by_tag(&self, tag: &str) -> Vec<ContentControlRef<'_>> {
self.content_controls()
.into_iter()
.filter(|control| control.tag() == Some(tag))
.collect()
}
pub fn content_controls_by_alias(&self, alias: &str) -> Vec<ContentControlRef<'_>> {
self.content_controls()
.into_iter()
.filter(|control| control.alias() == Some(alias))
.collect()
}
pub fn set_content_control_value_by_tag(&mut self, tag: &str, value: &str) -> Result<usize> {
self.apply_content_control_values(|control| {
(control.tag() == Some(tag)).then(|| value.to_owned())
})
}
pub fn fill_content_control_tags(&mut self, pairs: &[(&str, &str)]) -> Result<Vec<usize>> {
let mut seen = std::collections::HashSet::new();
for &(tag, value) in pairs {
if !seen.insert(tag) {
return Err(Error::Other(format!(
"duplicate content-control tag {tag:?}"
)));
}
oxml_core::xml::reject_non_xml_characters("content-control value", value)?;
}
if pairs.is_empty() {
return Ok(Vec::new());
}
let mut candidate = self.clone_for_staging();
let mut counts = Vec::with_capacity(pairs.len());
for &(tag, value) in pairs {
let count = candidate.set_content_control_value_by_tag(tag, value)?;
if count == 0 {
return Err(Error::Other(format!("no content control has tag {tag:?}")));
}
counts.push(count);
}
self.commit_staged_mutation(candidate.prepare_and_reopen_staged()?);
Ok(counts)
}
pub fn set_content_control_value_by_alias(
&mut self,
alias: &str,
value: &str,
) -> Result<usize> {
self.apply_content_control_values(|control| {
(control.alias() == Some(alias)).then(|| value.to_owned())
})
}
pub fn bind_content_controls(&mut self, values: &HashMap<String, String>) -> Result<usize> {
self.apply_content_control_values(|control| {
control
.tag()
.and_then(|tag| values.get(tag))
.or_else(|| control.alias().and_then(|alias| values.get(alias)))
.cloned()
})
}
fn apply_content_control_values(
&mut self,
select: impl Fn(ContentControlRef<'_>) -> Option<String>,
) -> Result<usize> {
let selected = self
.document
.body
.content_controls()
.into_iter()
.enumerate()
.filter_map(|(index, inner)| {
let value = select(ContentControlRef { inner })?;
let binding = inner
.properties
.as_ref()
.and_then(|properties| properties.data_binding.clone());
Some(prepare_control_value(inner, &value).map(|value| (index, value, binding)))
})
.collect::<Result<Vec<_>>>()?;
if selected.is_empty() {
return Ok(0);
}
let replacements = selected
.iter()
.map(|(index, value, _)| (*index, value.clone()))
.collect::<HashMap<_, _>>();
let mut staged_document = self.document.clone();
replace_document_controls(&mut staged_document.body, &replacements)?;
staged_document.to_xml()?;
let mut staged_package = self.package.clone();
let mut edits_by_part: HashMap<String, Vec<XmlEdit>> = HashMap::new();
for (_, value, binding) in &selected {
let Some(binding) = binding else {
continue;
};
let (part_name, edit) =
resolve_binding_edit(&staged_package, &self.doc_part_name, binding, &value.bound)?;
edits_by_part.entry(part_name).or_default().push(edit);
}
for (part_name, edits) in edits_by_part {
let original = staged_package
.get_part(&part_name)
.ok_or_else(|| Error::Other(format!("custom XML part {part_name} is missing")))?;
let changed = apply_xml_edits(original, edits)?;
staged_package.set_part(&part_name, changed);
}
self.document = staged_document;
self.package = staged_package;
self.invalidate_layout();
Ok(selected.len())
}
}
fn collect_sdt_text(sdt: &CT_Sdt, output: &mut String) {
for content in &sdt.content {
match content {
SdtContent::Paragraph(paragraph) => collect_paragraph_text(paragraph, output),
SdtContent::Table(table) => collect_table_text(table, output),
SdtContent::Row(row) => collect_row_text(row, output),
SdtContent::Cell(cell) => collect_cell_text(cell, output),
SdtContent::Run(run) => output.push_str(&run.text()),
SdtContent::ContentControl(_) | SdtContent::RawXml(_) => {}
}
}
}
fn collect_paragraph_text(paragraph: &CT_P, output: &mut String) {
for run in ¶graph.runs {
output.push_str(&run.text());
}
}
fn collect_table_text(table: &CT_Tbl, output: &mut String) {
for row in &table.rows {
collect_row_text(row, output);
}
}
fn collect_row_text(row: &CT_Row, output: &mut String) {
for cell in &row.cells {
collect_cell_text(cell, output);
}
}
fn collect_cell_text(cell: &CT_Tc, output: &mut String) {
for content in &cell.content {
match content {
CellContent::Paragraph(paragraph) => collect_paragraph_text(paragraph, output),
CellContent::Table(table) => collect_table_text(table, output),
CellContent::ContentControl(_) => {}
}
}
}
fn replace_document_controls(
body: &mut CT_Body,
replacements: &HashMap<usize, ControlValue>,
) -> Result<()> {
let expected_count = body.content_controls().len();
let mut index = 0usize;
for content in &mut body.content {
match content {
BodyContent::Paragraph(paragraph) => {
replace_paragraph_controls(paragraph, replacements, &mut index)?
}
BodyContent::Table(table) => replace_table_controls(table, replacements, &mut index)?,
BodyContent::ContentControl(control) => {
replace_one_control(control, replacements, &mut index)?
}
BodyContent::RawXml(_) => {}
}
}
if index != expected_count || replacements.keys().any(|selected| *selected >= index) {
return Err(Error::Other(
"content-control traversal changed during staged mutation".to_owned(),
));
}
Ok(())
}
fn replace_one_control(
control: &mut CT_Sdt,
replacements: &HashMap<usize, ControlValue>,
index: &mut usize,
) -> Result<()> {
if let Some(value) = replacements.get(index) {
let mut current = String::new();
collect_sdt_text(control, &mut current);
if current != value.display || value.checkbox.is_some() {
replace_display_text(
control,
&value.display,
value.checkbox.as_ref().map(|(_, font)| font.as_str()),
)?;
}
if let Some(properties) = &mut control.properties {
properties.set_value_metadata(value.full_date.as_deref())?;
}
if let Some((checked, _)) = &value.checkbox {
control
.properties
.as_mut()
.unwrap()
.set_checkbox_checked(*checked)?;
}
}
*index = index
.checked_add(1)
.ok_or_else(|| Error::Other("content-control count overflow".to_owned()))?;
replace_nested_controls(control, replacements, index)
}
fn replace_nested_controls(
control: &mut CT_Sdt,
replacements: &HashMap<usize, ControlValue>,
index: &mut usize,
) -> Result<()> {
for content in &mut control.content {
match content {
SdtContent::Paragraph(paragraph) => {
replace_paragraph_controls(paragraph, replacements, index)?
}
SdtContent::Table(table) => replace_table_controls(table, replacements, index)?,
SdtContent::Row(row) => replace_row_controls(row, replacements, index)?,
SdtContent::Cell(cell) => replace_cell_controls(cell, replacements, index)?,
SdtContent::ContentControl(nested) => replace_one_control(nested, replacements, index)?,
SdtContent::Run(_) | SdtContent::RawXml(_) => {}
}
}
Ok(())
}
fn replace_paragraph_controls(
paragraph: &mut CT_P,
replacements: &HashMap<usize, ControlValue>,
index: &mut usize,
) -> Result<()> {
for position in 0..=paragraph.runs.len() {
for (_, _, _, control) in paragraph
.content_controls
.iter_mut()
.filter(|(at, _, _, _)| *at == position)
{
replace_one_control(control, replacements, index)?;
}
}
Ok(())
}
fn replace_table_controls(
table: &mut CT_Tbl,
replacements: &HashMap<usize, ControlValue>,
index: &mut usize,
) -> Result<()> {
for position in 0..=table.rows.len() {
for (_, _, control) in table
.content_controls
.iter_mut()
.filter(|(at, _, _)| *at == position)
{
replace_one_control(control, replacements, index)?;
}
if let Some(row) = table.rows.get_mut(position) {
replace_row_controls(row, replacements, index)?;
}
}
Ok(())
}
fn replace_row_controls(
row: &mut CT_Row,
replacements: &HashMap<usize, ControlValue>,
index: &mut usize,
) -> Result<()> {
for position in 0..=row.cells.len() {
for (_, _, control) in row
.content_controls
.iter_mut()
.filter(|(at, _, _)| *at == position)
{
replace_one_control(control, replacements, index)?;
}
if let Some(cell) = row.cells.get_mut(position) {
replace_cell_controls(cell, replacements, index)?;
}
}
Ok(())
}
fn replace_cell_controls(
cell: &mut CT_Tc,
replacements: &HashMap<usize, ControlValue>,
index: &mut usize,
) -> Result<()> {
for content in &mut cell.content {
match content {
CellContent::Paragraph(paragraph) => {
replace_paragraph_controls(paragraph, replacements, index)?
}
CellContent::Table(table) => replace_table_controls(table, replacements, index)?,
CellContent::ContentControl(control) => {
replace_one_control(control, replacements, index)?
}
}
}
Ok(())
}
fn replace_display_text(control: &mut CT_Sdt, value: &str, font: Option<&str>) -> Result<()> {
let mut replacement = DisplayReplacement {
value,
font,
wrote_value: false,
saw_run: false,
};
replace_sdt_display(control, &mut replacement);
if replacement.wrote_value {
return Ok(());
}
if add_text_to_first_paragraph(control, value) {
if font.is_some() {
let mut replacement = DisplayReplacement {
value,
font,
wrote_value: false,
saw_run: false,
};
replace_sdt_display(control, &mut replacement);
}
return Ok(());
}
Err(Error::Other(
"content control has no typed text display location".to_owned(),
))
}
struct DisplayReplacement<'a> {
value: &'a str,
font: Option<&'a str>,
wrote_value: bool,
saw_run: bool,
}
fn replace_sdt_display(control: &mut CT_Sdt, replacement: &mut DisplayReplacement<'_>) {
for content in &mut control.content {
match content {
SdtContent::Paragraph(paragraph) => replace_paragraph_display(paragraph, replacement),
SdtContent::Table(table) => replace_table_display(table, replacement),
SdtContent::Row(row) => replace_row_display(row, replacement),
SdtContent::Cell(cell) => replace_cell_display(cell, replacement),
SdtContent::Run(run) => replace_run_display(run, replacement),
SdtContent::ContentControl(_) | SdtContent::RawXml(_) => {}
}
}
}
fn replace_run_display(run: &mut CT_R, replacement: &mut DisplayReplacement<'_>) {
let is_first_run = !replacement.saw_run;
replacement.saw_run = true;
if is_first_run && let Some(font) = replacement.font {
let properties = run.properties.get_or_insert_with(Default::default);
properties.font_ascii = Some(font.to_owned());
properties.font_hansi = Some(font.to_owned());
properties.font_east_asia = Some(font.to_owned());
properties.font_cs = Some(font.to_owned());
properties.font_ascii_theme = None;
properties.font_hansi_theme = None;
properties.font_east_asia_theme = None;
properties.font_cs_theme = None;
}
let insert_replacement = is_first_run
&& !replacement.wrote_value
&& !run
.content
.iter()
.any(|content| matches!(content, RunContent::Text(_)));
let original_raw_positions = insert_replacement.then(|| run.extra_xml_positions.clone());
let property_boundary = usize::from(run.properties.is_some());
let removed = run
.content
.iter()
.map(|content| {
!matches!(
content,
RunContent::Text(_)
| RunContent::DeletedText(_)
| RunContent::CommentReference { .. }
)
})
.collect::<Vec<_>>();
run.remap_removed_content(&removed);
run.content.retain(|content| {
matches!(
content,
RunContent::Text(_) | RunContent::DeletedText(_) | RunContent::CommentReference { .. }
)
});
for content in &mut run.content {
if let RunContent::Text(text) = content {
if is_first_run && !replacement.wrote_value {
*text = CT_Text::new(replacement.value);
replacement.wrote_value = true;
} else {
text.text.clear();
text.preserve_space = false;
}
}
}
if is_first_run && !replacement.wrote_value {
if let Some(original_raw_positions) = original_raw_positions {
for (position, original) in run
.extra_xml_positions
.iter_mut()
.zip(original_raw_positions)
{
if CT_R::raw_child_position(original) > property_boundary {
let shifted = CT_R::raw_child_position(*position) + 1;
CT_R::set_raw_child_position(position, shifted);
}
}
}
run.content
.insert(0, RunContent::Text(CT_Text::new(replacement.value)));
replacement.wrote_value = true;
}
}
fn replace_paragraph_display(paragraph: &mut CT_P, replacement: &mut DisplayReplacement<'_>) {
for run in &mut paragraph.runs {
replace_run_display(run, replacement);
}
}
fn replace_table_display(table: &mut CT_Tbl, replacement: &mut DisplayReplacement<'_>) {
for row in &mut table.rows {
replace_row_display(row, replacement);
}
}
fn replace_row_display(row: &mut CT_Row, replacement: &mut DisplayReplacement<'_>) {
for cell in &mut row.cells {
replace_cell_display(cell, replacement);
}
}
fn replace_cell_display(cell: &mut CT_Tc, replacement: &mut DisplayReplacement<'_>) {
for content in &mut cell.content {
match content {
CellContent::Paragraph(paragraph) => replace_paragraph_display(paragraph, replacement),
CellContent::Table(table) => replace_table_display(table, replacement),
CellContent::ContentControl(_) => {}
}
}
}
fn add_text_to_first_paragraph(control: &mut CT_Sdt, value: &str) -> bool {
for content in &mut control.content {
match content {
SdtContent::Paragraph(paragraph) => {
paragraph.runs.insert(0, CT_R::new(value));
return true;
}
SdtContent::Table(table) => {
if add_text_to_table(table, value) {
return true;
}
}
SdtContent::Row(row) => {
if add_text_to_row(row, value) {
return true;
}
}
SdtContent::Cell(cell) => {
if add_text_to_cell(cell, value) {
return true;
}
}
SdtContent::ContentControl(_) => {}
_ => {}
}
}
false
}
fn add_text_to_table(table: &mut CT_Tbl, value: &str) -> bool {
for row in &mut table.rows {
if add_text_to_row(row, value) {
return true;
}
}
false
}
fn add_text_to_row(row: &mut CT_Row, value: &str) -> bool {
for cell in &mut row.cells {
if add_text_to_cell(cell, value) {
return true;
}
}
false
}
fn add_text_to_cell(cell: &mut CT_Tc, value: &str) -> bool {
for content in &mut cell.content {
match content {
CellContent::Paragraph(paragraph) => {
paragraph.runs.insert(0, CT_R::new(value));
return true;
}
CellContent::Table(table) => {
if add_text_to_table(table, value) {
return true;
}
}
CellContent::ContentControl(_) => {}
}
}
false
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct XmlEdit {
start: usize,
end: usize,
replacement: Vec<u8>,
}
fn resolve_binding_edit(
package: &OpcPackage,
document_part: &str,
binding: &CT_DataBinding,
value: &str,
) -> Result<(String, XmlEdit)> {
let store_item_id = binding
.store_item_id
.as_deref()
.ok_or_else(|| Error::Other("content-control binding has no store item id".to_owned()))?;
let xpath = binding
.xpath
.as_deref()
.ok_or_else(|| Error::Other("content-control binding has no XPath".to_owned()))?;
let mappings = parse_prefix_mappings(binding.prefix_mappings.as_deref().unwrap_or(""))?;
let steps = parse_xpath(xpath, &mappings)?;
let part_name = resolve_custom_xml_part(package, document_part, store_item_id)?;
let xml = package
.get_part(&part_name)
.ok_or_else(|| Error::Other(format!("custom XML part {part_name} is missing")))?;
let selection = select_xml_element(xml, &steps)?;
let replacement = if selection.empty {
expand_empty_element(xml, &selection, value)?
} else {
quick_xml::escape::escape(value).as_bytes().to_vec()
};
Ok((
part_name,
XmlEdit {
start: selection.replace_start,
end: selection.replace_end,
replacement,
},
))
}
pub(crate) fn resolve_custom_xml_part(
package: &OpcPackage,
document_part: &str,
store_item_id: &str,
) -> Result<String> {
let matches = matching_custom_xml_parts(package, document_part, store_item_id)?;
match matches.as_slice() {
[part_name] => Ok(part_name.clone()),
[] => Err(Error::Other(format!(
"custom XML store item {store_item_id} was not found"
))),
_ => Err(Error::Other(format!(
"custom XML store item {store_item_id} is ambiguous"
))),
}
}
pub(crate) fn custom_xml_store_item_count(
package: &OpcPackage,
document_part: &str,
store_item_id: &str,
) -> Result<usize> {
Ok(matching_custom_xml_parts(package, document_part, store_item_id)?.len())
}
fn matching_custom_xml_parts(
package: &OpcPackage,
document_part: &str,
store_item_id: &str,
) -> Result<Vec<String>> {
let expected = normalize_store_item_id(store_item_id);
let mut matches = Vec::new();
let Some(relationships) = package.get_part_rels(document_part) else {
return Ok(matches);
};
for relationship in relationships
.items
.iter()
.filter(|relationship| relationship.rel_type == CUSTOM_XML_REL_TYPE)
{
let item_part = OpcPackage::resolve_rel_target(document_part, &relationship.target);
let Some(properties_relationship) = package
.get_part_rels(&item_part)
.and_then(|relationships| relationships.get_by_type(CUSTOM_XML_PROPS_REL_TYPE))
else {
continue;
};
let properties_part =
OpcPackage::resolve_rel_target(&item_part, &properties_relationship.target);
let Some(properties_xml) = package.get_part(&properties_part) else {
continue;
};
if parse_store_item_id(properties_xml)?
.is_some_and(|found| normalize_store_item_id(&found) == expected)
{
matches.push(item_part);
}
}
Ok(matches)
}
pub(crate) fn normalize_store_item_id(value: &str) -> String {
value
.trim()
.trim_start_matches('{')
.trim_end_matches('}')
.to_ascii_lowercase()
}
pub(crate) fn parse_store_item_id(xml: &[u8]) -> Result<Option<String>> {
let mut reader = NsReader::from_reader(xml);
let mut buffer = Vec::new();
loop {
let (namespace, event) = reader
.read_resolved_event_into(&mut buffer)
.map_err(|error| Error::Other(format!("invalid custom XML properties: {error}")))?;
let root_matches = namespace_matches(&namespace, Some(CUSTOM_XML_PROPS_NS));
let event = event.into_owned();
match event {
Event::Start(element) | Event::Empty(element)
if root_matches
&& matches_local_name(element.name().as_ref(), b"datastoreItem") =>
{
for attribute in element.attributes() {
let attribute = attribute.map_err(|error| {
Error::Other(format!("invalid custom XML properties attribute: {error}"))
})?;
let (attribute_namespace, local_name) =
reader.resolver().resolve_attribute(attribute.key);
if namespace_matches(&attribute_namespace, Some(CUSTOM_XML_PROPS_NS))
&& local_name.as_ref() == b"itemID"
{
return attribute
.decoded_and_normalized_value(
XmlVersion::Implicit1_0,
element.decoder(),
)
.map(|value| Some(value.into_owned()))
.map_err(|error| {
Error::Other(format!(
"invalid custom XML properties item id: {error}"
))
});
}
}
return Ok(None);
}
Event::Start(_) | Event::Empty(_) => return Ok(None),
Event::Text(text) if is_xml_whitespace(text.as_ref()) => {}
Event::Decl(_) | Event::Comment(_) | Event::PI(_) | Event::DocType(_) => {}
Event::Eof => return Ok(None),
_ => return Ok(None),
}
buffer.clear();
}
}
fn is_xml_whitespace(bytes: &[u8]) -> bool {
bytes.iter().all(u8::is_ascii_whitespace)
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct XPathStep {
namespace: Option<Vec<u8>>,
local_name: Vec<u8>,
index: Option<usize>,
}
fn parse_prefix_mappings(value: &str) -> Result<HashMap<String, Vec<u8>>> {
let bytes = value.as_bytes();
let mut mappings = HashMap::new();
let mut position = 0usize;
while position < bytes.len() {
skip_ascii_whitespace(bytes, &mut position);
if position == bytes.len() {
break;
}
if !bytes[position..].starts_with(b"xmlns") {
return Err(Error::Other("unsupported prefixMappings syntax".to_owned()));
}
position += b"xmlns".len();
let prefix = if bytes.get(position) == Some(&b':') {
position += 1;
let start = position;
while bytes
.get(position)
.is_some_and(|byte| is_xml_name_byte(*byte))
{
position += 1;
}
if start == position {
return Err(Error::Other(
"prefixMappings has an empty prefix".to_owned(),
));
}
let prefix = std::str::from_utf8(&bytes[start..position])
.map_err(|_| Error::Other("prefixMappings prefix is not UTF-8".to_owned()))?
.to_owned();
if !is_xml_name(&prefix) {
return Err(Error::Other(
"prefixMappings contains an unsupported prefix".to_owned(),
));
}
prefix
} else {
String::new()
};
skip_ascii_whitespace(bytes, &mut position);
if bytes.get(position) != Some(&b'=') {
return Err(Error::Other("prefixMappings is missing '='".to_owned()));
}
position += 1;
skip_ascii_whitespace(bytes, &mut position);
let quote = *bytes
.get(position)
.filter(|quote| **quote == b'\'' || **quote == b'"')
.ok_or_else(|| Error::Other("prefixMappings URI is not quoted".to_owned()))?;
position += 1;
let start = position;
while bytes.get(position).is_some_and(|byte| *byte != quote) {
position += 1;
}
if bytes.get(position) != Some("e) {
return Err(Error::Other(
"prefixMappings URI has no closing quote".to_owned(),
));
}
let uri = bytes[start..position].to_vec();
position += 1;
if let Some(previous) = mappings.insert(prefix, uri.clone())
&& previous != uri
{
return Err(Error::Other(
"prefixMappings defines one prefix more than once".to_owned(),
));
}
}
Ok(mappings)
}
fn parse_xpath(xpath: &str, mappings: &HashMap<String, Vec<u8>>) -> Result<Vec<XPathStep>> {
if !xpath.starts_with('/') || xpath.starts_with("//") {
return Err(Error::Other(
"content-control XPath must be an absolute child path".to_owned(),
));
}
let mut steps = Vec::new();
for segment in xpath[1..].split('/') {
if segment.is_empty() {
return Err(Error::Other(
"content-control XPath contains a descendant or empty step".to_owned(),
));
}
let (name, index) = parse_xpath_segment(segment)?;
let mut names = name.split(':');
let first = names.next().unwrap_or_default();
let second = names.next();
if names.next().is_some() {
return Err(Error::Other(
"content-control XPath contains an invalid qualified name".to_owned(),
));
}
let (namespace, local_name) = match second {
Some(local) => {
if !is_xml_name(first) {
return Err(Error::Other(
"content-control XPath contains an unsupported prefix".to_owned(),
));
}
let namespace = mappings.get(first).cloned().ok_or_else(|| {
Error::Other(format!(
"content-control XPath prefix {first} is not mapped"
))
})?;
(Some(namespace), local)
}
None => (mappings.get("").cloned(), first),
};
if !is_xml_name(local_name) {
return Err(Error::Other(
"content-control XPath contains an unsupported name".to_owned(),
));
}
steps.push(XPathStep {
namespace,
local_name: local_name.as_bytes().to_vec(),
index,
});
}
if steps.is_empty() {
return Err(Error::Other("content-control XPath is empty".to_owned()));
}
Ok(steps)
}
fn parse_xpath_segment(segment: &str) -> Result<(&str, Option<usize>)> {
let Some(open) = segment.find('[') else {
if segment.contains(['*', '(', ')', ']']) {
return Err(Error::Other(
"content-control XPath uses an unsupported selector".to_owned(),
));
}
return Ok((segment, None));
};
if !segment.ends_with(']') || segment[open + 1..segment.len() - 1].contains(['[', ']']) {
return Err(Error::Other(
"content-control XPath uses an unsupported predicate".to_owned(),
));
}
let name = &segment[..open];
let index = segment[open + 1..segment.len() - 1]
.parse::<usize>()
.ok()
.filter(|index| *index > 0)
.ok_or_else(|| Error::Other("content-control XPath index must be one-based".to_owned()))?;
if name.is_empty() || name.contains(['*', '(', ')', '[', ']']) {
return Err(Error::Other(
"content-control XPath uses an unsupported selector".to_owned(),
));
}
Ok((name, Some(index)))
}
fn skip_ascii_whitespace(bytes: &[u8], position: &mut usize) {
while bytes
.get(*position)
.is_some_and(|byte| byte.is_ascii_whitespace())
{
*position += 1;
}
}
fn is_xml_name_byte(byte: u8) -> bool {
byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.')
}
fn is_xml_name(value: &str) -> bool {
value
.bytes()
.next()
.is_some_and(|byte| byte.is_ascii_alphabetic() || byte == b'_')
&& value.bytes().skip(1).all(is_xml_name_byte)
}
#[derive(Debug, Clone)]
struct XmlNode {
parent: Option<usize>,
namespace: Option<Vec<u8>>,
local_name: Vec<u8>,
occurrence: usize,
replace_start: usize,
replace_end: usize,
element_start: usize,
element_end: usize,
qualified_name: Vec<u8>,
empty: bool,
simple_text: bool,
}
#[derive(Debug, Clone)]
struct XmlSelection {
replace_start: usize,
replace_end: usize,
element_start: usize,
element_end: usize,
qualified_name: Vec<u8>,
empty: bool,
}
fn select_xml_element(xml: &[u8], steps: &[XPathStep]) -> Result<XmlSelection> {
let nodes = parse_xml_nodes(xml)?;
let mut candidates = Vec::new();
for (step_index, step) in steps.iter().enumerate() {
let parents = candidates;
candidates = nodes
.iter()
.enumerate()
.filter(|(_, node)| {
let parent_matches = if step_index == 0 {
node.parent.is_none()
} else {
node.parent.is_some_and(|parent| parents.contains(&parent))
};
parent_matches
&& node.namespace == step.namespace
&& node.local_name == step.local_name
&& step.index.is_none_or(|index| node.occurrence == index)
})
.map(|(index, _)| index)
.collect();
}
let [selected_index] = candidates.as_slice() else {
return if candidates.is_empty() {
Err(Error::Other(
"content-control XPath selected no custom XML element".to_owned(),
))
} else {
Err(Error::Other(
"content-control XPath selected more than one custom XML element".to_owned(),
))
};
};
let selected = &nodes[*selected_index];
if !selected.simple_text {
return Err(Error::Other(
"content-control XPath selected an element with preserved markup".to_owned(),
));
}
Ok(XmlSelection {
replace_start: selected.replace_start,
replace_end: selected.replace_end,
element_start: selected.element_start,
element_end: selected.element_end,
qualified_name: selected.qualified_name.clone(),
empty: selected.empty,
})
}
fn parse_xml_nodes(xml: &[u8]) -> Result<Vec<XmlNode>> {
let mut reader = NsReader::from_reader(xml);
reader.config_mut().trim_text(false);
let mut buffer = Vec::new();
let mut nodes: Vec<XmlNode> = Vec::new();
let mut stack: Vec<usize> = Vec::new();
loop {
let before = usize::try_from(reader.buffer_position())
.map_err(|_| Error::Other("custom XML position overflow".to_owned()))?;
let (namespace, event) = reader
.read_resolved_event_into(&mut buffer)
.map_err(|error| Error::Other(format!("invalid custom XML part: {error}")))?;
let namespace = namespace_bytes(&namespace)?;
let event = event.into_owned();
let after = usize::try_from(reader.buffer_position())
.map_err(|_| Error::Other("custom XML position overflow".to_owned()))?;
match event {
Event::Start(element) => {
if let Some(parent) = stack.last().copied() {
nodes[parent].simple_text = false;
}
let parent = stack.last().copied();
let qualified_name = element.name().as_ref().to_vec();
let local_name = qualified_name
.rsplit(|byte| *byte == b':')
.next()
.unwrap_or(&qualified_name)
.to_vec();
let occurrence = nodes
.iter()
.filter(|node| {
node.parent == parent
&& node.namespace == namespace
&& node.local_name == local_name
})
.count()
.checked_add(1)
.ok_or_else(|| Error::Other("custom XML sibling count overflow".to_owned()))?;
let index = nodes.len();
nodes.push(XmlNode {
parent,
namespace,
local_name,
occurrence,
replace_start: after,
replace_end: 0,
element_start: before,
element_end: after,
qualified_name,
empty: false,
simple_text: true,
});
stack.push(index);
}
Event::Empty(element) => {
if let Some(parent) = stack.last().copied() {
nodes[parent].simple_text = false;
}
let parent = stack.last().copied();
let qualified_name = element.name().as_ref().to_vec();
let local_name = qualified_name
.rsplit(|byte| *byte == b':')
.next()
.unwrap_or(&qualified_name)
.to_vec();
let occurrence = nodes
.iter()
.filter(|node| {
node.parent == parent
&& node.namespace == namespace
&& node.local_name == local_name
})
.count()
.checked_add(1)
.ok_or_else(|| Error::Other("custom XML sibling count overflow".to_owned()))?;
nodes.push(XmlNode {
parent,
namespace,
local_name,
occurrence,
replace_start: before,
replace_end: after,
element_start: before,
element_end: after,
qualified_name,
empty: true,
simple_text: true,
});
}
Event::End(_) => {
let index = stack.pop().ok_or_else(|| {
Error::Other("custom XML contains an unmatched end element".to_owned())
})?;
nodes[index].replace_end = before;
nodes[index].element_end = after;
}
Event::Comment(_) | Event::PI(_) | Event::DocType(_) => {
if let Some(index) = stack.last().copied() {
nodes[index].simple_text = false;
}
}
Event::Eof => {
if !stack.is_empty() {
return Err(Error::Other(
"custom XML contains an unclosed element".to_owned(),
));
}
return Ok(nodes);
}
_ => {}
}
buffer.clear();
}
}
fn namespace_bytes(namespace: &ResolveResult<'_>) -> Result<Option<Vec<u8>>> {
match namespace {
ResolveResult::Bound(Namespace(uri)) => Ok(Some(uri.to_vec())),
ResolveResult::Unbound => Ok(None),
ResolveResult::Unknown(prefix) => Err(Error::Other(format!(
"custom XML uses unmapped namespace prefix {}",
String::from_utf8_lossy(prefix)
))),
}
}
fn namespace_matches(namespace: &ResolveResult<'_>, expected: Option<&[u8]>) -> bool {
match (namespace, expected) {
(ResolveResult::Bound(Namespace(actual)), Some(expected)) => *actual == expected,
(ResolveResult::Unbound, None) => true,
_ => false,
}
}
fn expand_empty_element(xml: &[u8], selection: &XmlSelection, value: &str) -> Result<Vec<u8>> {
let raw = xml
.get(selection.element_start..selection.element_end)
.ok_or_else(|| Error::Other("custom XML selection is out of bounds".to_owned()))?;
let slash = raw
.iter()
.rposition(|byte| *byte == b'/')
.filter(|slash| raw.get(slash + 1) == Some(&b'>'))
.ok_or_else(|| Error::Other("custom XML empty element is malformed".to_owned()))?;
let mut expanded = Vec::new();
expanded.extend_from_slice(&raw[..slash]);
expanded.push(b'>');
expanded.extend_from_slice(quick_xml::escape::escape(value).as_bytes());
expanded.extend_from_slice(b"</");
expanded.extend_from_slice(&selection.qualified_name);
expanded.push(b'>');
Ok(expanded)
}
fn apply_xml_edits(xml: &[u8], mut edits: Vec<XmlEdit>) -> Result<Vec<u8>> {
edits.sort_by_key(|edit| (edit.start, edit.end));
let mut unique: Vec<XmlEdit> = Vec::new();
for edit in edits {
if let Some(previous) = unique.last() {
if previous.start == edit.start && previous.end == edit.end {
if previous.replacement == edit.replacement {
continue;
}
return Err(Error::Other(
"content controls assign different values to one custom XML element".to_owned(),
));
}
if edit.start < previous.end {
return Err(Error::Other(
"content-control custom XML selections overlap".to_owned(),
));
}
}
unique.push(edit);
}
let mut output = xml.to_vec();
for edit in unique.into_iter().rev() {
if edit.start > edit.end || edit.end > output.len() {
return Err(Error::Other(
"content-control custom XML selection is out of bounds".to_owned(),
));
}
output.splice(edit.start..edit.end, edit.replacement);
}
validate_xml(&output)?;
Ok(output)
}
fn validate_xml(xml: &[u8]) -> Result<()> {
let mut reader = NsReader::from_reader(xml);
let mut buffer = Vec::new();
loop {
match reader.read_event_into(&mut buffer) {
Ok(Event::Eof) => return Ok(()),
Ok(_) => buffer.clear(),
Err(error) => {
return Err(Error::Other(format!(
"content-control custom XML edit is invalid: {error}"
)));
}
}
}
}