rust-apk 1.0.0

Build, align, sign, verify, and inspect Android APKs in pure Rust
Documentation
//! Decoding binary XML into a [`Document`].

use super::encode::{ATTRIBUTE_SIZE, AttributeRecord, ElementStart, NONE};
use super::{AttrName, Attribute, Document, Element, MAX_DEPTH, Namespace, Node, NodeKind, XmlError};
use crate::res::chunk::{Chunk, ChunkType, read_padded};
use crate::res::{ResourceId, StringPool, TextBudget, Value, data_type};

impl Document {
    /// Decodes a compiled binary XML file such as `AndroidManifest.xml`.
    ///
    /// Attribute values keep their compiled types; references hold resource
    /// IDs. Comments and line numbers are discarded.
    pub fn decode(bytes: &[u8]) -> Result<Self, XmlError> {
        let root = Chunk::parse_root(bytes, ChunkType::XML)?;
        let mut decoder = Decoder {
            strings: None,
            resource_map: &[],
            budget: TextBudget::for_input(bytes.len()),
            pending_namespaces: Vec::new(),
            open: Vec::new(),
            root: None,
            offset: 0,
        };
        for chunk in root.children() {
            decoder.chunk(&chunk?)?;
        }
        if !decoder.open.is_empty() {
            return Err(XmlError::Unclosed { count: decoder.open.len() });
        }
        decoder.root.map(Document::new).ok_or(XmlError::NoRoot)
    }
}

struct Decoder<'a> {
    strings: Option<StringPool<'a>>,
    /// The resource ID of each attribute name, by string index.
    resource_map: &'a [[u8; 4]],
    budget: TextBudget,
    pending_namespaces: Vec<Namespace>,
    open: Vec<Element>,
    root: Option<Element>,
    offset: usize,
}

/// The first `N` little-endian words of `body`.
fn words<const N: usize>(body: &[u8]) -> Option<[u32; N]> {
    let words: &[[u8; 4]; N] = body.as_chunks::<4>().0.get(..N)?.try_into().ok()?;
    Some(words.map(u32::from_le_bytes))
}

impl<'a> Decoder<'a> {
    fn string(&mut self, index: u32) -> Result<String, XmlError> {
        let strings = self.strings.as_ref().ok_or(XmlError::MissingStringPool)?;
        let text = strings.get(index).ok_or(XmlError::StringOutOfRange { index })?;
        Ok(self.budget.own(text)?)
    }

    fn optional_string(&mut self, index: u32) -> Result<Option<String>, XmlError> {
        (index != NONE).then(|| self.string(index)).transpose()
    }

    fn chunk(&mut self, chunk: &Chunk<'a>) -> Result<(), XmlError> {
        let offset = self.offset;
        self.offset += chunk.bytes.len();
        let truncated = |kind| XmlError::TruncatedNode { kind, offset };
        match chunk.ty {
            ChunkType::STRING_POOL if self.strings.is_none() => self.strings = Some(StringPool::parse(chunk)?),
            ChunkType::XML_RESOURCE_MAP => self.resource_map = chunk.body.as_chunks::<4>().0,
            ChunkType::XML_START_NAMESPACE => {
                let [prefix, uri] = words(chunk.body).ok_or(truncated(NodeKind::Namespace))?;
                let namespace = Namespace { prefix: self.optional_string(prefix)?, uri: self.string(uri)? };
                self.pending_namespaces.push(namespace);
            }
            ChunkType::XML_START_ELEMENT => {
                if self.open.len() >= MAX_DEPTH {
                    return Err(XmlError::TooDeep);
                }
                let element = self.start_element(chunk.body).ok_or(truncated(NodeKind::StartElement))??;
                self.open.push(element);
            }
            ChunkType::XML_END_ELEMENT => {
                let [namespace, name] = words(chunk.body).ok_or(truncated(NodeKind::EndElement))?;
                let found = self.string(name)?;
                let namespace = self.optional_string(namespace)?;
                let element = self.open.pop().ok_or_else(|| XmlError::UnexpectedEnd { found: found.clone() })?;
                if element.name != found {
                    return Err(XmlError::MismatchedEnd { expected: element.name, found });
                }
                if element.namespace != namespace {
                    return Err(XmlError::MismatchedNamespace { expected: element.namespace, found: namespace });
                }
                match self.open.last_mut() {
                    Some(parent) => parent.children.push(Node::Element(element)),
                    None if self.root.is_none() => self.root = Some(element),
                    None => return Err(XmlError::MultipleRoots),
                }
            }
            ChunkType::XML_CDATA => {
                let [data] = words(chunk.body).ok_or(truncated(NodeKind::Text))?;
                let text = self.string(data)?;
                if let Some(parent) = self.open.last_mut() {
                    parent.children.push(Node::Text(text));
                }
            }
            _ => {}
        }
        Ok(())
    }

    /// Returns `None` if the element chunk is truncated.
    fn start_element(&mut self, body: &[u8]) -> Option<Result<Element, XmlError>> {
        let start: ElementStart = read_padded(body.get(..20)?);
        let count = usize::from(start.attribute_count);
        if count != 0 && (start.attribute_size < ATTRIBUTE_SIZE || start.attribute_start < 20) {
            return None;
        }
        let stride = usize::from(start.attribute_size.max(ATTRIBUTE_SIZE));
        let records = body.get(usize::from(start.attribute_start)..)?;
        let records = records.get(..stride.checked_mul(count)?)?.chunks(stride);
        Some(self.element(&start, records.map(read_padded)))
    }

