Skip to main content

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 `&amp;`, `&lt;`, `&gt;`,
167///   `&quot;` and `&apos;` 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 &amp; Security &lt;pro&gt;&quot;v1&quot;\"");
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("&amp;")?,
216            '<' => f.write_str("&lt;")?,
217            '>' => f.write_str("&gt;")?,
218            '"' => f.write_str("&quot;")?,
219            '\'' => f.write_str("&apos;")?,
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/// `&amp;`, `&lt;` and `&gt;`; 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("&amp;")?,
245            '<' => f.write_str("&lt;")?,
246            '>' => f.write_str("&gt;")?,
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 &amp; 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=\"&amp; &lt;&gt;&quot;&apos;\""
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}