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