    fn element(
        &mut self,
        start: &ElementStart,
        records: impl ExactSizeIterator<Item = AttributeRecord>,
    ) -> Result<Element, XmlError> {
        let mut attributes = Vec::with_capacity(records.len());
        for record in records {
            let id = self.resource_map.get(record.name as usize).map(|&id| ResourceId(u32::from_le_bytes(id)));
            let name = AttrName {
                namespace: self.optional_string(record.namespace)?,
                name: self.string(record.name)?,
                id: id.filter(|id| id.0 != 0),
            };
            let raw = record.typed_value;
            let value = match raw.data_type == data_type::NULL && record.raw_value != NONE {
                // An attribute aapt did not type, such as `package`, is its raw string.
                true => Value::String(self.string(record.raw_value)?),
                false => Value::from_raw(raw, |index| self.string(index))?,
            };
            attributes.push(Attribute { name, value });
        }
        Ok(Element {
            name: self.string(start.name)?,
            namespace: self.optional_string(start.namespace)?,
            namespaces: std::mem::take(&mut self.pending_namespaces),
            attributes,
            children: Vec::new(),
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::android::attr;
    use crate::res::{Color, Dimension, DimensionUnit, ResourceRef};

    fn sample() -> Document {
        Document::new(
            Element::new("manifest")
                .with_android_namespace()
                .with_attr("package", "org.example")
                .with_attr(attr::VERSION_CODE, 7)
                .with_child(
                    Element::new("application")
                        .with_attr(attr::LABEL, "Hëllo 🦀")
                        .with_attr(attr::DEBUGGABLE, false)
                        .with_attr(attr::THEME, ResourceRef::Id(ResourceId(0x0103_0005)))
                        .with_child(Element::new("meta-data").with_attr(attr::VALUE, Color::rgb(0x336699)))
                        .with_child(
                            Element::new("layout")
                                .with_attr(attr::MIN_WIDTH, Dimension::new(4.0, DimensionUnit::Dp).unwrap()),
                        ),
                ),
        )
    }

    #[test]
    fn decodes_what_it_encodes() {
        let document = sample();
        assert_eq!(Document::decode(&document.encode(&()).unwrap()).unwrap(), document);
    }

    #[test]
    fn truncation_before_the_root_closes_always_fails() {
        let bytes = sample().encode(&()).unwrap();
        // The trailing 24-byte namespace-end chunk carries no content.
        const NAMESPACE_END: usize = 24;
        for len in 0..bytes.len() - NAMESPACE_END {
            let mut truncated = bytes[..len].to_vec();
            if len >= 8 {
                truncated[4..8].copy_from_slice(&(len as u32).to_le_bytes());
            }
            assert!(Document::decode(&truncated).is_err(), "truncation at {len} decoded");
        }
    }

    #[test]
    fn mismatched_closing_tags_are_rejected() {
        let mut bytes = Document::new(Element::new("a").with_child(Element::new("b"))).encode(&()).unwrap();
        // The document ends with `</b></a>`: two 24-byte chunks whose last field is the name.
        const END_CHUNK: usize = 24;
        let outer_name = bytes.len() - 4;
        let inner_name = outer_name - END_CHUNK;
        let a = bytes[outer_name..].to_vec();
        bytes[inner_name..inner_name + 4].copy_from_slice(&a);
        assert!(matches!(
            Document::decode(&bytes),
            Err(XmlError::MismatchedEnd { expected, found }) if expected == "b" && found == "a"
        ));
    }

    #[test]
    fn attributes_cannot_overlap_the_element_header_or_each_other() {
        let original = sample().encode(&()).unwrap();
        let root = Chunk::parse_root(&original, ChunkType::XML).unwrap();
        let element = root.children().map(Result::unwrap).find(|c| c.ty == ChunkType::XML_START_ELEMENT).unwrap();
        let body = element.body.as_ptr() as usize - original.as_ptr() as usize;
        for (field, value) in [(8, 0u16), (8, 19), (10, 0), (10, 19)] {
            let mut bytes = original.clone();
            bytes[body + field..body + field + 2].copy_from_slice(&value.to_le_bytes());
            assert!(matches!(Document::decode(&bytes), Err(XmlError::TruncatedNode { .. })), "{field}: {value}");
        }
    }

    #[test]
    fn closing_namespaces_must_match_even_when_local_names_match() {
        let original = sample().encode(&()).unwrap();
        let root = Chunk::parse_root(&original, ChunkType::XML).unwrap();
        let chunks: Vec<_> = root.children().map(Result::unwrap).collect();
        let namespace = chunks.iter().find(|c| c.ty == ChunkType::XML_START_NAMESPACE).unwrap();
        let uri = &namespace.body[4..8];
        let closing = chunks.iter().rev().find(|c| c.ty == ChunkType::XML_END_ELEMENT).unwrap();
        let at = closing.body.as_ptr() as usize - original.as_ptr() as usize;
        let mut bytes = original.clone();
        bytes[at..at + 4].copy_from_slice(uri);
        assert!(matches!(
            Document::decode(&bytes),
            Err(XmlError::MismatchedNamespace { expected: None, found: Some(found) })
                if found == crate::android::NAMESPACE
        ));
    }

    #[test]
    fn repeated_strings_cannot_amplify_a_document() {
        // 2,000 elements that all carry the same 8 KiB attribute value.
        let text = "x".repeat(8 << 10);
        let children = (0..2_000).map(|_| Element::new("e").with_attr("v", text.as_str()));
        let bytes = Document::new(Element::new("root").with_children(children)).encode(&()).unwrap();
        assert!(bytes.len() < 256 << 10, "the input stays small: {} bytes", bytes.len());
        assert!(matches!(
            Document::decode(&bytes),
            Err(XmlError::StringPool(crate::res::StringPoolError::TextBudgetExceeded { .. }))
        ));
    }
}