Skip to content

Rust API Reference

Emirhan Uçan edited this page Jul 20, 2026 · 11 revisions

Rust API Reference

Function-level reference for every native (Rust) crate in HydraDragonAV Mobile. The native engine is compiled into libhydradragonandroid.so (one per ABI under app/src/main/jniLibs/) and called from Java via JNI through com.hydradragon.antivirus.engine.NativeScanner.

The workspace contains five crates:

Crate Path Role
hydradragonandroid hydradragonandroid/ JNI bridge + full scan pipeline + on-device metadata build
hydradragonclamav hydradragonclamav/ Pure-Rust ClamAV-compatible signature engine + YARA-X bridge
hydradragonml hydradragonml/ tract-onnx binary APK classifier
hydradragonextractor hydradragonextractor/ Recursive archive/APK extraction + zip-bomb detection
hydradragonxorfilter hydradragonxorfilter/ Binary-Fuse xor filter format shared by writer + on-device reader

Cross-references: YARA-X-Modules for the androguard/hydradragon module schemas; Detection-Engines for how these crates combine; Developer-Guide for build instructions.


hydradragonandroid — JNI bridge + scan pipeline

The crate root is hydradragonandroid/src/. It exposes native methods on the Java class com.hydradragon.antivirus.engine.NativeScanner, loads every scan asset (ClamAV DB, compiled YARA .yrc rulesets, ONNX ML model, TLSH digests, NSRL xor-filter whitelist, package whitelist, MinHash benign signatures, URL/IP xor filters) from the APK assets/ via AAssetManager, runs the full per-file scan pipeline, and returns JSON verdicts to Java.

Source files: asset_reader.rs, benign_db.rs, dex_scan.rs, elf.rs, emulate.rs, ip_scan.rs, lib.rs, url_scan.rs.

asset_reader.rs — AAssetManager FFI wrapper

Thin FFI wrapper around Android's AAssetManager C API. Stores a native AAssetManager* in a process-global AtomicPtr so background threads (which do not hold a JNI env) can read bundled asset bytes by relative path.

Signature Behavior
pub fn from_java(env: *mut c_void, asset_manager: *mut c_void) -> *mut c_void Wraps AAssetManager_fromJava. Input: JNI env pointer + Java AssetManager jobject (both raw c_void). Output: native AAssetManager* or null on FFI failure. Call once on the JNI thread, then pass the result to init. (asset_reader.rs:34)
pub fn init(mgr: *mut c_void) Stores mgr into the static AASSET_MANAGER AtomicPtr (Relaxed). No-op return. Must be called before any read. (asset_reader.rs:43)
pub fn read_file_bytes(relative_path: &str) -> Option<Vec<u8>> Opens relative_path under the stored manager in buffer mode (AASSET_MODE_BUFFER = 3), reads its full length, closes the asset. Returns None if the manager is null, the path has an interior NUL, the asset doesn't exist, or AAsset_read returns negative. Truncates to the actually-read count if fewer bytes than AAsset_getLength were read. (asset_reader.rs:47)
pub fn list_assets(asset_dir: &str) -> Option<Vec<String>> Opens asset_dir as an AAssetDir and iterates AAssetDir_getNextFileName, returning all non-empty filenames. None if manager null / dir open fails / path has a NUL. (asset_reader.rs:72)
pub fn read_all_assets(asset_dir: &str) -> HashMap<String, Vec<u8>> Lists asset_dir, then read_file_bytes of every "{asset_dir}/{name}", returning a map keyed by filename. Empty map on failure. Used at init to slurp the whole assets/ tree into memory. (asset_reader.rs:99)

benign_db.rs — MinHash/Jaccard benign-content whitelist

Content-based benign-APK whitelist. At init it parses benign_signatures.bin; at scan time it computes a 64-value MinHash signature from an APK's token set and declares KNOWN_BENIGN if any stored signature for that package has estimated Jaccard similarity ≥ 0.85, so heavy scanning (ClamAV/ML/TLSH) can be skipped. This is the MinHash usage in the codebase — it is a benign-content skip, not the ML anomaly detector.

