use std::ops::Range;
use quick_xml::Reader;
use quick_xml::escape::{escape, resolve_predefined_entity};
use quick_xml::events::{BytesStart, Event};
use super::{Container, Format, FormatError, Leaf, LeafVisitor, Object, Splicer, comment};
#[derive(Debug, Clone, Copy, Default)]
pub struct Xml;
impl Format for Xml {
fn name(&self) -> &str {
"xml"
}
fn extensions(&self) -> &[&str] {
&[
"xml", "plist", "svg", "xsd", "xsl", "xslt", "csproj", "vbproj", "fsproj", "props",
"targets", "config", "resx", "xaml", "pom", "wsdl", "rss", "atom", "xhtml",
]
}
fn file_names(&self) -> &[&str] {
&["pom.xml", "web.config", "app.config", "nuget.config"]
}
fn sniff(&self, input: &[u8]) -> bool {
input.trim_ascii_start().starts_with(b"<?xml")
}
fn rewrite(&self, input: &[u8], visitor: &mut dyn LeafVisitor) -> Result<Vec<u8>, FormatError> {
let text = std::str::from_utf8(input).map_err(|e| FormatError::new("xml", e))?;
let mut splicer = Splicer::new(input);
if visitor.wants_comments() {
comment::visit(text, &comment_ranges(text)?, visitor, &mut splicer);
}
let mut reader = Reader::from_str(text);
let mut walker = Walker {
text,
visitor,
splicer,
elements: Vec::new(),
run: None,
dict_key: None,
};
loop {
let event = reader.read_event().map_err(|e| {
FormatError::new("xml", format!("at byte {}: {e}", reader.error_position()))
})?;
match event {
Event::Eof => {
walker.flush();
break;
}
Event::Text(t) => {
let range = walker.locate(&t)?;
walker.push_text(range, &t.xml_content(Default::default()));
}
Event::GeneralRef(r) => {
let name = walker.locate(&r)?;
let range = name.start - 1..name.end + 1;
let raw = &text[range.clone()];
let decoded = match r.resolve_char_ref() {
Ok(Some(c)) => c.to_string(),
_ => resolve_predefined_entity(&text[name])
.map_or_else(|| raw.to_owned(), str::to_owned),
};
walker.push_text(range, &decoded);
}
Event::CData(c) => {
walker.flush();
let content = walker.locate(&c)?;
let value = &text[content.clone()];
let raw = content.start - "<![CDATA[".len()..content.end + "]]>".len();
walker.scalar(raw, content, value, |v| format!("<![CDATA[{v}]]>"));
}
Event::Start(start) => {
walker.flush();
let name = walker.element(&start)?;
walker.elements.push(name);
}
Event::Empty(start) => {
walker.flush();
walker.element(&start)?;
walker.visitor.exit();
}
Event::End(_) => {
walker.flush();
walker.elements.pop();
walker.visitor.exit();
}
Event::Comment(_) | Event::Decl(_) | Event::PI(_) | Event::DocType(_) => {
walker.flush();
}
}
}
Ok(walker.splicer.finish())
}
}
fn comment_ranges(text: &str) -> Result<Vec<Range<usize>>, FormatError> {
let mut reader = Reader::from_str(text);
let mut ranges = Vec::new();
loop {
let at = reader.buffer_position() as usize;
match reader.read_event().map_err(|e| {
FormatError::new("xml", format!("at byte {}: {e}", reader.error_position()))
})? {
Event::Eof => return Ok(ranges),
Event::Comment(body) => {
let end = reader.buffer_position() as usize;
let start = at + "<!--".len();
if start + "-->".len() <= end && !body.is_empty() {
ranges.push(start..end - "-->".len());
}
}
_ => {}
}
}
}
struct TextRun {
range: Range<usize>,
value: String,
}
struct Walker<'a, 'v> {
text: &'a str,
visitor: &'v mut dyn LeafVisitor,
splicer: Splicer<'a>,
elements: Vec<String>,
run: Option<TextRun>,
dict_key: Option<String>,
}
impl Walker<'_, '_> {
fn locate(&self, part: &str) -> Result<Range<usize>, FormatError> {
let base = self.text.as_ptr() as usize;
let start = (part.as_ptr() as usize)
.checked_sub(base)
.filter(|s| s + part.len() <= self.text.len())
.ok_or_else(|| FormatError::new("xml", "event text is not part of the input"))?;
Ok(start..start + part.len())
}
fn push_text(&mut self, range: Range<usize>, decoded: &str) {
match &mut self.run {
Some(run) if run.range.end == range.start => {
run.range.end = range.end;
run.value.push_str(decoded);
}
_ => {
self.flush();
self.run = Some(TextRun {
range,
value: decoded.to_owned(),
});
}
}
}
fn flush(&mut self) {
let Some(run) = self.run.take() else { return };
if run.value.trim().is_empty() {
return;
}
if self.in_dict() && self.elements.last().is_some_and(|e| e == "key") {
self.dict_key = Some(run.value);
return;
}
self.scalar(run.range.clone(), run.range, &run.value, |v| {
escape(v).into_owned()
});
}
fn scalar(
&mut self,
raw: Range<usize>,
content: Range<usize>,
value: &str,
encode: impl FnOnce(&str) -> String,
) {
let key = if self.in_dict() {
self.dict_key.as_deref()
} else {
self.elements.last().map(String::as_str)
};
let leaf = Leaf::new(value).with_key(key).with_offset(content.start);
if let Some(replacement) = self.visitor.leaf(&leaf) {
self.splicer
.apply(raw, content, value, &replacement, encode);
}
}
fn in_dict(&self) -> bool {
self.elements.len() >= 2 && self.elements[self.elements.len() - 2] == "dict"
}
fn element(&mut self, start: &BytesStart<'_>) -> Result<String, FormatError> {
let name = start.name().as_ref().to_owned();
let mut attributes = Vec::new();
for attr in start.attributes().with_checks(false) {
let attr = attr.map_err(|e| FormatError::new("xml", e))?;
let key = attr.key.as_ref().to_owned();
let content = self.locate(&attr.value)?;
let value = attr
.normalized_value(Default::default())
.map_err(|e| FormatError::new("xml", e))?
.into_owned();
attributes.push((key, content, value));
}
let summary: Object<'_> = attributes
.iter()
.map(|(k, _, v)| (k.as_str(), Some(v.as_str())))
.collect();
self.visitor.enter(Some(&name), Container::Object(&summary));
for (key, content, value) in &attributes {
let leaf = Leaf::new(value)
.with_key(Some(key))
.with_offset(content.start);
if let Some(replacement) = self.visitor.leaf(&leaf) {
let raw = content.start - 1..content.end + 1;
self.splicer
.apply(raw, content.clone(), value, &replacement, |v| {
format!("\"{}\"", escape(v))
});
}
}
Ok(name)
}
}