ddc-dex 0.2.0

DEX file parser and Dalvik instruction decoder for the ddc decompiler
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
//! DEX container: header, id tables, class defs.

use std::collections::HashMap;

use crate::annotations::{self, EncodedValue};
use crate::code::CodeItem;
use crate::reader::Cursor;

/// Index sentinel for "absent".
pub const NO_INDEX: u32 = 0xffff_ffff;

#[derive(Debug, Clone)]
pub struct ProtoId {
    /// Index into string ids: the shorty descriptor.
    pub shorty_idx: u32,
    /// Index into type ids: return type.
    pub return_type_idx: u32,
    /// Offset of the parameter type list, or 0.
    pub parameters_off: u32,
}

#[derive(Debug, Clone)]
pub struct FieldId {
    pub class_idx: u32,
    pub type_idx: u32,
    pub name_idx: u32,
}

#[derive(Debug, Clone)]
pub struct MethodId {
    pub class_idx: u32,
    pub proto_idx: u32,
    pub name_idx: u32,
}

/// One `class_def_item`.
#[derive(Debug, Clone)]
pub struct ClassDef {
    /// Index into type ids.
    pub class_idx: u32,
    pub access_flags: u32,
    pub superclass_idx: u32,
    pub interfaces_off: u32,
    pub source_file_idx: u32,
    pub annotations_off: u32,
    pub class_data_off: u32,
    pub static_values_off: u32,
}

/// One `encoded_field`.
#[derive(Debug, Clone)]
pub struct EncodedField {
    pub field_idx: u32,
    pub access_flags: u32,
}

/// One `encoded_method`.
#[derive(Debug, Clone)]
pub struct EncodedMethod {
    pub method_idx: u32,
    pub access_flags: u32,
    pub code_off: u32,
}

/// One `class_data_item`, fully expanded (index diffs applied).
#[derive(Debug, Clone, Default)]
pub struct ClassData {
    pub static_fields: Vec<EncodedField>,
    pub instance_fields: Vec<EncodedField>,
    pub direct_methods: Vec<EncodedMethod>,
    pub virtual_methods: Vec<EncodedMethod>,
}

/// A parsed DEX image.
///
/// Id tables and class defs are parsed eagerly; code items, class data and
/// annotations are materialized on demand. All accessors degrade to
/// sentinels/`None` on out-of-range indices instead of panicking.
pub struct DexFile {
    /// The raw image. `None` after `release_data`/`mark_released` —
    /// retired images drop their inflated bytes (the full-decompile
    /// pipeline retires a dex once every class in it has been emitted;
    /// on weibo/lark that is ~360MB of images that used to stay resident
    /// for the whole run).
    data: std::sync::Mutex<Option<Vec<u8>>>,
    /// Set by `mark_released` from a shared (&) reference — the actual
    /// bytes drop immediately through the mutex.
    released: std::sync::atomic::AtomicBool,
    /// MUTF-8 string table as shared `Arc<str>` slices: pool-side
    /// structures (PoolMethod names, resolved names) clone the Arc
    /// instead of copying heap bytes per method — a dex string idx is
    /// already the dedup key.
    strings: Vec<std::sync::Arc<str>>,
    /// Type ids: descriptor string indices.
    types: Vec<u32>,
    protos: Vec<ProtoId>,
    /// Parameter type-id lists per proto, resolved once at parse: the
    /// invoke decode path used to re-read each `type_list` from the
    /// image on EVERY call (`read_type_list` hit the `raw()` mutex per
    /// u16 — a full lock cycle per parameter, per invoke, per method).
    proto_tys: Vec<Box<[u32]>>,
    /// Method-descriptor string per proto (`(ILjava/lang/String;)V`),
    /// formatted once at parse: pool-side PoolMethod descs clone the Arc
    /// instead of re-joining per method (a dex's proto table is the
    /// descriptor dedup layer — methods share protos heavily).
    proto_descs: Vec<std::sync::Arc<str>>,
    fields: Vec<FieldId>,
    methods: Vec<MethodId>,
    pub class_defs: Vec<ClassDef>,
    pub version: String,
    /// class_idx → index into class_defs.
    by_type: HashMap<u32, usize>,
    /// `method_handle_item`s (DEX 037+): (kind, field/method id).
    method_handles: Vec<MethodHandleItem>,
    /// `call_site_id_item`s (DEX 037+), resolved.
    call_sites: Vec<CallSiteInfo>,
}

