droidsaw 2.0.0

DROIDSAW — unified Android reverse engineering CLI. Hermes, DEX, APK signing. JSON output, MCP server. Bytecode is not a security layer.
Documentation
//! Test-only builder for a hand-constructed [`DexFile`] carrying two
//! `class_def` rows that share one `class_idx` (a duplicate-`class_idx`
//! pair). Row 0 is the canonical row that `class_def_for_type` /
//! `class_def_index` resolve to (first-wins); row 1 is the shadow row.
//!
//! The two rows are wired to point at DIFFERENT supporting data
//! (different `class_data_off` blobs, different `annotations_off`
//! directories) so a consumer that fails to shadow-gate produces an
//! observably wrong attribution (last-wins overwrite, set-union across
//! rows, or a duplicate persisted entry), while a gated consumer
//! reflects only the canonical row.
//!
//! All `DexFile` fields are `pub`, so this is a plain struct literal
//! plus a call to [`DexFile::rebuild_class_def_index`] to populate the
//! first-wins index the way the parser does.

use std::collections::BTreeMap;

use droidsaw_dex::DexString;
use droidsaw_dex::annotation::{
    AnnotationDirectoryItem, AnnotationItem, EncodedAnnotation, EncodedValue,
};
use droidsaw_dex::decode::EncodedMethod;
use droidsaw_dex::emit_dex::emit_class_data_item;
use droidsaw_dex::header::DexHeader;
use droidsaw_dex::ids::{ClassDefItem, MethodIdItem, MethodIdx, ProtoIdItem, StringIdx, TypeIdx};
use droidsaw_dex::parser::DexFile;

/// `class_idx` shared by the two duplicate rows (index into
/// `type_descriptors`).
pub const DUP_TYPE_IDX: u32 = 0;

/// A zeroed [`DexHeader`]. None of the consumers under test read the
/// header — they walk the resolved pools — so a zero header is
/// sufficient for a hand-built IR fixture.
fn zero_header() -> DexHeader {
    DexHeader {
        magic: [0u8; 8],
        checksum: 0,
        signature: [0u8; 20],
        file_size: 0,
        header_size: 0,
        endian_tag: 0,
        link_size: 0,
        link_off: 0,
        map_off: 0,
        string_ids_size: 0,
        string_ids_off: 0,
        type_ids_size: 0,
        type_ids_off: 0,
        proto_ids_size: 0,
        proto_ids_off: 0,
        field_ids_size: 0,
        field_ids_off: 0,
        method_ids_size: 0,
        method_ids_off: 0,
        class_defs_size: 0,
        class_defs_off: 0,
        data_size: 0,
        data_off: 0,
    }
}

/// Construct a `DexFile` skeleton with the supplied pools and two
/// duplicate-`class_idx` class_defs (`canonical` then `shadow`), then
/// rebuild the `class_def_index` (first-wins). Returns the file with
/// `class_def_index[DUP_TYPE_IDX] == Some(0)` so row 1 reports
/// `class_def_is_shadowed(1) == true`.
#[allow(clippy::too_many_arguments)]
fn build(
    strings: Vec<DexString>,
    type_descriptors: Vec<String>,
    protos: Vec<ProtoIdItem>,
    methods: Vec<MethodIdItem>,
    canonical: ClassDefItem,
    shadow: ClassDefItem,
    annotations: BTreeMap<u32, AnnotationDirectoryItem>,
    annotation_sets: BTreeMap<u32, Vec<u32>>,
    annotation_items: BTreeMap<u32, AnnotationItem>,
) -> DexFile {
    let mut dex = DexFile {
        header: zero_header(),
        strings,
        string_data_offs: Vec::new(),
        type_descriptors,
        protos,
        fields: Vec::new(),
        methods,
        class_defs: vec![canonical, shadow],
        annotations,
        type_lists: BTreeMap::new(),
        class_datas: BTreeMap::new(),
        raw_class_data_bytes: BTreeMap::new(),
        code_items: BTreeMap::new(),
        debug_infos: BTreeMap::new(),
        debug_info_raw_bytes: BTreeMap::new(),
        debug_info_section_layout: Vec::new(),
        annotation_set_section_layout: Vec::new(),
        input_checksums_canonical: true,
        annotation_sets,
        annotation_set_ref_lists: BTreeMap::new(),
        annotation_items,
        annotation_item_widths: BTreeMap::new(),
        encoded_arrays: BTreeMap::new(),
        encoded_array_widths: BTreeMap::new(),
        method_handles: Vec::new(),
        map_entries: Vec::new(),
        call_site_ids: Vec::new(),
        parse_errors: Vec::new(),
        class_def_index: Vec::new(),
    };
    dex.rebuild_class_def_index();
    dex
}

