Skip to main content

asc_engine/
resources.rs

1//! `resources.arsc` access and binary XML decoding with resource names.
2
3use 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
11/// File identity used to notice that the APK changed under a cached table.
12pub(crate) type FileIdent = (u64, Option<SystemTime>);
13
14/// A parsed table (or its absence) and the APK version it was read from.
15pub(crate) struct CachedTable {
16    ident: Option<FileIdent>,
17    table: Option<Arc<ResTable>>,
18    /// Bytes of an inflated table, on the heap (a stored one stays in the
19    /// APK mapping).
20    inflated: usize,
21}
22
23impl CachedTable {
24    /// Heap the table holds, roughly: its index and inflated bytes.
25    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
34/// A stored (uncompressed) entry, borrowed from the APK mapping.
35struct 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/// One resource matching a [`Engine::search_resources`] query.
49#[derive(Debug, Clone, PartialEq)]
50#[non_exhaustive]
51pub struct ResourceRow {
52    /// The resource id (`0xPPTTEEEE`).
53    pub id: u32,
54    /// `type/name` (or `package:type/name` outside the app package).
55    pub name: String,
56    /// Qualifier string, empty for the default configuration.
57    pub config: String,
58    /// The value as [`ResTable::format_entry`] renders it.
59    pub value: String,
60}
61
62impl ResourceRow {
63    /// `0x7f0d0001 | string/app_name [zh-rCN] | "应用"` (no brackets for the
64    /// default configuration): the configuration names which value it is,
65    /// so it stays with the name and the columns stay three.
66    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    /// The row as a JSON object, the id as `0x` hex text.
75    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/// What [`Engine::search_resources`] matches against.
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82#[non_exhaustive]
83pub enum ResourceField {
84    /// `type/name` of every resource.
85    Name,
86    /// String values (simple strings, array and plurals items).
87    Value,
88    /// Either.
89    Any,
90}
91
92impl Engine {
93    /// The APK's resource table, parsed once per APK version and kept for
94    /// later calls. `None` when the APK has no `resources.arsc`.
95    ///
96    /// # Errors
97    ///
98    /// [`EngineError::Apk`] when the APK or the table cannot be read,
99    /// [`EngineError::Arsc`] when the table is malformed.
100    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                // Stored tables (the norm since API 30) stay in the mapping.
116                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    /// Forget the parsed resource table (and its name index); the next
143    /// resource query parses it again (about a millisecond even for a large app).
144    pub fn release_resources(&self) {
145        self.resources.lock_or_default().take();
146    }
147
148    /// Decode a binary XML file of the APK (`res/xml/...`, a layout, the
149    /// manifest), naming references and app attributes from the resource table.
150    ///
151    /// # Errors
152    ///
153    /// [`EngineError::Query`] when the APK has no entry `path`, the errors of
154    /// [`Engine::resources`], and [`EngineError::Axml`] when the entry is not
155    /// valid binary XML.
156    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    /// [`Engine::manifest_xml`] with `@7F...` references shown by name.
169    ///
170    /// # Errors
171    ///
172    /// As [`Engine::xml`].
173    pub fn manifest_xml_resolved(&self) -> Result<String> {
174        self.xml("AndroidManifest.xml")
175    }
176
177    /// Call `f` with every value of every resource, in id order (one row per
178    /// resource and configuration).
179    ///
180    /// # Errors
181    ///
182    /// [`EngineError::Query`] when the APK has no `resources.arsc`, and the errors of
183    /// [`Engine::resources`].
184    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    /// Every configuration's value of the resource `query`: an id
196    /// (`0x7f0d0001`) or a name (`string/app_name`, `@pkg:type/name`).
197    ///
198    /// # Errors
199    ///
200    /// [`EngineError::Query`] when the APK has no `resources.arsc` or no resource
201    /// `query`, and the errors of [`Engine::resources`].
202    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    /// Resolve the reference chain of `query` for a device described by
220    /// `config` (default: [`Config::any_device`]). Returns the rendered
221    /// chain (`@string/a -> @string/b`) and the final value.
222    ///
223    /// # Errors
224    ///
225    /// [`EngineError::Query`] when the APK has no `resources.arsc`, no resource
226    /// `query`, or a chain that cannot be resolved (no value for the
227    /// configuration, a dangling or cyclic reference), and the errors of
228    /// [`Engine::resources`].
229    ///
230    /// # Panics
231    ///
232    /// Never: a resolved chain always starts with the queried id.
233    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    /// Resources whose name and/or string value matches the regex `pattern`
248    /// (a plain substring when `literal`), one row per matching configuration
249    /// value (names: one row per configuration). At most `limit` rows; the
250    /// second value is the total number of matches.
251    ///
252    /// # Errors
253    ///
254    /// [`EngineError::Query`] for an invalid regex or an APK without
255    /// `resources.arsc`, and the errors of [`Engine::resources`].
256    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                    // Already listed by the name pass.
284                    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    /// Name the resource ids of `smali`: a `const` (or `.array-data` element)
302    /// whose literal is a resource of this APK gets `# @type/name` appended,
303    /// e.g. `const v0, 0x7f0b001c  # @layout/activity_main`. Unchanged when
304    /// the APK has no (readable) resource table: names are a convenience,
305    /// never a reason for the class itself to fail.
306    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
346/// `0x7f0d0001` / `7f0d0001` / `@0x7f0d0001`.
347pub(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
353/// The value of `v` as a plain string: string values unquoted, others as
354/// [`ResTable::format_value`] renders them.
355pub(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
362/// The simple value `id` resolves to, if it resolves to one.
363pub(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}