/// One `method_handle_item`.
#[derive(Debug, Clone)]
pub struct MethodHandleItem {
    /// 0-3: field static-put/static-get/instance-put/instance-get;
    /// 4: invoke-static; 5: invoke-instance; 6: invoke-constructor;
    /// 7: invoke-direct; 8: invoke-interface.
    pub kind: u16,
    /// Field or method id (widened from u2).
    pub target_id: u32,
    pub is_field: bool,
}

/// A resolved `call_site_id_item`: the `call_site_off` encoded array.
#[derive(Debug, Clone)]
pub struct CallSiteInfo {
    /// Bootstrap method handle index.
    pub bootstrap_handle: u32,
    /// Call-site method name (string index).
    pub name_idx: u32,
    /// Call-site method type (proto index).
    pub proto_idx: u32,
    /// Linker arguments (raw encoded values).
    pub linker_args: Vec<crate::annotations::EncodedValue>,
}

impl Drop for DexFile {
    fn drop(&mut self) {
        *self.data.lock().unwrap() = None;
    }
}

impl DexFile {
    pub fn parse(data: Vec<u8>) -> Result<DexFile, String> {
        if data.len() < crate::DEX_HEADER_SIZE {
            return Err("file too small for DEX header".into());
        }
        let magic = &data[..8];
        if &magic[..4] != b"dex\n" || magic[7] != 0 {
            return Err(format!("not a DEX file: magic {:02x?}", magic));
        }
        let version = String::from_utf8_lossy(&magic[4..7]).into_owned();
        // 036 was an unofficial odex-era marker; ART accepts 035, 037-041.
        if !matches!(
            version.as_str(),
            "035" | "037" | "038" | "039" | "040" | "041"
        ) {
            return Err(format!("unsupported DEX version {}", version));
        }
        let endian = u32::from_le_bytes(data[40..44].try_into().unwrap());
        if endian != 0x1234_5678 {
            return Err(format!("unexpected endian tag {endian:#x}"));
        }

        let u32at = |off: usize| -> u32 {
            if off + 4 > data.len() {
                return 0;
            }
            u32::from_le_bytes(data[off..off + 4].try_into().unwrap())
        };
        let string_ids_size = u32at(0x38) as usize;
        let string_ids_off = u32at(0x3c) as usize;
        let type_ids_size = u32at(0x40) as usize;
        let type_ids_off = u32at(0x44) as usize;
        let proto_ids_size = u32at(0x48) as usize;
        let proto_ids_off = u32at(0x4c) as usize;
        let field_ids_size = u32at(0x50) as usize;
        let field_ids_off = u32at(0x54) as usize;
        let method_ids_size = u32at(0x58) as usize;
        let method_ids_off = u32at(0x5c) as usize;
        let class_defs_size = u32at(0x60) as usize;
        let class_defs_off = u32at(0x64) as usize;

        // Strings: MUTF-8 with uleb length.
        let mut strings = Vec::with_capacity(string_ids_size);
        if string_ids_off + 4 * string_ids_size <= data.len() {
            for i in 0..string_ids_size {
                let so = u32at(string_ids_off + 4 * i) as usize;
                let mut c = Cursor::at(&data, so);
                let s = c
                    .read_uleb128()
                    .and_then(|len| c.read_mutf8(c.pos, len))
                    .unwrap_or_default();
                strings.push(std::sync::Arc::from(s));
            }
        }

        // Type ids: u4 descriptor string indices.
        let mut types = Vec::with_capacity(type_ids_size);
        if type_ids_off + 4 * type_ids_size <= data.len() {
            for i in 0..type_ids_size {
                types.push(u32at(type_ids_off + 4 * i));
            }
        }

        // Proto ids: 12 bytes each.
        let mut protos = Vec::with_capacity(proto_ids_size);
        if proto_ids_off + 12 * proto_ids_size <= data.len() {
            for i in 0..proto_ids_size {
                let base = proto_ids_off + 12 * i;
                protos.push(ProtoId {
                    shorty_idx: u32at(base),
                    return_type_idx: {
                        let v = u16::from_le_bytes(data[base + 4..base + 6].try_into().unwrap());
                        v as u32
                    },
                    parameters_off: u32at(base + 8),
                });
            }
        }
        // Resolve every proto's parameter list once, straight off the
        // local image slice (no `raw()` mutex on this path at all).
        let proto_tys: Vec<Box<[u32]>> = protos
            .iter()
            .map(|p| read_type_list_at(&data, p.parameters_off as usize))
            .collect();
        // Descriptor per proto, straight off the local tables.
        let ty_name = |t: u32| -> &str {
            match types.get(t as usize) {
                Some(&si) => strings.get(si as usize).map(|s| &**s).unwrap_or(""),
                None => "",
            }
        };
        let proto_descs: Vec<std::sync::Arc<str>> = protos
            .iter()
            .zip(&proto_tys)
            .map(|(p, tys)| {
                let ret = ty_name(p.return_type_idx);
                let mut d = String::with_capacity(2 + ret.len() + tys.len() * 12);
                d.push('(');
                for &t in tys.iter() {
                    d.push_str(ty_name(t));
                }
                d.push(')');
                d.push_str(ret);
                std::sync::Arc::from(d)
            })
            .collect();

        // Field ids: 8 bytes each.
        let mut fields = Vec::with_capacity(field_ids_size);
        if field_ids_off + 8 * field_ids_size <= data.len() {
            for i in 0..field_ids_size {
                let base = field_ids_off + 8 * i;
                fields.push(FieldId {
                    class_idx: u16::from_le_bytes(data[base..base + 2].try_into().unwrap()) as u32,
                    type_idx: u16::from_le_bytes(data[base + 2..base + 4].try_into().unwrap())
                        as u32,
                    name_idx: u32at(base + 4),
                });
            }
        }

        // Method ids: 8 bytes each.
        let mut methods = Vec::with_capacity(method_ids_size);
        if method_ids_off + 8 * method_ids_size <= data.len() {
            for i in 0..method_ids_size {
                let base = method_ids_off + 8 * i;
                methods.push(MethodId {
                    class_idx: u16::from_le_bytes(data[base..base + 2].try_into().unwrap()) as u32,
                    proto_idx: u16::from_le_bytes(data[base + 2..base + 4].try_into().unwrap())
                        as u32,
                    name_idx: u32at(base + 4),
                });
            }
        }

        // Class defs: 32 bytes each.
        let mut class_defs = Vec::with_capacity(class_defs_size);
        if class_defs_off + 32 * class_defs_size <= data.len() {
            for i in 0..class_defs_size {
                let base = class_defs_off + 32 * i;
                class_defs.push(ClassDef {
                    class_idx: u32at(base),
                    access_flags: u32at(base + 4),
                    superclass_idx: u32at(base + 8),
                    interfaces_off: u32at(base + 12),
                    source_file_idx: u32at(base + 16),
                    annotations_off: u32at(base + 20),
                    class_data_off: u32at(base + 24),
                    static_values_off: u32at(base + 28),
                });
            }
        }

        let mut by_type = HashMap::with_capacity(class_defs.len());
        for (i, cd) in class_defs.iter().enumerate() {
            by_type.insert(cd.class_idx, i);
        }

        // DEX 037+ tables (call sites / method handles) live ONLY in the
        // map list — the header carries no offsets for them.
        let (method_handles, call_sites) = parse_map_tables(&data);

        Ok(DexFile {
            data: std::sync::Mutex::new(Some(data)),
            released: std::sync::atomic::AtomicBool::new(false),
            strings,
            types,
            protos,
            proto_tys,
            proto_descs,
            fields,
            methods,
            class_defs,
            version,
            by_type,
            method_handles,
            call_sites,
        })
    }