Item Signature Behavior
const pub const K: usize = 64 Number of MinHash permutations; must match gen_benign_signatures.py. (benign_db.rs:25)
const pub const THRESHOLD: f32 = 0.85 Jaccard similarity threshold for KNOWN_BENIGN. (benign_db.rs:28)
type pub type Sig = [u64; K] A single 64-value MinHash signature. (benign_db.rs:33)
struct pub struct BenignDb { sigs: HashMap<String, Vec<Sig>> } In-memory DB: package name → list of known-benign signatures. Fields private. (benign_db.rs:36)
method pub fn load(data: &[u8]) -> Option<Self> Parses the binary format: u32 package count, then per package u8 name length + UTF-8 name + u32 sig count + that many 64×u64 sigs (all little-endian). None on short read / UTF-8 / parse error. Duplicate packages merged via or_default().extend. (benign_db.rs:43)
method pub fn is_known_benign(&self, package_name: &str, tokens: &HashSet<u64>) -> bool false if the package isn't in the DB. Otherwise computes compute_sig(tokens) and returns true if jaccard(&query, s) >= THRESHOLD for any stored s. Used in run_scan's skip_heavy construction. (benign_db.rs:73)
method pub fn package_count(&self) -> usize Number of packages in the DB. (benign_db.rs:88)
method pub fn signature_count(&self) -> usize Total signatures across all packages. (benign_db.rs:93)
fn pub fn compute_sig(tokens: &HashSet<u64>) -> Sig MinHash: sig = [u64::MAX; K]; for each token t and permutation i, h = (t ^ i).wrapping_mul(FNV_PRIME) (FNV_PRIME = 0x0000_0100_0000_01b3), keeping the minimum per slot. (benign_db.rs:100)
fn pub fn jaccard(a: &Sig, b: &Sig) -> f32 Estimated Jaccard similarity = `

dex_scan.rs — DEX string pool + static analysis

DEX string + static-analysis extraction using the dex-analysis/dex-core crates (FossRust dex-parser-analyzer). Produces the decoded string pool, flattened static-analysis findings, and a unique API-call histogram that feeds the hydradragon.api_call() YARA-X module function.

Item Signature Behavior
struct pub struct DexScan { pub text: String, pub findings: Vec<DexFinding>, pub api_calls: Vec<String> } text = decoded string pool (strings + method/class/field names), \n-joined, capped at MAX_TEXT = 8 MiB. findings = static-analysis findings (any severity), capped at MAX_FINDINGS = 64. api_calls = unique API call signatures in Lpkg/Cls;->method(params)return format, serialized as "sig\tcount", capped at MAX_API_CALLS = 4096. (dex_scan.rs:12)
struct pub struct DexFinding { severity, kind: String, class_descriptor: String, message: String } Flattened from dex_analysis::Finding; message is "{id}: {message}". (dex_scan.rs:25)
fn pub fn scan(bytes: &[u8]) -> Option<DexScan> Parses the DEX via dex_core::parse_dex, builds the string pool (breaking at MAX_TEXT), runs dex_analysis::analyze_dex with AnalysisConfig::default() taking the first MAX_FINDINGS, then builds the API-call histogram via graphs::build_xrefs + semantics::pretty_method for each callee in xrefs.method_calls, accumulating counts per signature. The whole body is wrapped in catch_unwind(AssertUnwindSafe(...)) so a malformed DEX returns None instead of panicking. (dex_scan.rs:34)
fn pub fn is_severe(sev: Severity) -> bool matches!(sev, Severity::Critical) — only Critical findings count toward a malicious verdict; High and below are too false-positive-prone. (dex_scan.rs:91)

elf.rs — minimal ELF32/64 parser

Minimal, dependency-free ELF32/ELF64 (little-endian only) parser — just enough to drive Unicorn emulation: machine type, entry point, PT_LOAD segments, plus .dynsym/.dynstr linear scans to locate an exported symbol by name (e.g. JNI_OnLoad) and to enumerate imported (undefined) symbols with their GOT patch addresses.

Item Signature Behavior
const pub const EM_ARM: u16 = 40, EM_AARCH64: u16 = 183, EM_386: u16 = 3, EM_X86_64: u16 = 62 ELF e_machine constants. (elf.rs:8-11)
struct #[derive(Clone, Debug)] pub struct Segment { pub vaddr: u64, pub offset: u64, pub filesz: u64, pub memsz: u64 } One PT_LOAD segment. (elf.rs:16)
struct #[derive(Clone, Debug)] pub struct ElfInfo { pub is_64: bool, pub machine: u16, pub entry: u64, pub segments: Vec<Segment> } is_64 kept for API completeness; emulate.rs derives everything from machine. (elf.rs:24)
fn pub fn parse_elf(data: &[u8]) -> Option<ElfInfo> Validates \x7fELF magic, requires ei_data == 1 (little-endian; rejects big-endian as "not an Android target"), reads e_machine at offset 18, and depending on ei_class (1=32, 2=64) reads e_entry, e_phoff, e_phentsize, e_phnum. Iterates up to min(phnum, 4096) program headers, keeping only PT_LOAD (type 1) segments with 0 < memsz < 0x1000_0000. None for anything malformed. (elf.rs:52)
fn pub fn find_dynsym(data: &[u8], want: &str) -> Option<u64> Locates .dynsym via the section-header table (using e_shstrndx), resolves its paired .dynstr via sh_link, then linearly scans symbols (capped at 65536). Returns the virtual address (st_value) of the first symbol whose name equals want and whose st_value != 0 (exported/defined). None if malformed/big-endian/.dynsym absent/name not found. (elf.rs:125)
struct #[derive(Clone, Debug)] pub struct Import { pub name: String, pub got_addr: u64 } An undefined (imported) dynamic symbol and the relocation address (r_offset) that should hold its resolved pointer. (elf.rs:224)
fn pub fn find_imports(data: &[u8]) -> Vec<Import> Walks all SHT_REL (9) / SHT_RELA (4) relocation sections (capped at 1,000,000 entries each), extracts r_offset and r_info, computes sym_idx = r_info >> 32 (64-bit) or r_info >> 8 (32-bit), and for each nonzero sym_idx whose dynsym entry is undefined (st_value == 0) emits Import { name, got_addr: r_offset }. Rel vs Rela addend distinction intentionally ignored. Not handled: Android's packed .android.rela (APS2/LEB128) format. Empty vec for malformed/big-endian ELF or missing .dynsym. (elf.rs:234)

emulate.rs — Unicorn CPU emulation

Pure-CPU Unicorn-Engine emulation of native .so libraries to reveal runtime-decoded strings (rolling-XOR/RC4/custom-cipher decode loops) and traced calls into a curated set of suspicious imported APIs (network/file/exec/anti-debug/fingerprinting). No syscalls, no JNI, no libc are emulated; execution halts on the first real unmodeled call. Bounded by both instruction count and a wall-clock timeout, plus a watchdog that force-stops a hung emu_start via a direct uc_emu_stop FFI call to work around a known ARM64-host JIT hang.

Constants: MAX_INSN = 200_000, TIMEOUT_US = 500_000, STACK_BASE = 0x7000_0000, STUB_BASE = 0x6000_0000, HARD_DEADLINE = 600 ms, MIN_STRING_LEN = 6, MAX_CONCURRENT_EMULATIONS = 2, MAX_HARD_TIMEOUTS = 1.

Item Signature Behavior
fn pub fn probe_emulation() -> bool One-shot startup probe (cached in EMULATION_PROBE: AtomicI8, 0=untested/1=available/-1=unavailable). Spawns a thread running a 4-instruction ARM64 snippet (mov x0,#42; add x0,x0,#1; mov x1,#0; ret) under a 10 ms timeout; caller waits via rx.recv_timeout(300 ms). true iff it succeeded. Subsequent calls return the cached result. (emulate.rs:73)
fn pub fn emulation_available() -> bool Reads EMULATION_PROBE; if 0 (not yet probed) assumes true (the scan path has a per-call HARD_DEADLINE safety net), otherwise returns v > 0. (emulate.rs:121)
fn pub fn host_arch() -> &'static str Compile-time cfg!(target_arch=...)"ARM64 (AArch64)" / "ARMv7 (32-bit)" / "x86_64" / "x86 (32-bit)" / "unknown". (emulate.rs:137)
fn pub fn unsupported_reason() -> &'static str Constant diagnostic: "ARM64 JIT backend hang detected — Unicorn emulation disabled for this session". Read by nativeEmulationReason. (emulate.rs:155)
struct #[derive(Clone, Debug)] pub struct ApiCall { pub name: String } One observed call into a tracked imported API. (emulate.rs:245)
struct #[derive(Clone, Debug, Default)] pub struct EmulationResult { pub strings: Vec<String>, pub api_calls: Vec<ApiCall> } Result of emulating one buffer. (emulate.rs:254)
fn pub fn emulate(so_bytes: &[u8]) -> EmulationResult Entry point called from lib.rs (wrapped in catch_unwind). Fast-path returns Default if emulation_available() is false, parse_elf returns None, no segments, machine isn't ARM/AArch64/x86/x86_64, or find_dynsym(.., "JNI_OnLoad") (falling back to info.entry) is 0. Then claims one of 2 process-wide slots via a non-blocking CAS on EMULATION_SLOTS (skips immediately if both busy — never waits). Spawns a worker thread running emulate_body, sharing a stop_handle: Arc<AtomicUsize> that the worker stores the raw uc_engine handle into before emu_start. Caller waits via rx.recv_timeout(HARD_DEADLINE = 600 ms). On timeout it calls uc_emu_stop(handle) through C FFI, increments HARD_TIMEOUT_COUNT, and at MAX_HARD_TIMEOUTS = 1 permanently disables emulation (EMULATION_PROBE = -1). Returns the worker's result, or default on timeout. (emulate.rs:278)

Internal emulate_body (emulate.rs:363): selects (arch, mode, sp_reg, pc_reg) and lr_reg (None on x86) from info.machine. Maps every PT_LOAD segment page-aligned RWX (skipping segments > 64 MiB), copies file bytes in, maps a dedicated stack at 0x7000_0000 (size 4 * PAGE), sets SP near the top. Maps a stub page at STUB_BASE = 0x6000_0000; for every tracked import from elf::find_imports, patches that import's GOT slot to a unique stub address and records stub_addr -> name in a shared Rc<RefCell<HashMap>>. Adds a code hook over the stub page that, on hit, records the ApiCall (capped at 256), then "returns" from the fake call by setting PC to LR (ARM/AArch64) or popping the return address off the stack (x86/x86_64); if the return target is 0 it calls emu_stop. Arms stop_handle with uc.get_handle() before uc.emu_start(entry, 0, TIMEOUT_US = 500_000, MAX_INSN = 200_000), clears it after. Then harvests every mapped region's live memory and extracts new printable ASCII strings (length ≥ 6, capped at 64) that do NOT already occur verbatim in the original file bytes (extract_new_ascii_strings, emulate.rs:539).

TRACKED_APIS (emulate.rs:228): socket, connect, send, sendto, recv, recvfrom, gethostbyname, getaddrinfo, inet_addr, inet_pton, open, open64, fopen, creat, unlink, remove, rename, system, popen, execve, execl, execvp, fork, vfork, dlopen, dlsym, ptrace, kill, __system_property_get.

ip_scan.rs — malicious-IP exact-membership lookup

Native malicious-IP exact-membership lookup against per-category Binary-Fuse (xor) blocklists (the non-CIDR entries of the allips set). No subnet/CIDR matching.

Item Signature Behavior
struct pub struct IpScanner { filters: Vec<CatFilter> } Private CatFilter { category: &'static str, filter: XorFilter }. (ip_scan.rs:11)
fn pub fn from_bytes_map(files: &HashMap<String, Vec<u8>>) -> Option<IpScanner> For each (stem, category) in the priority-ordered CATS table (ipmalware→MALWARE_IP, ipphishing→PHISHING_IP, ipbruteforce→BRUTEFORCE_IP, ipddos→DDOS_IP, ipspam→SPAM_IP), loads {stem}.xf via XorFilter::from_bytes. None if zero filters loaded. Order = severity priority. (ip_scan.rs:26)
fn pub fn scan(&self, ip: &str) -> Option<&'static str> Trims input; None if empty. Iterates filters in priority order and returns Some(category) for the first filter.contains(ip) hit, else None. Exact match on the trimmed canonical textual IP. (ip_scan.rs:44)

url_scan.rs — URL/domain threat lookup

Native URL/domain threat lookup — the Rust port of Java's UrlThreatScanner. All membership is Binary-Fuse (xor) filters loaded from .xf assets. Mirrors Java: an http(s) URL with a real path → URL scan against the *_URL xor filters (full scheme-less URL); otherwise → domain scan against the domain xor filters (registrable main domain via the public-suffix list).

Item Signature Behavior
struct pub struct UrlScanner { filters: Vec<CatFilter>, suffixes: HashSet<String> } Private CatFilter { category: &'static str, is_url: bool, filter: XorFilter }. (url_scan.rs:24)
fn pub fn load_from_assets(files: &HashMap<String, Vec<u8>>) -> Option<UrlScanner> For each (stem, category, is_url) in CATS (malwareurl→MALWARE_URL/true, phishingurl→PHISHING_URL/true, phishing→PHISHING/false, malicious→MALICIOUS/false, malicious_mail→MAIL/false, abuse→ABUSE/false, spam→SPAM/false, mining→MINING/false), loads {stem}.xf. None if zero filters loaded. Also parses public_suffixes.txt into suffixes (lowercased, skipping blank and //-comment lines). (url_scan.rs:43)
fn pub fn scan(&self, url: &str) -> Option<&'static str> Lowercases + trims; None if not http:// / https://. Strips the scheme to get norm (host[:port]/path). Computes has_path (a slash that isn't the last char). Strips any :port from the host. If has_path → URL scan: first is_url filter whose filter.contains(norm) matches (http and https to the same host+path normalize to the same key). Else → domain scan: main_domain(host) against the non-URL filters. Some(category) or None. (url_scan.rs:96)
fn (private) fn main_domain<'a>(&self, host: &'a str) -> String PSL registrable-domain derivation: mc.yandex.ru → yandex.ru; a host whose suffix is listed (e.g. com.tk) returns p[i-1..].join("."). Falls back to last-two-labels. (url_scan.rs:73)

lib.rs — JNI bridge and scan pipeline (4196 lines)

The JNI bridge exposing native methods on com.hydradragon.antivirus.engine.NativeScanner, plus the whole per-file scan pipeline: extract → metadata build → YARA-x/ClamAV → ML → TLSH → result aggregation → JSON verdict. Manages the Engine (RwLock-guarded, OnceLock'd), the batch/deferred queue, hot rule learning, VPN rules, and all user-configurable atomic toggles.

Module-level constants and globals

Item Location Notes
const YRC_FILES: &[&str] lib.rs:83 Rulesets loaded at init: clean_rules_filtered_verified.yrc, valhalla-rules_filtered_verified.yrc, machine_learning_apk.yrc, androguard.yrc, hips_rules_filtered_verified.yrc.
const VPN_YRC_FILES: &[&str] = ["emerging-all.yrc"] lib.rs:92 13 MB network-threat rules, loaded lazily on nativeEnableVpnScan(true).
const MODEL_ONNX: &str = "model.onnx" lib.rs:95
const TLSH_DB: &str = "malware_tlsh.txt" lib.rs:98
const WHITELIST_XF: &str = "whitelist.xf" lib.rs:103
const WHITELIST_PACKAGES_DB: &str = "whitelist_packages.db" lib.rs:112
const BENIGN_SIGNATURES: &str = "benign_signatures.bin" lib.rs:113
static TLSH_THRESHOLD: AtomicI32 = 40 lib.rs:120 Mutable so the Settings slider takes effect immediately. Clamped 1–200 by the setter.
static NATIVE_EMULATION_ENABLED: AtomicBool = true lib.rs:157
static MAX_SCAN_SIZE_MB: AtomicU32 = 650 lib.rs:163
static SCAN_RELEVANT_ONLY: AtomicBool = true lib.rs:170
static VPN_RULES_LOADED / VPN_SCAN_ENABLED: AtomicBool lib.rs:175 / 180
static BATCH_MODE / BATCH_ABORT: AtomicBool lib.rs:231 / 238
static JAVA_VM: OnceLock<jni::JavaVM> lib.rs:145 Lets Rust background threads call back into Java.
static ENGINE: OnceLock<RwLock<Engine>> lib.rs:151
static ASSET_FILES: OnceLock<HashMap<String, Vec<u8>>> lib.rs:185
static INIT_DIR: OnceLock<String> lib.rs:190 Writable path for generated_rules/.
static INIT_STARTED: AtomicBool lib.rs:194
static LAST_PANIC: Mutex<Option<String>> lib.rs:199
static INIT_STATUS: Mutex<String> lib.rs:340
static SCAN_SERIAL: Mutex<()> lib.rs:224 SCAN_SERIAL_MAX_WAIT = 5s (lib.rs:225).
static DEFERRED_QUEUE: OnceLock<Mutex<Vec<DeferredItem>>> lib.rs:243

The Engine struct (lib.rs:122)

struct Engine {
    clamav: Option<ClamavEngine>,
    model: Option<Model>,
    tlsh_db: Vec<tlsh_rs::TlshDigest>,
    whitelist: Option<XorFilter>,
    package_whitelist: HashMap<String, String>,
    benign_db: Option<benign_db::BenignDb>,
    url_scanner: Option<url_scan::UrlScanner>,
    ip_scanner: Option<ip_scan::IpScanner>,
}

JNI-exposed functions (25 total)

All live on Java class com.hydradragon.antivirus.engine.NativeScanner. Each uses env.with_env(...).resolve::<LogErrorAndDefault>() so a pending JNI exception is logged and a default value returned rather than aborting. The Rust function names follow Java_com_hydradragon_antivirus_engine_NativeScanner_<methodName>.

Rust fn (file:line) Java signature Returns to Java
nativeInit (853) boolean nativeInit(String assetDir, boolean loadAutoRules, Object assetManager, String filesDir) true only if the engine was already loaded; otherwise spawns the ~70 s background init thread, sets INIT_STARTED, returns false. Stores the JVM in JAVA_VM, converts the Java AssetManager via asset_reader::from_java, reads all assets, builds the engine via do_init_from_assets, sets ENGINE if clamav or model loaded. Lowers its own scheduler priority to ANDROID_PRIORITY_BACKGROUND (nice 10) so ~8 s of cold-start CPU doesn't starve the UI thread.
nativeIsReady (949) boolean nativeIsReady() ENGINE.get().is_some() — true once the async init finished.
nativeSetEmulationEnabled (960) void nativeSetEmulationEnabled(boolean enabled) Stores into NATIVE_EMULATION_ENABLED.
nativeIsEmulationAvailable (972) boolean nativeIsEmulationAvailable() emulate::probe_emulation() — cached; Java shows R.string.unicorn_unsupported when false.
nativeEmulationReason (982) String nativeEmulationReason() emulate::unsupported_reason().
nativeHostArch (994) String nativeHostArch() emulate::host_arch().
nativeSetMaxScanSizeMb (1008) void nativeSetMaxScanSizeMb(int maxMb) Stores max_mb.max(1) into MAX_SCAN_SIZE_MB.
nativeSetDetectZipBomb (1021) void nativeSetDetectZipBomb(boolean enabled) Calls hydradragonextractor::set_bomb_detection_enabled.
nativeSetScanRelevantOnly (1033) void nativeSetScanRelevantOnly(boolean on) Stores into SCAN_RELEVANT_ONLY.
nativeLearnRule (1048) boolean nativeLearnRule(String yarPath) Hot-loads one .yar into the live ClamAV engine via a brief write lock on ENGINE; true if add_yara_source_file returned Ok(Some(_)).
nativeStatus (1070) String nativeStatus() The INIT_STATUS report string (what loaded/failed during init).
nativeIsHashWhitelisted (1084) boolean nativeIsHashWhitelisted(String md5) whitelist.contains(md5) against the NSRL xor filter.
nativeIsHashWhitelistedForFile (1107) boolean nativeIsHashWhitelistedForFile(String path, String md5) Same, but first reads the file's first 2 bytes and requires PK\x04\x03 (ZIP magic) so non-APKs are never whitelisted.
nativeScanUrl (1145) String nativeScanUrl(String url) The malicious category (e.g. "PHISHING") from url_scanner.scan, or "" if clean/not a URL.
nativeScanIp (1168) String nativeScanIp(String ip) The category (e.g. "MALWARE_IP") from ip_scanner.scan, or "".
nativeScanText (1194) String nativeScanText(String text) Comma-joined matched rule/sig names from scan_text, or "".
nativeScanHips (1214) String nativeScanHips(String hipsJson) JSON `{"malicious":..,"matches":[..],"suggestion":"uninstall"
nativeEnableVpnScan (1323) void nativeEnableVpnScan(boolean enable) On true: load_vpn_rules() then sets VPN_SCAN_ENABLED; on false: clears VPN_SCAN_ENABLED (rules stay loaded).
nativeScanPackets (1377) String nativeScanPackets(String packetsJson) JSON {"malicious":..,"matches":[..]} from scan_packets; {"malicious":false} no-op if VPN scan disabled.
nativeScanApk (1397) String nativeScanApk(String path, String hydradragonJson, String fileMd5, boolean zeroTrust) The full scan verdict JSON from scan_apkrun_scan (or {"status":"deferred","path":...} in batch mode).
nativeBeginBatchScan (1466) void nativeBeginBatchScan() Sets BATCH_MODE=true, BATCH_ABORT=false, clears the deferred queue.
nativeAbortBatchScan (1480) void nativeAbortBatchScan() Sets BATCH_ABORT=true so the flush loop bails early.
nativeEndBatchScan (1488) String nativeEndBatchScan() Clears BATCH_MODE, drains the queue, runs run_deferred_item for each (honoring BATCH_ABORT), returns "[v1,v2,...]" JSON array of verdicts.
nativeSetTlshThreshold (1879) void nativeSetTlshThreshold(int threshold) Stores threshold.clamp(1,200) into TLSH_THRESHOLD.
nativeTlshDiff (1892) int nativeTlshDiff(String tlsh1, String tlsh2) d1.diff(&d2), or -1 on parse/error. Used by the Anti-FP cache.

Key internal (non-JNI) functions

Signature Behavior
fn do_init_from_assets(files: &HashMap<String, Vec<u8>>, load_auto_rules: bool) -> Engine (lib.rs:348) Parallel init via std::thread::scope spawning 8 threads: ClamAV (+ all YRC_FILES compiled in parallel + optional generated_rules/*.yar from INIT_DIR), ONNX model, TLSH DB, NSRL whitelist, package whitelist, URL scanner, IP scanner, benign DB. Each wrapped in catch_unwind and producing a status-report fragment; set_status publishes the concatenated report. Returns the populated Engine.
fn load_package_whitelist_from_bytes(bytes: &[u8]) -> HashMap<String, String> (lib.rs:607) Writes bytes to a temp file hydra_wl_pkg.db (rusqlite needs a path), opens it read-only, runs SELECT key, md5 FROM whitelist_package WHERE md5 IS NOT NULL, lowercases the md5, removes the temp file. One 17 MB write per process lifetime.
fn load_vpn_rules() (lib.rs:1231) Idempotent (guarded by VPN_RULES_LOADED); under a write lock on ENGINE, compiles each VPN_YRC_FILES entry from ASSET_FILES and add_compiled_yara into the live ClamAV engine.
fn scan_hips(hips_json: &str) -> String (lib.rs:1254) Early-validates JSON; builds module_meta = [("hydradragon", hips_json)]; scans the JSON bytes named "hips_behavior"; returns `{"malicious":..,"matches":[..],"suggestion":"uninstall"
fn scan_text(text: &str) -> String (lib.rs:1293) Caps text at 8192 bytes; builds module_meta = [("hydradragon", r#"{"screen_text":"..."}"#)]; scans named "screen_text"; returns comma-joined match names.
fn scan_packets(packets_json: &str) -> String (lib.rs:1339) No-op {"malicious":false} if VPN_SCAN_ENABLED false; validates JSON; builds module_meta = [("hydradragon", r#"{"network":{"packets":...}}"#)]; scans named "vpn_traffic"; returns {"malicious":..,"matches":[..]}.
fn scan_apk(env, path, hydradragon_json, file_md5, zero_trust) -> String (lib.rs:1411) Reads the file, takes a read lock on ENGINE, calls run_scan on on_big_stack (64 MB stack). On a thread panic returns {"error":"scan panicked: ...","malicious":false} using LAST_PANIC.
fn run_deferred_item(engine: &Engine, item: DeferredItem) -> String (lib.rs:1532) The Phase-3 runner for batch mode: rebuilds module_meta from the stored androguard_json/hydradragon_meta, runs emulation (same MAX_EMULATED_BUFFERS=8 cap + dedupe), rescan_buffers_parallel for ClamAV/YARA, ML model, severe DEX findings, TLSH, then assembles the same verdict JSON as run_scan. skip_heavy is rebuilt as all-false because only non-whitelisted buffers were stored.
fn rescan_buffers_parallel(clamav, engine, buffers, skip_heavy, dex_scans, emulated, emulated_strings, module_meta, path, opts, max_dets, scan_timing) -> Vec<(String, String, Vec<String>)> (lib.rs:2032) The full-module-meta ClamAV/YARA pass over all buffers, parallelized across available_parallelism().clamp(1,4) scoped workers using an atomic-counter work-stealing scheme. Each worker: claims an index; skips by size / by SCAN_RELEVANT_ONLY (except top-level i==0); scans the raw buffer (scan_bytes_named_with_breakdown, panic-isolated per buffer); if Base64_Encoded_URL matched, runs extract_decode_base64_urls against url_scanner; scans the DEX string pool ({name}#dex); scans emulated strings ({name}#emulated) and runs extract_and_scan_urls; emits Behavior.Native: {api} dets for each unique emulated API call. Capped at max_dets (64). Merges per-worker timing via TimingBreakdown::accumulate.
fn run_scan(engine, bytes, path, hydradragon, file_md5, zero_trust) -> String (lib.rs:2239) The master pipeline (see data flow below). Returns the verdict JSON.
fn generate_yara_rule(file_hash, packages, detections, dex_scans, only_index) -> Option<String> (lib.rs:2838) Builds a yarGen-style YARA rule named auto_{file_hash}, import "androguard" + import "hydradragon". Strings: up to 40 DEX string-pool entries (length 8–128, no control chars, deduped), scoped to only_index if given. Condition groups: package-name ORs via androguard.package_name(...), N of them (N = strings.len().min(6).max(1)), androguard.rootkit_behavior() == 1, plus hydradragon.api_call(/.../) > 0 for launcher-hijack / suspicious-API patterns and hydradragon.dex_severe_finding_count() > 0 when severe findings exist. Also emits a HIPS runtime branch (OR'd) using hydradragon.ui_spam / notification_spam / clickjack / ransomware / strandhogg / removal_resistance / launcher_change / network_connections with a package regex. None if both strings and packages are empty.
fn parse_manifest(data: &[u8]) -> Option<Manifest> (lib.rs:3370) Full binary-AXML walker producing Manifest { package, app_name, permissions, activities, services, receivers, main_activity, min_sdk, max_sdk, target_sdk }. Tracks an activity_stack and in_intent_filter/has_action_main/has_category_launcher to find the enabled MAIN/LAUNCHER activity (the icon-hiding enabled="false" + kept intent-filter trick is handled). Guard-bounded (200k chunks, 256 attrs, 64-deep activity stack, 4096 components).
fn build_androguard_json(buffers: &[Buf]) -> Option<String> (lib.rs:3570) Finds the first parseable manifest, collects URLs, extracts the certificate, emits the androguard JSON (see module metadata contract).
fn collect_urls(buffers: &[Buf]) -> Vec<String> (lib.rs:3525) Sweeps all buffers for http:// / https:// substrings, extending until whitespace/quote/<>/backslash/control/>=0x80; keeps URLs 10–2048 bytes, deduped, capped at 4096.
fn extract_certificate(buffers: &[Buf]) -> Option<CertInfo> (lib.rs:3627) Finds the `META-INF/*.RSA
fn collect_buffers(data, top_md5, path) -> (Vec<Buf>, Vec<(String,String,Vec<String>)>) (lib.rs:4007) Phase 1 extractor. Parallel work-stealing over a shared Mutex<Vec<WorkItem>> stack with an outstanding counter for termination; available_parallelism().clamp(1,4) workers; each wrapped in catch_unwind (a panicking worker sets capped=true to force-stop the rest). Caps: depth < 16, 4096 buffers, 2 GB total decompressed. Zip-bomb errors (hydradragonextractor::is_bomb_error) become HDR.Bomb.Decompression detections. Each Buf carries its apk_lineage (MD5s of ancestor zips, including its own when it is a zip) and entry_name.
fn merge_dex_findings(hydradragon: Option<&[u8]>, dex_scans: &[Option<DexScan>]) -> Option<Vec<u8>> (lib.rs:3234) Folds DEX findings (any severity) into a dex_findings array and aggregates per-API-signature invocation counts into an api_calls array ("sig\tcount", sorted, capped 4096), merged on top of Java's hydradragon JSON (or an empty object). None if the result is an empty object.
fn max_dangerous_perms(buffers: &[Buf]) -> usize (lib.rs:3085) Counts distinct DANGEROUS_PERMS (lib.rs:3018, ~37 entries) present in any buffer ≤ 4 MiB, checking both UTF-8 and UTF-16LE encodings; returns the max over buffers.
fn collect_apk_hashes(buffers, top_md5) -> Vec<String> (lib.rs:3934) Lowercase MD5 of every zip/APK buffer (reuses top_md5 for the top-level), deduped, capped 64.
fn collect_packages(buffers: &[Buf]) -> Vec<String> (lib.rs:3957) axml_package of each buffer, deduped, capped 64.
fn tlsh_nearest(engine: &Engine, buf: &[u8]) -> Option<i32> (lib.rs:807) tlsh_rs::hash_bytes(buf); min diff over engine.tlsh_db; returns Some(best) only if best <= TLSH_THRESHOLD, else None. Empty DB → None.
fn extract_and_scan_urls(engine: &Engine, decoded: &[u8]) -> Vec<String> (lib.rs:1926) Splits decoded emulation strings on \n, keeps lines starting http(s)://, scans each via url_scanner, emits "URL.{cat}: {url}" (capped 16).
fn extract_decode_base64_urls(data, scanner) -> Vec<String> (lib.rs:1970) Scans for the B64_URL_PREFIXES (lib.rs:1956: standard + UTF-16LE base64 of http:///https://), extends over base64 chars + up to 2 = (≤100 bytes), decodes, scans decoded URLs via scanner. Emits "URL.{cat}: {s}" (capped 16).
helpers md5_hex (3921), sha1_hex/sha1_hash (3845/3855, hand-rolled RFC 3174), json_escape (320), is_text_like (657), is_obfuscated_xml (694), is_resource_path (700), is_relevant_buffer (716), has_network_indicators/has_base64/has_embedded_data (749/780/796), skip_by_size (642, ≤12 || > MAX_SCAN_SIZE_MB MB), tlsh_relevant (648, zip/ELF/DEX), on_big_stack (831, 64 MB stack thread), acquire_scan_serial_bounded (277, 5 s budget then proceeds without the lock), install_panic_hook (298), last_panic (4190), android_log/rust_timing_log! (62/75), deferred_queue (246).

The Buf struct (lib.rs:3985): { data: Vec<u8>, apk_lineage: Vec<String>, entry_name: Option<String> }. DeferredItem (lib.rs:254): { path, zero_trust, buffers, dex_scans, androguard_json, hydradragon_meta, bomb_dets, perm_count, packages, hashes, extract_ms, dex_ms }.

Data flow — the main scan pipeline

Entry: nativeScanApk (lib.rs:1397) → scan_apk (lib.rs:1411) → on_big_stack(|| run_scan(...)) (lib.rs:2239). The SCAN_SERIAL mutex is acquired with a 5 s bounded wait first (so one stuck file can't freeze every other scan).

  • Phase 0 — serial gate. acquire_scan_serial_bounded() (lib.rs:2248).
  • Phase 1 — extract (collect_buffers, lib.rs:4007, called at lib.rs:2272). Recursively unpacks the file and all nested archives (zip/gz/tar/xz/lzma/7z/rar via hydradragonextractor) into Vec<Buf> with apk_lineage MD5 chains. Zip-bomb errors become HDR.Bomb.Decompression detections returned alongside. Capped at 4096 buffers / 2 GB / depth 16. Timed as extract_ms.
  • Phase 2 — metadata + fast passes + skip computation (lib.rs:2283). All run before any heavy scan: max_dangerous_permsperm_count; collect_packages (AXML package names); collect_apk_hashes (reuses Java's MD5 for the top-level); build_androguard_json (manifest + URLs + cert); whitelist skip_heavy construction (NSRL hash, MinHash benign, ancestor-lineage hash/package checks); DEX static analysis (dex_scan::scan on each non-skipped dex\n buffer, timed as dex_ms); merge_dex_findingshydradragon_meta; module_meta built (("androguard", …) + ("hydradragon", …)); native emulation (emulate::emulate on each non-skipped \x7fELF buffer, deduped by MD5, capped at MAX_EMULATED_BUFFERS = 8, timed as emulate_ms).
  • Batch deferral (lib.rs:2465). If BATCH_MODE, store only non-whitelisted buffers + their Phase-2 data in a DeferredItem, push to DEFERRED_QUEUE, return {"status":"deferred","path":...}. Phase 3 runs later in nativeEndBatchScan via run_deferred_item.
  • All-whitelisted short-circuit (lib.rs:2500). If every skip_heavy[i] is true, skip all Phase 3 and return a clean verdict (only bomb_dets can make it malicious) with file_tlsh.
  • Phase 3a — ML model (lib.rs:2528). model.scan(&b.data) on every zip buffer (no per-buffer skip; either all were whitelisted or none), tracking ml_malicious, best confidence (ml_jaccard/ml_anomaly), ml_nearest, and ml_lineages (object_path → apk_lineage). Timed as ml_ms.
  • Phase 3b — ClamAV + YARA (rescan_buffers_parallel, lib.rs:2579). Full module_meta pass over all buffers with no_skip = vec![false; len], panic-isolated per buffer, plus DEX-string-pool scans, emulated-string scans, base64-URL decoding, emulation-API behavior signals. Returns (name, object_path, lineage) tuples, capped at 64. Produces clamav_ms and per-engine yara_total_ms.
  • Phase 3c — detection aggregation (lib.rs:2613). detections = bomb_dets + yara_dets + ml_lineages + severe DEX findings (DEX/{sev}: {msg}) + TLSH (TLSH.Malware/dist={d}).
  • Phase 3d — TLSH (lib.rs:2637). tlsh_nearest on each tlsh_relevant buffer; timed as tlsh_ms.
  • Phase 4 — generated rule + JSON assembly (lib.rs:2701). malicious = !detections.is_empty(). Computes scoped_entry_idx (when all detections share the same outer!/entry suffix, scope the generated rule to that entry). generate_yara_rule is built iff malicious || zero_trust. Builds per-entry entry_md5s/entry_tlshs maps (capped 1024) and top-level file_tlsh for the Anti-FP cache. Emits the per-stage timing breakdown to logcat (HydraDragon-RustTiming). Final JSON shape:
{"malicious":..,"matches":[..],"detections":[{"name","object_path","hashes"}],
 "permissions":N,"packages":[..],"hashes":[..],"md5":"..",
 "file_tlsh":"..",
 "ml":{"malicious":..,"jaccard":..,"anomaly":..,"nearest":..|null},
 "generated_rule":".."|null,"entry_md5s":{..},"entry_tlshs":{..}[,"error":".."]}

(The batch-path run_deferred_item JSON additionally includes a leading "path":"..." and omits the optional error field.)

Module metadata contract

The YARA-X module metadata is passed as module_meta: &[(&str, &[u8])] to hydradragonclamav::ClamavEngine::scan_bytes_named[_with_breakdown]. That engine forwards these to yara-x's set_module_metadata(name, bytes) for each named module before scanning. Two modules are populated for APK scans:

androguard (built by build_androguard_json, lib.rs:3570):

{
  "package_name": String|null,
  "app_name": String|null,
  "main_activity": String|null,
  "activities": [String],
  "services": [String],
  "receivers": [String],
  "permissions": [String],
  "new_permissions": [String],
  "urls": [String],
  "min_sdk_version": String|null,
  "max_sdk_version": String|null,
  "target_sdk_version": String|null,
  "certificate": { "subjectDN": String|null, "IssuerDN": String|null, "sha1": String|null }
}

Source: parse_manifest for everything except urls (collect_urls) and certificate.* (extract_certificateCertInfo). build_androguard_json returns None (no androguard entry pushed) if no buffer parses as a binary AXML manifest. certificate.* is parsed from the PKCS#7 signature block (META-INF/*.RSA|.DSA|.EC); subject/issuer are OpenSSL-style /key=value/... DNs and sha1 is the SHA-1 of the full certificate DER.

hydradragon (built by merge_dex_findings, lib.rs:3234): starts from Java's live-network/HIPS hydradragon JSON (the hydradragonJson arg to nativeScanApk, parsed if it's a JSON object) or an empty {}, then merges:

{
  "dex_findings": [ { "severity": "Critical"|..., "kind": "...", "class_descriptor": "...", "message": "..." } ],
  "api_calls": [ "Lpkg/Cls;->method(params)return\t<count>", ... ]
}

dex_findings is added only if non-empty; api_calls is the aggregated invocation-count histogram across all DEX buffers (each entry "sig\tcount", the count being the sum across buffers). merge_dex_findings returns None (no hydradragon entry pushed) if the merged object is empty.

Special-purpose scans build their own single-entry module_meta: scan_hips[("hydradragon", hips_json)] named "hips_behavior"; scan_text[("hydradragon", r#"{"screen_text":"..."}"#)] named "screen_text"; scan_packets[("hydradragon", r#"{"network":{"packets":...}}"#)] named "vpn_traffic".

The full schema of both modules (every field the YARA-X side reads) is in YARA-X-Modules.


hydradragonclamav — ClamAV engine

Detailed function-level reference for this crate is being added below. See ClamAV-Integration for the conceptual overview in the meantime.


hydradragonml — ONNX classifier

Detailed function-level reference for this crate is being added below. See AI-ML-Models for the conceptual overview in the meantime.


hydradragonextractor — archive extraction

Detailed function-level reference for this crate is being added below.


hydradragonxorfilter — Binary-Fuse filters

Detailed function-level reference for this crate is being added below. See NSRL-Whitelisting for the conceptual overview in the meantime.

Clone this wiki locally