use quick_xml::escape::unescape;
use quick_xml::events::attributes::AttrError;
use quick_xml::events::BytesStart;
pub(crate) struct RawAttrIter<'a> {
raw: &'a str,
pos: usize,
}
impl<'a> RawAttrIter<'a> {
#[inline]
fn new(raw: &'a str) -> Self {
Self { raw, pos: 0 }
}
}
impl<'a> Iterator for RawAttrIter<'a> {
type Item = Result<(&'a str, &'a str), AttrError>;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
let raw = self.raw;
let bytes = raw.as_bytes();
let len = bytes.len();
let mut pos = self.pos;
while pos < len && bytes[pos].is_ascii_whitespace() {
pos += 1;
}
if pos >= len {
self.pos = pos;
return None;
}
let key_start = pos;
while pos < len && bytes[pos] != b'=' {
pos += 1;
}
if pos >= len {
self.pos = len;
return Some(Err(AttrError::ExpectedEq(key_start)));
}
let key = raw[key_start..pos].trim_ascii_end();
pos += 1;
while pos < len && bytes[pos].is_ascii_whitespace() {
pos += 1;
}
let quote = bytes.get(pos).copied();
if quote != Some(b'"') && quote != Some(b'\'') {
self.pos = len;
return Some(Err(AttrError::UnquotedValue(pos)));
}
let quote = quote.unwrap();
pos += 1; let val_start = pos;
while pos < len && bytes[pos] != quote {
pos += 1;
}
let val = &raw[val_start..pos];
if pos < len {
pos += 1; }
self.pos = pos;
Some(Ok((key, val)))
}
}
#[inline]
pub(crate) fn local_name_matches(key: &str, local_name: &str) -> bool {
key == local_name
|| key
.strip_suffix(local_name) .is_some_and(|prefix| prefix.as_bytes().last() == Some(&b':'))
}
pub(crate) trait RawAttributes {
fn iter_raw_attrs(&self) -> RawAttrIter<'_>;
#[inline]
fn raw_attr(&self, name: &str) -> Result<Option<&str>, AttrError> {
for item in self.iter_raw_attrs() {
let (k, v) = item?;
if k == name {
return Ok(Some(v));
}
}
Ok(None)
}
#[allow(dead_code)] #[inline] fn raw_attr_local(&self, local_name: &str) -> Result<Option<&str>, AttrError> {
for item in self.iter_raw_attrs() {
let (k, v) = item?;
if local_name_matches(k, local_name) {
return Ok(Some(v));
}
}
Ok(None)
}
}
impl RawAttributes for BytesStart<'_> {
#[inline]
fn iter_raw_attrs(&self) -> RawAttrIter<'_> {
RawAttrIter::new(self.attributes_raw())
}
}
macro_rules! get_attrs {
($e:expr, $($key:expr => $var:ident),+ $(,)?) => {{
$(let mut $var = None;)+
let mut found = 0u8;
let total = get_attrs!(@count $($key),+);
let mut result = Ok(());
for item in $e.iter_raw_attrs() {
match item {
Ok((k, v)) => {
match k {
$($key => { $var = Some(v); found += 1; })+
_ => {}
}
if found == total {
break;
}
}
Err(e) => {
result = Err(e);
break;
}
}
}
result.map(|()| ($($var),+))
}};
(@count $first:expr $(, $rest:expr)*) => {
1u8 $(+ get_attrs!(@count_one $rest))*
};
(@count_one $e:expr) => { 1u8 };
}
pub(crate) fn decode_attr(val: &str) -> Result<String, quick_xml::Error> {
let unescaped = unescape(val).map_err(quick_xml::Error::from)?;
Ok(unescaped.into_owned())
}
#[cfg(test)]
mod tests {
use super::*;
fn collect(raw: &str) -> Vec<(&str, &str)> {
RawAttrIter::new(raw)
.map(|r| r.expect("unexpected malformed attribute"))
.collect()
}
#[test]
fn test_basic_attrs() {
let raw = "key1=\"val1\" key2='val2'";
let mut iter = RawAttrIter::new(raw);
assert_eq!(iter.next(), Some(Ok(("key1", "val1"))));
assert_eq!(iter.next(), Some(Ok(("key2", "val2"))));
assert_eq!(iter.next(), None);
}
#[test]
fn test_whitespace_around_equals() {
let raw = "key = \"value\"";
let mut iter = RawAttrIter::new(raw);
assert_eq!(iter.next(), Some(Ok(("key", "value"))));
assert_eq!(iter.next(), None);
}
#[test]
fn test_empty_value() {
let raw = "key=\"\"";
let mut iter = RawAttrIter::new(raw);
assert_eq!(iter.next(), Some(Ok(("key", ""))));
assert_eq!(iter.next(), None);
}
#[test]
fn test_no_trailing_space() {
let raw = "key=\"value\"";
let mut iter = RawAttrIter::new(raw);
assert_eq!(iter.next(), Some(Ok(("key", "value"))));
assert_eq!(iter.next(), None);
}
#[test]
fn test_no_attributes() {
assert_eq!(collect(""), vec![]);
assert_eq!(collect(" "), vec![]);
}
#[test]
fn test_leading_and_trailing_whitespace() {
assert_eq!(collect(" r=\"A1\" "), vec![("r", "A1")]);
}
#[test]
fn test_mixed_whitespace_and_quotes() {
let raw = " r = \"50\" spans = '1:4'";
assert_eq!(collect(raw), vec![("r", "50"), ("spans", "1:4")]);
}
#[test]
fn test_value_containing_other_quote() {
assert_eq!(collect("k='a\"b'"), vec![("k", "a\"b")]);
assert_eq!(collect("k=\"a'b\""), vec![("k", "a'b")]);
}
#[test]
fn test_namespaced_attributes() {
let raw = " xmlns:foo=\"bar\" foo:id=\"x\" r:id=\"rId1\"";
assert_eq!(
collect(raw),
vec![("xmlns:foo", "bar"), ("foo:id", "x"), ("r:id", "rId1"),]
);
}
#[test]
fn test_malformed_unquoted_value_reports_error() {
let mut iter = RawAttrIter::new("good=\"1\" bad=2");
assert_eq!(iter.next(), Some(Ok(("good", "1"))));
assert!(matches!(
iter.next(),
Some(Err(AttrError::UnquotedValue(_)))
));
assert_eq!(iter.next(), None);
}
#[test]
fn test_no_equals_reports_error() {
let mut iter = RawAttrIter::new("disabled");
assert!(matches!(iter.next(), Some(Err(AttrError::ExpectedEq(_)))));
assert_eq!(iter.next(), None);
let mut iter = RawAttrIter::new(" spans ");
assert!(matches!(iter.next(), Some(Err(AttrError::ExpectedEq(_)))));
assert_eq!(iter.next(), None);
}
#[test]
fn test_tabs_and_newlines_as_separators() {
assert_eq!(
collect("\tr=\"A1\"\n\ts=\"3\""),
vec![("r", "A1"), ("s", "3")]
);
}
#[test]
fn test_raw_attr_lookup() {
use quick_xml::events::Event;
use quick_xml::Reader;
let mut reader = Reader::from_str(r#"<c r="A1" s="3" t="s"/>"#);
let Event::Empty(e) = reader.read_event().unwrap() else {
panic!("expected empty element");
};
assert_eq!(e.raw_attr("r"), Ok(Some("A1")));
assert_eq!(e.raw_attr("s"), Ok(Some("3")));
assert_eq!(e.raw_attr("t"), Ok(Some("s")));
assert_eq!(e.raw_attr("missing"), Ok(None));
}
#[test]
fn test_local_name_matches() {
assert!(local_name_matches("id", "id"));
assert!(local_name_matches("r:id", "id"));
assert!(local_name_matches("x:embed", "embed"));
assert!(local_name_matches(":id", "id"));
assert!(!local_name_matches("xmlid", "id"));
assert!(!local_name_matches("rid", "id"));
assert!(!local_name_matches("name", "id"));
}
#[test]
fn test_raw_attr_local_lookup() {
use quick_xml::events::Event;
use quick_xml::Reader;
let mut reader = Reader::from_str(r#"<blip cstate="print" r:embed="rId1"/>"#);
let Event::Empty(e) = reader.read_event().unwrap() else {
panic!("expected empty element");
};
assert_eq!(e.raw_attr_local("embed"), Ok(Some("rId1")));
assert_eq!(e.raw_attr_local("cstate"), Ok(Some("print")));
assert_eq!(e.raw_attr_local("missing"), Ok(None));
}
#[test]
fn test_get_attrs_macro() {
use quick_xml::events::Event;
use quick_xml::Reader;
let mut reader = Reader::from_str(r#"<c r = "A1" t="s"/>"#);
let Event::Empty(e) = reader.read_event().unwrap() else {
panic!("expected empty element");
};
let (r, s, t) = get_attrs!(e, "r" => r, "s" => s, "t" => t).unwrap();
assert_eq!(r, Some("A1"));
assert_eq!(s, None);
assert_eq!(t, Some("s"));
}
}