    /// The raw image, or empty once retired.
    ///
    /// SAFETY of the lifetime: the bytes live in an `Arc<Vec<u8>>` whose
    /// clone is moved into a self-owned stash (`raw_keep`) that outlives
    /// every later call — each call swaps the stash, keeping the previous
    /// slice alive only for the previous caller, which is unsound in
    /// general. The callers here hold the slice only inside one method
    /// (`code_at` / parse-time table walks) and never across calls, and
    /// single-call scope is the documented contract; the alternative
    /// (returning a guard) would infect every parser signature.
    pub fn raw(&self) -> &[u8] {
        let guard = self.data.lock().unwrap();
        match guard.as_ref() {
            Some(bytes) => unsafe { std::mem::transmute::<&[u8], &[u8]>(bytes.as_slice()) },
            None => &[],
        }
    }

    /// Drop the inflated image (tables stay usable; code accessors return
    /// empty). Call only when no further code decoding will happen.
    pub fn release_data(&mut self) {
        *self.data.lock().unwrap() = None;
        self.released
            .store(true, std::sync::atomic::Ordering::Release);
    }

    /// Mark the image retired through a shared reference: code accessors
    /// go empty immediately; the bytes themselves drop when the last Arc
    /// snapshot drops (workers hold snapshots only mid-class, so the
    /// memory is reclaimed as chunks complete).
    pub fn mark_released(&self) {
        self.released
            .store(true, std::sync::atomic::Ordering::Release);
        // Free the bytes NOW (through the mutex, &self-safe).
        *self.data.lock().unwrap() = None;
    }

