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 {
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>>,
resource_map: &'a [[u8; 4]],
budget: TextBudget,
pending_namespaces: Vec<Namespace>,
open: Vec<Element>,
root: Option<Element>,
offset: usize,
}
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(())
}
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 {
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();
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();
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() {
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 { .. }))
));
}
}