/// A `class_def` for `DUP_TYPE_IDX` with the given offsets and no
/// superclass.
fn class_def(annotations_off: u32, class_data_off: u32) -> ClassDefItem {
    ClassDefItem {
        class_idx: TypeIdx(DUP_TYPE_IDX),
        access_flags: 0,
        superclass_idx: None,
        interfaces_off: 0,
        source_file_idx: None,
        annotations_off,
        class_data_off,
        static_values_off: 0,
    }
}

/// Outcome of [`with_native_method_rows`]: the `DexFile` plus the raw
/// byte buffer whose `class_data_off`s the two rows reference.
pub struct NativeFixture {
    pub dex: DexFile,
    pub raw: Vec<u8>,
}

/// Build a fixture where the canonical (first) row's class_data
/// declares one method `canonical_midx` and the shadow (second) row's
/// class_data declares a different method `shadow_midx`. Each method's
/// ACC_NATIVE bit is set per `canonical_native` / `shadow_native`.
///
/// The two methods carry distinct names (`"canonMethod"` /
/// `"shadowMethod"`) so name-keyed consumers can tell them apart.
///
/// Used by the `collect_native_methods` / `collect_unified_code_index`
/// / `frida` tests: a gated consumer reflects only the canonical row's
/// method; an ungated one also reflects the shadow row's.
pub fn with_native_method_rows(
    canonical_midx: u32,
    canonical_native: bool,
    shadow_midx: u32,
    shadow_native: bool,
) -> NativeFixture {
    const ACC_NATIVE: u32 = 0x0100;

    let canonical_flags = if canonical_native { ACC_NATIVE } else { 0 };
    let shadow_flags = if shadow_native { ACC_NATIVE } else { 0 };

    // Two distinct class_data blobs laid out back-to-back in one
    // buffer; `class_data_off` is the byte offset of each within `raw`.
    // Non-zero synthetic code_off on each method. The consumers under
    // test that gate on `code_off != 0` (the `by_method_name` builder)
    // require this to reach method registration; consumers that only
    // read access_flags (`collect_native_methods`) ignore it. The value
    // is never dereferenced as a real code_item by the paths under test.
    let canonical_blob = emit_class_data_item(
        &[],
        &[],
        &[EncodedMethod {
            method_idx: MethodIdx(canonical_midx),
            access_flags: canonical_flags,
            code_off: 0x100,
        }],
        &[],
    )
    .expect("emit canonical class_data");
    let shadow_blob = emit_class_data_item(
        &[],
        &[],
        &[EncodedMethod {
            method_idx: MethodIdx(shadow_midx),
            access_flags: shadow_flags,
            code_off: 0x200,
        }],
        &[],
    )
    .expect("emit shadow class_data");

    // Leading pad byte so neither blob sits at offset 0 (0 is the
    // DEX sentinel for "no class_data").
    let mut raw = vec![0u8];
    let canonical_off = u32::try_from(raw.len()).expect("offset fits u32");
    raw.extend_from_slice(&canonical_blob);
    let shadow_off = u32::try_from(raw.len()).expect("offset fits u32");
    raw.extend_from_slice(&shadow_blob);

    // type_descriptors[0] is the duplicated class; one Object super.
    let type_descriptors = vec!["LDup;".to_string(), "Ljava/lang/Object;".to_string()];
    // protos[0]: ()V
    let protos = vec![ProtoIdItem {
        shorty_idx: StringIdx(0),
        return_type_idx: TypeIdx(1),
        parameters_off: 0,
    }];
    // strings: [0]="V" shorty, [1]="canonMethod", [2]="shadowMethod".
    let strings = vec![
        DexString::from_decoded_str("V"),
        DexString::from_decoded_str("canonMethod"),
        DexString::from_decoded_str("shadowMethod"),
    ];
    // methods indexed by MethodIdx: a flat pool large enough to cover
    // both midx values. name_idx points each at its label string.
    let max_midx = canonical_midx.max(shadow_midx) as usize;
    let mut methods = Vec::with_capacity(max_midx.saturating_add(1));
    for i in 0..=max_midx {
        let i_u32 = u32::try_from(i).expect("method pool index fits u32");
        let name_idx = if i_u32 == canonical_midx {
            StringIdx(1)
        } else if i_u32 == shadow_midx {
            StringIdx(2)
        } else {
            StringIdx(0)
        };
        methods.push(MethodIdItem {
            class_idx: TypeIdx(DUP_TYPE_IDX),
            proto_idx: droidsaw_dex::ids::ProtoIdx(0),
            name_idx,
        });
    }

    let dex = build(
        strings,
        type_descriptors,
        protos,
        methods,
        class_def(0, canonical_off),
        class_def(0, shadow_off),
        BTreeMap::new(),
        BTreeMap::new(),
        BTreeMap::new(),
    );
    NativeFixture { dex, raw }
}