    #[inline]
    #[allow(dead_code)] // debug hook for retirement postmortems
    fn is_released(&self) -> bool {
        self.released.load(std::sync::atomic::Ordering::Acquire)
    }

    pub fn string(&self, idx: u32) -> &str {
        self.strings
            .get(idx as usize)
            .map(|s| &**s)
            .unwrap_or("")
    }

    pub fn string_count(&self) -> usize {
        self.strings.len()
    }

    /// Type id → CLASS internal name (`java/lang/String`), descriptor
    /// stripped; array descriptors pass through unchanged.
    pub fn class_name(&self, idx: u32) -> String {
        let d = self.type_name(idx);
        if d.len() > 2 && d.starts_with('L') && d.ends_with(';') {
            d[1..d.len() - 1].to_string()
        } else {
            d.to_string()
        }
    }

    /// Type id → descriptor string (`Ljava/lang/String;`, `[I`, ...).
    pub fn type_name(&self, idx: u32) -> &str {
        match self.types.get(idx as usize) {
            Some(&si) => self.string(si),
            None => "",
        }
    }

    pub fn type_count(&self) -> usize {
        self.types.len()
    }

    pub fn proto(&self, idx: u32) -> &ProtoId {
        static EMPTY: std::sync::OnceLock<ProtoId> = std::sync::OnceLock::new();
        self.protos.get(idx as usize).unwrap_or_else(|| {
            EMPTY.get_or_init(|| ProtoId {
                shorty_idx: 0,
                return_type_idx: 0,
                parameters_off: 0,
            })
        })
    }

    /// Parameter type ids of a proto (empty for `(V)`).
    /// Shared clone of a table string (the pool's method names).
    pub fn string_arc(&self, idx: u32) -> std::sync::Arc<str> {
        self.strings
            .get(idx as usize)
            .cloned()
            .unwrap_or_else(|| std::sync::Arc::from(""))
    }

    /// Method descriptor of a proto as a shared clone.
    pub fn proto_desc(&self, idx: u32) -> std::sync::Arc<str> {
        self.proto_descs
            .get(idx as usize)
            .cloned()
            .unwrap_or_else(|| std::sync::Arc::from("()V"))
    }

    /// Parameter type ids of a proto, resolved once at parse (shared
    /// slice; the old per-call `type_list` re-read is gone).
    pub fn proto_params(&self, idx: u32) -> &[u32] {
        self.proto_tys
            .get(idx as usize)
            .map(|b| &**b)
            .unwrap_or(&[])
    }

    /// `type_list` at `off` (interface lists): one `raw()` snapshot for
    /// the whole walk — the shape below used to lock per u16 read.
    fn read_type_list(&self, off: u32) -> Option<Vec<u32>> {
        let raw = self.raw();
        Some(read_type_list_at(raw, off as usize).into())
    }

    pub fn field(&self, idx: u32) -> &FieldId {
        static EMPTY: std::sync::OnceLock<FieldId> = std::sync::OnceLock::new();
        self.fields.get(idx as usize).unwrap_or_else(|| {
            EMPTY.get_or_init(|| FieldId {
                class_idx: 0,
                type_idx: 0,
                name_idx: 0,
            })
        })
    }

    pub fn field_count(&self) -> usize {
        self.fields.len()
    }

    pub fn method(&self, idx: u32) -> &MethodId {
        static EMPTY: std::sync::OnceLock<MethodId> = std::sync::OnceLock::new();
        self.methods.get(idx as usize).unwrap_or_else(|| {
            EMPTY.get_or_init(|| MethodId {
                class_idx: 0,
                proto_idx: 0,
                name_idx: 0,
            })
        })
    }

    pub fn method_count(&self) -> usize {
        self.methods.len()
    }

    /// Class def index for a type descriptor, when this DEX defines it.
    pub fn class_def_of(&self, type_idx: u32) -> Option<usize> {
        self.by_type.get(&type_idx).copied()
    }

