Skip to main content

apk/xml/
encode.rs

1//! Compiling a [`Document`] to binary XML.
2//!
3//! Layout follows aapt2: a UTF-16 string pool whose first entries are the
4//! names of attributes with resource IDs (in ID order, mirrored by the
5//! resource map), then namespace and element chunks. Attributes within an
6//! element are sorted by resource ID, which Android's attribute lookup
7//! requires.
8
9use std::collections::BTreeMap;
10
11use binrw::binrw;
12
13use super::{ANDROID_NAMESPACE, AttrName, Attribute, Document, Element, MAX_DEPTH, Step, XmlError, walk};
14use crate::android::attr;
15use crate::res::chunk::{ChunkType, NoHeader, Put, write_chunk};
16use crate::res::{ChunkError, Encoding, RawValue, ResourceId, ResourceLookup, StringPoolBuilder, data_type, resolve};
17
18/// `ResXMLTree_node` (after the base chunk header).
19#[binrw]
20#[brw(little)]
21pub(super) struct NodeHeader {
22    pub line: u32,
23    pub comment: u32,
24}
25
26/// `ResXMLTree_attrExt`.
27#[binrw]
28#[brw(little)]
29pub(super) struct ElementStart {
30    pub namespace: u32,
31    pub name: u32,
32    pub attribute_start: u16,
33    pub attribute_size: u16,
34    pub attribute_count: u16,
35    /// 1-based positions of the `android:id`, `class`, and `style` attributes.
36    pub id_index: u16,
37    pub class_index: u16,
38    pub style_index: u16,
39}
40
41/// `ResXMLTree_attribute`.
42#[binrw]
43#[brw(little)]
44pub(super) struct AttributeRecord {
45    pub namespace: u32,
46    pub name: u32,
47    pub raw_value: u32,
48    pub typed_value: RawValue,
49}
50
51pub(super) const NONE: u32 = u32::MAX;
52pub(super) const ATTRIBUTE_SIZE: u16 = 20;
53
54impl Document {
55    /// Compiles this document to binary XML, resolving `@type/name`
56    /// references to application resources through `resources`.
57    pub fn encode(&self, resources: &impl ResourceLookup) -> Result<Vec<u8>, XmlError> {
58        let pool = Pool::collect(&self.root)?;
59        let mut line = 0;
60        let mut node = || {
61            line += 1;
62            NodeHeader { line, comment: NONE }
63        };
64        let mut out = Vec::new();
65        write_chunk(&mut out, ChunkType::XML, &NoHeader, |out| {
66            pool.strings.write(out, Encoding::Utf16)?;
67            write_chunk(out, ChunkType::XML_RESOURCE_MAP, &NoHeader, |out| {
68                pool.ids.iter().for_each(|id| out.put_u32(id.0));
69                Ok::<_, XmlError>(())
70            })?;
71            for (_, step) in walk(&self.root) {
72                match step {
73                    Step::Open(element) => {
74                        for namespace in &element.namespaces {
75                            let words = [pool.optional(namespace.prefix.as_deref()), pool.other(&namespace.uri)];
76                            write_chunk(out, ChunkType::XML_START_NAMESPACE, &node(), |out| put_words(out, words))?;
77                        }
78                        let start = pool.element(element, resources)?;
79                        write_chunk(out, ChunkType::XML_START_ELEMENT, &node(), |out| {
80                            out.put(&start.header);
81                            start.attributes.iter().for_each(|record| out.put(record));
82                            Ok::<_, XmlError>(())
83                        })?;
84                    }
85                    Step::Text(text) => {
86                        write_chunk(out, ChunkType::XML_CDATA, &node(), |out| {
87                            out.put_u32(pool.other(text));
88                            out.put(&RawValue::new(data_type::NULL, 0));
89                            Ok::<_, XmlError>(())
90                        })?;
91                    }
92                    Step::Close(element) => {
93                        let words = [pool.optional(element.namespace.as_deref()), pool.other(&element.name)];
94                        write_chunk(out, ChunkType::XML_END_ELEMENT, &node(), |out| put_words(out, words))?;
95                        for namespace in element.namespaces.iter().rev() {
96                            let words = [pool.optional(namespace.prefix.as_deref()), pool.other(&namespace.uri)];
97                            write_chunk(out, ChunkType::XML_END_NAMESPACE, &node(), |out| put_words(out, words))?;
98                        }
99                    }
100                }
101            }
102            Ok::<_, XmlError>(())
103        })?;
104        Ok(out)
105    }
106
107    /// Checks what [`encode`](Self::encode) checks before it resolves
108    /// references: nesting depth, and every attribute name and value.
109    pub(crate) fn check(&self) -> Result<(), XmlError> {
110        Pool::collect(&self.root).map(drop)
111    }
112}
113
114fn put_words(out: &mut Vec<u8>, words: [u32; 2]) -> Result<(), XmlError> {
115    words.into_iter().for_each(|word| out.put_u32(word));
116    Ok(())
117}
118
119/// A document's string pool, borrowing every string from the document.
120struct Pool<'a> {
121    strings: StringPoolBuilder<'a>,
122    /// The resource map: the ID of each leading attribute name.
123    ids: Vec<ResourceId>,
124    attr_index: BTreeMap<ResourceId, u32>,
125}
126
127/// An encoded element start.
128struct Start {
129    header: ElementStart,
130    attributes: Vec<AttributeRecord>,
131}
132
133impl<'a> Pool<'a> {
134    fn collect(root: &'a Element) -> Result<Self, XmlError> {
135        let mut attrs: BTreeMap<ResourceId, &'a str> = BTreeMap::new();
136        let mut others: Vec<&'a str> = Vec::new();
137        for (depth, step) in walk(root) {
138            match step {
139                Step::Open(_) if depth >= MAX_DEPTH => return Err(XmlError::TooDeep),
140                Step::Open(element) => {
141                    for namespace in &element.namespaces {
142                        others.extend(namespace.prefix.as_deref());
143                        others.push(&namespace.uri);
144                    }
145                    others.extend(element.namespace.as_deref());
146                    others.push(&element.name);
147                    for attribute in &element.attributes {
148                        match attribute_id(attribute)? {
149                            Some(id) => _ = attrs.insert(id, &attribute.name.name),
150                            None => others.push(&attribute.name.name),
151                        }
152                        others.extend(attribute.name.namespace.as_deref());
153                        others.extend(attribute.value.as_str());
154                    }
155                }
156                Step::Text(text) => others.push(text),
157                Step::Close(_) => {}
158            }
159        }
160        let mut strings = StringPoolBuilder::default();
161        let ids = attrs.keys().copied().collect();
162        let attr_index = attrs.into_iter().map(|(id, name)| (id, strings.push(name))).collect();
163        // Names without IDs must not alias the resource-mapped prefix of the
164        // pool, which was pushed without interning, so they are added again
165        // even if the text repeats.
166        others.into_iter().for_each(|s| _ = strings.intern(s));
167        Ok(Self { strings, ids, attr_index })
168    }
169
170    fn other(&self, s: &str) -> u32 {
171        self.strings.get(s).expect("collected every string of the document")
172    }
173
174    fn optional(&self, s: Option<&str>) -> u32 {
175        s.map_or(NONE, |s| self.other(s))
176    }
177
178    fn element(&self, element: &Element, resources: &impl ResourceLookup) -> Result<Start, XmlError> {
179        let mut attributes: Vec<(&AttrName, AttributeRecord)> = Vec::with_capacity(element.attributes.len());
180        for attribute in &element.attributes {
181            let name = match attribute_id(attribute)? {
182                Some(id) => self.attr_index[&id],
183                None => self.other(&attribute.name.name),
184            };
185            let typed_value = attribute.value.to_raw(
186                |s| self.other(s),
187                |reference| {
188                    resolve(reference, resources)
189                        .ok_or_else(|| XmlError::UnresolvedReference { reference: reference.clone() })
190                },
191            )?;
192            let raw_value = if typed_value.data_type == data_type::STRING { typed_value.data } else { NONE };
193            let namespace = self.optional(attribute.name.namespace.as_deref());
194            attributes.push((&attribute.name, AttributeRecord { namespace, name, raw_value, typed_value }));
195        }
196        attributes.sort_by(|a, b| a.0.sort_key().cmp(&b.0.sort_key()));
197        let index_of = |wanted: &dyn Fn(&AttrName) -> bool| {
198            attributes.iter().position(|(name, _)| wanted(name)).map_or(0, |i| i as u16 + 1)
199        };
200        let header = ElementStart {
201            namespace: self.optional(element.namespace.as_deref()),
202            name: self.other(&element.name),
203            attribute_start: ATTRIBUTE_SIZE,
204            attribute_size: ATTRIBUTE_SIZE,
205            attribute_count: u16::try_from(attributes.len()).map_err(|_| ChunkError::TooLarge)?,
206            id_index: index_of(&|name| name.android_def() == Some(attr::ID)),
207            class_index: index_of(&|name| name.namespace.is_none() && name.name == "class"),
208            style_index: index_of(&|name| name.namespace.is_none() && name.name == "style"),
209        };
210        Ok(Start { header, attributes: attributes.into_iter().map(|(_, record)| record).collect() })
211    }
212}
213
214/// The namespace of attributes an app defines for itself, and the prefix
215/// of the per-package form older tools used.
216const RES_AUTO_NAMESPACE: &str = "http://schemas.android.com/apk/res-auto";
217const PACKAGE_NAMESPACE_PREFIX: &str = "http://schemas.android.com/apk/res/";
218
219/// The resource ID `attribute` compiles to, if any.
220///
221/// A framework attribute must exist and hold a value of a format it
222/// accepts. An app-defined attribute must already carry its ID, as in a
223/// decoded document: this crate does not compile `<attr>` resources, and
224/// without an ID Android would never see the attribute.
225fn attribute_id(attribute: &Attribute) -> Result<Option<ResourceId>, XmlError> {
226    let name = &attribute.name;
227    if let Some(def) = name.android_def() {
228        if !def.accepts(&attribute.value) {
229            let value = attribute.value.clone();
230            return Err(XmlError::MistypedValue { attribute: def.name(), value, format: def.format() });
231        }
232        return Ok(Some(def.id()));
233    }
234    match name.namespace.as_deref() {
235        // A decoded document may carry IDs of private framework attributes.
236        _ if name.id.is_some() => Ok(name.id),
237        Some(ANDROID_NAMESPACE) => Err(XmlError::UnknownAttribute { name: name.name.clone() }),
238        Some(namespace) if namespace == RES_AUTO_NAMESPACE || namespace.starts_with(PACKAGE_NAMESPACE_PREFIX) => {
239            Err(XmlError::AppAttribute { namespace: namespace.to_owned(), name: name.name.clone() })
240        }
241        _ => Ok(None),
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248    use crate::res::chunk::Chunk;
249    use crate::res::{ResourceRef, StringPool, Value};
250    use crate::xml::Document;
251
252    /// The string pool and resource map of an encoded document.
253    fn pool_and_map(bytes: &[u8]) -> (Vec<String>, Vec<u32>) {
254        let root = Chunk::parse_root(bytes, ChunkType::XML).unwrap();
255        let mut children = root.children();
256        let pool = StringPool::parse(&children.next().unwrap().unwrap()).unwrap();
257        let strings = pool.iter().map(|s| s.unwrap().into_owned()).collect();
258        let map = children.next().unwrap().unwrap();
259        (strings, map.body.as_chunks::<4>().0.iter().map(|&b| u32::from_le_bytes(b)).collect())
260    }
261
262    /// The `ResXMLTree_attrExt` of the root element.
263    fn root_start(bytes: &[u8]) -> ElementStart {
264        let root = Chunk::parse_root(bytes, ChunkType::XML).unwrap();
265        let start = root.children().map(Result::unwrap).find(|c| c.ty == ChunkType::XML_START_ELEMENT).unwrap();
266        crate::res::chunk::read_padded(start.body)
267    }
268
269    #[test]
270    fn resource_mapped_names_lead_the_pool_in_id_order() {
271        let element = Element::new("activity")
272            .with_android_namespace()
273            .with_attr(attr::EXPORTED, true)
274            .with_attr("name", "plain")
275            .with_attr(attr::NAME, "android.app.NativeActivity")
276            .with_attr(attr::LABEL, "Hello");
277        let (strings, ids) = pool_and_map(&Document::new(element).encode(&()).unwrap());
278        assert_eq!(ids, [attr::LABEL.id().0, attr::NAME.id().0, attr::EXPORTED.id().0]);
279        assert_eq!(strings[..3], ["label", "name", "exported"]);
280        // The plain `name` attribute gets its own, unmapped pool entry.
281        assert!(strings[3..].iter().any(|s| s == "name"));
282    }
283
284    #[test]
285    fn records_the_id_class_and_style_attributes() {
286        let style: ResourceRef = "@android:style/Theme.Material".parse().unwrap();
287        let element = Element::new("View")
288            .with_android_namespace()
289            .with_attr(attr::ID, ResourceRef::Id(ResourceId(0x7f08_0001)))
290            .with_attr(attr::LAYOUT_WIDTH, -1)
291            .with_attr("style", style)
292            .with_attr("class", "org.example.View");
293        let start = root_start(&Document::new(element).encode(&()).unwrap());
294        // Attributes are ordered by ID (id, then layout_width), then by name (class, style).
295        assert_eq!((start.id_index, start.class_index, start.style_index), (1, 3, 4));
296    }
297
298    #[test]
299    fn unknown_framework_attributes_are_rejected() {
300        let mut element = Element::new("application");
301        element.set_attr(
302            AttrName { namespace: Some(ANDROID_NAMESPACE.into()), name: "bogusAttribute".into(), id: None },
303            true,
304        );
305        assert!(matches!(
306            Document::new(element).encode(&()),
307            Err(XmlError::UnknownAttribute { name }) if name == "bogusAttribute"
308        ));
309    }
310
311    #[test]
312    fn values_must_fit_the_attribute_format() {
313        // `TypedArray.getBoolean` would read these as `false`, behind the
314        // back of every check that reads the manifest.
315        for value in [Value::String("false".into()), Value::Int(0)] {
316            let element = Element::new("application").with_attr(attr::HAS_CODE, value);
317            assert!(matches!(
318                Document::new(element).encode(&()),
319                Err(XmlError::MistypedValue { attribute: "hasCode", .. })
320            ));
321        }
322        let reference = Element::new("application").with_attr(attr::HAS_CODE, ResourceRef::Id(ResourceId(0x7f01_0000)));
323        assert!(Document::new(reference).encode(&()).is_ok());
324    }
325
326    #[test]
327    fn app_attributes_without_ids_are_rejected() {
328        for namespace in ["http://schemas.android.com/apk/res-auto", "http://schemas.android.com/apk/res/org.example"] {
329            let name = AttrName { namespace: Some(namespace.into()), name: "tint".into(), id: None };
330            let element = Element::new("ImageView").with_attr(name.clone(), "#fff");
331            assert!(matches!(Document::new(element).encode(&()), Err(XmlError::AppAttribute { .. })));
332            let compiled = AttrName { id: Some(ResourceId(0x7f01_0000)), ..name };
333            assert!(Document::new(Element::new("ImageView").with_attr(compiled, "#fff")).encode(&()).is_ok());
334        }
335    }
336
337    #[test]
338    fn attributes_with_the_same_id_are_one_attribute() {
339        let renamed =
340            AttrName { namespace: Some(ANDROID_NAMESPACE.into()), name: "a".into(), id: Some(attr::LABEL.id()) };
341        let element = Element::new("application").with_attr(renamed, "old").with_attr(attr::LABEL, "new");
342        assert_eq!(element.attributes.len(), 1);
343        assert_eq!(element.android_attr(attr::LABEL), Some(&Value::String("new".into())));
344    }
345
346    #[test]
347    fn unresolved_app_references_are_rejected() {
348        let element =
349            Element::new("application").with_attr(attr::LABEL, "@string/missing".parse::<ResourceRef>().unwrap());
350        assert!(matches!(Document::new(element).encode(&()), Err(XmlError::UnresolvedReference { .. })));
351    }
352
353    #[test]
354    fn depth_is_limited_without_recursion() {
355        let mut element = Element::new("leaf");
356        for _ in 0..MAX_DEPTH {
357            element = Element::new("node").with_child(element);
358        }
359        assert!(matches!(Document::new(element).encode(&()), Err(XmlError::TooDeep)));
360    }
361}