Skip to main content

apk_info_axml/
arsc.rs

1use std::cell::RefCell;
2use std::collections::HashMap;
3
4use log::warn;
5use winnow::combinator::repeat;
6use winnow::prelude::*;
7
8use crate::errors::ARCSError;
9use crate::structs::{
10    ResTableConfig, ResTableEntry, ResTableHeader, ResTablePackage, ResourceValueType, StringPool,
11};
12
13/// Represents an Android Resource Table (ARSC) file.
14///
15/// This struct holds the parsed global string pool and resource packages.
16/// It provides methods to query resources by ID or by name.
17#[derive(Debug)]
18pub struct ARSC {
19    global_string_pool: StringPool,
20    packages: HashMap<u8, ResTablePackage>,
21
22    /// Cache for resolved reference names to avoid repeated lookups.
23    reference_names: RefCell<HashMap<u32, String>>,
24}
25
26impl ARSC {
27    /// Parses raw ARSC bytes into an `ARSC` structure.
28    pub fn new(input: &mut &[u8]) -> Result<ARSC, ARCSError> {
29        if input.len() < 12 {
30            return Err(ARCSError::TooSmallError);
31        }
32
33        let header = ResTableHeader::parse(input).map_err(|_| ARCSError::HeaderError)?;
34
35        if header.package_count < 1 {
36            warn!(
37                "expected at least one resource package, but got {}",
38                header.package_count
39            );
40        }
41
42        let global_string_pool =
43            StringPool::parse(input).map_err(|_| ARCSError::StringPoolError)?;
44
45        let table_packages: Vec<ResTablePackage> =
46            repeat(header.package_count as usize, ResTablePackage::parse)
47                .parse_next(input)
48                .map_err(|_| ARCSError::ResourceTableError)?;
49
50        // There is often a single package, so we do a little optimization (i think)
51        let packages = match table_packages.len() {
52            0 => HashMap::new(),
53            1 => {
54                let pkg = table_packages
55                    .into_iter()
56                    .next()
57                    .expect("is rust broken? one element must be");
58                HashMap::from([((pkg.header.id & 0xff) as u8, pkg)])
59            }
60            _ => {
61                let mut packages = HashMap::with_capacity(table_packages.len());
62                for pkg in table_packages {
63                    let id = (pkg.header.id & 0xff) as u8;
64                    if packages.contains_key(&id) {
65                        warn!(
66                            "malformed resource packages, duplicate package id - 0x{:02x}, skipped",
67                            id
68                        );
69                        continue;
70                    }
71
72                    packages.insert(id, pkg);
73                }
74                packages
75            }
76        };
77
78        Ok(ARSC {
79            global_string_pool,
80            packages,
81            // preallocate some space
82            reference_names: RefCell::new(HashMap::with_capacity(32)),
83        })
84    }
85
86    /// Retrieves a resource value by its numeric ID.
87    ///
88    /// Recursively resolves references if the value is a reference type.
89    pub fn get_resource_value(&self, id: u32) -> Option<String> {
90        // TODO: need somehow option for dynamic config, not hardcoded
91        let config = ResTableConfig::default();
92
93        let (package_id, type_id, entry_id) = self.split_resource_id(id);
94
95        let entry = self
96            .packages
97            .get(&package_id)?
98            .find_entry(&config, type_id, entry_id)?;
99
100        match entry {
101            ResTableEntry::Default(e) => match e.value.data_type {
102                ResourceValueType::Reference => {
103                    // recursion protect?
104                    if e.value.data == id {
105                        return None;
106                    }
107
108                    self.get_resource_value(e.value.data)
109                }
110                _ => Some(e.value.to_string(&self.global_string_pool, Some(self))),
111            },
112            // if got nothing - gg
113            ResTableEntry::NoEntry => None,
114            e => {
115                warn!("for now don't how to handle this: {:#?}", e);
116                None
117            }
118        }
119    }
120
121    /// Retrieves a resource value by its resolved name.
122    pub fn get_resource_value_by_name(&self, name: &str) -> Option<String> {
123        let (&id, _) = self
124            .reference_names
125            .borrow()
126            .iter()
127            .find(|(_, v)| v == &name)?;
128
129        self.get_resource_value(id)
130    }
131
132    /// Returns the full resource name for a given resource ID.
133    ///
134    /// Uses a cache to speed up repeated lookups.
135    pub fn get_resource_name(&self, id: u32) -> Option<String> {
136        // fast path: if we've already have this name in cache
137        if let Some(name) = self.reference_names.borrow().get(&id) {
138            return Some(name.clone());
139        }
140
141        // split id into components
142        let (package_id, type_id, entry_id) = self.split_resource_id(id);
143
144        // lookup package
145        let package = self.packages.get(&package_id)?;
146
147        // default config
148        // TODO: need somehow option for dynamic config, not hardcoded
149        let config = ResTableConfig::default();
150
151        // search entry
152        let entry = package.find_entry(&config, type_id, entry_id)?;
153
154        // get full name
155        let name = package.get_entry_full_name(entry, type_id)?;
156
157        // save in cache
158        self.reference_names.borrow_mut().insert(id, name.clone());
159
160        Some(name)
161    }
162
163    /// Splits a 32-bit resource ID into its package ID, type ID, and entry ID.
164    #[inline(always)]
165    fn split_resource_id(&self, id: u32) -> (u8, u8, u16) {
166        (
167            (id >> 24) as u8,
168            ((id >> 16) & 0xff) as u8,
169            (id & 0xffff) as u16,
170        )
171    }
172}