/// Build a fixture for the SQLite class-export test: two rows sharing
/// `class_idx`, no class_data / annotations needed. The export reads
/// only `class_idx` → descriptor and `superclass_idx` → descriptor.
pub fn for_export() -> DexFile {
    let type_descriptors = vec!["LDup;".to_string(), "Ljava/lang/Object;".to_string()];
    build(
        vec![DexString::from_decoded_str("V")],
        type_descriptors,
        Vec::new(),
        Vec::new(),
        class_def(0, 0),
        class_def(0, 0),
        BTreeMap::new(),
        BTreeMap::new(),
        BTreeMap::new(),
    )
}

/// Build a fixture for the React `@ReactModule(name=X)` scan test. The
/// canonical row carries an annotation directory whose single
/// annotation_item declares `@ReactModule(name="CanonicalModule")`; the
/// shadow row carries a directory whose annotation declares
/// `name="ShadowModule"`. A gated scan resolves `CanonicalModule` for
/// the type; an ungated one last-wins to `ShadowModule`.
pub fn for_react_module() -> DexFile {
    // Annotation descriptor the resolver matches on.
    const REACT_MODULE_DESC: &str = "Lcom/facebook/react/module/annotations/ReactModule;";

    // type_descriptors: [0]=LDup; (the duplicated class), [1]=ReactModule
    // annotation type.
    let type_descriptors = vec!["LDup;".to_string(), REACT_MODULE_DESC.to_string()];

    // strings: [0]="name" element key, [1]="CanonicalModule",
    // [2]="ShadowModule".
    let strings = vec![
        DexString::from_decoded_str("name"),
        DexString::from_decoded_str("CanonicalModule"),
        DexString::from_decoded_str("ShadowModule"),
    ];

    // annotation_items keyed by synthetic offset.
    let canonical_item_off: u32 = 0x10;
    let shadow_item_off: u32 = 0x20;
    let mut annotation_items = BTreeMap::new();
    let mut canon_elems = BTreeMap::new();
    canon_elems.insert(StringIdx(0), EncodedValue::String(StringIdx(1)));
    annotation_items.insert(
        canonical_item_off,
        AnnotationItem {
            visibility: 0,
            annotation: EncodedAnnotation {
                type_idx: TypeIdx(1),
                elements: canon_elems,
            },
        },
    );
    let mut shadow_elems = BTreeMap::new();
    shadow_elems.insert(StringIdx(0), EncodedValue::String(StringIdx(2)));
    annotation_items.insert(
        shadow_item_off,
        AnnotationItem {
            visibility: 0,
            annotation: EncodedAnnotation {
                type_idx: TypeIdx(1),
                elements: shadow_elems,
            },
        },
    );

    // annotation_sets keyed by synthetic offset → list of item offsets.
    let canonical_set_off: u32 = 0x40;
    let shadow_set_off: u32 = 0x50;
    let mut annotation_sets = BTreeMap::new();
    annotation_sets.insert(canonical_set_off, vec![canonical_item_off]);
    annotation_sets.insert(shadow_set_off, vec![shadow_item_off]);

    // annotation directories keyed by the class_def's annotations_off.
    let canonical_dir_off: u32 = 0x80;
    let shadow_dir_off: u32 = 0x90;
    let mut annotations = BTreeMap::new();
    annotations.insert(
        canonical_dir_off,
        AnnotationDirectoryItem {
            class_annotations_off: canonical_set_off,
            fields: Vec::new(),
            methods: Vec::new(),
            parameters: Vec::new(),
        },
    );
    annotations.insert(
        shadow_dir_off,
        AnnotationDirectoryItem {
            class_annotations_off: shadow_set_off,
            fields: Vec::new(),
            methods: Vec::new(),
            parameters: Vec::new(),
        },
    );

    build(
        strings,
        type_descriptors,
        Vec::new(),
        Vec::new(),
        class_def(canonical_dir_off, 0),
        class_def(shadow_dir_off, 0),
        annotations,
        annotation_sets,
        annotation_items,
    )
}