1use 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#[binrw]
20#[brw(little)]
21pub(super) struct NodeHeader {
22 pub line: u32,
23 pub comment: u32,
24}
25
26#[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 pub id_index: u16,
37 pub class_index: u16,
38 pub style_index: u16,
39}
40
41#[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 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 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
119struct Pool<'a> {
121 strings: StringPoolBuilder<'a>,
122 ids: Vec<ResourceId>,
124 attr_index: BTreeMap<ResourceId, u32>,
125}
126
127struct 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 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
214const RES_AUTO_NAMESPACE: &str = "http://schemas.android.com/apk/res-auto";
217const PACKAGE_NAMESPACE_PREFIX: &str = "http://schemas.android.com/apk/res/";
218
219fn 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 _ 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 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 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 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 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 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}