    /// Interfaces of a class def as type ids.
    pub fn interfaces_of(&self, cd: &ClassDef) -> Vec<u32> {
        if cd.interfaces_off == 0 {
            return Vec::new();
        }
        self.read_type_list(cd.interfaces_off).unwrap_or_default()
    }

    pub fn class_data(&self, cd: &ClassDef) -> ClassData {
        if cd.class_data_off == 0 {
            return ClassData::default();
        }
        self.read_class_data(cd.class_data_off).unwrap_or_default()
    }

    fn read_class_data(&self, off: u32) -> Option<ClassData> {
        let mut c = Cursor::at(self.raw(), off as usize);
        let sf = c.read_uleb128()? as usize;
        let inf = c.read_uleb128()? as usize;
        let dm = c.read_uleb128()? as usize;
        let vm = c.read_uleb128()? as usize;

        let read_fields = |n: usize, cur: &mut Cursor| -> Option<Vec<EncodedField>> {
            let mut out = Vec::with_capacity(n);
            let mut idx: u64 = 0;
            for _ in 0..n {
                idx += cur.read_uleb128()?;
                out.push(EncodedField {
                    field_idx: idx as u32,
                    access_flags: cur.read_uleb128()? as u32,
                });
            }
            Some(out)
        };
        let static_fields = read_fields(sf, &mut c)?;
        let instance_fields = read_fields(inf, &mut c)?;

        let read_methods = |n: usize, cur: &mut Cursor| -> Option<Vec<EncodedMethod>> {
            let mut out = Vec::with_capacity(n);
            let mut idx: u64 = 0;
            for _ in 0..n {
                idx += cur.read_uleb128()?;
                out.push(EncodedMethod {
                    method_idx: idx as u32,
                    access_flags: cur.read_uleb128()? as u32,
                    code_off: cur.read_uleb128()? as u32,
                });
            }
            Some(out)
        };
        let direct_methods = read_methods(dm, &mut c)?;
        let virtual_methods = read_methods(vm, &mut c)?;

        Some(ClassData {
            static_fields,
            instance_fields,
            direct_methods,
            virtual_methods,
        })
    }

    /// (registers_size, insns_size) read straight from the code_item header
    /// — no instruction decoding. Hot path for risk checks.
    pub fn code_stats(&self, off: u32) -> Option<(u16, u32)> {
        if off == 0 {
            return None;
        }
        let o = off as usize;
        if o + 16 > self.raw().len() {
            return None;
        }
        let regs = u16::from_le_bytes([self.raw()[o], self.raw()[o + 1]]);
        let insns = u32::from_le_bytes([
            self.raw()[o + 12],
            self.raw()[o + 13],
            self.raw()[o + 14],
            self.raw()[o + 15],
        ]);
        Some((regs, insns))
    }

    /// debug_info_off straight from the code_item header (pool building).
    pub fn debug_info_off_at(&self, off: u32) -> Option<u32> {
        if off == 0 {
            return None;
        }
        let o = off as usize;
        if o + 12 > self.raw().len() {
            return None;
        }
        Some(u32::from_le_bytes([
            self.raw()[o + 8],
            self.raw()[o + 9],
            self.raw()[o + 10],
            self.raw()[o + 11],
        ]))
    }

    pub fn code_at(&self, off: u32) -> Option<CodeItem> {
        if off == 0 {
            return None;
        }
        CodeItem::parse(self.raw(), off as usize)
    }

