1use base64::Engine as _;
2
3fn format_signed_hex(v: i64) -> String {
5 if v < 0 {
6 format!("-{:x}", v.unsigned_abs())
7 } else {
8 format!("{v:x}")
9 }
10}
11
12#[derive(Debug, Clone, PartialEq)]
29pub enum AttributeValue {
30 Null,
32 String(String),
34 BytesHex(Vec<u8>),
36 BytesBase64(Vec<u8>),
38 Int(i32),
40 IntHex(u32),
42 Long(i64),
44 LongHex(u64),
46 Float(f32),
48 Double(f64),
50 Boolean(bool),
52}
53
54impl AttributeValue {
55 pub fn as_str(&self) -> std::borrow::Cow<'_, str> {
76 use std::borrow::Cow;
77 match self {
78 AttributeValue::Null => Cow::Borrowed(""),
79 AttributeValue::String(s) => Cow::Borrowed(s.as_str()),
80 AttributeValue::BytesHex(b) => Cow::Owned(faster_hex::hex_string(b)),
81 AttributeValue::BytesBase64(b) => {
82 Cow::Owned(base64::engine::general_purpose::STANDARD.encode(b))
83 }
84 AttributeValue::Int(v) => Cow::Owned(v.to_string()),
85 AttributeValue::IntHex(v) => Cow::Owned(format_signed_hex(*v as i32 as i64)),
86 AttributeValue::Long(v) => Cow::Owned(v.to_string()),
87 AttributeValue::LongHex(v) => Cow::Owned(format_signed_hex(*v as i64)),
88 AttributeValue::Float(v) => {
89 if v.fract() == 0.0 && v.is_finite() {
90 Cow::Owned(format!("{:.1}", v))
91 } else {
92 Cow::Owned(v.to_string())
93 }
94 }
95 AttributeValue::Double(v) => {
96 if v.fract() == 0.0 && v.is_finite() {
97 Cow::Owned(format!("{:.1}", v))
98 } else {
99 Cow::Owned(v.to_string())
100 }
101 }
102 AttributeValue::Boolean(b) => {
103 if *b {
104 Cow::Borrowed("true")
105 } else {
106 Cow::Borrowed("false")
107 }
108 }
109 }
110 }
111
112 pub fn as_string(&self) -> Option<&str> {
114 if let AttributeValue::String(s) = self {
115 Some(s)
116 } else {
117 None
118 }
119 }
120 pub fn as_int(&self) -> Option<i32> {
122 if let AttributeValue::Int(v) = self {
123 Some(*v)
124 } else {
125 None
126 }
127 }
128 pub fn as_int_hex(&self) -> Option<u32> {
130 if let AttributeValue::IntHex(v) = self {
131 Some(*v)
132 } else {
133 None
134 }
135 }
136 pub fn as_long(&self) -> Option<i64> {
138 if let AttributeValue::Long(v) = self {
139 Some(*v)
140 } else {
141 None
142 }
143 }
144 pub fn as_long_hex(&self) -> Option<u64> {
146 if let AttributeValue::LongHex(v) = self {
147 Some(*v)
148 } else {
149 None
150 }
151 }
152 pub fn as_float(&self) -> Option<f32> {
154 if let AttributeValue::Float(v) = self {
155 Some(*v)
156 } else {
157 None
158 }
159 }
160 pub fn as_double(&self) -> Option<f64> {
162 if let AttributeValue::Double(v) = self {
163 Some(*v)
164 } else {
165 None
166 }
167 }
168 pub fn as_bool(&self) -> Option<bool> {
170 if let AttributeValue::Boolean(b) = self {
171 Some(*b)
172 } else {
173 None
174 }
175 }
176 pub fn as_bytes(&self) -> Option<&[u8]> {
179 match self {
180 AttributeValue::BytesHex(b) | AttributeValue::BytesBase64(b) => Some(b),
181 _ => None,
182 }
183 }
184}
185
186pub type InternedStr = smol_str::SmolStr;
191
192#[derive(Debug, Clone, PartialEq)]
194pub struct Attribute {
195 pub name: InternedStr,
197 pub value: AttributeValue,
199}
200
201impl Attribute {
202 pub fn as_str(&self) -> std::borrow::Cow<'_, str> {
204 self.value.as_str()
205 }
206}
207
208#[derive(Debug, Clone, PartialEq)]
213pub enum Event {
214 StartDocument,
216 EndDocument,
218 StartTag {
220 name: InternedStr,
222 attributes: Vec<Attribute>,
224 },
225 EndTag {
227 name: InternedStr,
229 },
230 Text(String),
232 CdataSection(String),
234 Comment(String),
236 ProcessingInstruction(String),
238 EntityReference(String),
240 IgnorableWhitespace(String),
242 DocDecl(String),
244}
245
246pub(crate) fn xml_escape(s: &str) -> std::borrow::Cow<'_, str> {
247 match s.find(['<', '>', '&', '"', '\'']) {
248 None => std::borrow::Cow::Borrowed(s),
249 Some(first) => {
250 let mut out = String::with_capacity(s.len() + 8);
251 out.push_str(&s[..first]);
252 for c in s[first..].chars() {
253 match c {
254 '<' => out.push_str("<"),
255 '>' => out.push_str(">"),
256 '&' => out.push_str("&"),
257 '"' => out.push_str("""),
258 '\'' => out.push_str("'"),
259 other => out.push(other),
260 }
261 }
262 std::borrow::Cow::Owned(out)
263 }
264 }
265}
266
267fn push_attr_value(buf: &mut String, value: &AttributeValue) {
268 use std::fmt::Write as _;
269 match value {
270 AttributeValue::Null => {}
271 AttributeValue::String(s) => buf.push_str(&xml_escape(s)),
272 AttributeValue::BytesHex(b) => buf.push_str(&faster_hex::hex_string(b)),
273 AttributeValue::BytesBase64(b) => {
274 base64::engine::general_purpose::STANDARD.encode_string(b, buf);
275 }
276 AttributeValue::Int(v) => {
277 let _ = write!(buf, "{v}");
278 }
279 AttributeValue::IntHex(v) => {
280 let _ = write!(buf, "{}", format_signed_hex(*v as i32 as i64));
281 }
282 AttributeValue::Long(v) => {
283 let _ = write!(buf, "{v}");
284 }
285 AttributeValue::LongHex(v) => {
286 let _ = write!(buf, "{}", format_signed_hex(*v as i64));
287 }
288 AttributeValue::Float(v) => {
289 if v.fract() == 0.0 && v.is_finite() {
290 let _ = write!(buf, "{v:.1}");
291 } else {
292 let _ = write!(buf, "{v}");
293 }
294 }
295 AttributeValue::Double(v) => {
296 if v.fract() == 0.0 && v.is_finite() {
297 let _ = write!(buf, "{v:.1}");
298 } else {
299 let _ = write!(buf, "{v}");
300 }
301 }
302 AttributeValue::Boolean(b) => buf.push_str(if *b { "true" } else { "false" }),
303 }
304}
305
306pub(crate) fn render_event(ev: &Event, buf: &mut String) {
307 match ev {
308 Event::StartDocument | Event::EndDocument => {}
309 Event::StartTag { name, attributes } => {
310 buf.push('<');
311 buf.push_str(name);
312 for attr in attributes {
313 buf.push(' ');
314 buf.push_str(&attr.name);
315 buf.push_str("=\"");
316 push_attr_value(buf, &attr.value);
317 buf.push('"');
318 }
319 buf.push('>');
320 }
321 Event::EndTag { name } => {
322 buf.push_str("</");
323 buf.push_str(name);
324 buf.push('>');
325 }
326 Event::Text(t) if !t.is_empty() => buf.push_str(&xml_escape(t)),
327 Event::CdataSection(t) => {
328 buf.push_str("<![CDATA[");
329 buf.push_str(t);
330 buf.push_str("]]>");
331 }
332 Event::Comment(t) => {
333 buf.push_str("<!--");
334 buf.push_str(t);
335 buf.push_str("-->");
336 }
337 Event::ProcessingInstruction(t) => {
338 buf.push_str("<?");
339 buf.push_str(t);
340 buf.push_str("?>");
341 }
342 Event::EntityReference(t) => {
343 buf.push('&');
344 buf.push_str(t);
345 buf.push(';');
346 }
347 Event::IgnorableWhitespace(t) => buf.push_str(t),
348 Event::DocDecl(t) => {
349 buf.push_str("<!DOCTYPE ");
350 buf.push_str(t);
351 buf.push('>');
352 }
353 _ => {}
354 }
355}