calamine 0.37.0

An Excel/OpenDocument Spreadsheet reader and deserializer in pure Rust
Documentation
// SPDX-License-Identifier: MIT
//
// Copyright 2016-2025, Johann Tuffe.

//! Zero-allocation XML attribute extraction utilities.
//!
//! These replace quick_xml's own `Attributes` iterator, avoiding its per-item
//! `Cow`/`QName` newtypes, quote-type tracking, namespace bookkeeping, and
//! entity decoding. We can do this as we have a constrained
//! set of keys that we access internally (all of them are simple ASCII).
//!
//! Like quick_xml, malformed attributes are reported as [`AttrError`] items rather
//! than being skipped; a key with no `=` yields [`AttrError::ExpectedEq`] and an
//! unquoted values yield [`AttrError::UnquotedValue`]. (Unclosed quotes never
//! reach here as quick_xml's tokenizer rejects them at `read_event` time).

use quick_xml::escape::unescape;
use quick_xml::events::attributes::AttrError;
use quick_xml::events::BytesStart;

/// Zero-allocation iterator over raw XML attributes, yielding
/// `(key, value)` string-slice pairs or an [`AttrError`] for malformed input.
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();

        // note: make "pos" local to ensure the compiler keeps it in
        // a register across the scan loops (struct field otherwise)
        let mut pos = self.pos;

        // skip whitespace before the key
        while pos < len && bytes[pos].is_ascii_whitespace() {
            pos += 1;
        }
        if pos >= len {
            self.pos = pos;
            return None;
        }

        // key: scan to '=' with a single comparison
        // per byte (handling whitespace around '=')
        let key_start = pos;
        while pos < len && bytes[pos] != b'=' {
            pos += 1;
        }
        if pos >= len {
            // a key with no '=' is malformed; park the cursor at the end so the
            // next call returns None rather than re-reporting the same error
            self.pos = len;
            return Some(Err(AttrError::ExpectedEq(key_start)));
        }
        let key = raw[key_start..pos].trim_ascii_end();
        pos += 1; // skip '='

        // skip whitespace after '=' (normally none: loop exits immediately)
        while pos < len && bytes[pos].is_ascii_whitespace() {
            pos += 1;
        }

        // quoted value (XML mandates quotes; anything else, including a missing
        // value at end-of-input, is malformed)
        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; // skip opening quote
        let val_start = pos;
        while pos < len && bytes[pos] != quote {
            pos += 1;
        }
        let val = &raw[val_start..pos];
        if pos < len {
            pos += 1; // skip closing quote
        }
        self.pos = pos;
        Some(Ok((key, val)))
    }
}

/// Compare an attribute key against a local name, ignoring any namespace
/// prefix, eg: both `id` and `r:id` would match a `local_name` of `"id"`.
#[inline]
pub(crate) fn local_name_matches(key: &str, local_name: &str) -> bool {
    // exact match, or `<prefix>:<local>`
    key == local_name
        || key
            .strip_suffix(local_name) // compare on subslice to avoid allocation
            .is_some_and(|prefix| prefix.as_bytes().last() == Some(&b':'))
}

/// Extension trait for fast/raw attribute access on XML elements.
pub(crate) trait RawAttributes {
    /// Iterate over attributes, yielding `(key, value)` string-slice
    /// pairs (or an [`AttrError`] for malformed input).
    fn iter_raw_attrs(&self) -> RawAttrIter<'_>;

    /// Get a single attribute by exact key, returning `Ok(None)` if absent and
    /// an `Err` if a malformed attribute is encountered while scanning.
    #[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)
    }

    /// Like `raw_attr`, but matches on the attribute's *local* name, ignoring
    /// any namespace prefix (so both `id` and `r:id` would match `"id"`).
    #[allow(dead_code)] // note: currently only reached from `picture`-gated code
    #[inline] // but the function is general-purpose and will be useful elsewhere
    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())
    }
}

/// Get a set of named attributes from an element in a single
/// pass, with early exit as soon as all items are found.
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 };
}

/// Decode a raw attribute into a `String`, with XML entity unescaping.
/// Only needed for values that can contain entities (eg: sheet names, table names, etc).
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::*;

    /// Collect well-formed attributes, panicking on any malformed item.
    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() {
        // `attributes_raw()` yields a leading space; a self-closing tag can
        // leave a trailing one (e.g. `<c r="A1" />` -> ` r="A1" `).
        assert_eq!(collect(" r=\"A1\" "), vec![("r", "A1")]);
    }

    #[test]
    fn test_mixed_whitespace_and_quotes() {
        // Whitespace around `=` combined with both quote styles, as a real
        // (if unusual) tag like `<row r = "50" spans = '1:4'>` would produce.
        let raw = " r = \"50\" spans = '1:4'";
        assert_eq!(collect(raw), vec![("r", "50"), ("spans", "1:4")]);
    }

    #[test]
    fn test_value_containing_other_quote() {
        // A single-quoted value may contain double quotes and vice versa.
        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() {
        // Namespace declarations and prefixed names are yielded verbatim with
        // their prefix; call sites match the full raw key (see issue #632).
        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() {
        // Unquoted values are malformed: yield the valid attr,
        // then the UnquotedValue error, then stop.
        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() {
        // A bare name with no '=' is reported as ExpectedEq, then iteration ends.
        let mut iter = RawAttrIter::new("disabled");
        assert!(matches!(iter.next(), Some(Err(AttrError::ExpectedEq(_)))));
        assert_eq!(iter.next(), None);

        // Leading whitespace then a bare name behaves the same.
        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() {
        // Attributes may be split by tabs/newlines, not just spaces.
        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() {
        // match
        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"));

        // don't match
        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");
        };
        // namespaced key matched on local name, exact key still works
        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;

        // Test with/without whitespace around `=`
        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"));
    }
}