1use crate::{Engine, EngineError, Result};
4use asc_apk::{Apk, Entry, METHOD_STORED};
5use asc_arsc::{Config, EntryValue, ResTable, Value};
6use asc_cache::LockExt;
7use std::borrow::Cow;
8use std::sync::Arc;
9use std::time::SystemTime;
10
11pub(crate) type FileIdent = (u64, Option<SystemTime>);
13
14pub(crate) struct CachedTable {
16 ident: Option<FileIdent>,
17 table: Option<Arc<ResTable>>,
18 inflated: usize,
21}
22
23impl CachedTable {
24 pub(crate) fn heap_bytes(&self) -> usize {
26 self.inflated + self.table.as_ref().map_or(0, |t| t.index_bytes())
27 }
28}
29
30pub(crate) fn file_ident(apk: &Apk) -> Option<FileIdent> {
31 apk.stamp().ok()
32}
33
34struct MappedEntry {
36 apk: Apk,
37 entry: Entry,
38 start: usize,
39 len: usize,
40}
41
42impl AsRef<[u8]> for MappedEntry {
43 fn as_ref(&self) -> &[u8] {
44 &self.apk.archive_data(&self.entry)[self.start..self.start + self.len]
45 }
46}
47
48#[derive(Debug, Clone, PartialEq)]
50#[non_exhaustive]
51pub struct ResourceRow {
52 pub id: u32,
54 pub name: String,
56 pub config: String,
58 pub value: String,
60}
61
62impl ResourceRow {
63 pub fn line(&self) -> String {
67 if self.config.is_empty() {
68 format!("0x{:08x} | {} | {}", self.id, self.name, self.value)
69 } else {
70 format!("0x{:08x} | {} [{}] | {}", self.id, self.name, self.config, self.value)
71 }
72 }
73
74 pub fn json(&self) -> serde_json::Value {
76 serde_json::json!({ "id": format!("0x{:08x}", self.id), "name": self.name, "config": self.config, "value": self.value })
77 }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82#[non_exhaustive]
83pub enum ResourceField {
84 Name,
86 Value,
88 Any,
90}
91
92impl Engine {
93 pub fn resources(&self) -> Result<Option<Arc<ResTable>>> {
101 let apk = self.open()?;
102 let ident = file_ident(&apk);
103 if let Some(cached) = &*self.resources.lock_or_default()
104 && ident.is_some()
105 && cached.ident == ident
106 {
107 return Ok(cached.table.clone());
108 }
109 let mut inflated = 0;
110 let t0 = std::time::Instant::now();
111 let table = match apk.find_entry("resources.arsc")? {
112 None => None,
113 Some(entry) => {
114 let bytes = apk.read_entry(&entry)?;
115 let table = match bytes {
117 Cow::Borrowed(b) if entry.method == METHOD_STORED => {
118 let (start, len) = (b.as_ptr() as usize - apk.archive_data(&entry).as_ptr() as usize, b.len());
119 ResTable::parse(MappedEntry { apk, entry: entry.clone(), start, len })?
120 }
121 b => {
122 inflated = b.len();
123 ResTable::parse(b.into_owned())?
124 }
125 };
126 if self.debug {
127 self.log(format!(
128 "[APK] resources.arsc {} bytes packages={} warnings={} parse={} us",
129 entry.uncomp_size,
130 table.packages().len(),
131 table.warnings().len(),
132 t0.elapsed().as_micros()
133 ));
134 }
135 Some(Arc::new(table))
136 }
137 };
138 *self.resources.lock_or_default() = Some(CachedTable { ident, table: table.clone(), inflated });
139 Ok(table)
140 }
141
142 pub fn release_resources(&self) {
145 self.resources.lock_or_default().take();
146 }
147
148 pub fn xml(&self, path: &str) -> Result<String> {
157 let data = match self.open()?.read(path) {
158 Ok(d) => d,
159 Err(asc_apk::ApkError::NotFound(_)) => {
160 return Err(EngineError::Query(format!("entry {path} not found{}", self.entry_not_found_hint(path))));
161 }
162 Err(e) => return Err(e.into()),
163 };
164 let table = self.resources()?;
165 Ok(asc_axml::decode_with(&data, table.as_deref().map(|t| t as &dyn asc_axml::Resolver))?)
166 }
167
168 pub fn manifest_xml_resolved(&self) -> Result<String> {
174 self.xml("AndroidManifest.xml")
175 }
176
177 pub fn dump_resources(&self, mut f: impl FnMut(ResourceRow)) -> Result<()> {
185 let table = self.resources()?.ok_or_else(|| EngineError::Query("APK has no resources.arsc".into()))?;
186 for id in table.ids() {
187 let name = table.ref_name(id);
188 for v in table.values(id) {
189 f(ResourceRow { id, name: name.clone(), config: v.config.to_string(), value: table.format_entry(id, &v.value) });
190 }
191 }
192 Ok(())
193 }
194
195 pub fn resource(&self, query: &str) -> Result<Vec<ResourceRow>> {
203 let table = self.resources()?.ok_or_else(|| EngineError::Query("APK has no resources.arsc".into()))?;
204 let id = parse_id(query)
205 .or_else(|| table.lookup(query))
206 .ok_or_else(|| EngineError::Query(format!("no resource {query}{}", self.resource_not_found_hint(&table, query))))?;
207 let name = table.ref_name(id);
208 let rows: Vec<ResourceRow> = table
209 .values(id)
210 .into_iter()
211 .map(|v| ResourceRow { id, name: name.clone(), config: v.config.to_string(), value: table.format_entry(id, &v.value) })
212 .collect();
213 if rows.is_empty() {
214 return Err(EngineError::Query(format!("no resource {query}")));
215 }
216 Ok(rows)
217 }
218
219 pub fn resolve_resource(&self, query: &str, config: Option<&Config>) -> Result<(Vec<String>, ResourceRow)> {
234 let table = self.resources()?.ok_or_else(|| EngineError::Query("APK has no resources.arsc".into()))?;
235 let id = parse_id(query)
236 .or_else(|| table.lookup(query))
237 .ok_or_else(|| EngineError::Query(format!("no resource {query}{}", self.resource_not_found_hint(&table, query))))?;
238 let r = table.resolve(id, config.unwrap_or(&Config::any_device())).map_err(|e| EngineError::Query(e.to_string()))?;
239 let last = *r.chain.last().expect("chain starts with id");
240 let chain = r.chain.iter().map(|&id| format!("@{}", table.ref_name(id))).collect();
241 Ok((
242 chain,
243 ResourceRow { id: last, name: table.ref_name(last), config: r.config.to_string(), value: table.format_entry(last, &r.value) },
244 ))
245 }
246
247 pub fn search_resources(&self, pattern: &str, literal: bool, field: ResourceField, limit: usize) -> Result<(Vec<ResourceRow>, usize)> {
257 let source = if literal { regex::escape(pattern) } else { pattern.to_string() };
258 let pattern = regex::Regex::new(&source).map_err(|e| EngineError::Query(format!("bad pattern: {e}")))?;
259 let table = self.resources()?.ok_or_else(|| EngineError::Query("APK has no resources.arsc".into()))?;
260 let mut rows = Vec::new();
261 let mut total = 0usize;
262 if field != ResourceField::Value {
263 for id in table.ids() {
264 let name = table.ref_name(id);
265 if pattern.is_match(&name) {
266 for v in table.values(id) {
267 total += 1;
268 if rows.len() < limit {
269 rows.push(ResourceRow {
270 id,
271 name: name.clone(),
272 config: v.config.to_string(),
273 value: table.format_entry(id, &v.value),
274 });
275 }
276 }
277 }
278 }
279 }
280 if field != ResourceField::Name {
281 table.for_each_string(|id, config, text| {
282 if pattern.is_match(text) {
283 if field == ResourceField::Any && pattern.is_match(&table.ref_name(id)) {
285 return;
286 }
287 total += 1;
288 if rows.len() < limit {
289 let mut value = String::new();
290 asc_query::text::quote_into(&mut value, text);
291 rows.push(ResourceRow { id, name: table.ref_name(id), config: config.to_string(), value });
292 }
293 }
294 });
295 }
296 Ok((rows, total))
297 }
298}
299
300impl Engine {
301 pub fn annotate_resource_ids(&self, smali: &str) -> String {
307 match self.resources() {
308 Ok(Some(table)) => annotate(&table, smali),
309 Ok(None) => smali.to_string(),
310 Err(e) => {
311 if self.debug {
312 self.log(format!("[APK] resources.arsc unreadable, constants left unnamed: {e}"));
313 }
314 smali.to_string()
315 }
316 }
317 }
318}
319
320fn annotate(table: &ResTable, smali: &str) -> String {
321 let mut out = String::with_capacity(smali.len() + smali.len() / 16);
322 for line in smali.split_inclusive('\n') {
323 let body = line.trim_end_matches('\n');
324 let t = body.trim_start();
325 let literal = if t.starts_with("const ") || t.starts_with("const/high16 ") {
326 t.rsplit(", ").next()
327 } else if t.starts_with("0x") && !t.contains(' ') {
328 Some(t)
329 } else {
330 None
331 };
332 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());
333 match id.filter(|&id| id >> 24 != 0 && table.contains(id)) {
334 Some(id) => {
335 out.push_str(body);
336 out.push_str(" # @");
337 out.push_str(&table.ref_name(id));
338 out.push_str(&line[body.len()..]);
339 }
340 None => out.push_str(line),
341 }
342 }
343 out
344}
345
346pub(crate) fn parse_id(s: &str) -> Option<u32> {
348 let s = s.trim_start_matches('@');
349 let hex = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")).unwrap_or(s);
350 (hex.len() == 8).then(|| u32::from_str_radix(hex, 16).ok()).flatten()
351}
352
353pub(crate) fn plain_value(table: &ResTable, v: &Value) -> String {
356 if v.kind == asc_arsc::TYPE_STRING {
357 return table.string(v.data).map(|s| s.into_owned()).unwrap_or_default();
358 }
359 table.format_value(v)
360}
361
362pub(crate) fn resolve_simple(table: &ResTable, id: u32) -> Option<Value> {
364 match table.resolve_default(id).ok()?.value {
365 EntryValue::Simple(v) => Some(v),
366 EntryValue::Bag { .. } => None,
367 }
368}