Skip to main content

coreshift_core/
dex.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Minimal ZIP + DEX parser for reading TRANSACTION_* static int field values
6//! from `framework.jar` without any subprocess or external tool.
7//!
8//! Only handles STORED (uncompressed) DEX entries. Android framework JARs
9//! store DEX uncompressed so ART can mmap directly from the ZIP.
10
11// ── Byte readers ──────────────────────────────────────────────────────────────
12
13fn u16le(b: &[u8], off: usize) -> Option<u16> {
14    let s = b.get(off..off + 2)?;
15    Some(u16::from_le_bytes([s[0], s[1]]))
16}
17
18fn u32le(b: &[u8], off: usize) -> Option<u32> {
19    let s = b.get(off..off + 4)?;
20    Some(u32::from_le_bytes([s[0], s[1], s[2], s[3]]))
21}
22
23fn uleb128(b: &[u8], off: &mut usize) -> Option<u32> {
24    let mut result = 0u32;
25    let mut shift = 0u32;
26    loop {
27        let byte = *b.get(*off)?;
28        *off += 1;
29        result |= ((byte & 0x7f) as u32) << shift;
30        if byte & 0x80 == 0 { return Some(result); }
31        shift += 7;
32        if shift >= 35 { return None; }
33    }
34}
35
36// ── ZIP ───────────────────────────────────────────────────────────────────────
37
38const ZIP_EOCD_SIG:   [u8; 4] = [0x50, 0x4b, 0x05, 0x06];
39const ZIP_CD_SIG:     [u8; 4] = [0x50, 0x4b, 0x01, 0x02];
40const ZIP_LOCAL_SIG:  [u8; 4] = [0x50, 0x4b, 0x03, 0x04];
41const ZIP_STORED: u16 = 0;
42
43struct ZipEntry {
44    local_off: usize,
45    comp_size: usize,
46}
47
48fn zip_find_dex_entries(data: &[u8]) -> Vec<ZipEntry> {
49    // Scan backwards for EOCD (ignore ZIP comment — Android JARs have none).
50    let scan_start = data.len().saturating_sub(65558);
51    let eocd_pos = match data[scan_start..]
52        .windows(4)
53        .rposition(|w| w == ZIP_EOCD_SIG)
54    {
55        Some(p) => scan_start + p,
56        None    => return Vec::new(),
57    };
58
59    let cd_size = match u32le(data, eocd_pos + 12) { Some(v) => v as usize, None => return Vec::new() };
60    let cd_off  = match u32le(data, eocd_pos + 16) { Some(v) => v as usize, None => return Vec::new() };
61
62    let mut pos = cd_off;
63    let cd_end  = cd_off.saturating_add(cd_size);
64    let mut entries = Vec::new();
65
66    while pos + 46 <= cd_end && pos + 46 <= data.len() {
67        if data.get(pos..pos + 4) != Some(&ZIP_CD_SIG) { break; }
68
69        let compression = match u16le(data, pos + 10) { Some(v) => v, None => break };
70        let comp_size   = match u32le(data, pos + 20) { Some(v) => v as usize, None => break };
71        let local_off   = match u32le(data, pos + 42) { Some(v) => v as usize, None => break };
72        let fname_len   = match u16le(data, pos + 28) { Some(v) => v as usize, None => break };
73        let extra_len   = match u16le(data, pos + 30) { Some(v) => v as usize, None => break };
74        let comment_len = match u16le(data, pos + 32) { Some(v) => v as usize, None => break };
75
76        let fname_end = pos + 46 + fname_len;
77        if fname_end > data.len() { break; }
78        let fname = &data[pos + 46..fname_end];
79
80        // Collect classes*.dex entries that are STORED.
81        let is_dex = fname.starts_with(b"classes") && fname.ends_with(b".dex");
82        if is_dex && compression == ZIP_STORED {
83            entries.push(ZipEntry {
84                local_off,
85                comp_size,
86            });
87        }
88
89        pos = match pos.checked_add(46 + fname_len + extra_len + comment_len) {
90            Some(v) => v,
91            None    => break,
92        };
93    }
94
95    // Sort by comp_size descending: the largest DEX is most likely to
96    // contain IActivityManager.
97    entries.sort_by_key(|e| std::cmp::Reverse(e.comp_size));
98    entries
99}
100
101fn zip_entry_data<'a>(data: &'a [u8], entry: &ZipEntry) -> Option<&'a [u8]> {
102    let lh = entry.local_off;
103    if data.get(lh..lh + 4) != Some(&ZIP_LOCAL_SIG) { return None; }
104    let fname_len = u16le(data, lh + 26)? as usize;
105    let extra_len = u16le(data, lh + 28)? as usize;
106    let data_start = lh + 30 + fname_len + extra_len;
107    data.get(data_start..data_start + entry.comp_size)
108}
109
110// ── DEX ───────────────────────────────────────────────────────────────────────
111
112const DEX_MAGIC: &[u8] = b"dex\n";
113
114fn dex_string(dex: &[u8], string_ids_off: usize, idx: usize) -> Option<&[u8]> {
115    let str_data_off = u32le(dex, string_ids_off + idx * 4)? as usize;
116    // Skip ULEB128 UTF-16 length
117    let mut off = str_data_off;
118    loop {
119        let b = *dex.get(off)?;
120        off += 1;
121        if b & 0x80 == 0 { break; }
122    }
123    // Null-terminated MUTF-8 bytes
124    let start = off;
125    while *dex.get(off)? != 0 { off += 1; }
126    dex.get(start..off)
127}
128
129fn skip_encoded_value(dex: &[u8], off: &mut usize) -> Option<()> {
130    let vbyte = *dex.get(*off)?;
131    *off += 1;
132    let vtype = vbyte & 0x1f;
133    let varg  = (vbyte >> 5) as usize;
134    match vtype {
135        // value_arg+1 bytes follow
136        0x00 | 0x02 | 0x03 | 0x04 | 0x06 |
137        0x10 | 0x11 | 0x15 | 0x16 | 0x17 |
138        0x18 | 0x19 | 0x1a | 0x1b => {
139            *off = off.checked_add(varg + 1)?;
140        }
141        0x1c => { // VALUE_ARRAY
142            let size = uleb128(dex, off)?;
143            for _ in 0..size { skip_encoded_value(dex, off)?; }
144        }
145        0x1d => { // VALUE_ANNOTATION
146            uleb128(dex, off)?; // type_idx
147            let size = uleb128(dex, off)?;
148            for _ in 0..size {
149                uleb128(dex, off)?; // name_idx
150                skip_encoded_value(dex, off)?;
151            }
152        }
153        0x1e | 0x1f => {} // VALUE_NULL / VALUE_BOOLEAN — no extra bytes
154        _ => return None,
155    }
156    Some(())
157}
158
159fn read_int_encoded_value(dex: &[u8], off: &mut usize) -> Option<u32> {
160    let vbyte = *dex.get(*off)?;
161    *off += 1;
162    let vtype = vbyte & 0x1f;
163    let varg  = (vbyte >> 5) as usize;
164    // VALUE_INT = 0x04, value_arg+1 bytes, little-endian
165    if vtype != 0x04 { return None; }
166    let size = varg + 1;
167    if size > 4 { return None; }
168    let mut val = 0u32;
169    for i in 0..size {
170        val |= (*dex.get(*off + i)? as u32) << (i * 8);
171    }
172    *off += size;
173    Some(val)
174}
175
176fn find_in_dex(dex: &[u8], class_desc: &[u8], field_name: &[u8]) -> Option<u32> {
177    if !dex.starts_with(DEX_MAGIC) || dex.len() < 112 { return None; }
178
179    let string_ids_size = u32le(dex, 56)? as usize;
180    let string_ids_off  = u32le(dex, 60)? as usize;
181    let type_ids_size   = u32le(dex, 64)? as usize;
182    let type_ids_off    = u32le(dex, 68)? as usize;
183    let field_ids_size  = u32le(dex, 80)? as usize;
184    let field_ids_off   = u32le(dex, 84)? as usize;
185    let class_defs_size = u32le(dex, 96)? as usize;
186    let class_defs_off  = u32le(dex, 100)? as usize;
187
188    // Find string index for class descriptor
189    let mut class_str_idx: Option<usize> = None;
190    for i in 0..string_ids_size {
191        if dex_string(dex, string_ids_off, i) == Some(class_desc) {
192            class_str_idx = Some(i);
193            break;
194        }
195    }
196    let class_str_idx = class_str_idx?;
197
198    // Find type index
199    let mut class_type_idx: Option<usize> = None;
200    for i in 0..type_ids_size {
201        if u32le(dex, type_ids_off + i * 4)? as usize == class_str_idx {
202            class_type_idx = Some(i);
203            break;
204        }
205    }
206    let class_type_idx = class_type_idx?;
207
208    // Find string index for field name
209    let mut field_str_idx: Option<u32> = None;
210    for i in 0..string_ids_size {
211        if dex_string(dex, string_ids_off, i) == Some(field_name) {
212            field_str_idx = Some(i as u32);
213            break;
214        }
215    }
216    let field_str_idx = field_str_idx?;
217
218    // Find global field_idx in field_ids
219    let mut target_field_idx: Option<u32> = None;
220    for i in 0..field_ids_size {
221        let foff = field_ids_off + i * 8;
222        let fclass = u16le(dex, foff)? as usize;
223        let fname  = u32le(dex, foff + 4)?;
224        if fclass == class_type_idx && fname == field_str_idx {
225            target_field_idx = Some(i as u32);
226            break;
227        }
228    }
229    let target_field_idx = target_field_idx?;
230
231    // Find class def
232    let mut class_data_off  = None;
233    let mut static_vals_off = None;
234    for i in 0..class_defs_size {
235        let coff = class_defs_off + i * 32;
236        if u32le(dex, coff)? as usize == class_type_idx {
237            class_data_off  = Some(u32le(dex, coff + 24)? as usize);
238            static_vals_off = Some(u32le(dex, coff + 28)? as usize);
239            break;
240        }
241    }
242    let class_data_off  = class_data_off?;
243    let static_vals_off = static_vals_off?;
244    if class_data_off == 0 || static_vals_off == 0 { return None; }
245
246    // Walk class_data_item static fields to find position of target_field_idx
247    let mut off = class_data_off;
248    let static_fields_size  = uleb128(dex, &mut off)?;
249    let _instance_fields    = uleb128(dex, &mut off)?;
250    let _direct_methods     = uleb128(dex, &mut off)?;
251    let _virtual_methods    = uleb128(dex, &mut off)?;
252
253    let mut field_pos: Option<usize> = None;
254    let mut cur_field_idx = 0u32;
255    for i in 0..static_fields_size as usize {
256        let diff         = uleb128(dex, &mut off)?;
257        let _access_flags = uleb128(dex, &mut off)?;
258        cur_field_idx += diff;
259        if cur_field_idx == target_field_idx {
260            field_pos = Some(i);
261            break;
262        }
263    }
264    let field_pos = field_pos?;
265
266    // Read encoded_array at static_vals_off, skip to field_pos, read int
267    let mut sv = static_vals_off;
268    let sv_size = uleb128(dex, &mut sv)? as usize;
269    if field_pos >= sv_size { return None; }
270
271    for i in 0..=field_pos {
272        if i == field_pos {
273            return read_int_encoded_value(dex, &mut sv);
274        }
275        skip_encoded_value(dex, &mut sv)?;
276    }
277    None
278}
279
280/// Return the declared parameter types of a DEX method's proto.
281///
282/// Looks up `class_desc.method_name` in the method_ids table and returns the
283/// type descriptor list from the referenced proto's type_list. Returns `None`
284/// when any section is unreadable, the class is absent, or the method does
285/// not exist (so callers can distinguish "method present" from "not found").
286///
287/// Needed DEX sections: string_ids (56/60), type_ids (64/68), proto_ids
288/// (72/76), method_ids (88/92).
289fn method_param_types<'a>(dex: &'a [u8], class_desc: &[u8], method_name: &[u8]) -> Option<Vec<&'a [u8]>> {
290    if !dex.starts_with(DEX_MAGIC) || dex.len() < 112 { return None; }
291
292    let string_ids_size = u32le(dex, 56)? as usize;
293    let string_ids_off  = u32le(dex, 60)? as usize;
294    let type_ids_size   = u32le(dex, 64)? as usize;
295    let type_ids_off    = u32le(dex, 68)? as usize;
296    let proto_ids_size  = u32le(dex, 72)? as usize;
297    let proto_ids_off   = u32le(dex, 76)? as usize;
298    let method_ids_size = u32le(dex, 88)? as usize;
299    let method_ids_off  = u32le(dex, 92)? as usize;
300
301    // String idx for the class descriptor
302    let mut class_str_idx: Option<usize> = None;
303    for i in 0..string_ids_size {
304        if dex_string(dex, string_ids_off, i) == Some(class_desc) {
305            class_str_idx = Some(i);
306            break;
307        }
308    }
309    let class_str_idx = class_str_idx?;
310
311    // Type idx for the class descriptor
312    let mut class_type_idx: Option<usize> = None;
313    for i in 0..type_ids_size {
314        if u32le(dex, type_ids_off + i * 4)? as usize == class_str_idx {
315            class_type_idx = Some(i);
316            break;
317        }
318    }
319    let class_type_idx = class_type_idx?;
320
321    // String idx for the method name
322    let mut method_str_idx: Option<u32> = None;
323    for i in 0..string_ids_size {
324        if dex_string(dex, string_ids_off, i) == Some(method_name) {
325            method_str_idx = Some(i as u32);
326            break;
327        }
328    }
329    let method_str_idx = method_str_idx?;
330
331    // Method idx → proto_idx, matching class and name
332    let mut proto_idx: Option<usize> = None;
333    for i in 0..method_ids_size {
334        let moff = method_ids_off + i * 8;
335        let fclass = u16le(dex, moff)? as usize;
336        let fproto = u16le(dex, moff + 2)? as usize;
337        let fname  = u32le(dex, moff + 4)?;
338        if fclass == class_type_idx && fname == method_str_idx {
339            proto_idx = Some(fproto);
340            break;
341        }
342    }
343    let proto_idx = proto_idx?;
344    if proto_idx >= proto_ids_size { return None; }
345
346    // proto_id_item: shorty_idx, return_type_idx, parameters_off
347    let poff = proto_ids_off + proto_idx * 12;
348    let params_off = u32le(dex, poff + 8)? as usize;
349    if params_off == 0 { return Some(Vec::new()); }
350
351    // type_list: size u32, then u16 type indices
352    let param_count = u32le(dex, params_off)? as usize;
353    let mut params = Vec::with_capacity(param_count);
354    for i in 0..param_count {
355        let tidx = u16le(dex, params_off + 4 + i * 2)? as usize;
356        let desc_str_idx = u32le(dex, type_ids_off + tidx * 4)? as usize;
357        let desc = dex_string(dex, string_ids_off, desc_str_idx)?;
358        params.push(desc);
359    }
360    Some(params)
361}
362
363// ── Public API ────────────────────────────────────────────────────────────────
364
365/// Search `framework.jar` for the value of a static int field.
366///
367/// `class_desc` uses DEX descriptor syntax, e.g.
368/// `"Landroid/app/IActivityManager$Stub;"`.
369///
370/// Returns `None` if the JAR is unreadable, the class/field is absent, or
371/// the entry is compressed (DEFLATE — not expected for framework DEX).
372pub fn find_transaction_code(jar_path: &str, class_desc: &str, field_name: &str) -> Option<u32> {
373    let data = std::fs::read(jar_path).ok()?;
374    let entries = zip_find_dex_entries(&data);
375    for entry in &entries {
376        if let Some(dex) = zip_entry_data(&data, entry) {
377            if let Some(code) = find_in_dex(dex, class_desc.as_bytes(), field_name.as_bytes()) {
378                return Some(code);
379            }
380        }
381    }
382    None
383}
384
385/// Resolve all four tx codes needed for binder observer mode.
386///
387/// Returns `(observer_code, query_code, api_mode, fg_code)` where:
388/// - `observer_code` = `TRANSACTION_registerProcessObserver`
389/// - `query_code`    = `TRANSACTION_getFocusedRootTaskInfo` (or StackInfo on API 29)
390/// - `api_mode`      = 1 (RootTaskInfo) or 2 (StackInfo)
391/// - `fg_code`       = `TRANSACTION_onForegroundActivitiesChanged`
392pub fn resolve_tx_codes_from_dex() -> Option<(u32, u32, u8, u32)> {
393    const JAR: &str = "/system/framework/framework.jar";
394    const AM_STUB:  &str = "Landroid/app/IActivityManager$Stub;";
395    const OBS_STUB: &str = "Landroid/app/IProcessObserver$Stub;";
396
397    let observer_code = find_transaction_code(JAR, AM_STUB, "TRANSACTION_registerProcessObserver")?;
398    let fg_code = find_transaction_code(JAR, OBS_STUB, "TRANSACTION_onForegroundActivitiesChanged")?;
399
400    if let Some(query_code) = find_transaction_code(JAR, AM_STUB, "TRANSACTION_getFocusedRootTaskInfo") {
401        return Some((observer_code, query_code, 1, fg_code));
402    }
403    // API 29 fallback
404    let query_code = find_transaction_code(JAR, AM_STUB, "TRANSACTION_getFocusedStackInfo")?;
405    Some((observer_code, query_code, 2, fg_code))
406}
407
408/// Resolve `IPowerManager.isInteractive()` transaction code from DEX.
409pub fn resolve_is_interactive_tx() -> Option<u32> {
410    const JAR: &str = "/system/framework/framework.jar";
411    find_transaction_code(JAR, "Landroid/os/IPowerManager$Stub;", "TRANSACTION_isInteractive")
412}
413
414/// Resolve the `IForegroundProcessObserver` registration tx codes from DEX.
415///
416/// Returns `(register_code, on_change_code)` where:
417/// - `register_code` = `TRANSACTION_registerForegroundProcessObserver`
418/// - `on_change_code` = `TRANSACTION_onForegroundProcessChanged`
419pub fn resolve_fgproc_codes() -> Option<(u32, u32)> {
420    const JAR: &str = "/system/framework/framework.jar";
421    const AM_STUB: &str = "Landroid/app/IActivityManager$Stub;";
422    const FGPROC_STUB: &str = "Landroid/app/IForegroundProcessObserver$Stub;";
423
424    let register_code =
425        find_transaction_code(JAR, AM_STUB, "TRANSACTION_registerForegroundProcessObserver")?;
426    let on_change_code =
427        find_transaction_code(JAR, FGPROC_STUB, "TRANSACTION_onForegroundProcessChanged")?;
428    Some((register_code, on_change_code))
429}
430
431/// Resolve the fallback `IProcessObserver` foreground observer tx codes used
432/// by ROMs that removed `IForegroundProcessObserver`.
433///
434/// Custom ROMs drop the stock `IForegroundProcessObserver` (no
435/// `registerForegroundProcessObserver` / `onForegroundProcessChanged`) but
436/// repurpose `IProcessObserver.onForegroundActivitiesChanged` to deliver the
437/// foreground **pid** (`(I,I,Z)V` — pid, uid, fg) instead of the stock
438/// package string (`(String,I,Z)V`). For those ROMs the daemon can register
439/// via the classic `registerProcessObserver` and read the pid straight out of
440/// the callback parcel.
441///
442/// Returns `None` when the stock [`resolve_fgproc_codes`] path succeeds
443/// (callers must prefer it), when `onForegroundActivitiesChanged` does not
444/// lead with an `int` parameter, or when either tx code cannot be resolved.
445pub fn resolve_fgproc_codes_fallback() -> Option<(u32, u32)> {
446    const JAR: &str = "/system/framework/framework.jar";
447    const AM_STUB:   &str = "Landroid/app/IActivityManager$Stub;";
448    const OBS_STUB:  &str = "Landroid/app/IProcessObserver$Stub;";
449    const OBS_IFACE: &str = "Landroid/app/IProcessObserver;";
450
451    // Stock IForegroundProcessObserver present → stock path is fine.
452    if resolve_fgproc_codes().is_some() {
453        return None;
454    }
455
456    let data = std::fs::read(JAR).ok()?;
457    let entries = zip_find_dex_entries(&data);
458
459    // onForegroundActivitiesChanged must declare an int first param (pid);
460    // stock String-first (pkg) form cannot serve this daemon.
461    let mut pid_first = false;
462    for entry in &entries {
463        if let Some(dex) = zip_entry_data(&data, entry) {
464            if let Some(params) = method_param_types(dex, OBS_IFACE.as_bytes(), b"onForegroundActivitiesChanged") {
465                pid_first = params.first().map(|t| *t == b"I").unwrap_or(false);
466                break;
467            }
468        }
469    }
470    if !pid_first { return None; }
471
472    let register_code = find_transaction_code(JAR, AM_STUB, "TRANSACTION_registerProcessObserver")?;
473    let on_change_code =
474        find_transaction_code(JAR, OBS_STUB, "TRANSACTION_onForegroundActivitiesChanged")?;
475    Some((register_code, on_change_code))
476}
477
478/// Resolve the `IWindowManager` task-FPS callback tx codes from DEX.
479///
480/// Returns `(register_code, unregister_code, on_fps_code)` where:
481/// - `register_code`   = `TRANSACTION_registerTaskFpsCallback`
482/// - `unregister_code` = `TRANSACTION_unregisterTaskFpsCallback`
483/// - `on_fps_code`     = `TRANSACTION_onFpsReported` on `ITaskFpsCallback`
484pub fn resolve_fps_codes() -> Option<(u32, u32, u32)> {
485    const JAR: &str = "/system/framework/framework.jar";
486    const IWM_STUB: &str = "Landroid/view/IWindowManager$Stub;";
487    const FPS_STUB: &str = "Landroid/window/ITaskFpsCallback$Stub;";
488
489    let register_code = find_transaction_code(JAR, IWM_STUB, "TRANSACTION_registerTaskFpsCallback")?;
490    let unregister_code =
491        find_transaction_code(JAR, IWM_STUB, "TRANSACTION_unregisterTaskFpsCallback")?;
492    let on_fps_code = find_transaction_code(JAR, FPS_STUB, "TRANSACTION_onFpsReported")?;
493    Some((register_code, unregister_code, on_fps_code))
494}
495
496/// Resolve the `IActivityTaskManager` task-stack listener tx codes from DEX.
497///
498/// Returns `(register_code, unregister_code)`:
499/// - `register_code`   = `TRANSACTION_registerTaskStackListener`
500/// - `unregister_code` = `TRANSACTION_unregisterTaskStackListener`
501///
502/// This target's ROM exposes the legacy `ITaskStackListener` /
503/// `registerTaskStackListener` pair on `IActivityTaskManager`; the newer
504/// `ITaskChangeListener` / `registerTaskChangeListener` interface is absent
505/// and must not be relied on.
506pub fn resolve_task_stack_codes() -> Option<(u32, u32)> {
507    const JAR: &str = "/system/framework/framework.jar";
508    const ATM_STUB: &str = "Landroid/app/IActivityTaskManager$Stub;";
509
510    let register_code =
511        find_transaction_code(JAR, ATM_STUB, "TRANSACTION_registerTaskStackListener")?;
512    let unregister_code =
513        find_transaction_code(JAR, ATM_STUB, "TRANSACTION_unregisterTaskStackListener")?;
514    Some((register_code, unregister_code))
515}
516
517#[cfg(test)]
518mod tests {
519    use super::*;
520
521    fn u32put(b: &mut [u8], off: usize, v: u32) {
522        b[off..off + 4].copy_from_slice(&v.to_le_bytes());
523    }
524
525    // Build a minimal DEX header + string/type/proto/method id tables for a
526    // single class with a single method whose proto has the given param type
527    // descriptors. Only the sections method_param_types() reads are populated.
528    fn build_dex(class_desc: &[u8], method_name: &[u8], params: &[&[u8]]) -> Vec<u8> {
529        let mut out = vec![0u8; 112];
530        out[0..4].copy_from_slice(DEX_MAGIC);
531        out[4..8].copy_from_slice(b"035\0");
532
533        let mut strings: Vec<Vec<u8>> = vec![class_desc.to_vec(), method_name.to_vec()];
534        for p in params {
535            if !strings.iter().any(|s| s == p) {
536                strings.push(p.to_vec());
537            }
538        }
539        let n_string = strings.len();
540        let mut types: Vec<Vec<u8>> = vec![class_desc.to_vec()];
541        for p in params {
542            if !types.iter().any(|t| t == p) {
543                types.push(p.to_vec());
544            }
545        }
546        let n_type = types.len();
547
548        let off_string_ids = 112;
549        let off_type_ids = off_string_ids + n_string * 4;
550        let off_proto_ids = off_type_ids + n_type * 4;
551        let off_method_ids = off_proto_ids + 12; // one proto
552
553        // Reserve id table space.
554        out.resize(off_method_ids + 8, 0); // one method
555
556        // string_data items (in file order, referenced by string_ids)
557        let mut string_offs = Vec::with_capacity(n_string);
558        for s in &strings {
559            string_offs.push(out.len());
560            let n = s.len() as u8;
561            out.push(n); // short ULEB128 length (ASCII only)
562            out.extend_from_slice(s);
563            out.push(0);
564        }
565
566        // type_list for the single proto's parameters.
567        let params_off = out.len();
568        out.extend_from_slice(&(params.len() as u32).to_le_bytes());
569        for p in params {
570            let ti = types.iter().position(|t| t == p).unwrap();
571            out.extend_from_slice(&(ti as u16).to_le_bytes());
572        }
573
574        // Header: counts + offsets (offsets 56..=100).
575        u32put(&mut out, 56, n_string as u32);
576        u32put(&mut out, 60, off_string_ids as u32);
577        u32put(&mut out, 64, n_type as u32);
578        u32put(&mut out, 68, off_type_ids as u32);
579        u32put(&mut out, 72, 1); // proto count
580        u32put(&mut out, 76, off_proto_ids as u32);
581        u32put(&mut out, 80, 0); // field_ids count
582        u32put(&mut out, 84, 0);
583        u32put(&mut out, 88, 1); // method count
584        u32put(&mut out, 92, off_method_ids as u32);
585        u32put(&mut out, 96, 0); // class_defs count
586        u32put(&mut out, 100, 0);
587
588        // string_ids
589        for (i, &so) in string_offs.iter().enumerate() {
590            u32put(&mut out, off_string_ids + i * 4, so as u32);
591        }
592        // type_ids → descriptor string index
593        for (i, t) in types.iter().enumerate() {
594            let si = strings.iter().position(|s| s == t).unwrap();
595            u32put(&mut out, off_type_ids + i * 4, si as u32);
596        }
597        // proto_ids: shorty, return, params_off
598        u32put(&mut out, off_proto_ids, 0);
599        u32put(&mut out, off_proto_ids + 4, 0);
600        u32put(&mut out, off_proto_ids + 8, params_off as u32);
601        // method_ids: class_idx, proto_idx, name_idx
602        let m = off_method_ids;
603        out[m..m + 2].copy_from_slice(&0u16.to_le_bytes());
604        out[m + 2..m + 4].copy_from_slice(&0u16.to_le_bytes());
605        u32put(&mut out, m + 4, 1);
606
607        out
608    }
609
610    #[test]
611    fn parses_int_first_proto() {
612        let dex = build_dex(b"Landroid/app/IProcessObserver;", b"onForegroundActivitiesChanged", &[b"I", b"I", b"Z"]);
613        let params = method_param_types(&dex, b"Landroid/app/IProcessObserver;", b"onForegroundActivitiesChanged")
614            .expect("method present");
615        assert_eq!(params, [&b"I"[..], &b"I"[..], &b"Z"[..]]);
616    }
617
618    #[test]
619    fn rejects_string_first_proto() {
620        // Stock signature onForegroundActivitiesChanged(String, int, boolean)
621        let dex = build_dex(b"Landroid/app/IProcessObserver;", b"onForegroundActivitiesChanged", &[b"Ljava/lang/String;", b"I", b"Z"]);
622        let params = method_param_types(&dex, b"Landroid/app/IProcessObserver;", b"onForegroundActivitiesChanged")
623            .expect("method present");
624        assert_eq!(params.first().map(|t| *t == b"I"), Some(false));
625    }
626
627    #[test]
628    fn absent_method_returns_none() {
629        let dex = build_dex(b"Landroid/app/IProcessObserver;", b"onForegroundActivitiesChanged", &[b"I"]);
630        assert!(method_param_types(&dex, b"Landroid/app/IProcessObserver;", b"onProcessDied").is_none());
631    }
632}