1use base64::Engine as _;
6
7fn format_signed_hex(v: i64) -> String {
12 if v < 0 {
13 format!("-{:x}", v.unsigned_abs())
14 } else {
15 format!("{v:x}")
16 }
17}
18
19#[derive(Debug, Clone, PartialEq)]
21pub enum AttributeValue {
22 Null,
24 String(String),
26 BytesHex(Vec<u8>),
28 BytesBase64(Vec<u8>),
30 Int(i32),
32 IntHex(u32),
35 Long(i64),
37 LongHex(u64),
40 Float(f32),
42 Double(f64),
44 Boolean(bool),
46}
47
48impl AttributeValue {
49 pub fn as_str(&self) -> std::borrow::Cow<'_, str> {
52 use std::borrow::Cow;
53 match self {
54 AttributeValue::Null => Cow::Borrowed(""),
55 AttributeValue::String(s) => Cow::Borrowed(s.as_str()),
56 AttributeValue::BytesHex(b) => Cow::Owned(faster_hex::hex_string(b)),
57 AttributeValue::BytesBase64(b) => {
58 Cow::Owned(base64::engine::general_purpose::STANDARD.encode(b))
59 }
60 AttributeValue::Int(v) => Cow::Owned(v.to_string()),
61 AttributeValue::IntHex(v) => Cow::Owned(format_signed_hex(*v as i32 as i64)),
62 AttributeValue::Long(v) => Cow::Owned(v.to_string()),
63 AttributeValue::LongHex(v) => Cow::Owned(format_signed_hex(*v as i64)),
64 AttributeValue::Float(v) => {
65 if v.fract() == 0.0 && v.is_finite() {
66 Cow::Owned(format!("{:.1}", v))
67 } else {
68 Cow::Owned(v.to_string())
69 }
70 }
71 AttributeValue::Double(v) => {
72 if v.fract() == 0.0 && v.is_finite() {
73 Cow::Owned(format!("{:.1}", v))
74 } else {
75 Cow::Owned(v.to_string())
76 }
77 }
78 AttributeValue::Boolean(b) => {
79 if *b {
80 Cow::Borrowed("true")
81 } else {
82 Cow::Borrowed("false")
83 }
84 }
85 }
86 }
87
88 pub fn as_string(&self) -> Option<&str> {
92 if let AttributeValue::String(s) = self {
93 Some(s)
94 } else {
95 None
96 }
97 }
98 pub fn as_int(&self) -> Option<i32> {
100 if let AttributeValue::Int(v) = self {
101 Some(*v)
102 } else {
103 None
104 }
105 }
106 pub fn as_int_hex(&self) -> Option<u32> {
108 if let AttributeValue::IntHex(v) = self {
109 Some(*v)
110 } else {
111 None
112 }
113 }
114 pub fn as_long(&self) -> Option<i64> {
116 if let AttributeValue::Long(v) = self {
117 Some(*v)
118 } else {
119 None
120 }
121 }
122 pub fn as_long_hex(&self) -> Option<u64> {
124 if let AttributeValue::LongHex(v) = self {
125 Some(*v)
126 } else {
127 None
128 }
129 }
130 pub fn as_float(&self) -> Option<f32> {
132 if let AttributeValue::Float(v) = self {
133 Some(*v)
134 } else {
135 None
136 }
137 }
138 pub fn as_double(&self) -> Option<f64> {
140 if let AttributeValue::Double(v) = self {
141 Some(*v)
142 } else {
143 None
144 }
145 }
146 pub fn as_bool(&self) -> Option<bool> {
148 if let AttributeValue::Boolean(b) = self {
149 Some(*b)
150 } else {
151 None
152 }
153 }
154 pub fn as_bytes(&self) -> Option<&[u8]> {
157 match self {
158 AttributeValue::BytesHex(b) | AttributeValue::BytesBase64(b) => Some(b),
159 _ => None,
160 }
161 }
162}
163
164pub type InternedStr = smol_str::SmolStr;
174
175#[derive(Debug, Clone, PartialEq)]
177pub struct Attribute {
178 pub name: InternedStr,
180 pub value: AttributeValue,
182}
183
184impl Attribute {
185 pub fn as_str(&self) -> std::borrow::Cow<'_, str> {
188 self.value.as_str()
189 }
190}
191
192#[derive(Debug, Clone, PartialEq)]
195pub enum Event {
196 StartDocument,
198 EndDocument,
200 StartTag {
202 name: InternedStr,
204 attributes: Vec<Attribute>,
206 },
207 EndTag {
209 name: InternedStr,
211 },
212 Text(String),
214 CdataSection(String),
216 Comment(String),
218 ProcessingInstruction(String),
220 EntityReference(String),
223 IgnorableWhitespace(String),
226 DocDecl(String),
228}
229
230pub(crate) fn xml_escape(s: &str) -> std::borrow::Cow<'_, str> {
231 match s.find(['<', '>', '&', '"', '\'']) {
232 None => std::borrow::Cow::Borrowed(s),
233 Some(first) => {
234 let mut out = String::with_capacity(s.len() + 8);
235 out.push_str(&s[..first]);
236 for c in s[first..].chars() {
237 match c {
238 '<' => out.push_str("<"),
239 '>' => out.push_str(">"),
240 '&' => out.push_str("&"),
241 '"' => out.push_str("""),
242 '\'' => out.push_str("'"),
243 other => out.push(other),
244 }
245 }
246 std::borrow::Cow::Owned(out)
247 }
248 }
249}
250
251fn push_attr_value(buf: &mut String, value: &AttributeValue) {
256 use std::fmt::Write as _;
257 match value {
258 AttributeValue::Null => {}
259 AttributeValue::String(s) => buf.push_str(&xml_escape(s)),
260 AttributeValue::BytesHex(b) => buf.push_str(&faster_hex::hex_string(b)),
261 AttributeValue::BytesBase64(b) => {
262 base64::engine::general_purpose::STANDARD.encode_string(b, buf);
263 }
264 AttributeValue::Int(v) => {
265 let _ = write!(buf, "{v}");
266 }
267 AttributeValue::IntHex(v) => {
268 let _ = write!(buf, "{}", format_signed_hex(*v as i32 as i64));
269 }
270 AttributeValue::Long(v) => {
271 let _ = write!(buf, "{v}");
272 }
273 AttributeValue::LongHex(v) => {
274 let _ = write!(buf, "{}", format_signed_hex(*v as i64));
275 }
276 AttributeValue::Float(v) => {
277 if v.fract() == 0.0 && v.is_finite() {
278 let _ = write!(buf, "{v:.1}");
279 } else {
280 let _ = write!(buf, "{v}");
281 }
282 }
283 AttributeValue::Double(v) => {
284 if v.fract() == 0.0 && v.is_finite() {
285 let _ = write!(buf, "{v:.1}");
286 } else {
287 let _ = write!(buf, "{v}");
288 }
289 }
290 AttributeValue::Boolean(b) => buf.push_str(if *b { "true" } else { "false" }),
291 }
292}
293
294pub(crate) fn render_event(ev: &Event, buf: &mut String) {
296 match ev {
297 Event::StartDocument | Event::EndDocument => {}
298 Event::StartTag { name, attributes } => {
299 buf.push('<');
300 buf.push_str(name);
301 for attr in attributes {
302 buf.push(' ');
303 buf.push_str(&attr.name);
304 buf.push_str("=\"");
305 push_attr_value(buf, &attr.value);
306 buf.push('"');
307 }
308 buf.push('>');
309 }
310 Event::EndTag { name } => {
311 buf.push_str("</");
312 buf.push_str(name);
313 buf.push('>');
314 }
315 Event::Text(t) if !t.is_empty() => buf.push_str(&xml_escape(t)),
316 Event::CdataSection(t) => {
317 buf.push_str("<![CDATA[");
318 buf.push_str(t);
319 buf.push_str("]]>");
320 }
321 Event::Comment(t) => {
322 buf.push_str("<!--");
323 buf.push_str(t);
324 buf.push_str("-->");
325 }
326 Event::ProcessingInstruction(t) => {
327 buf.push_str("<?");
328 buf.push_str(t);
329 buf.push_str("?>");
330 }
331 Event::EntityReference(t) => {
332 buf.push('&');
333 buf.push_str(t);
334 buf.push(';');
335 }
336 Event::IgnorableWhitespace(t) => buf.push_str(t),
337 Event::DocDecl(t) => {
338 buf.push_str("<!DOCTYPE ");
339 buf.push_str(t);
340 buf.push('>');
341 }
342 _ => {}
343 }
344}