xml_stinks/
attribute.rs

1//! Attribute.
2
3use std::borrow::Cow;
4use std::str::Utf8Error;
5
6use quick_xml::events::attributes::{
7    AttrError,
8    Attribute as QuickXMLAttribute,
9    Attributes,
10};
11use quick_xml::name::QName;
12use quick_xml::Error as QuickXMLError;
13
14use crate::escape::EscapeError;
15
16/// Represent a XML attribute.
17#[derive(Debug, Clone, PartialEq)]
18pub struct Attribute<'data>
19{
20    inner: QuickXMLAttribute<'data>,
21}
22
23impl<'data> Attribute<'data>
24{
25    /// Returns a new `Attribute`.
26    pub fn new(
27        key: &'data (impl AsRef<[u8]> + ?Sized),
28        value: impl Into<Cow<'data, [u8]>>,
29    ) -> Self
30    {
31        Self {
32            inner: QuickXMLAttribute {
33                key: QName(key.as_ref()),
34                value: value.into(),
35            },
36        }
37    }
38
39    /// Returns the key.
40    ///
41    /// # Errors
42    /// Will return `Err` if the key is invalid UTF-8.
43    pub fn key(&self) -> Result<&str, Error>
44    {
45        std::str::from_utf8(self.key_bytes()).map_err(Error::KeyNotUTF8)
46    }
47
48    /// Returns the key as bytes.
49    #[must_use]
50    pub fn key_bytes(&self) -> &[u8]
51    {
52        self.inner.key.as_ref()
53    }
54
55    /// Returns the value.
56    ///
57    /// # Errors
58    /// Will return `Err` if:
59    /// - The value is invalid UTF-8
60    /// - Unescaping the value fails
61    pub fn value(&self) -> Result<Cow<str>, Error>
62    {
63        self.inner.unescape_value().map_err(|err| match err {
64            QuickXMLError::NonDecodable(Some(utf8_error)) => {
65                Error::ValueNotUTF8(utf8_error)
66            }
67            QuickXMLError::EscapeError(escape_err) => {
68                Error::UnescapeValueFailed(EscapeError::from_quick_xml(escape_err))
69            }
70            _ => {
71                unreachable!();
72            }
73        })
74    }
75
76    /// Returns the value as bytes. They may or may not be escaped.
77    #[must_use]
78    pub fn value_bytes(&self) -> &[u8]
79    {
80        &self.inner.value
81    }
82}
83
84// Crate-local functions
85impl<'a> Attribute<'a>
86{
87    pub(crate) fn from_inner(inner: QuickXMLAttribute<'a>) -> Self
88    {
89        Self { inner }
90    }
91
92    pub(crate) fn into_inner(self) -> QuickXMLAttribute<'a>
93    {
94        self.inner
95    }
96}
97
98/// Errors that can be raised when parsing [`Attribute`]s.
99///
100/// Recovery position in examples shows the position from which parsing of the
101/// next attribute will be attempted.
102#[derive(Debug, thiserror::Error)]
103#[non_exhaustive]
104pub enum Error
105{
106    /// Attribute key was not followed by `=`, position relative to the start of
107    /// the owning tag is provided.
108    ///
109    /// Example of input that raises this error:
110    /// ```xml
111    /// <tag key another="attribute"/>
112    /// <!--     ^~~ error position, recovery position (8) -->
113    /// ```
114    #[error("Position {0}: attribute key must be directly followed by `=` or space")]
115    ExpectedEq(usize),
116
117    /// Attribute value was not found after `=`, position relative to the start
118    /// of the owning tag is provided.
119    ///
120    /// Example of input that raises this error:
121    /// ```xml
122    /// <tag key = />
123    /// <!--       ^~~ error position, recovery position (10) -->
124    /// ```
125    ///
126    /// This error can be returned only for the last attribute in the list,
127    /// because otherwise any content after `=` will be threated as a value.
128    /// The XML
129    /// ```xml
130    /// <tag key = another-key = "value"/>
131    /// <!--                   ^ ^- recovery position (24) -->
132    /// <!--                   '~~ error position (22) -->
133    /// ```
134    ///
135    /// will be treated as `Attribute { key = b"key", value = b"another-key" }`
136    /// and or [`Attribute`] is returned, or [`Error::UnquotedValue`] is raised,
137    /// depending on the parsing mode.
138    #[error("Position {0}: `=` must be followed by an attribute value")]
139    ExpectedValue(usize),
140
141    /// Attribute value is not quoted, position relative to the start of the
142    /// owning tag is provided.
143    ///
144    /// Example of input that raises this error:
145    /// ```xml
146    /// <tag key = value />
147    /// <!--       ^    ^~~ recovery position (15) -->
148    /// <!--       '~~ error position (10) -->
149    /// ```
150    #[error("Position {0}: attribute value must be enclosed in `\"` or `'`")]
151    UnquotedValue(usize),
152
153    /// Attribute value was not finished with a matching quote, position relative
154    /// to the start of owning tag and a quote is provided. That position is always
155    /// a last character in the tag content.
156    ///
157    /// Example of input that raises this error:
158    /// ```xml
159    /// <tag key = "value  />
160    /// <tag key = 'value  />
161    /// <!--               ^~~ error position, recovery position (18) -->
162    /// ```
163    ///
164    /// This error can be returned only for the last attribute in the list,
165    /// because all input was consumed during scanning for a quote.
166    #[error("Position {0}: missing closing quote `{1}` in attribute value")]
167    ExpectedQuote(usize, u8),
168
169    /// An attribute with the same name was already encountered. Two parameters
170    /// define (1) the error position relative to the start of the owning tag
171    /// for a new attribute and (2) the start position of a previously encountered
172    /// attribute with the same name.
173    ///
174    /// Example of input that raises this error:
175    /// ```xml
176    /// <tag key = 'value'  key="value2" attr3='value3' />
177    /// <!-- ^              ^            ^~~ recovery position (32) -->
178    /// <!-- |              '~~ error position (19) -->
179    /// <!-- '~~ previous position (4) -->
180    /// ```
181    #[error("Position {0}: duplicated attribute, previous declaration at position {1}")]
182    Duplicated(usize, usize),
183
184    /// Attribute key is not valid UTF-8.
185    #[error("Attribute key is not valid UTF-8")]
186    KeyNotUTF8(#[source] Utf8Error),
187
188    /// Attribute value is not valid UTF-8.
189    #[error("Attribute value is not valid UTF-8")]
190    ValueNotUTF8(#[source] Utf8Error),
191
192    /// Failed to unescape value.
193    #[error("Failed to unescape value")]
194    UnescapeValueFailed(#[source] EscapeError),
195}
196
197impl From<AttrError> for Error
198{
199    fn from(attr_err: AttrError) -> Self
200    {
201        match attr_err {
202            AttrError::ExpectedEq(pos) => Self::ExpectedEq(pos),
203            AttrError::ExpectedValue(pos) => Self::ExpectedValue(pos),
204            AttrError::UnquotedValue(pos) => Self::UnquotedValue(pos),
205            AttrError::ExpectedQuote(pos, quote) => Self::ExpectedQuote(pos, quote),
206            AttrError::Duplicated(pos, same_attr_pos) => {
207                Self::Duplicated(pos, same_attr_pos)
208            }
209        }
210    }
211}
212
213/// Iterates through [`Attribute`]s.
214#[derive(Debug)]
215pub struct Iter<'a>
216{
217    attrs: Attributes<'a>,
218}
219
220impl<'a> Iter<'a>
221{
222    pub(crate) fn new(attrs: Attributes<'a>) -> Self
223    {
224        Self { attrs }
225    }
226}
227
228impl<'a> Iterator for Iter<'a>
229{
230    type Item = Result<Attribute<'a>, Error>;
231
232    fn next(&mut self) -> Option<Self::Item>
233    {
234        let attr = self.attrs.next()?;
235
236        Some(attr.map(Attribute::from_inner).map_err(Into::into))
237    }
238}