use quick_xml::escape::unescape;
use quick_xml::events::attributes::AttrError;
use quick_xml::events::BytesStart;
use quick_xml::Decoder;
pub(crate) struct RawAttrIter<'a> {
raw: &'a [u8],
pos: usize,
}
impl<'a> RawAttrIter<'a> {
#[inline]
fn new(raw: &'a [u8]) -> Self {
Self { raw, pos: 0 }
}
}
impl<'a> Iterator for RawAttrIter<'a> {
type Item = Result<(&'a [u8], &'a [u8]), AttrError>;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
let raw = self.raw;
let len = raw.len();
let mut pos = self.pos;
while pos < len && raw[pos].is_ascii_whitespace() {
pos += 1;
}
if pos >= len {
self.pos = pos;
return None;
}
let key_start = pos;
while pos < len && raw[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 && raw[pos].is_ascii_whitespace() {
pos += 1;
}
let quote = raw.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 && raw[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: &[u8], local_name: &[u8]) -> bool {
key == local_name
|| key
.strip_suffix(local_name) .is_some_and(|prefix| prefix.last() == Some(&b':'))
}
pub(crate) trait RawAttributes {
fn iter_raw_attrs(&self) -> RawAttrIter<'_>;
#[inline]
fn raw_attr(&self, name: &[u8]) -> Result<Option<&[u8]>, 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: &[u8]) -> Result<Option<&[u8]>, 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(decoder: &Decoder, val: &[u8]) -> Result<String, quick_xml::Error> {
let decoded = decoder.decode(val)?;
let unescaped = unescape(&decoded).map_err(quick_xml::Error::from)?;
Ok(unescaped.into_owned())
}
#[cfg(test)]
mod tests {
use super::*;
fn collect(raw: &[u8]) -> Vec<(&[u8], &[u8])> {
RawAttrIter::new(raw)
.map(|r| r.expect("unexpected malformed attribute"))
.collect()
}
#[test]
fn test_basic_attrs() {
let bytes = b"key1=\"val1\" key2='val2'";
let mut iter = RawAttrIter::new(bytes);
assert_eq!(iter.next(), Some(Ok((&b"key1"[..], &b"val1"[..]))));
assert_eq!(iter.next(), Some(Ok((&b"key2"[..], &b"val2"[..]))));
assert_eq!(iter.next(), None);
}
#[test]
fn test_whitespace_around_equals() {
let bytes = b"key = \"value\"";
let mut iter = RawAttrIter::new(bytes);
assert_eq!(iter.next(), Some(Ok((&b"key"[..], &b"value"[..]))));
assert_eq!(iter.next(), None);
}
#[test]
fn test_empty_value() {
let bytes = b"key=\"\"";
let mut iter = RawAttrIter::new(bytes);
assert_eq!(iter.next(), Some(Ok((&b"key"[..], &b""[..]))));
assert_eq!(iter.next(), None);
}
#[test]
fn test_no_trailing_space() {
let bytes = b"key=\"value\"";
let mut iter = RawAttrIter::new(bytes);
assert_eq!(iter.next(), Some(Ok((&b"key"[..], &b"value"[..]))));
assert_eq!(iter.next(), None);
}
#[test]
fn test_no_attributes() {
assert_eq!(collect(b""), vec![]);
assert_eq!(collect(b" "), vec![]);
}
#[test]
fn test_leading_and_trailing_whitespace() {
assert_eq!(collect(b" r=\"A1\" "), vec![(&b"r"[..], &b"A1"[..])]);
}
#[test]
fn test_mixed_whitespace_and_quotes() {
let bytes = b" r = \"50\" spans = '1:4'";
assert_eq!(
collect(bytes),
vec![(&b"r"[..], &b"50"[..]), (&b"spans"[..], &b"1:4"[..])]
);
}
#[test]
fn test_value_containing_other_quote() {
assert_eq!(collect(b"k='a\"b'"), vec![(&b"k"[..], &b"a\"b"[..])]);
assert_eq!(collect(b"k=\"a'b\""), vec![(&b"k"[..], &b"a'b"[..])]);
}
#[test]
fn test_namespaced_attributes() {
let bytes = b" xmlns:foo=\"bar\" foo:id=\"x\" r:id=\"rId1\"";
assert_eq!(
collect(bytes),
vec![
(&b"xmlns:foo"[..], &b"bar"[..]),
(&b"foo:id"[..], &b"x"[..]),
(&b"r:id"[..], &b"rId1"[..]),
]
);
}
#[test]
fn test_malformed_unquoted_value_reports_error() {
let mut iter = RawAttrIter::new(b"good=\"1\" bad=2");
assert_eq!(iter.next(), Some(Ok((&b"good"[..], &b"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(b"disabled");
assert!(matches!(iter.next(), Some(Err(AttrError::ExpectedEq(_)))));
assert_eq!(iter.next(), None);
let mut iter = RawAttrIter::new(b" 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(b"\tr=\"A1\"\n\ts=\"3\""),
vec![(&b"r"[..], &b"A1"[..]), (&b"s"[..], &b"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(b"r"), Ok(Some(&b"A1"[..])));
assert_eq!(e.raw_attr(b"s"), Ok(Some(&b"3"[..])));
assert_eq!(e.raw_attr(b"t"), Ok(Some(&b"s"[..])));
assert_eq!(e.raw_attr(b"missing"), Ok(None));
}
#[test]
fn test_local_name_matches() {
assert!(local_name_matches(b"id", b"id"));
assert!(local_name_matches(b"r:id", b"id"));
assert!(local_name_matches(b"x:embed", b"embed"));
assert!(local_name_matches(b":id", b"id"));
assert!(!local_name_matches(b"xmlid", b"id"));
assert!(!local_name_matches(b"rid", b"id"));
assert!(!local_name_matches(b"name", b"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(b"embed"), Ok(Some(&b"rId1"[..])));
assert_eq!(e.raw_attr_local(b"cstate"), Ok(Some(&b"print"[..])));
assert_eq!(e.raw_attr_local(b"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, b"r" => r, b"s" => s, b"t" => t).unwrap();
assert_eq!(r, Some(&b"A1"[..]));
assert_eq!(s, None);
assert_eq!(t, Some(&b"s"[..]));
}
}