Skip to main content

apk/xml/
text.rs

1//! Text XML: parsing with framework-aware typing, and printing.
2
3use std::fmt::{self, Write as _};
4
5use quick_xml::XmlVersion;
6use quick_xml::escape::resolve_predefined_entity;
7use quick_xml::events::{BytesStart, Event};
8use quick_xml::name::{PrefixDeclaration, ResolveResult};
9use quick_xml::reader::NsReader;
10
11use super::{
12    ANDROID_NAMESPACE, AttrName, Attribute, Document, Element, MAX_DEPTH, Namespace, Node, Step, XmlError, walk,
13};
14use crate::android::{self, AttrDef, AttrFormat};
15use crate::res::{ResourceRef, ResourceType, Value};
16
17/// Attributes in this namespace are build-tool hints and are dropped, as aapt2 does.
18const TOOLS_NAMESPACE: &str = "http://schemas.android.com/tools";
19
20impl Document {
21    /// Parses a text XML document such as a hand-written `AndroidManifest.xml`.
22    ///
23    /// Attributes are typed as aapt2 types them: `android:` attributes by
24    /// their framework format (`true` becomes a boolean, `landscape` an enum
25    /// value, `@string/x` a reference), the unqualified `style` attribute as
26    /// a reference, and others as strings. `tools:` attributes and elements
27    /// are removed, and only the predefined XML entities are expanded.
28    pub fn parse(text: &str) -> Result<Self, XmlError> {
29        let mut reader = NsReader::from_str(text);
30        reader.config_mut().expand_empty_elements = true;
31        // Elements still open, innermost last: the tree is built without
32        // recursion, so nesting depth is bounded only by `MAX_DEPTH`.
33        let mut open: Vec<Element> = Vec::new();
34        let mut root = None;
35        // How deep inside a dropped `tools:` element the reader is.
36        let mut skipping = 0usize;
37        loop {
38            let (namespace, event) = reader.read_resolved_event()?;
39            let namespace = match namespace {
40                ResolveResult::Bound(namespace) => Some(namespace.0),
41                ResolveResult::Unbound => None,
42                ResolveResult::Unknown(prefix) => return Err(XmlError::UndeclaredPrefix { prefix }),
43            };
44            match event {
45                Event::Start(start) => {
46                    if skipping > 0 || namespace == Some(TOOLS_NAMESPACE) {
47                        skipping += 1;
48                    } else if open.len() >= MAX_DEPTH {
49                        return Err(XmlError::TooDeep);
50                    } else if root.is_some() && open.is_empty() {
51                        return Err(XmlError::MultipleRoots);
52                    } else {
53                        let namespace = namespace.map(str::to_owned);
54                        open.push(shell(&reader, &start, namespace)?);
55                    }
56                }
57                Event::End(_) if skipping > 0 => skipping -= 1,
58                Event::End(end) => {
59                    let found = || end.local_name().as_ref().to_owned();
60                    let mut element = open.pop().ok_or_else(|| XmlError::UnexpectedEnd { found: found() })?;
61                    element.children.retain(|child| !matches!(child, Node::Text(text) if text.trim().is_empty()));
62                    match open.last_mut() {
63                        Some(parent) => parent.children.push(Node::Element(element)),
64                        None => root = Some(element),
65                    }
66                }
67                Event::Text(text) if skipping == 0 => push_text(&mut open, &text.xml10_content()),
68                Event::CData(data) if skipping == 0 => push_text(&mut open, &data.xml10_content()),
69                Event::GeneralRef(reference) if skipping == 0 => match reference.resolve_char_ref()? {
70                    Some(character) => push_text(&mut open, character.encode_utf8(&mut [0; 4])),
71                    None => {
72                        let name = reference.xml10_content();
73                        let text = resolve_predefined_entity(&name);
74                        push_text(&mut open, text.ok_or_else(|| XmlError::UnknownEntity { name: name.into_owned() })?);
75                    }
76                },
77                Event::Eof => break,
78                _ => {}
79            }
80        }
81        if !open.is_empty() {
82            return Err(XmlError::Unclosed { count: open.len() });
83        }
84        root.map(Self::new).ok_or(XmlError::NoRoot)
85    }
86}
87
88/// Appends text to the innermost open element, joining adjacent pieces
89/// (text is split around entity references).
90fn push_text(open: &mut [Element], text: &str) {
91    let Some(element) = open.last_mut() else { return };
92    match element.children.last_mut() {
93        Some(Node::Text(previous)) => previous.push_str(text),
94        _ => element.children.push(Node::Text(text.to_owned())),
95    }
96}
97
98/// An element without its children: name, namespace declarations, and
99/// typed attributes.
100fn shell(reader: &NsReader<&[u8]>, start: &BytesStart<'_>, namespace: Option<String>) -> Result<Element, XmlError> {
101    let name = start.local_name().as_ref().to_owned();
102    let mut element = Element { name, namespace, ..Element::default() };
103    for attribute in start.attributes() {
104        let attribute = attribute.map_err(quick_xml::Error::from)?;
105        let text = attribute.normalized_value(XmlVersion::Implicit1_0)?;
106        if let Some(binding) = attribute.key.as_namespace_binding() {
107            let prefix = match binding {
108                PrefixDeclaration::Default => None,
109                PrefixDeclaration::Named(prefix) => Some(prefix.to_owned()),
110            };
111            // `xmlns=""` only undoes an outer default, which the resolved
112            // element names already reflect.
113            if text != TOOLS_NAMESPACE && prefix.as_deref() != Some("xml") && !(prefix.is_none() && text.is_empty()) {
114                element.namespaces.push(Namespace { prefix, uri: text.into_owned() });
115            }
116            continue;
117        }
118        let (resolved, local) = reader.resolver().resolve_attribute(attribute.key);
119        let local = local.as_ref().to_owned();
120        let name = match resolved {
121            ResolveResult::Unknown(prefix) => return Err(XmlError::UndeclaredPrefix { prefix }),
122            ResolveResult::Bound(namespace) if namespace.as_ref() == TOOLS_NAMESPACE => continue,
123            ResolveResult::Bound(namespace) if namespace.as_ref() == ANDROID_NAMESPACE => {
124                let def = android::find_attr(&local).ok_or(XmlError::UnknownAttribute { name: local })?;
125                element.set_attr(def, def.parse(&text)?);
126                continue;
127            }
128            ResolveResult::Bound(namespace) => {
129                AttrName { namespace: Some(namespace.as_ref().to_owned()), name: local, id: None }
130            }
131            ResolveResult::Unbound => AttrName::plain(local),
132        };
133        let value = match is_style(&name) {
134            true if text.trim_start().starts_with(['@', '?']) => reference(&text)?,
135            // As for framework attributes, a leading `\` escapes a literal.
136            true => Value::String(text.strip_prefix('\\').unwrap_or(&text).to_owned()),
137            false => Value::String(text.into_owned()),
138        };
139        element.set_attr(name, value);
140    }
141    Ok(element)
142}
143
144/// The unqualified `style` attribute of layouts, which aapt2 compiles as a
145/// reference.
146fn is_style(name: &AttrName) -> bool {
147    name.namespace.is_none() && name.name == "style"
148}
149
150/// `@type/name`, `?type/name`, `@null`, or `@empty`.
151fn reference(text: &str) -> Result<Value, XmlError> {
152    let trimmed = text.trim();
153    Ok(match trimmed {
154        "@null" => Value::Null,
155        "@empty" => Value::Empty,
156        _ => match trimmed.strip_prefix('?') {
157            Some(body) => Value::Attribute(ResourceRef::parse_body(body, Some(ResourceType::Attr))?),
158            None => {
159                let body = trimmed.strip_prefix('@').unwrap_or(trimmed).trim_start_matches('+');
160                Value::Reference(ResourceRef::parse_body(body, None)?)
161            }
162        },
163    })
164}
165
166impl AttrDef {
167    /// Types `text` by the formats this attribute accepts, as aapt2 does:
168    ///
169    /// ```
170    /// use apk::android::attr;
171    /// use apk::res::Value;
172    ///
173    /// assert_eq!(attr::SCREEN_ORIENTATION.parse("landscape")?, Value::Int(0));
174    /// assert_eq!(attr::CONFIG_CHANGES.parse("orientation|screenSize")?, Value::Hex(0x480));
175    /// assert_eq!(attr::EXPORTED.parse("true")?, Value::Bool(true));
176    /// assert!(attr::EXPORTED.parse("yes").is_err());
177    /// # Ok::<(), apk::xml::XmlError>(())
178    /// ```
179    pub fn parse(&self, text: &str) -> Result<Value, XmlError> {
180        let trimmed = text.trim();
181        if trimmed.starts_with(['@', '?']) {
182            return reference(text);
183        }
184        let format = self.format();
185        if let Some(value) = self.parse_typed(trimmed, format) {
186            return Ok(value);
187        }
188        if format.contains(AttrFormat::STRING) {
189            // A leading `\` escapes a literal `@` or `?`.
190            return Ok(Value::String(text.strip_prefix('\\').unwrap_or(text).to_owned()));
191        }
192        Err(XmlError::InvalidAttributeValue { attribute: self.name(), value: text.to_owned(), format })
193    }
194
195    /// `text` as the first of this attribute's non-string formats it fits.
196    fn parse_typed(&self, text: &str, format: AttrFormat) -> Option<Value> {
197        let number = || match text.strip_prefix("0x").or_else(|| text.strip_prefix("0X")) {
198            Some(hex) => u32::from_str_radix(hex, 16).ok().map(Value::Hex),
199            None => text.parse().ok().map(Value::Int),
200        };
201        if format.contains(AttrFormat::ENUM)
202            && let Some(value) = self.symbol(text)
203        {
204            return Some(Value::Int(value as i32));
205        }
206        if format.contains(AttrFormat::FLAGS)
207            && let Some(bits) = text.split('|').try_fold(0, |bits, flag| Some(bits | self.symbol(flag.trim())?))
208        {
209            return Some(Value::Hex(bits));
210        }
211        // Values without a symbol (as printed for decoded documents) are numeric.
212        if format.intersects(AttrFormat::ENUM | AttrFormat::FLAGS)
213            && let Some(value) = number()
214        {
215            return Some(match value {
216                Value::Int(value) if format.contains(AttrFormat::FLAGS) => Value::Hex(value as u32),
217                value => value,
218            });
219        }
220        // The spellings aapt2 accepts.
221        if format.contains(AttrFormat::BOOLEAN) {
222            match text {
223                "true" | "True" | "TRUE" => return Some(Value::Bool(true)),
224                "false" | "False" | "FALSE" => return Some(Value::Bool(false)),
225                _ => {}
226            }
227        }
228        if format.contains(AttrFormat::INTEGER)
229            && let Some(value) = number()
230        {
231            return Some(value);
232        }
233        if format.contains(AttrFormat::COLOR)
234            && let Ok(color) = text.parse()
235        {
236            return Some(Value::Color(color));
237        }
238        if format.contains(AttrFormat::DIMENSION)
239            && let Ok(dimension) = text.parse()
240        {
241            return Some(Value::Dimension(dimension));
242        }
243        if format.contains(AttrFormat::FRACTION)
244            && let Ok(fraction) = text.parse()
245        {
246            return Some(Value::Fraction(fraction));
247        }
248        if format.contains(AttrFormat::FLOAT)
249            && let Ok(float) = text.parse()
250        {
251            return Some(Value::Float(float));
252        }
253        None
254    }
255}
256
257impl fmt::Display for Document {
258    /// Prints indented text XML. Framework enum, flag, and resource values are shown by name,
259    /// and elements with text content are printed without added whitespace.
260    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261        writeln!(f, r#"<?xml version="1.0" encoding="utf-8"?>"#)?;
262        let mut printer = Printer { f, scope: Vec::new(), marks: Vec::new(), inline_from: None };
263        walk(&self.root).try_for_each(|(depth, step)| printer.step(depth, step))
264    }
265}
266
267/// A namespace binding in scope while printing.
268struct Binding<'a> {
269    prefix: Option<std::borrow::Cow<'a, str>>,
270    uri: &'a str,
271}
272
273struct Printer<'a, 'f, 'g> {
274    f: &'f mut fmt::Formatter<'g>,
275    /// Bindings in scope, innermost last.
276    scope: Vec<Binding<'a>>,
277    /// The scope length before each open element's declarations.
278    marks: Vec<usize>,
279    /// The depth of the element whose content is printed inline.
280    inline_from: Option<usize>,
281}
282
283impl<'a> Printer<'a, '_, '_> {
284    fn step(&mut self, depth: usize, step: Step<'a>) -> fmt::Result {
285        match step {
286            Step::Open(element) => self.open(depth, element),
287            Step::Text(text) => write!(self.f, "{}", Escaped { text, attribute: false }),
288            Step::Close(element) => self.close(depth, element),
289        }
290    }
291
292    fn open(&mut self, depth: usize, element: &'a Element) -> fmt::Result {
293        let inline = self.inline_from.is_some();
294        if !inline {
295            indent(self.f, depth)?;
296        }
297        self.marks.push(self.scope.len());
298        let declared = self.scope.len();
299        self.scope.extend(
300            element.namespaces.iter().map(|ns| Binding { prefix: ns.prefix.as_deref().map(Into::into), uri: &ns.uri }),
301        );
302        // An element in no namespace must undo a default one in scope.
303        let default = self.scope.iter().rev().find(|b| b.prefix.is_none());
304        if element.namespace.is_none() && default.is_some_and(|b| !b.uri.is_empty()) {
305            self.scope.push(Binding { prefix: None, uri: "" });
306        }
307        // Namespaces used here but not declared in scope get declarations.
308        let attributes = element.attributes.iter().filter_map(|a| Some((a.name.namespace.as_deref()?, true)));
309        for (uri, attribute) in element.namespace.as_deref().map(|uri| (uri, false)).into_iter().chain(attributes) {
310            if prefix(&self.scope, uri, attribute).is_none() {
311                let bound = |name: &str| self.scope.iter().any(|b| b.prefix.as_deref() == Some(name));
312                let prefix = match uri == ANDROID_NAMESPACE && !bound("android") {
313                    true => "android".into(),
314                    false => (0..).map(|n| format!("ns{n}")).find(|name| !bound(name)).expect("unbounded").into(),
315                };
316                self.scope.push(Binding { prefix: Some(prefix), uri });
317            }
318        }
319        write!(self.f, "<{}", qualified(&self.scope, element.namespace.as_deref(), &element.name, false))?;
320        // Elements with several attributes put each on its own line.
321        let own_lines = !inline && self.scope.len() - declared + element.attributes.len() > 1;
322        let separator = |f: &mut fmt::Formatter<'_>| match own_lines {
323            true => f.write_char('\n').and_then(|()| indent(f, depth + 1)),
324            false => f.write_char(' '),
325        };
326        for binding in &self.scope[declared..] {
327            separator(self.f)?;
328            let uri = Escaped { text: binding.uri, attribute: true };
329            match &binding.prefix {
330                Some(prefix) => write!(self.f, "xmlns:{prefix}=\"{uri}\"")?,
331                None => write!(self.f, "xmlns=\"{uri}\"")?,
332            }
333        }
334        for attribute in &element.attributes {
335            separator(self.f)?;
336            let name = qualified(&self.scope, attribute.name.namespace.as_deref(), &attribute.name.name, true);
337            write!(self.f, "{name}=\"")?;
338            write_value(self.f, attribute)?;
339            self.f.write_char('"')?;
340        }
341        if element.children.is_empty() {
342            self.f.write_str(" />")?;
343            self.scope.truncate(self.marks.pop().expect("pushed above"));
344            return if inline { Ok(()) } else { self.f.write_char('\n') };
345        }
346        self.f.write_char('>')?;
347        if inline {
348            return Ok(());
349        }
350        // Whitespace is significant around text, so mixed content stays inline.
351        if element.children.iter().any(|child| matches!(child, Node::Text(_))) {
352            self.inline_from = Some(depth);
353            return Ok(());
354        }
355        self.f.write_char('\n')
356    }
357
358    fn close(&mut self, depth: usize, element: &'a Element) -> fmt::Result {
359        if element.children.is_empty() {
360            return Ok(());
361        }
362        if self.inline_from.is_none() {
363            indent(self.f, depth)?;
364        }
365        write!(self.f, "</{}>", qualified(&self.scope, element.namespace.as_deref(), &element.name, false))?;
366        self.scope.truncate(self.marks.pop().expect("opened before"));
367        if self.inline_from.is_some_and(|from| from < depth) {
368            return Ok(());
369        }
370        self.inline_from = None;
371        self.f.write_char('\n')
372    }
373}
374
375fn indent(f: &mut fmt::Formatter<'_>, depth: usize) -> fmt::Result {
376    (0..depth).try_for_each(|_| f.write_str("    "))
377}
378
379/// The prefix that currently names `uri` in `scope`: the innermost binding
380/// of it whose prefix no inner binding shadows. Attributes need a named
381/// prefix; elements can use a default namespace.
382fn prefix<'s>(scope: &'s [Binding<'_>], uri: &str, attribute: bool) -> Option<Option<&'s str>> {
383    let current = |prefix: Option<&str>| scope.iter().rev().find(|b| b.prefix.as_deref() == prefix).map(|b| b.uri);
384    scope
385        .iter()
386        .rev()
387        .filter(|b| b.uri == uri && !(attribute && b.prefix.is_none()))
388        .map(|b| b.prefix.as_deref())
389        .find(|&prefix| current(prefix) == Some(uri))
390}
391
392fn qualified<'s, 'n>(
393    scope: &'s [Binding<'_>],
394    namespace: Option<&str>,
395    name: &'n str,
396    attribute: bool,
397) -> Qualified<'s, 'n> {
398    let prefix = namespace.and_then(|uri| prefix(scope, uri, attribute)).flatten();
399    Qualified { prefix, name }
400}
401
402/// `prefix:name`, or `name`.
403struct Qualified<'p, 'n> {
404    prefix: Option<&'p str>,
405    name: &'n str,
406}
407
408impl fmt::Display for Qualified<'_, '_> {
409    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
410        match self.prefix {
411            Some(prefix) => write!(f, "{prefix}:{}", self.name),
412            None => f.write_str(self.name),
413        }
414    }
415}
416
417/// Writes an attribute value the way it would be written by hand: framework
418/// symbols by name, framework resources as `@android:type/name`, and string
419/// literals that would otherwise parse as another value escaped.
420fn write_value(f: &mut fmt::Formatter<'_>, attribute: &Attribute) -> fmt::Result {
421    let escaped = |text| Escaped { text, attribute: true };
422    let def = attribute.name.android_def();
423    // Text parsed as a reference, or typed by the attribute's format.
424    let typed = |s: &str| {
425        (s.trim_start().starts_with(['@', '?', '\\']) && (def.is_some() || is_style(&attribute.name)))
426            || def.is_some_and(|def| def.parse_typed(s.trim(), def.format()).is_some())
427    };
428    match (&attribute.value, def) {
429        (Value::Int(value), Some(def)) if def.format().contains(AttrFormat::ENUM) => match def.enum_name(*value as u32)
430        {
431            Some(name) => f.write_str(name),
432            None => write!(f, "{value}"),
433        },
434        (Value::Hex(value), Some(def)) if def.format().contains(AttrFormat::FLAGS) => match def.flag_names(*value) {
435            Some(names) if !names.is_empty() => f.write_str(&names.join("|")),
436            _ => match def.enum_name(*value) {
437                Some(name) => f.write_str(name),
438                None => write!(f, "{}", attribute.value),
439            },
440        },
441        (Value::Reference(ResourceRef::Id(id)), _) => match android::resource_name(*id) {
442            Some((ty, name)) => write!(f, "@android:{ty}/{name}"),
443            None => write!(f, "{}", attribute.value),
444        },
445        (Value::String(s), _) if typed(s) => write!(f, "\\{}", escaped(s)),
446        (Value::String(s), _) => write!(f, "{}", escaped(s)),
447        (value, _) => write!(f, "{}", escaped(&value.to_string())),
448    }
449}
450
451/// Text escaped for element content or an attribute value. In attribute
452/// values, whitespace characters are escaped too, since parsers normalize
453/// them to spaces.
454struct Escaped<'a> {
455    text: &'a str,
456    attribute: bool,
457}
458
459impl fmt::Display for Escaped<'_> {
460    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
461        let mut rest = self.text;
462        while let Some(at) = rest.find(|c: char| {
463            matches!(c, '&' | '<' | '>' | '"')
464                || (self.attribute && c.is_ascii_whitespace() && c != ' ')
465                || (c.is_control() && !matches!(c, '\t' | '\n'))
466        }) {
467            f.write_str(&rest[..at])?;
468            let c = rest[at..].chars().next().expect("found a character");
469            match c {
470                '&' => f.write_str("&amp;")?,
471                '<' => f.write_str("&lt;")?,
472                '>' => f.write_str("&gt;")?,
473                '"' => f.write_str("&quot;")?,
474                c => write!(f, "&#x{:x};", u32::from(c))?,
475            }
476            rest = &rest[at + c.len_utf8()..];
477        }
478        f.write_str(rest)
479    }
480}
481
482#[cfg(test)]
483mod tests {
484    use super::*;
485    use crate::android::attr;
486
487    const MANIFEST: &str = r#"<?xml version="1.0" encoding="utf-8"?>
488<manifest xmlns:android="http://schemas.android.com/apk/res/android"
489          xmlns:tools="http://schemas.android.com/tools"
490          package="org.example.game"
491          android:versionCode="0x10"
492          android:versionName="1.0">
493    <uses-sdk android:minSdkVersion="24" android:targetSdkVersion="35" />
494    <application android:label="@string/app_name"
495                 android:hasCode="false"
496                 android:theme="@android:style/Theme.Material.NoActionBar"
497                 tools:ignore="GoogleAppIndexingWarning">
498        <activity android:name="android.app.NativeActivity"
499                  android:screenOrientation="landscape"
500                  android:configChanges="orientation|keyboardHidden|screenSize"
501                  android:windowSoftInputMode="stateHidden|adjustResize"
502                  android:exported="true">
503            <meta-data android:name="android.app.lib_name" android:value="\@game" />
504        </activity>
505    </application>
506</manifest>"#;
507
508    #[test]
509    fn types_framework_attributes_like_aapt2() {
510        let document = Document::parse(MANIFEST).unwrap();
511        let root = &document.root;
512        assert_eq!(root.attr(None, "package"), Some(&Value::String("org.example.game".into())));
513        assert_eq!(root.android_attr(attr::VERSION_CODE), Some(&Value::Hex(0x10)));
514        let application = root.element("application").unwrap();
515        assert_eq!(application.android_attr(attr::HAS_CODE), Some(&Value::Bool(false)));
516        assert!(application.attributes.iter().all(|a| a.name.namespace.as_deref() != Some(TOOLS_NAMESPACE)));
517        let activity = application.element("activity").unwrap();
518        assert_eq!(activity.android_attr(attr::SCREEN_ORIENTATION), Some(&Value::Int(0)));
519        assert_eq!(activity.android_attr(attr::CONFIG_CHANGES), Some(&Value::Hex(0x80 | 0x20 | 0x400)));
520        assert_eq!(activity.android_attr(attr::WINDOW_SOFT_INPUT_MODE), Some(&Value::Hex(0x12)));
521        let meta = activity.element("meta-data").unwrap();
522        assert_eq!(meta.android_attr(attr::VALUE), Some(&Value::String("@game".into())));
523    }
524
525    #[test]
526    fn printing_and_reparsing_is_stable() {
527        let document = Document::parse(MANIFEST).unwrap();
528        let printed = document.to_string();
529        assert!(printed.contains(r#"android:screenOrientation="landscape""#), "{printed}");
530        assert!(printed.contains(r#"android:windowSoftInputMode="adjustResize|stateHidden""#), "{printed}");
531        assert_eq!(Document::parse(&printed).unwrap(), document);
532    }
533
534    #[test]
535    fn mixed_content_and_literals_survive_printing() {
536        for text in [
537            "<a>Hello <b>world</b>!</a>",
538            r#"<a src="@literal" note="tab&#x9;and&#xA;newline"/>"#,
539            r#"<View xmlns:android="http://schemas.android.com/apk/res/android" style="@android:style/Theme.Material"/>"#,
540            r#"<a xmlns:x="urn:one"><b xmlns:x="urn:two" xmlns:y="urn:one" y:attr="1"/></a>"#,
541        ] {
542            let document = Document::parse(text).unwrap();
543            let printed = document.to_string();
544            assert_eq!(Document::parse(&printed).unwrap(), document, "{printed}");
545        }
546        let style = Document::parse(r#"<View style="@android:style/Theme.Material"/>"#).unwrap();
547        assert!(matches!(style.root.attr(None, "style"), Some(Value::Reference(_))));
548    }
549
550    #[test]
551    fn printed_literals_keep_their_types() {
552        let document = Document::new(
553            Element::new("meta-data")
554                .with_android_namespace()
555                .with_attr(attr::VALUE, "123")
556                .with_attr(attr::LABEL, " @x")
557                .with_attr("style", "@literal")
558                .with_attr("note", "carriage\rreturn"),
559        );
560        let printed = document.to_string();
561        assert!(printed.contains(r#"android:value="\123""#), "{printed}");
562        assert_eq!(Document::parse(&printed).unwrap(), document, "{printed}");
563        let float =
564            Document::new(Element::new("meta-data").with_android_namespace().with_attr(attr::VALUE, Value::Float(2.0)));
565        assert_eq!(Document::parse(&float.to_string()).unwrap(), float);
566        let text = Document::new(Element::new("a").with_child(Element::new("b")).with_attr("x", "y"));
567        let mut text = text;
568        text.root.children.push(Node::Text("line\r\n".into()));
569        assert_eq!(Document::parse(&text.to_string()).unwrap(), text);
570    }
571
572    #[test]
573    fn printed_colors_preserve_their_literal_type() {
574        for literal in ["#f00", "#8f00", "#123456", "#80123456"] {
575            let color = literal.parse::<crate::res::Color>().unwrap();
576            assert_eq!(color.to_string(), literal);
577            let document =
578                Document::new(Element::new("meta-data").with_android_namespace().with_attr(attr::VALUE, color));
579            let printed = document.to_string();
580            assert_eq!(Document::parse(&printed).unwrap(), document, "{printed}");
581        }
582    }
583
584    #[test]
585    fn unqualified_children_leave_a_default_namespace() {
586        let mut root = Element::new("a");
587        root.namespace = Some("urn:default".into());
588        root.namespaces.push(Namespace { prefix: None, uri: "urn:default".into() });
589        let document = Document::new(root.with_child(Element::new("b")));
590        let printed = document.to_string();
591        assert!(printed.contains(r#"<b xmlns="" />"#), "{printed}");
592        assert_eq!(Document::parse(&printed).unwrap(), document);
593    }
594
595    #[test]
596    fn attributes_parse_into_binary_order() {
597        let text = r#"<a xmlns:android="http://schemas.android.com/apk/res/android" z="1" android:name="n" android:label="l"/>"#;
598        let document = Document::parse(text).unwrap();
599        let names: Vec<_> = document.root.attributes.iter().map(|a| a.name.name.as_str()).collect();
600        assert_eq!(names, ["label", "name", "z"]);
601        assert_eq!(Document::decode(&document.encode(&()).unwrap()).unwrap(), document);
602    }
603
604    #[test]
605    fn undeclared_namespaces_get_unused_prefixes() {
606        let mut element = Element::new("a");
607        element.namespaces.push(Namespace { prefix: Some("ns0".into()), uri: "urn:user".into() });
608        element.set_attr(AttrName { namespace: Some("urn:user".into()), name: "x".into(), id: None }, "1");
609        element.set_attr(AttrName { namespace: Some("urn:other".into()), name: "y".into(), id: None }, "2");
610        let printed = Document::new(element).to_string();
611        let reparsed = Document::parse(&printed).unwrap();
612        assert_eq!(reparsed.root.attr(Some("urn:user"), "x"), Some(&Value::String("1".into())), "{printed}");
613        assert_eq!(reparsed.root.attr(Some("urn:other"), "y"), Some(&Value::String("2".into())), "{printed}");
614    }
615
616    #[test]
617    fn handles_the_full_depth_limit_without_recursion() {
618        let depth = MAX_DEPTH;
619        let text = format!("{}{}", "<a>".repeat(depth), "</a>".repeat(depth));
620        let document = Document::parse(&text).unwrap();
621        let decoded = Document::decode(&document.encode(&()).unwrap()).unwrap();
622        assert_eq!(decoded, document);
623        assert_eq!(document.to_string().matches("<a").count(), depth);
624        let too_deep = format!("{}{}", "<a>".repeat(MAX_DEPTH + 1), "</a>".repeat(MAX_DEPTH + 1));
625        assert!(matches!(Document::parse(&too_deep), Err(XmlError::TooDeep)));
626    }
627
628    #[test]
629    fn rejects_unknown_entities_and_prefixes() {
630        assert!(
631            matches!(Document::parse("<a>&custom;</a>"), Err(XmlError::UnknownEntity { name }) if name == "custom")
632        );
633        assert!(matches!(Document::parse(r#"<a x:y="1"/>"#), Err(XmlError::UndeclaredPrefix { .. })));
634        assert_eq!(Document::parse("<a>&lt;&#x41;&amp;</a>").unwrap().root.children, [Node::Text("<A&".into())]);
635    }
636
637    #[test]
638    fn values_without_symbols_print_and_parse_back() {
639        let document = Document::new(
640            Element::new("activity")
641                .with_android_namespace()
642                .with_attr(attr::CONFIG_CHANGES, Value::Hex(0))
643                .with_attr(attr::LAUNCH_MODE, Value::Int(42)),
644        );
645        let printed = document.to_string();
646        assert!(printed.contains(r#"android:configChanges="0x00000000""#), "{printed}");
647        assert_eq!(Document::parse(&printed).unwrap(), document);
648    }
649
650    #[test]
651    fn rejects_values_outside_the_attribute_format() {
652        let parse = |attribute: &str| {
653            Document::parse(&format!(r#"<a xmlns:android="http://schemas.android.com/apk/res/android" {attribute}/>"#))
654        };
655        assert!(matches!(
656            parse(r#"android:exported="yes""#),
657            Err(XmlError::InvalidAttributeValue { attribute: "exported", .. })
658        ));
659        assert!(matches!(parse(r#"android:launchMode="sometimes""#), Err(XmlError::InvalidAttributeValue { .. })));
660        assert!(matches!(parse(r#"android:notReal="1""#), Err(XmlError::UnknownAttribute { .. })));
661    }
662}