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}