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