Skip to main content

ddc_dex/
file.rs

1//! DEX container: header, id tables, class defs.
2
3use std::collections::HashMap;
4
5use crate::annotations::{self, EncodedValue};
6use crate::code::CodeItem;
7use crate::reader::Cursor;
8
9/// Index sentinel for "absent".
10pub const NO_INDEX: u32 = 0xffff_ffff;
11
12#[derive(Debug, Clone)]
13pub struct ProtoId {
14    /// Index into string ids: the shorty descriptor.
15    pub shorty_idx: u32,
16    /// Index into type ids: return type.
17    pub return_type_idx: u32,
18    /// Offset of the parameter type list, or 0.
19    pub parameters_off: u32,
20}
21
22#[derive(Debug, Clone)]
23pub struct FieldId {
24    pub class_idx: u32,
25    pub type_idx: u32,
26    pub name_idx: u32,
27}
28
29#[derive(Debug, Clone)]
30pub struct MethodId {
31    pub class_idx: u32,
32    pub proto_idx: u32,
33    pub name_idx: u32,
34}
35
36/// One `class_def_item`.
37#[derive(Debug, Clone)]
38pub struct ClassDef {
39    /// Index into type ids.
40    pub class_idx: u32,
41    pub access_flags: u32,
42    pub superclass_idx: u32,
43    pub interfaces_off: u32,
44    pub source_file_idx: u32,
45    pub annotations_off: u32,
46    pub class_data_off: u32,
47    pub static_values_off: u32,
48}
49
50/// One `encoded_field`.
51#[derive(Debug, Clone)]
52pub struct EncodedField {
53    pub field_idx: u32,
54    pub access_flags: u32,
55}
56
57/// One `encoded_method`.
58#[derive(Debug, Clone)]
59pub struct EncodedMethod {
60    pub method_idx: u32,
61    pub access_flags: u32,
62    pub code_off: u32,
63}
64
65/// One `class_data_item`, fully expanded (index diffs applied).
66#[derive(Debug, Clone, Default)]
67pub struct ClassData {
68    pub static_fields: Vec<EncodedField>,
69    pub instance_fields: Vec<EncodedField>,
70    pub direct_methods: Vec<EncodedMethod>,
71    pub virtual_methods: Vec<EncodedMethod>,
72}
73
74/// A parsed DEX image.
75///
76/// Id tables and class defs are parsed eagerly; code items, class data and
77/// annotations are materialized on demand. All accessors degrade to
78/// sentinels/`None` on out-of-range indices instead of panicking.
79pub struct DexFile {
80    /// The raw image. `None` after `release_data`/`mark_released` —
81    /// retired images drop their inflated bytes (the full-decompile
82    /// pipeline retires a dex once every class in it has been emitted;
83    /// on weibo/lark that is ~360MB of images that used to stay resident
84    /// for the whole run).
85    data: std::sync::RwLock<Option<Vec<u8>>>,
86    /// Set by `mark_released` from a shared (&) reference — the actual
87    /// bytes drop immediately through the mutex.
88    released: std::sync::atomic::AtomicBool,
89    /// MUTF-8 string table as shared `Arc<str>` slices: pool-side
90    /// structures (PoolMethod names, resolved names) clone the Arc
91    /// instead of copying heap bytes per method — a dex string idx is
92    /// already the dedup key.
93    strings: Vec<std::sync::Arc<str>>,
94    /// Type ids: descriptor string indices.
95    types: Vec<u32>,
96    protos: Vec<ProtoId>,
97    /// Parameter type-id lists per proto, resolved once at parse: the
98    /// invoke decode path used to re-read each `type_list` from the
99    /// image on EVERY call (`read_type_list` hit the `raw()` mutex per
100    /// u16 — a full lock cycle per parameter, per invoke, per method).
101    proto_tys: Vec<Box<[u32]>>,
102    /// Method-descriptor string per proto (`(ILjava/lang/String;)V`),
103    /// formatted once at parse: pool-side PoolMethod descs clone the Arc
104    /// instead of re-joining per method (a dex's proto table is the
105    /// descriptor dedup layer — methods share protos heavily).
106    proto_descs: Vec<std::sync::Arc<str>>,
107    fields: Vec<FieldId>,
108    methods: Vec<MethodId>,
109    pub class_defs: Vec<ClassDef>,
110    pub version: String,
111    /// class_idx → index into class_defs.
112    by_type: HashMap<u32, usize>,
113    /// `method_handle_item`s (DEX 037+): (kind, field/method id).
114    method_handles: Vec<MethodHandleItem>,
115    /// `call_site_id_item`s (DEX 037+), resolved.
116    call_sites: Vec<CallSiteInfo>,
117}
118
119/// One `method_handle_item`.
120#[derive(Debug, Clone)]
121pub struct MethodHandleItem {
122    /// 0-3: field static-put/static-get/instance-put/instance-get;
123    /// 4: invoke-static; 5: invoke-instance; 6: invoke-constructor;
124    /// 7: invoke-direct; 8: invoke-interface.
125    pub kind: u16,
126    /// Field or method id (widened from u2).
127    pub target_id: u32,
128    pub is_field: bool,
129}
130
131/// A resolved `call_site_id_item`: the `call_site_off` encoded array.
132#[derive(Debug, Clone)]
133pub struct CallSiteInfo {
134    /// Bootstrap method handle index.
135    pub bootstrap_handle: u32,
136    /// Call-site method name (string index).
137    pub name_idx: u32,
138    /// Call-site method type (proto index).
139    pub proto_idx: u32,
140    /// Linker arguments (raw encoded values).
141    pub linker_args: Vec<crate::annotations::EncodedValue>,
142}
143
144impl Drop for DexFile {
145    fn drop(&mut self) {
146        *self.data.write().unwrap() = None;
147    }
148}
149
150impl DexFile {
151    pub fn parse(data: Vec<u8>) -> Result<DexFile, String> {
152        if data.len() < crate::DEX_HEADER_SIZE {
153            return Err("file too small for DEX header".into());
154        }
155        let magic = &data[..8];
156        if &magic[..4] != b"dex\n" || magic[7] != 0 {
157            return Err(format!("not a DEX file: magic {:02x?}", magic));
158        }
159        let version = String::from_utf8_lossy(&magic[4..7]).into_owned();
160        // 036 was an unofficial odex-era marker; ART accepts 035, 037-041.
161        if !matches!(
162            version.as_str(),
163            "035" | "037" | "038" | "039" | "040" | "041"
164        ) {
165            return Err(format!("unsupported DEX version {}", version));
166        }
167        let endian = u32::from_le_bytes(data[40..44].try_into().unwrap());
168        if endian != 0x1234_5678 {
169            return Err(format!("unexpected endian tag {endian:#x}"));
170        }
171
172        let u32at = |off: usize| -> u32 {
173            if off + 4 > data.len() {
174                return 0;
175            }
176            u32::from_le_bytes(data[off..off + 4].try_into().unwrap())
177        };
178        let string_ids_size = u32at(0x38) as usize;
179        let string_ids_off = u32at(0x3c) as usize;
180        let type_ids_size = u32at(0x40) as usize;
181        let type_ids_off = u32at(0x44) as usize;
182        let proto_ids_size = u32at(0x48) as usize;
183        let proto_ids_off = u32at(0x4c) as usize;
184        let field_ids_size = u32at(0x50) as usize;
185        let field_ids_off = u32at(0x54) as usize;
186        let method_ids_size = u32at(0x58) as usize;
187        let method_ids_off = u32at(0x5c) as usize;
188        let class_defs_size = u32at(0x60) as usize;
189        let class_defs_off = u32at(0x64) as usize;
190
191        // Strings: MUTF-8 with uleb length.
192        let mut strings = Vec::with_capacity(string_ids_size);
193        if string_ids_off + 4 * string_ids_size <= data.len() {
194            for i in 0..string_ids_size {
195                let so = u32at(string_ids_off + 4 * i) as usize;
196                let mut c = Cursor::at(&data, so);
197                let s = c
198                    .read_uleb128()
199                    .and_then(|len| c.read_mutf8(c.pos, len))
200                    .unwrap_or_default();
201                strings.push(std::sync::Arc::from(s));
202            }
203        }
204
205        // Type ids: u4 descriptor string indices.
206        let mut types = Vec::with_capacity(type_ids_size);
207        if type_ids_off + 4 * type_ids_size <= data.len() {
208            for i in 0..type_ids_size {
209                types.push(u32at(type_ids_off + 4 * i));
210            }
211        }
212
213        // Proto ids: 12 bytes each.
214        let mut protos = Vec::with_capacity(proto_ids_size);
215        if proto_ids_off + 12 * proto_ids_size <= data.len() {
216            for i in 0..proto_ids_size {
217                let base = proto_ids_off + 12 * i;
218                protos.push(ProtoId {
219                    shorty_idx: u32at(base),
220                    return_type_idx: {
221                        let v = u16::from_le_bytes(data[base + 4..base + 6].try_into().unwrap());
222                        v as u32
223                    },
224                    parameters_off: u32at(base + 8),
225                });
226            }
227        }
228        // Resolve every proto's parameter list once, straight off the
229        // local image slice (no `raw()` mutex on this path at all).
230        let proto_tys: Vec<Box<[u32]>> = protos
231            .iter()
232            .map(|p| read_type_list_at(&data, p.parameters_off as usize))
233            .collect();
234        // Descriptor per proto, straight off the local tables.
235        let ty_name = |t: u32| -> &str {
236            match types.get(t as usize) {
237                Some(&si) => strings.get(si as usize).map(|s| &**s).unwrap_or(""),
238                None => "",
239            }
240        };
241        let proto_descs: Vec<std::sync::Arc<str>> = protos
242            .iter()
243            .zip(&proto_tys)
244            .map(|(p, tys)| {
245                let ret = ty_name(p.return_type_idx);
246                let mut d = String::with_capacity(2 + ret.len() + tys.len() * 12);
247                d.push('(');
248                for &t in tys.iter() {
249                    d.push_str(ty_name(t));
250                }
251                d.push(')');
252                d.push_str(ret);
253                std::sync::Arc::from(d)
254            })
255            .collect();
256
257        // Field ids: 8 bytes each.
258        let mut fields = Vec::with_capacity(field_ids_size);
259        if field_ids_off + 8 * field_ids_size <= data.len() {
260            for i in 0..field_ids_size {
261                let base = field_ids_off + 8 * i;
262                fields.push(FieldId {
263                    class_idx: u16::from_le_bytes(data[base..base + 2].try_into().unwrap()) as u32,
264                    type_idx: u16::from_le_bytes(data[base + 2..base + 4].try_into().unwrap())
265                        as u32,
266                    name_idx: u32at(base + 4),
267                });
268            }
269        }
270
271        // Method ids: 8 bytes each.
272        let mut methods = Vec::with_capacity(method_ids_size);
273        if method_ids_off + 8 * method_ids_size <= data.len() {
274            for i in 0..method_ids_size {
275                let base = method_ids_off + 8 * i;
276                methods.push(MethodId {
277                    class_idx: u16::from_le_bytes(data[base..base + 2].try_into().unwrap()) as u32,
278                    proto_idx: u16::from_le_bytes(data[base + 2..base + 4].try_into().unwrap())
279                        as u32,
280                    name_idx: u32at(base + 4),
281                });
282            }
283        }
284
285        // Class defs: 32 bytes each.
286        let mut class_defs = Vec::with_capacity(class_defs_size);
287        if class_defs_off + 32 * class_defs_size <= data.len() {
288            for i in 0..class_defs_size {
289                let base = class_defs_off + 32 * i;
290                class_defs.push(ClassDef {
291                    class_idx: u32at(base),
292                    access_flags: u32at(base + 4),
293                    superclass_idx: u32at(base + 8),
294                    interfaces_off: u32at(base + 12),
295                    source_file_idx: u32at(base + 16),
296                    annotations_off: u32at(base + 20),
297                    class_data_off: u32at(base + 24),
298                    static_values_off: u32at(base + 28),
299                });
300            }
301        }
302
303        let mut by_type = HashMap::with_capacity(class_defs.len());
304        for (i, cd) in class_defs.iter().enumerate() {
305            by_type.insert(cd.class_idx, i);
306        }
307
308        // DEX 037+ tables (call sites / method handles) live ONLY in the
309        // map list — the header carries no offsets for them.
310        let (method_handles, call_sites) = parse_map_tables(&data);
311
312        Ok(DexFile {
313            data: std::sync::RwLock::new(Some(data)),
314            released: std::sync::atomic::AtomicBool::new(false),
315            strings,
316            types,
317            protos,
318            proto_tys,
319            proto_descs,
320            fields,
321            methods,
322            class_defs,
323            version,
324            by_type,
325            method_handles,
326            call_sites,
327        })
328    }
329
330    /// The raw image, or empty once retired.
331    ///
332    /// SAFETY of the lifetime: the bytes live in an `Arc<Vec<u8>>` whose
333    /// clone is moved into a self-owned stash (`raw_keep`) that outlives
334    /// every later call — each call swaps the stash, keeping the previous
335    /// slice alive only for the previous caller, which is unsound in
336    /// general. The callers here hold the slice only inside one method
337    /// (`code_at` / parse-time table walks) and never across calls, and
338    /// single-call scope is the documented contract; the alternative
339    /// (returning a guard) would infect every parser signature.
340    pub fn raw(&self) -> &[u8] {
341        // RwLock read: `raw()` ran once PER CLASS through an exclusive
342        // mutex while 18 materialize threads hammered the same per-image
343        // lock (3.9% of weixin's CPU in mutexwait/mutexdrop). Readers
344        // never exclude each other; only retirement takes the write lock.
345        let guard = self.data.read().unwrap();
346        match guard.as_ref() {
347            Some(bytes) => unsafe { std::mem::transmute::<&[u8], &[u8]>(bytes.as_slice()) },
348            None => &[],
349        }
350    }
351
352    /// Drop the inflated image (tables stay usable; code accessors return
353    /// empty). Call only when no further code decoding will happen.
354    pub fn release_data(&mut self) {
355        *self.data.write().unwrap() = None;
356        self.released
357            .store(true, std::sync::atomic::Ordering::Release);
358    }
359
360    /// Mark the image retired through a shared reference: code accessors
361    /// go empty immediately; the bytes themselves drop when the last Arc
362    /// snapshot drops (workers hold snapshots only mid-class, so the
363    /// memory is reclaimed as chunks complete).
364    pub fn mark_released(&self) {
365        self.released
366            .store(true, std::sync::atomic::Ordering::Release);
367        // Free the bytes NOW (through the write lock, &self-safe).
368        *self.data.write().unwrap() = None;
369    }
370
371    #[inline]
372    #[allow(dead_code)] // debug hook for retirement postmortems
373    fn is_released(&self) -> bool {
374        self.released.load(std::sync::atomic::Ordering::Acquire)
375    }
376
377    pub fn string(&self, idx: u32) -> &str {
378        self.strings
379            .get(idx as usize)
380            .map(|s| &**s)
381            .unwrap_or("")
382    }
383
384    pub fn string_count(&self) -> usize {
385        self.strings.len()
386    }
387
388    /// Type id → CLASS internal name (`java/lang/String`), descriptor
389    /// stripped; array descriptors pass through unchanged.
390    pub fn class_name(&self, idx: u32) -> String {
391        let d = self.type_name(idx);
392        if d.len() > 2 && d.starts_with('L') && d.ends_with(';') {
393            d[1..d.len() - 1].to_string()
394        } else {
395            d.to_string()
396        }
397    }
398
399    /// Type id → descriptor string (`Ljava/lang/String;`, `[I`, ...).
400    /// Number of entries in the type-id table (bounds for `type_name`).
401    pub fn num_types(&self) -> usize {
402        self.types.len()
403    }
404
405    pub fn type_name(&self, idx: u32) -> &str {
406        match self.types.get(idx as usize) {
407            Some(&si) => self.string(si),
408            None => "",
409        }
410    }
411
412    /// Type descriptor as a shared clone (the pool's field descriptors —
413    /// the string table IS the dedup layer).
414    pub fn type_name_arc(&self, idx: u32) -> std::sync::Arc<str> {
415        match self.types.get(idx as usize) {
416            Some(&si) => self.string_arc(si),
417            None => std::sync::Arc::from(""),
418        }
419    }
420
421    pub fn type_count(&self) -> usize {
422        self.types.len()
423    }
424
425    pub fn proto_count(&self) -> usize {
426        self.protos.len()
427    }
428
429    pub fn proto(&self, idx: u32) -> &ProtoId {
430        static EMPTY: std::sync::OnceLock<ProtoId> = std::sync::OnceLock::new();
431        self.protos.get(idx as usize).unwrap_or_else(|| {
432            EMPTY.get_or_init(|| ProtoId {
433                shorty_idx: 0,
434                return_type_idx: 0,
435                parameters_off: 0,
436            })
437        })
438    }
439
440    /// Parameter type ids of a proto (empty for `(V)`).
441    /// Shared clone of a table string (the pool's method names).
442    pub fn string_arc(&self, idx: u32) -> std::sync::Arc<str> {
443        self.strings
444            .get(idx as usize)
445            .cloned()
446            .unwrap_or_else(|| std::sync::Arc::from(""))
447    }
448
449    /// Method descriptor of a proto as a shared clone.
450    pub fn proto_desc(&self, idx: u32) -> std::sync::Arc<str> {
451        self.proto_descs
452            .get(idx as usize)
453            .cloned()
454            .unwrap_or_else(|| std::sync::Arc::from("()V"))
455    }
456
457    /// Parameter type ids of a proto, resolved once at parse (shared
458    /// slice; the old per-call `type_list` re-read is gone).
459    pub fn proto_params(&self, idx: u32) -> &[u32] {
460        self.proto_tys
461            .get(idx as usize)
462            .map(|b| &**b)
463            .unwrap_or(&[])
464    }
465
466    /// `type_list` at `off` (interface lists): one `raw()` snapshot for
467    /// the whole walk — the shape below used to lock per u16 read.
468    fn read_type_list(&self, off: u32) -> Option<Vec<u32>> {
469        let raw = self.raw();
470        Some(read_type_list_at(raw, off as usize).into())
471    }
472
473    pub fn field(&self, idx: u32) -> &FieldId {
474        static EMPTY: std::sync::OnceLock<FieldId> = std::sync::OnceLock::new();
475        self.fields.get(idx as usize).unwrap_or_else(|| {
476            EMPTY.get_or_init(|| FieldId {
477                class_idx: 0,
478                type_idx: 0,
479                name_idx: 0,
480            })
481        })
482    }
483
484    pub fn field_count(&self) -> usize {
485        self.fields.len()
486    }
487
488    pub fn method(&self, idx: u32) -> &MethodId {
489        static EMPTY: std::sync::OnceLock<MethodId> = std::sync::OnceLock::new();
490        self.methods.get(idx as usize).unwrap_or_else(|| {
491            EMPTY.get_or_init(|| MethodId {
492                class_idx: 0,
493                proto_idx: 0,
494                name_idx: 0,
495            })
496        })
497    }
498
499    pub fn method_count(&self) -> usize {
500        self.methods.len()
501    }
502
503    /// Class def index for a type descriptor, when this DEX defines it.
504    pub fn class_def_of(&self, type_idx: u32) -> Option<usize> {
505        self.by_type.get(&type_idx).copied()
506    }
507
508    /// Interfaces of a class def as type ids.
509    pub fn interfaces_of(&self, cd: &ClassDef) -> Vec<u32> {
510        if cd.interfaces_off == 0 {
511            return Vec::new();
512        }
513        self.read_type_list(cd.interfaces_off).unwrap_or_default()
514    }
515
516    pub fn class_data(&self, cd: &ClassDef) -> ClassData {
517        if cd.class_data_off == 0 {
518            return ClassData::default();
519        }
520        self.read_class_data(cd.class_data_off).unwrap_or_default()
521    }
522
523    fn read_class_data(&self, off: u32) -> Option<ClassData> {
524        let mut c = Cursor::at(self.raw(), off as usize);
525        let sf = c.read_uleb128()? as usize;
526        let inf = c.read_uleb128()? as usize;
527        let dm = c.read_uleb128()? as usize;
528        let vm = c.read_uleb128()? as usize;
529
530        let read_fields = |n: usize, cur: &mut Cursor| -> Option<Vec<EncodedField>> {
531            let mut out = Vec::with_capacity(n);
532            let mut idx: u64 = 0;
533            for _ in 0..n {
534                idx += cur.read_uleb128()?;
535                out.push(EncodedField {
536                    field_idx: idx as u32,
537                    access_flags: cur.read_uleb128()? as u32,
538                });
539            }
540            Some(out)
541        };
542        let static_fields = read_fields(sf, &mut c)?;
543        let instance_fields = read_fields(inf, &mut c)?;
544
545        let read_methods = |n: usize, cur: &mut Cursor| -> Option<Vec<EncodedMethod>> {
546            let mut out = Vec::with_capacity(n);
547            let mut idx: u64 = 0;
548            for _ in 0..n {
549                idx += cur.read_uleb128()?;
550                out.push(EncodedMethod {
551                    method_idx: idx as u32,
552                    access_flags: cur.read_uleb128()? as u32,
553                    code_off: cur.read_uleb128()? as u32,
554                });
555            }
556            Some(out)
557        };
558        let direct_methods = read_methods(dm, &mut c)?;
559        let virtual_methods = read_methods(vm, &mut c)?;
560
561        Some(ClassData {
562            static_fields,
563            instance_fields,
564            direct_methods,
565            virtual_methods,
566        })
567    }
568
569    /// (registers_size, insns_size) read straight from the code_item header
570    /// — no instruction decoding. Hot path for risk checks.
571    pub fn code_stats(&self, off: u32) -> Option<(u16, u32)> {
572        if off == 0 {
573            return None;
574        }
575        let o = off as usize;
576        // One snapshot: each raw() takes the RwLock read side (this ran
577        // 5 locks per call on per-method hot paths).
578        let d = self.raw();
579        if o + 16 > d.len() {
580            return None;
581        }
582        let regs = u16::from_le_bytes([d[o], d[o + 1]]);
583        let insns = u32::from_le_bytes([d[o + 12], d[o + 13], d[o + 14], d[o + 15]]);
584        Some((regs, insns))
585    }
586
587    /// debug_info_off straight from the code_item header (pool building).
588    pub fn debug_info_off_at(&self, off: u32) -> Option<u32> {
589        if off == 0 {
590            return None;
591        }
592        let o = off as usize;
593        let d = self.raw();
594        if o + 12 > d.len() {
595            return None;
596        }
597        Some(u32::from_le_bytes([d[o + 8], d[o + 9], d[o + 10], d[o + 11]]))
598    }
599
600    pub fn code_at(&self, off: u32) -> Option<CodeItem> {
601        if off == 0 {
602            return None;
603        }
604        CodeItem::parse(self.raw(), off as usize)
605    }
606
607    /// Class descriptors straight from an inflated image, WITHOUT a full
608    /// `DexFile::parse` (which decodes the whole string table — the class
609    /// names are a small slice of it). Class listing pays for exactly what
610    /// it prints.
611    pub fn class_names_from_image(image: &[u8]) -> Vec<String> {
612        if image.len() < 0x70 || &image[..4] != b"dex\n" {
613            return Vec::new();
614        }
615        let u4 = |o: usize| -> u32 {
616            u32::from_le_bytes([image[o], image[o + 1], image[o + 2], image[o + 3]])
617        };
618        let strings_size = u4(0x38) as usize;
619        let strings_off = u4(0x3c) as usize;
620        let types_size = u4(0x40) as usize;
621        let types_off = u4(0x44) as usize;
622        let classes_size = u4(0x60) as usize;
623        let classes_off = u4(0x64) as usize;
624        if strings_off + 4 * strings_size > image.len()
625            || types_off + 4 * types_size > image.len()
626            || classes_off + 32 * classes_size > image.len()
627        {
628            return Vec::new();
629        }
630        let u4at = |o: usize| -> u32 {
631            u32::from_le_bytes([image[o], image[o + 1], image[o + 2], image[o + 3]])
632        };
633        let string_at = |idx: u32| -> Option<String> {
634            let so = u4at(strings_off + 4 * idx as usize) as usize;
635            if so >= image.len() {
636                return None;
637            }
638            // mutf8 string_data_item: uleb len (utf16 units), bytes, NUL.
639            let mut p = so;
640            // uleb128
641            let mut len = 0u64;
642            let mut shift = 0;
643            loop {
644                if p >= image.len() {
645                    return None;
646                }
647                let b = image[p];
648                p += 1;
649                len |= ((b & 0x7f) as u64) << shift;
650                shift += 7;
651                if b & 0x80 == 0 {
652                    break;
653                }
654            }
655            let end = image[p..].iter().position(|&b| b == 0)? + p;
656            crate::reader::mutf8_decode(&image[p..end], len)
657        };
658        let mut out = Vec::with_capacity(classes_size);
659        for i in 0..classes_size {
660            let cd = classes_off + 32 * i;
661            let class_idx = u4at(cd) as usize;
662            if class_idx >= types_size {
663                continue;
664            }
665            let string_idx = u4at(types_off + 4 * class_idx);
666            if let Some(name) = string_at(string_idx) {
667                // type strings are descriptors (Lcom/foo/Bar;): strip the
668                // L; shell to match DexFile::class_name's plain form.
669                let plain = name
670                    .strip_prefix('L')
671                    .and_then(|s| s.strip_suffix(';'))
672                    .map(str::to_string)
673                    .unwrap_or(name);
674                out.push(plain);
675            }
676        }
677        out
678    }
679
680    /// The raw insns byte section of one code item (zero-copy) for the
681    /// boundary-walking scan path — skips try tables and full decode.
682    pub fn code_insns_bytes_at(&self, off: u32) -> Option<&[u8]> {
683        if off == 0 {
684            return None;
685        }
686        let off = off as usize;
687        let d = self.raw();
688        if off + 16 > d.len() {
689            return None;
690        }
691        // code_item: 4×u2 header, u4 debug_info_off, u4 insns_size(units).
692        let insns_size =
693            u32::from_le_bytes([d[off + 12], d[off + 13], d[off + 14], d[off + 15]]) as usize;
694        let start = off + 16;
695        let end = start + 2 * insns_size;
696        if end > d.len() {
697            return None;
698        }
699        Some(&d[start..end])
700    }
701
702    /// Static field initial values (aligned with `static_fields` order).
703    pub fn static_values(&self, off: u32) -> Vec<EncodedValue> {
704        if off == 0 {
705            return Vec::new();
706        }
707        annotations::read_encoded_array(self.raw(), off as usize).unwrap_or_default()
708    }
709
710    /// `method_handle_item` by index.
711    pub fn method_handle(&self, idx: u32) -> Option<&MethodHandleItem> {
712        self.method_handles.get(idx as usize)
713    }
714
715    pub fn method_handle_count(&self) -> usize {
716        self.method_handles.len()
717    }
718
719    /// Resolved call site by index.
720    pub fn call_site(&self, idx: u32) -> Option<&CallSiteInfo> {
721        self.call_sites.get(idx as usize)
722    }
723
724    pub fn call_site_count(&self) -> usize {
725        self.call_sites.len()
726    }
727
728    /// Parameter names from debug info (NO_INDEX → absent). `debug_info_off`
729    /// comes from a code item; the entry layout is `line_start` uleb,
730    /// `parameters_size` uleb, then `uleb128p1` names.
731    /// Local-variable table of a `debug_info_item` (DBG_START_LOCAL /
732    /// END / RESTART ranges). Release APKs strip it (empty result, zero
733    /// cost); debug-enabled builds recover the ORIGINAL source names
734    /// (`bArr`, `i3`, …) instead of synthetic `vN` registers.
735    pub fn debug_locals(&self, debug_info_off: u32) -> Vec<DebugLocal> {
736        if debug_info_off == 0 {
737            return Vec::new();
738        }
739        let raw = self.raw();
740        let mut c = Cursor::at(raw, debug_info_off as usize);
741        if c.read_uleb128().is_none() {
742            return Vec::new();
743        }
744        let n = match c.read_uleb128() {
745            Some(v) => v as usize,
746            None => return Vec::new(),
747        };
748        // Skip the parameter-name indices.
749        for _ in 0..n {
750            if c.read_uleb128p1().is_none() {
751                return Vec::new();
752            }
753        }
754        let mut out: Vec<DebugLocal> = Vec::new();
755        // Open ranges per register + the last closed name per register
756        // (DBG_RESTART_LOCAL resumes it). Small linear tables: a method
757        // touches a handful of registers at any address.
758        // (register, start pc, name, optional type descriptor)
759        type OpenLocal = (u16, u32, std::sync::Arc<str>, Option<std::sync::Arc<str>>);
760        let mut open: Vec<OpenLocal> = Vec::new();
761        let mut last: Vec<(u16, std::sync::Arc<str>, Option<std::sync::Arc<str>>)> = Vec::new();
762        let mut addr: u32 = 0;
763        // Corrupt-stream guard: the bytecode is variable-length; cap the
764        // opcode count far above any real method (tolerant-sentinel rule).
765        for _ in 0..2_000_000u32 {
766            let Some(op) = c.u1() else { break };
767            match op {
768                0x00 => break, // DBG_END_SEQUENCE
769                0x01 => {
770                    // DBG_ADVANCE_PC
771                    let Some(d) = c.read_uleb128() else { break };
772                    addr = addr.saturating_add(d as u32);
773                }
774                0x02 => {
775                    // DBG_ADVANCE_LINE (line numbers are not recovered)
776                    if c.read_sleb128().is_none() {
777                        break;
778                    }
779                }
780                0x03 | 0x04 => {
781                    // DBG_START_LOCAL[_EXTENDED]
782                    let (Some(reg), Some(ni)) = (c.read_uleb128(), c.read_uleb128p1()) else {
783                        break;
784                    };
785                    let ti = match c.read_uleb128p1() {
786                        Some(v) => v,
787                        None => break,
788                    };
789                    if op == 0x04 && c.read_uleb128p1().is_none() {
790                        break; // signature_idx
791                    }
792                    // read_uleb128p1 ALREADY applies the -1 (NO_INDEX =
793                    // -1); decrementing again shifted every name one
794                    // string-table slot down (pm→pi, name→line).
795                    if ni >= 0 {
796                        let reg = reg as u16;
797                        if let Some(pos) = open.iter().position(|(r, ..)| *r == reg) {
798                            let (_, start, name, ty) = open.remove(pos);
799                            out.push(DebugLocal { reg, start, end: addr, name, ty });
800                        }
801                        let name = self.string_arc(ni as u32);
802                        let ty = if ti >= 0 {
803                            Some(std::sync::Arc::from(self.type_name(ti as u32)))
804                        } else {
805                            None
806                        };
807                        if let Some(pos) = last.iter().position(|(r, ..)| *r == reg) {
808                            last[pos] = (reg, name.clone(), ty.clone());
809                        } else {
810                            last.push((reg, name.clone(), ty.clone()));
811                        }
812                        open.push((reg, addr, name, ty));
813                    }
814                }
815                0x05 => {
816                    // DBG_END_LOCAL
817                    let Some(reg) = c.read_uleb128() else { break };
818                    let reg = reg as u16;
819                    if let Some(pos) = open.iter().position(|(r, ..)| *r == reg) {
820                        let (_, start, name, ty) = open.remove(pos);
821                        out.push(DebugLocal { reg, start, end: addr, name, ty });
822                    }
823                }
824                0x06 => {
825                    // DBG_RESTART_LOCAL
826                    let Some(reg) = c.read_uleb128() else { break };
827                    let reg = reg as u16;
828                    if let Some((_, name, ty)) = last.iter().find(|(r, ..)| *r == reg) {
829                        let name = name.clone();
830                        let ty = ty.clone();
831                        if !open.iter().any(|(r, ..)| *r == reg) {
832                            open.push((reg, addr, name, ty));
833                        }
834                    }
835                }
836                0x07 | 0x08 => {} // prologue-end / epilogue-begin
837                0x09 => {
838                    // DBG_SET_FILE
839                    if c.read_uleb128p1().is_none() {
840                        break;
841                    }
842                }
843                // Special opcodes (>= 0x0a): DWARF-style fused
844                // (address, line) advance in ONE byte — addr +=
845                // (op - 0x0a) / 15, line += -4 + (op - 0x0a) % 15.
846                // Line numbers are not recovered; the address advance
847                // positions the local ranges. Treating these as invalid
848                // (an earlier shape broke here) lost EVERY local after
849                // the first special opcode — i.e. all of them.
850                c => {
851                    let adjusted = (c - 0x0a) as u32;
852                    addr = addr.saturating_add(adjusted / 15);
853                }
854            }
855        }
856        for (reg, start, name, ty) in open {
857            out.push(DebugLocal {
858                reg,
859                start,
860                end: u32::MAX,
861                name,
862                ty,
863            });
864        }
865        out
866    }
867
868    pub fn parameter_names(&self, debug_info_off: u32) -> Vec<Option<String>> {
869        if debug_info_off == 0 {
870            return Vec::new();
871        }
872        let mut c = Cursor::at(self.raw(), debug_info_off as usize);
873        let _line_start = match c.read_uleb128() {
874            Some(v) => v,
875            None => return Vec::new(),
876        };
877        let n = match c.read_uleb128() {
878            Some(v) => v as usize,
879            None => return Vec::new(),
880        };
881        let mut out = Vec::with_capacity(n);
882        for _ in 0..n {
883            let idx = c.read_uleb128p1().unwrap_or(-1);
884            if idx < 0 {
885                out.push(None);
886            } else {
887                out.push(Some(self.string(idx as u32).to_string()));
888            }
889        }
890        out
891    }
892}
893
894impl std::fmt::Debug for DexFile {
895    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
896        f.debug_struct("DexFile")
897            .field("version", &self.version)
898            .field("strings", &self.strings.len())
899            .field("types", &self.types.len())
900            .field("protos", &self.protos.len())
901            .field("fields", &self.fields.len())
902            .field("methods", &self.methods.len())
903            .field("class_defs", &self.class_defs.len())
904            .finish()
905    }
906}
907
908/// `type_list` at `off` straight off a byte slice: parse-time proto
909/// resolution and the interfaces read both go through here — one
910/// snapshot of the slice, no per-element locking. Reads past the end
911/// are truncated (the tolerant-sentinel rule for corrupt images).
912fn read_type_list_at(data: &[u8], off: usize) -> Box<[u32]> {
913    if off == 0 || off + 4 > data.len() {
914        return Vec::new().into_boxed_slice();
915    }
916    let size = u32::from_le_bytes(data[off..off + 4].try_into().unwrap()) as usize;
917    let mut p = off + 4;
918    let mut out = Vec::with_capacity(size);
919    for _ in 0..size {
920        if p + 2 > data.len() {
921            break;
922        }
923        out.push(u16::from_le_bytes(data[p..p + 2].try_into().unwrap()) as u32);
924        p += 2;
925    }
926    out.into_boxed_slice()
927}
928
929/// Sections 0x0007 (call_site_id) and 0x0008 (method_handle_id) from the
930/// map list. Layouts (dx `ItemType`/`MethodHandleItem`):
931/// * `call_site_id_item`: u4 offset → `call_site_off` encoded array
932///   [bootstrap METHOD_HANDLE, name STRING, type METHOD_TYPE, linker args…].
933/// * `method_handle_item`: u2 kind, u2 reserved, u2 target id, u2 reserved.
934fn parse_map_tables(data: &[u8]) -> (Vec<MethodHandleItem>, Vec<CallSiteInfo>) {
935    let mut handles = Vec::new();
936    let mut sites = Vec::new();
937    if data.len() < 0x70 {
938        return (handles, sites);
939    }
940    let map_off = u32le(data, 0x34) as usize;
941    if map_off + 4 > data.len() {
942        return (handles, sites);
943    }
944    let n = u32le(data, map_off) as usize;
945    let mut p = map_off + 4;
946    for _ in 0..n {
947        if p + 12 > data.len() {
948            break;
949        }
950        let t = u16le(data, p);
951        let size = u32le(data, p + 4) as usize;
952        let off = u32le(data, p + 8) as usize;
953        p += 12;
954        match t {
955            0x0007 => {
956                // call_site_id_item: u4 call_site_off each.
957                for i in 0..size {
958                    let q = off + 4 * i;
959                    if q + 4 > data.len() {
960                        break;
961                    }
962                    let cs_off = u32le(data, q) as usize;
963                    if let Some(info) = parse_call_site(data, cs_off) {
964                        sites.push(info);
965                    }
966                }
967            }
968            0x0008 => {
969                // method_handle_item: (u2 kind, u2 res, u2 id, u2 res).
970                for i in 0..size {
971                    let q = off + 8 * i;
972                    if q + 8 > data.len() {
973                        break;
974                    }
975                    let kind = u16le(data, q);
976                    let id = u16le(data, q + 4);
977                    handles.push(MethodHandleItem {
978                        kind,
979                        target_id: id as u32,
980                        is_field: kind <= 3,
981                    });
982                }
983            }
984            _ => {}
985        }
986    }
987    (handles, sites)
988}
989
990fn parse_call_site(data: &[u8], off: usize) -> Option<CallSiteInfo> {
991    let vals = crate::annotations::read_encoded_array(data, off)?;
992    if vals.len() < 3 {
993        return None;
994    }
995    let bootstrap_handle = match &vals[0] {
996        crate::annotations::EncodedValue::MethodHandle(h) => *h,
997        _ => return None,
998    };
999    let name_idx = match &vals[1] {
1000        crate::annotations::EncodedValue::String(s) => *s,
1001        _ => return None,
1002    };
1003    let proto_idx = match &vals[2] {
1004        crate::annotations::EncodedValue::MethodType(p) => *p,
1005        _ => return None,
1006    };
1007    Some(CallSiteInfo {
1008        bootstrap_handle,
1009        name_idx,
1010        proto_idx,
1011        linker_args: vals[3..].to_vec(),
1012    })
1013}
1014
1015fn u16le(data: &[u8], off: usize) -> u16 {
1016    if off + 2 > data.len() {
1017        return 0;
1018    }
1019    u16::from_le_bytes([data[off], data[off + 1]])
1020}
1021
1022fn u32le(data: &[u8], off: usize) -> u32 {
1023    if off + 4 > data.len() {
1024        return 0;
1025    }
1026    u32::from_le_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]])
1027}
1028
1029#[cfg(test)]
1030mod tests {
1031    use super::*;
1032
1033    #[test]
1034    fn rejects_non_dex() {
1035        assert!(DexFile::parse(vec![0u8; 128]).is_err());
1036    }
1037}
1038
1039
1040/// One source-level local from the debug-info local table: the name is
1041/// valid for `reg` over `[start, end)` in code units.
1042#[derive(Debug, Clone)]
1043pub struct DebugLocal {
1044    pub reg: u16,
1045    pub start: u32,
1046    pub end: u32,
1047    pub name: std::sync::Arc<str>,
1048    /// Type descriptor from the local table (`[C`, `Ljava/lang/String;`),
1049    /// when present — lets the consumer reject a range whose type does
1050    /// not match the value being named (register reuse: `new-array v0,
1051    // v0` reads the OLD v0 as a size inside the NEW v0's range).
1052    pub ty: Option<std::sync::Arc<str>>,
1053}