    /// Class descriptors straight from an inflated image, WITHOUT a full
    /// `DexFile::parse` (which decodes the whole string table — the class
    /// names are a small slice of it). Class listing pays for exactly what
    /// it prints.
    pub fn class_names_from_image(image: &[u8]) -> Vec<String> {
        if image.len() < 0x70 || &image[..4] != b"dex\n" {
            return Vec::new();
        }
        let u4 = |o: usize| -> u32 {
            u32::from_le_bytes([image[o], image[o + 1], image[o + 2], image[o + 3]])
        };
        let strings_size = u4(0x38) as usize;
        let strings_off = u4(0x3c) as usize;
        let types_size = u4(0x40) as usize;
        let types_off = u4(0x44) as usize;
        let classes_size = u4(0x60) as usize;
        let classes_off = u4(0x64) as usize;
        if strings_off + 4 * strings_size > image.len()
            || types_off + 4 * types_size > image.len()
            || classes_off + 32 * classes_size > image.len()
        {
            return Vec::new();
        }
        let u4at = |o: usize| -> u32 {
            u32::from_le_bytes([image[o], image[o + 1], image[o + 2], image[o + 3]])
        };
        let string_at = |idx: u32| -> Option<String> {
            let so = u4at(strings_off + 4 * idx as usize) as usize;
            if so >= image.len() {
                return None;
            }
            // mutf8 string_data_item: uleb len (utf16 units), bytes, NUL.
            let mut p = so;
            // uleb128
            let mut len = 0u64;
            let mut shift = 0;
            loop {
                if p >= image.len() {
                    return None;
                }
                let b = image[p];
                p += 1;
                len |= ((b & 0x7f) as u64) << shift;
                shift += 7;
                if b & 0x80 == 0 {
                    break;
                }
            }
            let end = image[p..].iter().position(|&b| b == 0)? + p;
            crate::reader::mutf8_decode(&image[p..end], len)
        };
        let mut out = Vec::with_capacity(classes_size);
        for i in 0..classes_size {
            let cd = classes_off + 32 * i;
            let class_idx = u4at(cd) as usize;
            if class_idx >= types_size {
                continue;
            }
            let string_idx = u4at(types_off + 4 * class_idx);
            if let Some(name) = string_at(string_idx) {
                // type strings are descriptors (Lcom/foo/Bar;): strip the
                // L; shell to match DexFile::class_name's plain form.
                let plain = name
                    .strip_prefix('L')
                    .and_then(|s| s.strip_suffix(';'))
                    .map(str::to_string)
                    .unwrap_or(name);
                out.push(plain);
            }
        }
        out
    }

    /// The raw insns byte section of one code item (zero-copy) for the
    /// boundary-walking scan path — skips try tables and full decode.
    pub fn code_insns_bytes_at(&self, off: u32) -> Option<&[u8]> {
        if off == 0 {
            return None;
        }
        let off = off as usize;
        let d = self.raw();
        if off + 16 > d.len() {
            return None;
        }
        // code_item: 4×u2 header, u4 debug_info_off, u4 insns_size(units).
        let insns_size =
            u32::from_le_bytes([d[off + 12], d[off + 13], d[off + 14], d[off + 15]]) as usize;
        let start = off + 16;
        let end = start + 2 * insns_size;
        if end > d.len() {
            return None;
        }
        Some(&d[start..end])
    }

    /// Static field initial values (aligned with `static_fields` order).
    pub fn static_values(&self, off: u32) -> Vec<EncodedValue> {
        if off == 0 {
            return Vec::new();
        }
        annotations::read_encoded_array(self.raw(), off as usize).unwrap_or_default()
    }

    /// `method_handle_item` by index.
    pub fn method_handle(&self, idx: u32) -> Option<&MethodHandleItem> {
        self.method_handles.get(idx as usize)
    }

    pub fn method_handle_count(&self) -> usize {
        self.method_handles.len()
    }

    /// Resolved call site by index.
    pub fn call_site(&self, idx: u32) -> Option<&CallSiteInfo> {
        self.call_sites.get(idx as usize)
    }

    pub fn call_site_count(&self) -> usize {
        self.call_sites.len()
    }

    /// Parameter names from debug info (NO_INDEX → absent). `debug_info_off`
    /// comes from a code item; the entry layout is `line_start` uleb,
    /// `parameters_size` uleb, then `uleb128p1` names.
    pub fn parameter_names(&self, debug_info_off: u32) -> Vec<Option<String>> {
        if debug_info_off == 0 {
            return Vec::new();
        }
        let mut c = Cursor::at(self.raw(), debug_info_off as usize);
        let _line_start = match c.read_uleb128() {
            Some(v) => v,
            None => return Vec::new(),
        };
        let n = match c.read_uleb128() {
            Some(v) => v as usize,
            None => return Vec::new(),
        };
        let mut out = Vec::with_capacity(n);
        for _ in 0..n {
            let idx = c.read_uleb128p1().unwrap_or(-1);
            if idx < 0 {
                out.push(None);
            } else {
                out.push(Some(self.string(idx as u32).to_string()));
            }
        }
        out
    }
}

