apk_info_xml/lib.rs
1//! A small library that allows you to build an XML DOM tree.
2
3#![doc(
4 html_logo_url = "https://raw.githubusercontent.com/delvinru/apk-info/main/docs/assets/logo/logo-256.png"
5)]
6#![doc(
7 html_favicon_url = "https://raw.githubusercontent.com/delvinru/apk-info/main/docs/assets/logo/favicon-32.png"
8)]
9
10use std::fmt::{Display as _, Write as _};
11
12/// Represents a single XML attribute, including an optional namespace prefix.
13///
14/// This struct models attributes like `id="123"` or `android:name="..."`.
15/// It is typically owned by an [`Element`].
16///
17/// # Examples
18/// ```
19/// use apk_info_xml::Attribute;
20///
21/// let attr = Attribute::new(None, "id", "123");
22/// assert_eq!(attr.name(), "id");
23/// assert_eq!(attr.value(), "123");
24///
25/// let prefixed = Attribute::new(Some("android"), "name", "android.intent.action.PACKAGE_REMOVED");
26/// assert_eq!(prefixed.to_string(), "android:name=\"android.intent.action.PACKAGE_REMOVED\"");
27/// ```
28#[derive(Default, Debug, PartialEq, Eq, Hash)]
29pub struct Attribute {
30 prefix: Option<String>,
31 name: String,
32 value: String,
33}
34
35impl Attribute {
36 /// Creates a new [`Attribute`] with an optional namespace prefix.
37 ///
38 /// # Examples
39 /// ```
40 /// use apk_info_xml::Attribute;
41 ///
42 /// let attr = Attribute::new(Some("xml"), "lang", "en");
43 /// assert_eq!(attr.to_string(), "xml:lang=\"en\"");
44 /// ```
45 pub fn new(prefix: Option<&str>, name: &str, value: &str) -> Attribute {
46 Self {
47 prefix: prefix.map(String::from),
48 name: name.to_owned(),
49 value: value.to_owned(),
50 }
51 }
52
53 /// Returns the local name of the attribute
54 #[inline(always)]
55 pub fn name(&self) -> &str {
56 &self.name
57 }
58
59 /// Returns the local name of the attribute
60 #[inline(always)]
61 pub fn value(&self) -> &str {
62 &self.value
63 }
64}
65
66impl std::fmt::Display for Attribute {
67 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68 // attribute value may contain characters that break XML output:
69 // - control chars (except \t \n \r) are invalid in XML entirely
70 // - newlines would break the pretty-printed structure
71 // - &, <, >, ", ' need to be escaped for valid XML
72 if let Some(prefix) = &self.prefix {
73 f.write_str(prefix)?;
74 f.write_char(':')?;
75 }
76 f.write_str(&self.name)?;
77 f.write_str("=\"")?;
78 write_attr_value(f, &self.value)?; // empty -> writes nothing here
79 f.write_str("\"")
80 }
81}
82
83/// Returns whether `ch` is a code point permitted in XML 1.0 attribute values.
84///
85/// The allowed ranges are `#x20`–`#xD7FF`, `#xE000`–`#xFFFD` and
86/// `#x10000`–`#x10FFFF`. Everything outside them (control characters below
87/// `#x20` except `\t`/`\n`/`\r`, surrogates, and `#xFFFE`/`#xFFFF`) is not
88/// representable in a well-formed XML 1.0 document.
89#[inline]
90fn is_valid_xml_char(ch: char) -> bool {
91 matches!(
92 ch,
93 '\u{20}'..='\u{D7FF}' | '\u{E000}'..='\u{FFFD}' | '\u{10000}'..='\u{10FFFF}'
94 )
95}
96
97/// Returns whether `name` can be written as an element or attribute local name:
98/// a non-empty XML 1.0 `Name` without `:` (an `NCName`), since the prefix is
99/// written separately.
100///
101/// # Examples
102/// ```
103/// use apk_info_xml::is_valid_name;
104///
105/// assert!(is_valid_name("versionCode"));
106/// assert!(is_valid_name("intent-filter"));
107/// assert!(!is_valid_name("1.0"));
108/// assert!(!is_valid_name("a:b"));
109/// assert!(!is_valid_name(""));
110/// ```
111pub fn is_valid_name(name: &str) -> bool {
112 let bytes = name.as_bytes();
113 let Some(&first) = bytes.first() else {
114 return false;
115 };
116
117 // fast path: real manifests only use ASCII names
118 if name.is_ascii() {
119 return (first.is_ascii_alphabetic() || first == b'_')
120 && bytes[1..]
121 .iter()
122 .all(|&b| b.is_ascii_alphanumeric() || matches!(b, b'_' | b'-' | b'.'));
123 }
124
125 let mut chars = name.chars();
126 chars.next().is_some_and(is_name_start_char) && chars.all(is_name_char)
127}
128
129/// `NameStartChar` of XML 1.0 (Fifth Edition), without `:`.
130fn is_name_start_char(ch: char) -> bool {
131 matches!(
132 ch,
133 'A'..='Z'
134 | '_'
135 | 'a'..='z'
136 | '\u{C0}'..='\u{D6}'
137 | '\u{D8}'..='\u{F6}'
138 | '\u{F8}'..='\u{2FF}'
139 | '\u{370}'..='\u{37D}'
140 | '\u{37F}'..='\u{1FFF}'
141 | '\u{200C}'..='\u{200D}'
142 | '\u{2070}'..='\u{218F}'
143 | '\u{2C00}'..='\u{2FEF}'
144 | '\u{3001}'..='\u{D7FF}'
145 | '\u{F900}'..='\u{FDCF}'
146 | '\u{FDF0}'..='\u{FFFD}'
147 | '\u{10000}'..='\u{EFFFF}'
148 )
149}
150
151/// `NameChar` of XML 1.0 (Fifth Edition), without `:`.
152fn is_name_char(ch: char) -> bool {
153 is_name_start_char(ch)
154 || matches!(
155 ch,
156 '-' | '.' | '0'..='9' | '\u{B7}' | '\u{300}'..='\u{36F}' | '\u{203F}'..='\u{2040}'
157 )
158}
159
160/// Writes `value` to `f` escaped for use inside an XML attribute value.
161///
162/// The written output is always well-formed XML 1.0:
163///
164/// * `\t`, `\n`, `\r` are replaced with a single space (` `), so multi-line and
165/// tab-indented values cannot break the pretty-printed element layout.
166/// * `&`, `<`, `>`, `"` and `'` are escaped as `&`, `<`, `>`,
167/// `"` and `'` respectively.
168/// * Code points that are not valid in XML 1.0 (see [`is_valid_xml_char`]) are
169/// dropped entirely.
170/// * A value that contains no valid characters writes nothing.
171///
172/// # Examples
173///
174/// Escaping of the XML-special characters (newlines become spaces):
175///
176/// ```
177/// use apk_info_xml::Attribute;
178///
179/// let attr = Attribute::new(None, "label", "AVG Antivirus & Security\n<pro>\"v1\"");
180/// assert_eq!(attr.to_string(), "label=\"AVG Antivirus & Security <pro>"v1"\"");
181/// ```
182///
183/// A value made up only of invalid characters renders as an empty attribute:
184///
185/// ```
186/// use apk_info_xml::Attribute;
187///
188/// let attr = Attribute::new(None, "a", "\u{1}\u{FFFF}");
189/// assert_eq!(attr.to_string(), "a=\"\"");
190/// ```
191#[inline]
192fn write_attr_value(f: &mut std::fmt::Formatter<'_>, value: &str) -> std::fmt::Result {
193 let bytes = value.as_bytes();
194 let mut run_start = 0;
195 let mut i = 0;
196
197 while i < bytes.len() {
198 let b = bytes[i];
199 // Fast path: batch a run of already-safe ASCII bytes into one write_str.
200 let ascii_safe =
201 (0x20..0x80).contains(&b) && !matches!(b, b'&' | b'<' | b'>' | b'"' | b'\'');
202 if ascii_safe {
203 i += 1;
204 continue;
205 }
206
207 if run_start < i {
208 f.write_str(&value[run_start..i])?;
209 }
210
211 // `i` is always on a char boundary here.
212 let ch = value[i..].chars().next().unwrap();
213 match ch {
214 '\t' | '\n' | '\r' => f.write_char(' ')?,
215 '&' => f.write_str("&")?,
216 '<' => f.write_str("<")?,
217 '>' => f.write_str(">")?,
218 '"' => f.write_str(""")?,
219 '\'' => f.write_str("'")?,
220 c if is_valid_xml_char(c) => f.write_char(c)?, // non-ASCII valid char
221 _ => {} // invalid XML char, drop it (same as before)
222 }
223
224 i += ch.len_utf8();
225 run_start = i;
226 }
227
228 if run_start < i {
229 f.write_str(&value[run_start..i])?;
230 }
231 Ok(())
232}
233
234/// Writes `value` as XML text content (between tags).
235///
236/// Unlike attribute values, whitespace and newlines are preserved verbatim, so
237/// multi-line content does not get collapsed. `&`, `<` and `>` are escaped as
238/// `&`, `<` and `>`; code points that are not valid in XML 1.0 are
239/// dropped.
240#[inline]
241fn write_text(f: &mut std::fmt::Formatter<'_>, value: &str) -> std::fmt::Result {
242 for ch in value.chars() {
243 match ch {
244 '&' => f.write_str("&")?,
245 '<' => f.write_str("<")?,
246 '>' => f.write_str(">")?,
247 '\t' | '\n' | '\r' => f.write_char(ch)?, // valid XML whitespace in content
248 c if is_valid_xml_char(c) => f.write_char(c)?,
249 _ => {} // invalid XML char, drop it
250 }
251 }
252 Ok(())
253}
254
255/// Represents an XML element, including its name, attributes, and child elements.
256///
257/// This is the core abstraction over an XML DOM node.
258/// It provides methods for building and formatting XML trees programmatically.
259///
260/// # Examples
261/// ```
262/// use apk_info_xml::Element;
263///
264/// let mut root = Element::new("root");
265///
266/// root.set_attribute("version", "1.0");
267/// root.set_attribute_with_prefix(Some("xml"), "lang", "en");
268///
269/// let mut child = Element::new("child");
270/// child.append_child(Element::new("grandchild"));
271///
272/// println!("{}", root);
273/// ```
274#[derive(Debug, Default, PartialEq, Eq)]
275pub struct Element {
276 name: String,
277 attributes: Vec<Attribute>,
278 text: Option<String>,
279 childrens: Vec<Element>,
280}
281
282impl Element {
283 /// Creates a new [`Element`] with the specified tag name and no attributes or children.
284 ///
285 /// # Example
286 /// ```
287 /// use apk_info_xml::Element;
288 ///
289 /// let e = Element::new("root");
290 /// assert_eq!(e.name(), "root");
291 /// ```
292 pub fn new(name: &str) -> Element {
293 Element {
294 name: name.to_owned(),
295 ..Default::default()
296 }
297 }
298
299 /// Creates a new [`Element`] with the specified tag name and preallocated space for attributes
300 ///
301 /// # Example
302 /// ```
303 /// use apk_info_xml::Element;
304 ///
305 /// let e = Element::with_capacity("root", 16);
306 /// assert_eq!(e.name(), "root");
307 /// ```
308 pub fn with_capacity(name: &str, capacity: usize) -> Element {
309 Element {
310 name: name.to_owned(),
311 attributes: Vec::with_capacity(capacity),
312 ..Default::default()
313 }
314 }
315
316 /// Adds a new attribute without a namespace prefix to the element.
317 ///
318 /// # Example
319 /// ```
320 /// use apk_info_xml::Element;
321 ///
322 /// let e = Element::new("node").set_attribute("id", "42");
323 /// ```
324 pub fn set_attribute(&mut self, name: &str, value: &str) {
325 // if attribute with same already exists - do nothing
326 if self.attributes.iter().any(|a| a.name() == name) {
327 return;
328 }
329
330 self.attributes.push(Attribute::new(None, name, value));
331 }
332
333 /// Adds a new attribute with an optional namespace prefix to the element.
334 ///
335 /// # Example
336 /// ```
337 /// use apk_info_xml::Element;
338 ///
339 /// let mut e = Element::new("svg");
340 /// e.set_attribute_with_prefix(Some("xlink"), "href", "image.png");
341 ///
342 /// assert!(e.attributes().collect::<Vec<_>>().len() > 0)
343 /// ```
344 pub fn set_attribute_with_prefix(&mut self, prefix: Option<&str>, name: &str, value: &str) {
345 // if attribute with same already exists - do nothing
346 if self
347 .attributes
348 .iter()
349 .any(|a| a.name() == name && a.prefix.as_deref() == prefix)
350 {
351 return;
352 }
353
354 self.attributes.push(Attribute::new(prefix, name, value));
355 }
356
357 /// Sets the text content of this element (e.g. `<string>value</string>`).
358 ///
359 /// # Example
360 /// ```
361 /// use apk_info_xml::Element;
362 ///
363 /// let mut s = Element::new("string");
364 /// s.set_attribute("name", "app_name");
365 /// s.set_text("Hello & welcome");
366 /// assert_eq!(s.to_string(), "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<string name=\"app_name\">Hello & welcome</string>\n");
367 /// ```
368 pub fn set_text(&mut self, text: &str) {
369 if self.childrens.is_empty() {
370 self.text = Some(text.to_owned());
371 }
372 }
373
374 /// Appends a new child [`Element`] to this element.
375 ///
376 /// # Example
377 /// ```
378 /// use apk_info_xml::Element;
379 ///
380 /// let mut root = Element::new("root");
381 /// root.append_child(Element::new("child"));
382 /// ```
383 #[inline]
384 pub fn append_child(&mut self, child: Element) {
385 self.text = None;
386 self.childrens.push(child);
387 }
388
389 /// Returns an iterator over all child elements.
390 ///
391 /// # Example
392 /// ```
393 /// use apk_info_xml::Element;
394 ///
395 /// let mut root = Element::new("root");
396 /// root.append_child(Element::new("child"));
397 ///
398 /// assert_eq!(root.childrens().count(), 1);
399 /// ```
400 #[inline]
401 pub fn childrens(&self) -> impl Iterator<Item = &Element> {
402 self.childrens.iter()
403 }
404
405 /// Return an iterator over all [Element]'s from the current root
406 ///
407 /// # Example
408 ///
409 /// ```
410 /// use apk_info_xml::Element;
411 ///
412 /// let mut root = Element::new("root");
413 /// let mut child = Element::new("child");
414 /// child.append_child(Element::new("subchild"));
415 /// root.append_child(child);
416 ///
417 /// assert_eq!(root.descendants().count(), 2);
418 /// ```
419 #[inline]
420 pub fn descendants(&self) -> Descendants<'_> {
421 Descendants::new(self)
422 }
423
424 /// Returns an iterator over the element's attributes.
425 ///
426 /// # Example
427 /// ```
428 /// use apk_info_xml::Element;
429 ///
430 /// let mut e = Element::new("node");
431 /// e.set_attribute("id", "42");
432 /// assert_eq!(e.attributes().count(), 1);
433 /// ```
434 #[inline]
435 pub fn attributes(&self) -> impl Iterator<Item = &Attribute> {
436 self.attributes.iter()
437 }
438
439 /// Returns the element's tag name.
440 #[inline]
441 pub fn name(&self) -> &str {
442 &self.name
443 }
444
445 /// Retrieves the value of an attribute by name, if present.
446 ///
447 /// # Example
448 /// ```
449 /// use apk_info_xml::Element;
450 ///
451 /// let mut e = Element::new("node");
452 /// e.set_attribute("id", "42");
453 /// assert_eq!(e.attr("id"), Some("42"));
454 /// assert_eq!(e.attr("missing"), None);
455 /// ```
456 #[inline]
457 pub fn attr(&self, name: &str) -> Option<&str> {
458 self.attributes
459 .iter()
460 .find(|x| x.name() == name)
461 .map(|x| x.value())
462 }
463
464 pub(crate) fn fmt_with_indent(
465 &self,
466 f: &mut std::fmt::Formatter<'_>,
467 indent: usize,
468 ) -> std::fmt::Result {
469 write_indent(f, indent)?;
470 f.write_char('<')?;
471 f.write_str(&self.name)?;
472
473 match self.attributes.len() {
474 0 => {}
475 1 => {
476 f.write_char(' ')?;
477 self.attributes[0].fmt(f)?;
478 }
479 n => {
480 let child_indent = indent + 1;
481 f.write_char('\n')?;
482 write_indent(f, child_indent)?;
483 for (idx, attr) in self.attributes.iter().enumerate() {
484 attr.fmt(f)?;
485 if idx + 1 != n {
486 f.write_char('\n')?;
487 write_indent(f, child_indent)?;
488 }
489 }
490 }
491 }
492
493 if self.childrens.is_empty() {
494 match self.text.as_deref() {
495 Some(text) => {
496 f.write_char('>')?;
497 write_text(f, text)?;
498 f.write_str("</")?;
499 f.write_str(&self.name)?;
500 f.write_str(">")?;
501 }
502 None => {
503 f.write_str("/>")?;
504 }
505 }
506 f.write_char('\n')?;
507 } else {
508 f.write_char('>')?;
509 f.write_char('\n')?;
510 for child in &self.childrens {
511 child.fmt_with_indent(f, indent + 1)?;
512 }
513 write_indent(f, indent)?;
514 f.write_str("</")?;
515 f.write_str(&self.name)?;
516 f.write_str(">\n")?;
517 }
518 Ok(())
519 }
520}
521
522/// Writes `indent` levels of two-space indentation into `f` without allocating.
523#[inline]
524fn write_indent(f: &mut std::fmt::Formatter<'_>, indent: usize) -> std::fmt::Result {
525 for _ in 0..indent {
526 f.write_str(" ")?;
527 }
528 Ok(())
529}
530
531impl std::fmt::Display for Element {
532 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
533 // default xml header
534 writeln!(f, "<?xml version=\"1.0\" encoding=\"utf-8\"?>")?;
535
536 // pretty print
537 self.fmt_with_indent(f, 0)
538 }
539}
540
541pub struct Descendants<'a> {
542 stack: Vec<std::slice::Iter<'a, Element>>,
543}
544
545impl<'a> Descendants<'a> {
546 fn new(root: &'a Element) -> Descendants<'a> {
547 Descendants {
548 stack: vec![root.childrens.iter()],
549 }
550 }
551}
552
553impl<'a> Iterator for Descendants<'a> {
554 type Item = &'a Element;
555
556 fn next(&mut self) -> Option<Self::Item> {
557 while let Some(top_iter) = self.stack.last_mut() {
558 if let Some(next_elem) = top_iter.next() {
559 if !next_elem.childrens.is_empty() {
560 self.stack.push(next_elem.childrens.iter());
561 }
562 return Some(next_elem);
563 } else {
564 self.stack.pop();
565 }
566 }
567
568 None
569 }
570}
571
572#[cfg(test)]
573mod tests {
574 use super::*;
575
576 #[test]
577 fn attribute_plain_renders_quoted() {
578 assert_eq!(Attribute::new(None, "id", "123").to_string(), "id=\"123\"");
579 }
580
581 #[test]
582 fn attribute_prefixed_renders() {
583 assert_eq!(
584 Attribute::new(Some("android"), "name", "x").to_string(),
585 "android:name=\"x\""
586 );
587 }
588
589 #[test]
590 fn attribute_escapes_specials() {
591 assert_eq!(
592 Attribute::new(None, "a", "&\n<>\"'").to_string(),
593 "a=\"& <>"'\""
594 );
595 }
596
597 #[test]
598 fn attribute_tabs_cr_to_space() {
599 assert_eq!(
600 Attribute::new(None, "a", "x\ty\r").to_string(),
601 "a=\"x y \""
602 );
603 }
604
605 #[test]
606 fn attribute_all_invalid_renders_empty() {
607 assert_eq!(
608 Attribute::new(None, "a", "\u{1}\u{FFFF}\u{FFFE}").to_string(),
609 "a=\"\""
610 );
611 }
612
613 #[test]
614 fn attribute_non_ascii_preserved() {
615 assert_eq!(
616 Attribute::new(None, "a", "café 😀").to_string(),
617 "a=\"café 😀\""
618 );
619 }
620
621 #[test]
622 fn element_render_golden() {
623 let mut root = Element::new("root");
624 root.set_attribute("a", "1");
625 root.set_attribute("b", "2");
626
627 let mut child = Element::new("child");
628 child.set_attribute("x", "y");
629 root.append_child(child);
630
631 let mut ns_kid = Element::new("ns:kid");
632 ns_kid.set_attribute("prefix", "p");
633 root.append_child(ns_kid);
634
635 let expected = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\
636<root\n a=\"1\"\n b=\"2\">\n <child x=\"y\"/>\n <ns:kid prefix=\"p\"/>\n</root>\n";
637 assert_eq!(root.to_string(), expected);
638 }
639}