1use std::borrow::Cow;
2
3use apk_info_xml::Element;
4use log::warn;
5use winnow::error::{ContextError, ErrMode};
6use winnow::prelude::*;
7use winnow::token::take;
8
9use crate::ARSC;
10use crate::errors::AXMLError;
11use crate::structs::{
12 ResChunkHeader, ResourceHeaderType, StringPool, XMLHeader, XMLResourceMap, XmlCData,
13 XmlEndElement, XmlNamespace, XmlParse, XmlStartElement, attrs_manifest,
14};
15
16pub const ANDROID_NAMESPACE: &str = "http://schemas.android.com/apk/res/android";
18
19#[derive(Debug)]
26pub struct AXML {
27 pub root: Element,
28}
29
30impl AXML {
31 pub fn new(input: &mut &[u8], arsc: Option<&ARSC>) -> Result<AXML, AXMLError> {
39 if input.len() < 8 {
41 return Err(AXMLError::TooSmallError);
42 }
43
44 let header = ResChunkHeader::parse(input).map_err(|_| AXMLError::HeaderError)?;
46
47 if header.header_size != 8 {
49 return Err(AXMLError::HeaderSizeError(header.header_size));
50 }
51
52 let string_pool = StringPool::parse(input).map_err(|_| AXMLError::StringPoolError)?;
54
55 let xml_resource = XMLResourceMap::parse(input).map_err(|_| AXMLError::ResourceMapError)?;
57
58 let root = Self::get_xml_tree(input, arsc, &string_pool, &xml_resource)
60 .ok_or(AXMLError::MissingRoot)?;
61
62 Ok(AXML { root })
63 }
64
65 fn get_xml_tree<'a>(
66 input: &mut &[u8],
67 arsc: Option<&ARSC>,
68 string_pool: &'a StringPool,
69 xml_resource: &'a XMLResourceMap,
70 ) -> Option<Element> {
71 let mut stack: Vec<Element> = Vec::with_capacity(16);
72
73 loop {
74 let chunk_header = match ResChunkHeader::parse(input) {
75 Ok(v) => v,
76 Err(ErrMode::Backtrack(_)) => break,
77 Err(_) => return None,
78 };
79
80 if chunk_header.type_ < ResourceHeaderType::XmlStartNamespace
82 || chunk_header.type_ > ResourceHeaderType::XmlLastChunk
83 {
84 warn!("not a xml resource chunk: {chunk_header:?}");
85
86 let _ =
87 take::<u32, &[u8], ContextError>(chunk_header.content_size()).parse_next(input);
88 continue;
89 }
90
91 if chunk_header.header_size != 0x10 {
93 warn!("xml resource chunk header size is not 0x10: {chunk_header:?}, skipped");
94
95 let _ =
96 take::<u32, &[u8], ContextError>(chunk_header.content_size()).parse_next(input);
97 continue;
98 }
99
100 let xml_header = match XMLHeader::parse(input, chunk_header) {
101 Ok(v) => v,
102 Err(_) => break,
103 };
104
105 match xml_header.header.type_ {
106 ResourceHeaderType::XmlStartNamespace => {
107 let _ = XmlNamespace::parse(input, xml_header);
108 }
109 ResourceHeaderType::XmlEndNamespace => {
110 let _ = XmlNamespace::parse(input, xml_header);
111 }
112 ResourceHeaderType::XmlStartElement => {
113 let node = match XmlStartElement::parse(input, xml_header) {
114 Ok(v) => v,
115 Err(_) => break,
116 };
117
118 let Some(name) = string_pool.get(node.name) else {
119 continue;
120 };
121
122 let mut element = Element::with_capacity(name, node.attributes.len());
123
124 if name == "manifest" {
125 element.set_attribute_with_prefix(
126 Some("xlmns"),
127 "android",
128 ANDROID_NAMESPACE,
129 );
130 }
131
132 for attribute in &node.attributes {
133 let Some(attribute_name) =
134 string_pool.get_with_resources(attribute.name, xml_resource, true)
135 else {
136 continue;
137 };
138
139 if attribute_name.contains(char::is_whitespace) {
141 warn!("skipped garbage attribute name: {:?}", attribute_name);
142 continue;
143 }
144
145 let ns_prefix = if string_pool
146 .get_with_resources(attribute.namespace_uri, xml_resource, false)
147 .is_some()
148 {
149 Some("android")
150 } else {
151 None
152 };
153
154 let value_str = attrs_manifest::get_attr_value(
155 attribute_name,
156 &attribute.typed_value.data,
157 )
158 .unwrap_or_else(|| {
159 Cow::Owned(attribute.typed_value.to_string(string_pool, arsc))
160 });
161
162 element.set_attribute_with_prefix(ns_prefix, attribute_name, &value_str);
163 }
164
165 stack.push(element);
166 }
167 ResourceHeaderType::XmlEndElement => {
168 let _ = XmlEndElement::parse(input, xml_header);
169
170 if stack.len() > 1 {
171 let finished = stack.pop().unwrap();
172 stack.last_mut().unwrap().append_child(finished);
173 }
174 }
175 ResourceHeaderType::XmlCdata => {
176 let _ = XmlCData::parse(input, xml_header);
177 }
178 _ => {
179 warn!("unknown header type: {:#?}", xml_header.header.type_);
180 }
181 }
182 }
183
184 (!stack.is_empty()).then(|| stack.remove(0))
185 }
186
187 #[inline]
196 pub fn get_xml_string(&self) -> String {
197 self.root.to_string()
198 }
199
200 pub fn get_attribute_value(
202 &self,
203 tag: &str,
204 name: &str,
205 arsc: Option<&ARSC>,
206 ) -> Option<String> {
207 let value = if self.root.name() == tag {
209 self.root.attr(name)
210 } else {
211 self.root
213 .descendants()
214 .find(|el| el.name() == tag)
215 .and_then(|el| el.attr(name))
216 };
217
218 match value {
219 Some(v) if v.starts_with('@') => {
221 if let Some(arsc) = arsc {
222 let name = &v[1..];
224 arsc.get_resource_value_by_name(name)
225 } else {
226 Some(v.to_string())
227 }
228 }
229 Some(v) => Some(v.to_string()),
231 None => None,
232 }
233 }
234
235 #[inline]
239 pub fn get_root_attribute_values<'a>(
240 &'a self,
241 tag: &'a str,
242 name: &'a str,
243 ) -> impl Iterator<Item = &'a str> + 'a {
244 self.root
245 .childrens()
246 .filter(move |el| el.name() == tag)
247 .flat_map(move |el| {
248 el.attributes()
249 .filter(move |attr| attr.name() == name)
250 .map(|attr| attr.value())
251 })
252 }
253
254 #[inline]
256 pub fn get_all_attribute_values<'a>(
257 &'a self,
258 tag: &'a str,
259 name: &'a str,
260 ) -> impl Iterator<Item = &'a str> + 'a {
261 self.root
262 .descendants()
263 .filter(move |el| el.name() == tag)
264 .flat_map(move |el| {
265 el.attributes()
266 .filter(move |attr| attr.name() == name)
267 .map(|attr| attr.value())
268 })
269 }
270
271 pub fn get_main_activities(&self) -> impl Iterator<Item = &str> {
279 self.root
280 .childrens()
281 .filter(|c| c.name() == "application")
282 .flat_map(|app| app.childrens())
283 .filter_map(|activity| {
284 let tag = activity.name();
286 if (tag != "activity" && tag != "activity-alias")
287 || activity.attr("enabled") == Some("false")
288 {
289 return None;
290 }
291
292 for intent_filter in activity.childrens() {
293 if intent_filter.name() != "intent-filter" {
294 continue;
295 }
296
297 let mut has_main = false;
298 let mut has_launcher = false;
299
300 for child in intent_filter.childrens() {
301 match (child.name(), child.attr("name")) {
302 ("action", Some("android.intent.action.MAIN")) => has_main = true,
303 ("category", Some("android.intent.category.LAUNCHER"))
304 | ("category", Some("android.intent.category.INFO")) => {
305 has_launcher = true
306 }
307 _ => {}
308 }
309 }
310
311 if has_main && has_launcher {
312 return activity.attr("name");
313 }
314 }
315
316 None
317 })
318 }
319}