pyaxml_rs/
lib.rs

1#[cfg(feature = "python")]
2use pyo3::prelude::*;
3#[cfg(feature = "python")]
4use pyo3::types::PyBytes;
5
6pub mod arsc;
7pub mod axml;
8mod error;
9pub mod json_serde;
10#[allow(clippy::all, dead_code)]
11mod proto;
12mod proto_compute;
13mod proto_conv;
14mod public;
15mod resource_map;
16mod string_pool;
17mod typed_value;
18mod xml_element;
19
20/// Re-export the error type so downstream crates can name it.
21pub use error::AxmlError;
22
23#[cfg(feature = "python")]
24use arsc::Arsc;
25#[cfg(feature = "python")]
26use axml::Axml;
27
28/// Python-exposed AXML class.
29#[cfg(feature = "python")]
30#[pyclass(name = "AXML")]
31pub struct PyAxml {
32    inner: Axml,
33}
34
35#[cfg(feature = "python")]
36#[pymethods]
37impl PyAxml {
38    #[new]
39    pub fn new() -> Self {
40        PyAxml { inner: Axml::new() }
41    }
42
43    /// Parse binary AXML data. Returns an AXML instance.
44    #[staticmethod]
45    pub fn from_axml(data: &[u8]) -> PyResult<Self> {
46        let inner = Axml::from_axml(data)
47            .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
48        Ok(PyAxml { inner })
49    }
50
51    /// Serialize AXML to binary bytes.
52    pub fn pack<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
53        let data = self.inner.pack();
54        Ok(PyBytes::new(py, &data))
55    }
56
57    /// Convert binary AXML to an XML string.
58    pub fn to_xml(&self) -> PyResult<String> {
59        self.inner
60            .to_xml()
61            .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))
62    }
63
64    /// Serialize AXML to protobuf text format.
65    pub fn to_proto_text(&self) -> String {
66        self.inner.to_proto_text()
67    }
68
69    /// Serialize AXML to indented protobuf text format.
70    pub fn to_proto_text_pretty(&self) -> String {
71        self.inner.to_proto_text_pretty()
72    }
73
74    /// Recalculate chunk sizes on next `pack()`.  Call before `pack()` for a valid binary.
75    pub fn compute(&mut self) {
76        self.inner.compute();
77    }
78
79    /// Apply new string-pool bytes without updating the chunk total-size header.
80    ///
81    /// `new_strings` is a list of raw string-data bytes (as stored in
82    /// `StringBlock.data` in the proto: no length prefix, no null terminator).
83    /// `pack()` after this call returns an intentionally "corrupted" binary where
84    /// string content changed but the chunk-size field is stale.
85    /// Call `compute()` before `pack()` to get a correctly-sized binary instead.
86    pub fn apply_stringblocks_no_compute(&mut self, new_strings: Vec<Vec<u8>>) {
87        self.inner.apply_stringblocks_no_compute(new_strings);
88    }
89
90    /// Build AXML from an XML string.
91    pub fn from_xml(&mut self, xml: &str) -> PyResult<()> {
92        self.inner
93            .from_xml(xml)
94            .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))
95    }
96
97    /// Return the stored AXML total-file-size value (bytes 4..8 of the chunk header).
98    /// `pack()` writes this verbatim; `compute()` sets it to the correct value.
99    pub fn get_total_size(&self) -> u32 {
100        self.inner.total_size
101    }
102
103    /// Set the AXML total-file-size header field.
104    /// Use this to produce intentionally malformed output without calling `compute()`.
105    pub fn set_total_size(&mut self, size: u32) {
106        self.inner.total_size = size;
107    }
108
109    /// Number of strings in the string pool.
110    pub fn string_count(&self) -> usize {
111        self.inner.stringblocks.inner.strings.len()
112    }
113
114    /// Decode a string from the string pool by index.
115    pub fn get_string(&self, idx: u32) -> PyResult<String> {
116        self.inner
117            .stringblocks
118            .inner
119            .decode(idx)
120            .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))
121    }
122
123    /// Serialize AXML to binary protobuf bytes (proto encoding, not Android binary XML).
124    pub fn to_proto_bytes<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
125        Ok(PyBytes::new(py, &self.inner.to_proto_bytes()))
126    }
127
128    /// Deserialize AXML from binary protobuf bytes produced by `to_proto_bytes`.
129    #[staticmethod]
130    pub fn from_proto_bytes(data: &[u8]) -> PyResult<Self> {
131        let inner = Axml::from_proto_bytes(data)
132            .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
133        Ok(PyAxml { inner })
134    }
135
136    /// Write binary proto file (proto encoding, not Android binary XML).
137    pub fn export_proto_file(&self, path: &str) -> PyResult<()> {
138        self.inner
139            .export_proto_file(path)
140            .map_err(|e| pyo3::exceptions::PyIOError::new_err(e.to_string()))
141    }
142
143    /// Load AXML from a binary proto file written by `export_proto_file`.
144    #[staticmethod]
145    pub fn from_proto_file(path: &str) -> PyResult<Self> {
146        let inner = Axml::from_proto_file(path)
147            .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
148        Ok(PyAxml { inner })
149    }
150
151    /// Write the string pool as a binary proto file.
152    pub fn export_stringblocks_proto_file(&self, path: &str) -> PyResult<()> {
153        self.inner
154            .export_stringblocks_proto_file(path)
155            .map_err(|e| pyo3::exceptions::PyIOError::new_err(e.to_string()))
156    }
157
158    /// Replace the string pool from a binary proto file.
159    pub fn import_stringblocks_proto_file(&mut self, path: &str) -> PyResult<()> {
160        self.inner
161            .import_stringblocks_proto_file(path)
162            .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))
163    }
164
165    pub fn __repr__(&self) -> String {
166        format!(
167            "AXML(strings={}, elements={})",
168            self.inner.stringblocks.inner.strings.len(),
169            self.inner.elements.len()
170        )
171    }
172}
173
174#[cfg(feature = "python")]
175impl Default for PyAxml {
176    fn default() -> Self {
177        Self::new()
178    }
179}
180
181/// Python-exposed ARSC class.
182#[cfg(feature = "python")]
183#[pyclass(name = "ARSC")]
184pub struct PyArsc {
185    inner: Arsc,
186}
187
188#[cfg(feature = "python")]
189#[pymethods]
190impl PyArsc {
191    /// Parse a binary resources.arsc file (plain file or APK entry).
192    #[staticmethod]
193    pub fn from_axml(data: &[u8]) -> PyResult<Self> {
194        let inner = Arsc::from_axml(data)
195            .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
196        Ok(PyArsc { inner })
197    }
198
199    /// List resource entries grouped by locale.
200    /// Pass `language=None` for all locales, `language=Some("default")` for no-locale
201    /// entries, or a language tag like `Some("en")` or `Some("fr-FR")`.
202    #[pyo3(signature = (language=None))]
203    pub fn list_packages(&self, language: Option<&str>) -> String {
204        self.inner.list_packages(language)
205    }
206
207    /// Return the stored ARSC total-file-size header field (bytes 4..8).
208    /// `pack()` writes this verbatim; `compute()` recalculates it.
209    pub fn get_total_size(&self) -> u32 {
210        self.inner.total_size
211    }
212
213    /// Set the ARSC total-file-size header field.
214    pub fn set_total_size(&mut self, size: u32) {
215        self.inner.total_size = size;
216    }
217
218    /// Return the stored package-count header field (bytes 8..12).
219    /// `pack()` writes this verbatim; `compute()` sets it to the actual package count.
220    pub fn get_header_package_count(&self) -> u32 {
221        self.inner.package_count
222    }
223
224    /// Set the package-count header field.
225    pub fn set_header_package_count(&mut self, count: u32) {
226        self.inner.package_count = count;
227    }
228
229    /// Return the stored chunk-size header field for package at `idx` (bytes 4..8 of the package chunk).
230    /// Returns 0 if `idx` is out of range.
231    pub fn get_pkg_chunk_size(&self, idx: usize) -> u32 {
232        self.inner
233            .packages
234            .get(idx)
235            .map(|p| p.chunk_size)
236            .unwrap_or(0)
237    }
238
239    /// Set the chunk-size header field for package at `idx`.
240    /// No-op if `idx` is out of range.
241    pub fn set_pkg_chunk_size(&mut self, idx: usize, size: u32) {
242        if let Some(pkg) = self.inner.packages.get_mut(idx) {
243            pkg.chunk_size = size;
244        }
245    }
246
247    /// Number of packages in this ARSC file.
248    pub fn package_count(&self) -> usize {
249        self.inner.packages.len()
250    }
251
252    /// Recalculate string-pool chunk sizes on next `pack()`.
253    ///
254    /// `recursive=true` (default): recomputes global pool + all package type/key pools.
255    /// `recursive=false`: recomputes only the global string pool.
256    #[pyo3(signature = (recursive = true))]
257    pub fn compute(&mut self, recursive: bool) {
258        self.inner.compute(recursive);
259    }
260
261    /// Serialize ARSC back to binary `resources.arsc` format.
262    pub fn pack<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
263        Ok(PyBytes::new(py, &self.inner.pack()))
264    }
265
266    /// Add a new resource entry (type_name / name → file_path string value).
267    ///
268    /// Finds or creates `type_name` in the first package's type string pool,
269    /// adds `name` to the key string pool, adds `file_path` to the global
270    /// string pool, then appends TypeSpec + TypeType chunks for the default
271    /// locale.  Returns the new resource ID (0x7fTTEEEE).
272    pub fn add_resource(&mut self, type_name: &str, name: &str, file_path: &str) -> u32 {
273        self.inner.add_resource(type_name, name, file_path)
274    }
275
276    /// Serialize ARSC to protobuf text format.
277    pub fn to_proto_text(&self) -> String {
278        self.inner.to_proto_text()
279    }
280
281    /// Serialize ARSC to indented protobuf text format.
282    pub fn to_proto_text_pretty(&self) -> String {
283        self.inner.to_proto_text_pretty()
284    }
285
286    /// Serialize ARSC to binary protobuf bytes (proto encoding, not Android binary ARSC format).
287    pub fn to_proto_bytes<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyBytes>> {
288        Ok(PyBytes::new(py, &self.inner.to_proto_bytes()))
289    }
290
291    /// Deserialize ARSC from binary protobuf bytes produced by `to_proto_bytes`.
292    #[staticmethod]
293    pub fn from_proto_bytes(data: &[u8]) -> PyResult<Self> {
294        let inner = Arsc::from_proto_bytes(data)
295            .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
296        Ok(PyArsc { inner })
297    }
298
299    /// Write binary proto file.
300    pub fn export_proto_file(&self, path: &str) -> PyResult<()> {
301        self.inner
302            .export_proto_file(path)
303            .map_err(|e| pyo3::exceptions::PyIOError::new_err(e.to_string()))
304    }
305
306    /// Load ARSC from a binary proto file written by `export_proto_file`.
307    #[staticmethod]
308    pub fn from_proto_file(path: &str) -> PyResult<Self> {
309        let inner = Arsc::from_proto_file(path)
310            .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))?;
311        Ok(PyArsc { inner })
312    }
313
314    /// Write package `idx` as a binary proto file.
315    pub fn export_pkg_proto_file(&self, idx: usize, path: &str) -> PyResult<()> {
316        self.inner
317            .export_pkg_proto_file(idx, path)
318            .map_err(|e| pyo3::exceptions::PyIOError::new_err(e.to_string()))
319    }
320
321    /// Replace package `idx` from a binary proto file.
322    pub fn import_pkg_proto_file(&mut self, idx: usize, path: &str) -> PyResult<()> {
323        self.inner
324            .import_pkg_proto_file(idx, path)
325            .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))
326    }
327
328    pub fn __repr__(&self) -> String {
329        format!("ARSC(packages={})", self.inner.packages.len())
330    }
331}
332
333// ──────────────────────── compute bytes-in/bytes-out ─────────────────────────
334
335#[cfg(feature = "python")]
336#[pyfunction]
337fn compute_string_block_proto<'py>(
338    py: Python<'py>,
339    data: &[u8],
340    is_utf8: bool,
341) -> PyResult<Bound<'py, PyBytes>> {
342    proto_compute::compute_string_block_bytes(data, is_utf8)
343        .map(|b| PyBytes::new(py, &b))
344        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))
345}
346
347#[cfg(feature = "python")]
348#[pyfunction]
349fn compute_string_blocks_proto<'py>(
350    py: Python<'py>,
351    data: &[u8],
352    recursive: bool,
353) -> PyResult<Bound<'py, PyBytes>> {
354    proto_compute::compute_string_blocks_bytes(data, recursive)
355        .map(|b| PyBytes::new(py, &b))
356        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))
357}
358
359#[cfg(feature = "python")]
360#[pyfunction]
361fn compute_resource_map_proto<'py>(py: Python<'py>, data: &[u8]) -> PyResult<Bound<'py, PyBytes>> {
362    proto_compute::compute_resource_map_bytes(data)
363        .map(|b| PyBytes::new(py, &b))
364        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))
365}
366
367#[cfg(feature = "python")]
368#[pyfunction]
369fn compute_xml_element_proto<'py>(py: Python<'py>, data: &[u8]) -> PyResult<Bound<'py, PyBytes>> {
370    proto_compute::compute_xml_element_bytes(data)
371        .map(|b| PyBytes::new(py, &b))
372        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))
373}
374
375#[cfg(feature = "python")]
376#[pyfunction]
377fn compute_arsc_res_table_config_proto<'py>(
378    py: Python<'py>,
379    data: &[u8],
380) -> PyResult<Bound<'py, PyBytes>> {
381    proto_compute::compute_arsc_res_table_config_bytes(data)
382        .map(|b| PyBytes::new(py, &b))
383        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))
384}
385
386#[cfg(feature = "python")]
387#[pyfunction]
388fn compute_arsc_res_type_proto<'py>(
389    py: Python<'py>,
390    data: &[u8],
391    recursive: bool,
392) -> PyResult<Bound<'py, PyBytes>> {
393    proto_compute::compute_arsc_res_type_bytes(data, recursive)
394        .map(|b| PyBytes::new(py, &b))
395        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))
396}
397
398#[cfg(feature = "python")]
399#[pyfunction]
400fn compute_arsc_res_table_package_proto<'py>(
401    py: Python<'py>,
402    data: &[u8],
403    recursive: bool,
404) -> PyResult<Bound<'py, PyBytes>> {
405    proto_compute::compute_arsc_res_table_package_bytes(data, recursive)
406        .map(|b| PyBytes::new(py, &b))
407        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))
408}
409
410#[cfg(feature = "python")]
411#[pyfunction]
412fn compute_axml_proto_bytes<'py>(
413    py: Python<'py>,
414    data: &[u8],
415    recursive: bool,
416) -> PyResult<Bound<'py, PyBytes>> {
417    proto_compute::compute_axml_bytes(data, recursive)
418        .map(|b| PyBytes::new(py, &b))
419        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))
420}
421
422#[cfg(feature = "python")]
423#[pyfunction]
424fn compute_arsc_proto_bytes<'py>(
425    py: Python<'py>,
426    data: &[u8],
427    recursive: bool,
428) -> PyResult<Bound<'py, PyBytes>> {
429    proto_compute::compute_arsc_bytes(data, recursive)
430        .map(|b| PyBytes::new(py, &b))
431        .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string()))
432}
433
434/// The pyaxml_rs Python extension module.
435#[cfg(feature = "python")]
436#[pymodule]
437fn _pyaxml(m: &Bound<'_, PyModule>) -> PyResult<()> {
438    m.add_class::<PyAxml>()?;
439    m.add_class::<PyArsc>()?;
440    m.add_function(wrap_pyfunction!(compute_string_block_proto, m)?)?;
441    m.add_function(wrap_pyfunction!(compute_string_blocks_proto, m)?)?;
442    m.add_function(wrap_pyfunction!(compute_resource_map_proto, m)?)?;
443    m.add_function(wrap_pyfunction!(compute_xml_element_proto, m)?)?;
444    m.add_function(wrap_pyfunction!(compute_arsc_res_table_config_proto, m)?)?;
445    m.add_function(wrap_pyfunction!(compute_arsc_res_type_proto, m)?)?;
446    m.add_function(wrap_pyfunction!(compute_arsc_res_table_package_proto, m)?)?;
447    m.add_function(wrap_pyfunction!(compute_axml_proto_bytes, m)?)?;
448    m.add_function(wrap_pyfunction!(compute_arsc_proto_bytes, m)?)?;
449    Ok(())
450}