1use 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
17const TOOLS_NAMESPACE: &str = "http://schemas.android.com/tools";
19
20impl Document {
21 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 let mut open: Vec<Element> = Vec::new();
34 let mut root = None;
35 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
88fn 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
98fn 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 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 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
144fn is_style(name: &AttrName) -> bool {
147 name.namespace.is_none() && name.name == "style"
148}
149
150fn 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 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 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 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 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 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 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
267struct 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 scope: Vec<Binding<'a>>,
277 marks: Vec<usize>,
279 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 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 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 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 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
379fn 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
402struct 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
417fn 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 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
451struct 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("&")?,
471 '<' => f.write_str("<")?,
472 '>' => f.write_str(">")?,
473 '"' => f.write_str(""")?,
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	and
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><A&</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}