impl std::fmt::Debug for DexFile {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("DexFile")
            .field("version", &self.version)
            .field("strings", &self.strings.len())
            .field("types", &self.types.len())
            .field("protos", &self.protos.len())
            .field("fields", &self.fields.len())
            .field("methods", &self.methods.len())
            .field("class_defs", &self.class_defs.len())
            .finish()
    }
}

/// `type_list` at `off` straight off a byte slice: parse-time proto
/// resolution and the interfaces read both go through here — one
/// snapshot of the slice, no per-element locking. Reads past the end
/// are truncated (the tolerant-sentinel rule for corrupt images).
fn read_type_list_at(data: &[u8], off: usize) -> Box<[u32]> {
    if off == 0 || off + 4 > data.len() {
        return Vec::new().into_boxed_slice();
    }
    let size = u32::from_le_bytes(data[off..off + 4].try_into().unwrap()) as usize;
    let mut p = off + 4;
    let mut out = Vec::with_capacity(size);
    for _ in 0..size {
        if p + 2 > data.len() {
            break;
        }
        out.push(u16::from_le_bytes(data[p..p + 2].try_into().unwrap()) as u32);
        p += 2;
    }
    out.into_boxed_slice()
}

/// Sections 0x0007 (call_site_id) and 0x0008 (method_handle_id) from the
/// map list. Layouts (dx `ItemType`/`MethodHandleItem`):
/// * `call_site_id_item`: u4 offset → `call_site_off` encoded array
///   [bootstrap METHOD_HANDLE, name STRING, type METHOD_TYPE, linker args…].
/// * `method_handle_item`: u2 kind, u2 reserved, u2 target id, u2 reserved.
fn parse_map_tables(data: &[u8]) -> (Vec<MethodHandleItem>, Vec<CallSiteInfo>) {
    let mut handles = Vec::new();
    let mut sites = Vec::new();
    if data.len() < 0x70 {
        return (handles, sites);
    }
    let map_off = u32le(data, 0x34) as usize;
    if map_off + 4 > data.len() {
        return (handles, sites);
    }
    let n = u32le(data, map_off) as usize;
    let mut p = map_off + 4;
    for _ in 0..n {
        if p + 12 > data.len() {
            break;
        }
        let t = u16le(data, p);
        let size = u32le(data, p + 4) as usize;
        let off = u32le(data, p + 8) as usize;
        p += 12;
        match t {
            0x0007 => {
                // call_site_id_item: u4 call_site_off each.
                for i in 0..size {
                    let q = off + 4 * i;
                    if q + 4 > data.len() {
                        break;
                    }
                    let cs_off = u32le(data, q) as usize;
                    if let Some(info) = parse_call_site(data, cs_off) {
                        sites.push(info);
                    }
                }
            }
            0x0008 => {
                // method_handle_item: (u2 kind, u2 res, u2 id, u2 res).
                for i in 0..size {
                    let q = off + 8 * i;
                    if q + 8 > data.len() {
                        break;
                    }
                    let kind = u16le(data, q);
                    let id = u16le(data, q + 4);
                    handles.push(MethodHandleItem {
                        kind,
                        target_id: id as u32,
                        is_field: kind <= 3,
                    });
                }
            }
            _ => {}
        }
    }
    (handles, sites)
}

fn parse_call_site(data: &[u8], off: usize) -> Option<CallSiteInfo> {
    let vals = crate::annotations::read_encoded_array(data, off)?;
    if vals.len() < 3 {
        return None;
    }
    let bootstrap_handle = match &vals[0] {
        crate::annotations::EncodedValue::MethodHandle(h) => *h,
        _ => return None,
    };
    let name_idx = match &vals[1] {
        crate::annotations::EncodedValue::String(s) => *s,
        _ => return None,
    };
    let proto_idx = match &vals[2] {
        crate::annotations::EncodedValue::MethodType(p) => *p,
        _ => return None,
    };
    Some(CallSiteInfo {
        bootstrap_handle,
        name_idx,
        proto_idx,
        linker_args: vals[3..].to_vec(),
    })
}

fn u16le(data: &[u8], off: usize) -> u16 {
    if off + 2 > data.len() {
        return 0;
    }
    u16::from_le_bytes([data[off], data[off + 1]])
}

fn u32le(data: &[u8], off: usize) -> u32 {
    if off + 4 > data.len() {
        return 0;
    }
    u32::from_le_bytes([data[off], data[off + 1], data[off + 2], data[off + 3]])
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn rejects_non_dex() {
        assert!(DexFile::parse(vec![0u8; 128]).is_err());
    }
}