1use crate::{Engine, EngineError, Result};
4use asc_apk::{Apk, Entry, METHOD_STORED};
5use asc_arsc::{Config, EntryValue, ResTable, Value};
6use std::borrow::Cow;
7use std::sync::Arc;
8use std::time::SystemTime;
9
10pub(crate) type FileIdent = (u64, Option<SystemTime>);
12
13pub(crate) struct CachedTable {
15 ident: Option<FileIdent>,
16 table: Option<Arc<ResTable>>,
17 inflated: usize,
20}
21
22impl CachedTable {
23 pub(crate) fn heap_bytes(&self) -> usize {
25 self.inflated + self.table.as_ref().map_or(0, |t| t.index_bytes())
26 }
27}
28
29pub(crate) fn file_ident(apk: &Apk) -> Option<FileIdent> {
30 apk.stamp().ok()
31}
32
33struct MappedEntry {
35 apk: Apk,
36 entry: Entry,
37 start: usize,
38 len: usize,
39}
40
41impl AsRef<[u8]> for MappedEntry {
42 fn as_ref(&self) -> &[u8] {
43 &self.apk.archive_data(&self.entry)[self.start..self.start + self.len]
44 }
45}
46
47#[derive(Debug, Clone, PartialEq)]
49pub struct ResourceRow {
50 pub id: u32,
51 pub name: String,
53 pub config: String,
55 pub value: String,
57}
58
59impl ResourceRow {
60 pub fn line(&self) -> String {
64 if self.config.is_empty() {
65 format!("0x{:08x} | {} | {}", self.id, self.name, self.value)
66 } else {
67 format!("0x{:08x} | {} [{}] | {}", self.id, self.name, self.config, self.value)
68 }
69 }
70
71 pub fn json(&self) -> serde_json::Value {
72 serde_json::json!({ "id": format!("0x{:08x}", self.id), "name": self.name, "config": self.config, "value": self.value })
73 }
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum ResourceField {
79 Name,
81 Value,
83 Any,
85}
86
87impl Engine {
88 pub fn resources(&self) -> Result<Option<Arc<ResTable>>> {
91 let apk = self.open()?;
92 let ident = file_ident(&apk);
93 if let Some(cached) = &*self.resources.lock().unwrap()
94 && ident.is_some()
95 && cached.ident == ident
96 {
97 return Ok(cached.table.clone());
98 }
99 let mut inflated = 0;
100 let t0 = std::time::Instant::now();
101 let table = match apk.find_entry("resources.arsc")? {
102 None => None,
103 Some(entry) => {
104 let bytes = apk.read_entry(&entry)?;
105 let table = match bytes {
107 Cow::Borrowed(b) if entry.method == METHOD_STORED => {
108 let (start, len) = (b.as_ptr() as usize - apk.archive_data(&entry).as_ptr() as usize, b.len());
109 ResTable::parse(MappedEntry { apk, entry: entry.clone(), start, len })?
110 }
111 b => {
112 inflated = b.len();
113 ResTable::parse(b.into_owned())?
114 }
115 };
116 if self.debug {
117 self.log(format!(
118 "[APK] resources.arsc {} bytes packages={} warnings={} parse={} us",
119 entry.uncomp_size,
120 table.packages().len(),
121 table.warnings().len(),
122 t0.elapsed().as_micros()
123 ));
124 }
125 Some(Arc::new(table))
126 }
127 };
128 *self.resources.lock().unwrap() = Some(CachedTable { ident, table: table.clone(), inflated });
129 Ok(table)
130 }
131
132 pub fn release_resources(&self) {
135 self.resources.lock().unwrap().take();
136 }
137
138 pub fn xml(&self, path: &str) -> Result<String> {
141 let data = match self.open()?.read(path) {
142 Ok(d) => d,
143 Err(asc_apk::ApkError::NotFound(_)) => {
144 return Err(EngineError::Query(format!("entry {path} not found{}", self.entry_not_found_hint(path))));
145 }
146 Err(e) => return Err(e.into()),
147 };
148 let table = self.resources()?;
149 Ok(asc_axml::decode_with(&data, table.as_deref().map(|t| t as &dyn asc_axml::Resolver))?)
150 }
151
152 pub fn manifest_xml_resolved(&self) -> Result<String> {
154 self.xml("AndroidManifest.xml")
155 }
156
157 pub fn dump_resources(&self, mut f: impl FnMut(ResourceRow)) -> Result<()> {
160 let table = self.resources()?.ok_or_else(|| EngineError::Query("APK has no resources.arsc".into()))?;
161 for id in table.ids() {
162 let name = table.ref_name(id);
163 for v in table.values(id) {
164 f(ResourceRow { id, name: name.clone(), config: v.config.to_string(), value: table.format_entry(id, &v.value) });
165 }
166 }
167 Ok(())
168 }
169
170 pub fn resource(&self, query: &str) -> Result<Vec<ResourceRow>> {
173 let table = self.resources()?.ok_or_else(|| EngineError::Query("APK has no resources.arsc".into()))?;
174 let id = parse_id(query)
175 .or_else(|| table.lookup(query))
176 .ok_or_else(|| EngineError::Query(format!("no resource {query}{}", self.resource_not_found_hint(&table, query))))?;
177 let name = table.ref_name(id);
178 let rows: Vec<ResourceRow> = table
179 .values(id)
180 .into_iter()
181 .map(|v| ResourceRow { id, name: name.clone(), config: v.config.to_string(), value: table.format_entry(id, &v.value) })
182 .collect();
183 if rows.is_empty() {
184 return Err(EngineError::Query(format!("no resource {query}")));
185 }
186 Ok(rows)
187 }
188
189 pub fn resolve_resource(&self, query: &str, config: Option<&Config>) -> Result<(Vec<String>, ResourceRow)> {
193 let table = self.resources()?.ok_or_else(|| EngineError::Query("APK has no resources.arsc".into()))?;
194 let id = parse_id(query)
195 .or_else(|| table.lookup(query))
196 .ok_or_else(|| EngineError::Query(format!("no resource {query}{}", self.resource_not_found_hint(&table, query))))?;
197 let r = table.resolve(id, config.unwrap_or(&Config::any_device())).map_err(|e| EngineError::Query(e.to_string()))?;
198 let last = *r.chain.last().expect("chain starts with id");
199 let chain = r.chain.iter().map(|&id| format!("@{}", table.ref_name(id))).collect();
200 Ok((
201 chain,
202 ResourceRow { id: last, name: table.ref_name(last), config: r.config.to_string(), value: table.format_entry(last, &r.value) },
203 ))
204 }
205
206 pub fn search_resources(&self, pattern: &str, literal: bool, field: ResourceField, limit: usize) -> Result<(Vec<ResourceRow>, usize)> {
211 let source = if literal { regex::escape(pattern) } else { pattern.to_string() };
212 let pattern = regex::Regex::new(&source).map_err(|e| EngineError::Query(format!("bad pattern: {e}")))?;
213 let table = self.resources()?.ok_or_else(|| EngineError::Query("APK has no resources.arsc".into()))?;
214 let mut rows = Vec::new();
215 let mut total = 0usize;
216 if field != ResourceField::Value {
217 for id in table.ids() {
218 let name = table.ref_name(id);
219 if pattern.is_match(&name) {
220 for v in table.values(id) {
221 total += 1;
222 if rows.len() < limit {
223 rows.push(ResourceRow {
224 id,
225 name: name.clone(),
226 config: v.config.to_string(),
227 value: table.format_entry(id, &v.value),
228 });
229 }
230 }
231 }
232 }
233 }
234 if field != ResourceField::Name {
235 table.for_each_string(|id, config, text| {
236 if pattern.is_match(text) {
237 if field == ResourceField::Any && pattern.is_match(&table.ref_name(id)) {
239 return;
240 }
241 total += 1;
242 if rows.len() < limit {
243 let mut value = String::new();
244 asc_arsc::quote(text, &mut value);
245 rows.push(ResourceRow { id, name: table.ref_name(id), config: config.to_string(), value });
246 }
247 }
248 });
249 }
250 Ok((rows, total))
251 }
252}
253
254impl Engine {
255 pub fn annotate_resource_ids(&self, smali: &str) -> String {
261 match self.resources() {
262 Ok(Some(table)) => annotate(&table, smali),
263 Ok(None) => smali.to_string(),
264 Err(e) => {
265 if self.debug {
266 self.log(format!("[APK] resources.arsc unreadable, constants left unnamed: {e}"));
267 }
268 smali.to_string()
269 }
270 }
271 }
272}
273
274fn annotate(table: &ResTable, smali: &str) -> String {
275 let mut out = String::with_capacity(smali.len() + smali.len() / 16);
276 for line in smali.split_inclusive('\n') {
277 let body = line.trim_end_matches('\n');
278 let t = body.trim_start();
279 let literal = if t.starts_with("const ") || t.starts_with("const/high16 ") {
280 t.rsplit(", ").next()
281 } else if t.starts_with("0x") && !t.contains(' ') {
282 Some(t)
283 } else {
284 None
285 };
286 let id = literal.and_then(|l| l.strip_prefix("0x")).filter(|h| h.len() == 8).and_then(|h| u32::from_str_radix(h, 16).ok());
287 match id.filter(|&id| id >> 24 != 0 && table.contains(id)) {
288 Some(id) => {
289 out.push_str(body);
290 out.push_str(" # @");
291 out.push_str(&table.ref_name(id));
292 out.push_str(&line[body.len()..]);
293 }
294 None => out.push_str(line),
295 }
296 }
297 out
298}
299
300pub fn parse_id(s: &str) -> Option<u32> {
302 let s = s.trim_start_matches('@');
303 let hex = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")).unwrap_or(s);
304 (hex.len() == 8).then(|| u32::from_str_radix(hex, 16).ok()).flatten()
305}
306
307pub(crate) fn plain_value(table: &ResTable, v: &Value) -> String {
310 if v.kind == asc_arsc::TYPE_STRING {
311 return table.string(v.data).map(|s| s.into_owned()).unwrap_or_default();
312 }
313 table.format_value(v)
314}
315
316pub(crate) fn resolve_simple(table: &ResTable, id: u32) -> Option<Value> {
318 match table.resolve_default(id).ok()?.value {
319 EntryValue::Simple(v) => Some(v),
320 EntryValue::Bag { .. } => None,
321 }
322}