Skip to content

Rust API Reference

Emirhan Uçan edited this page Aug 2, 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/ Burn neural-network 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 hydradragon module schema (incl. the former androguard static-APK part); 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, Burn ML model .mpk + vocab.json, 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) 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 → result aggregation → JSON verdict. Manages the Engine (RwLock-guarded, OnceLock'd), 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:96 Rulesets loaded at init: clean_rules_filtered_verified.yrc, valhalla-rules_filtered_verified.yrc, machine_learning_apk.yrc, hydradragon.yrc, hips_rules_filtered_verified.yrc.
const MODEL_MPK: &str = "model.mpk" lib.rs:111 Burn ML model file (.mpk recorder), loaded via Model::load together with VOCAB_JSON.
const VOCAB_JSON: &str = "vocab.json" lib.rs:112 Tokenizer vocabulary for the Burn model.
const WHITELIST_XF: &str = "whitelist.xf" lib.rs:125
const WHITELIST_PACKAGES_DB: &str = "whitelist_packages.csv" lib.rs:134
const BENIGN_SIGNATURES: &str = "benign_signatures.bin" lib.rs:135
static NATIVE_EMULATION_ENABLED: AtomicBool = true lib.rs:184
static MAX_SCAN_SIZE_MB: AtomicU32 = 650 lib.rs:196
static SCAN_RELEVANT_ONLY: AtomicBool = true lib.rs:214
static VPN_RULES_LOADED / VPN_SCAN_ENABLED: AtomicBool lib.rs:238 / 243
static JAVA_VM: OnceLock<jni::JavaVM> lib.rs:172 Lets Rust background threads call back into Java.
static ENGINE: OnceLock<RwLock<Engine>> lib.rs:178
static ASSET_FILES: Mutex<Option<HashMap<String, Vec<u8>>>> lib.rs:257
static INIT_DIR: OnceLock<String> lib.rs:267 Writable path for generated_rules/.
static INIT_STARTED: AtomicBool lib.rs:271
static LAST_PANIC: Mutex<Option<String>> lib.rs:387
static INIT_STATUS: Mutex<String> lib.rs:481
static SCAN_SERIAL: Mutex<()> lib.rs:412 SCAN_SERIAL_MAX_WAIT = 5s (lib.rs:413).

The Engine struct (lib.rs:144)

struct Engine {
    clamav: Option<ClamavEngine>,
    model: Option<Model>,
    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 (1689) 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 (1810) boolean nativeIsReady() ENGINE.get().is_some() — true once the async init finished.
nativeSetEmulationEnabled (1821) void nativeSetEmulationEnabled(boolean enabled) Stores into NATIVE_EMULATION_ENABLED.
nativeIsEmulationAvailable (1858) boolean nativeIsEmulationAvailable() emulate::probe_emulation() — cached; Java shows R.string.unicorn_unsupported when false.
nativeEmulationReason (1868) String nativeEmulationReason() emulate::unsupported_reason().
nativeHostArch (1880) String nativeHostArch() emulate::host_arch().
nativeSetMaxScanSizeMb (1894) void nativeSetMaxScanSizeMb(int maxMb) Stores max_mb.max(1) into MAX_SCAN_SIZE_MB.
nativeSetDetectZipBomb (1939) void nativeSetDetectZipBomb(boolean enabled) Calls hydradragonextractor::set_bomb_detection_enabled.
nativeSetScanRelevantOnly (1951) void nativeSetScanRelevantOnly(boolean on) Stores into SCAN_RELEVANT_ONLY.
nativeLearnRule (1996) 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 (2145) String nativeStatus() The INIT_STATUS report string (what loaded/failed during init).
nativeIsHashWhitelisted (2173) boolean nativeIsHashWhitelisted(String md5) whitelist.contains(md5) against the NSRL xor filter.
nativeIsHashWhitelistedForFile (2196) 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 (2234) String nativeScanUrl(String url) The malicious category (e.g. "PHISHING") from url_scanner.scan, or "" if clean/not a URL.
nativeScanIp (2257) String nativeScanIp(String ip) The category (e.g. "MALWARE_IP") from ip_scanner.scan, or "".
nativeScanText (2283) String nativeScanText(String text) Comma-joined matched rule/sig names from scan_text, or "".
nativeScanHips (2303) String nativeScanHips(String hipsJson) JSON `{"malicious":..,"matches":[..],"suggestion":"uninstall"
nativeEnableVpnScan (2421) void nativeEnableVpnScan(boolean enable) On true: load_vpn_rules() then sets VPN_SCAN_ENABLED; on false: clears VPN_SCAN_ENABLED (rules stay loaded).
nativeScanPackets (2459) String nativeScanPackets(String packetsJson) JSON {"malicious":..,"matches":[..]} from scan_packets; {"malicious":false} no-op if VPN scan disabled.
nativeScanApk (2479) String nativeScanApk(String path, String hydradragonJson, String fileMd5, boolean zeroTrust) The full scan verdict JSON from scan_apkrun_scan.

Key internal (non-JNI) functions

Signature Behavior
fn do_init_from_assets(files: &HashMap<String, Vec<u8>>, load_auto_rules: bool) -> Engine (lib.rs:489) Parallel init via std::thread::scope spawning 7 threads: ClamAV (+ all YRC_FILES compiled in parallel + optional generated_rules/*.yar from INIT_DIR), Burn ML model, 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:796) 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.
fn load_vpn_rules() -> bool (lib.rs:2321) Idempotent (guarded by VPN_RULES_LOADED); under a write lock on ENGINE, compiles emerging-all.yrc from ASSET_FILES and add_compiled_yara into the live ClamAV engine.
fn scan_hips(hips_json: &str) -> String (lib.rs:2351) 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:2390) 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:2440) 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:2493) 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_scan(engine, bytes, path, hydradragon, file_md5, zero_trust) -> String (lib.rs:2697) 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:3582) Builds a yarGen-style YARA rule named auto_{file_hash}, 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 hydradragon.package_name(...), N of them (N = strings.len().min(6).max(1)), hydradragon.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:4126) 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_hydradragon_json(buffers: &[Buf], urls: &[String]) -> Option<String> (lib.rs:4293) Finds the first parseable manifest, extracts the certificate, emits the hydradragon JSON (see module metadata contract).
fn extract_certificate(buffers: &[Buf]) -> Option<CertInfo> (lib.rs:4357) Finds the `META-INF/*.RSA
fn collect_buffers(data, top_md5, path) -> (Vec<Buf>, Vec<(String,String,Vec<String>)>) (lib.rs:4746) 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>], manifest_report: Option<&str>) -> Option<Vec<u8>> (lib.rs:3978) Folds the manifest report (AXML + URLs + cert) into the root object, adds 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:3829) Counts distinct DANGEROUS_PERMS (lib.rs:3762, ~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:4664) 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:4689) axml_package of each buffer, deduped, capped 64.
fn extract_and_scan_urls(engine: &Engine, decoded: &[u8]) -> Vec<String> (lib.rs:2598) 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:2644) Scans for the B64_URL_PREFIXES (lib.rs:2628: 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 (4651), sha1_hex/sha1_hash (4575/4585, hand-rolled RFC 3174), json_escape (461), is_obfuscated_xml (1185), is_resource_path (1191), is_relevant_buffer (1365), is_executable_buffer (1225), is_media_file (1278), is_image_buffer (1305), has_embedded_data (1424), skip_by_size (853, ≤12 || > MAX_SCAN_SIZE_MB MB), on_big_stack (1667, 64 MB stack thread), acquire_scan_serial_bounded (418, 5 s budget then proceeds without the lock), install_panic_hook (439), last_panic (5016), build_engine_features (917), android_log/rust_timing_log! (62/75).

The Buf struct (lib.rs:4718): { data: Vec<u8>, apk_lineage: Vec<String>, entry_name: Option<String>, fmt: Option<&'static str> }.

Data flow — the main scan pipeline

Entry: nativeScanApk (lib.rs:2479) → scan_apk (lib.rs:2493) → on_big_stack(|| run_scan(...)) (lib.rs:2697). 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:418, called at lib.rs:2709).
  • Phase 1 — streaming extract+scan (collect_buffers, lib.rs:4746, called at lib.rs:2764). 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, while ClamAV+YARA scan each buffer inline as extraction proceeds (streaming timing/dets accumulated). 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:2768). 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_hydradragon_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 (single ("hydradragon", …)); native emulation (emulate::emulate on each non-skipped \x7fELF buffer, deduped by MD5, capped at MAX_EMULATED_BUFFERS = 8, timed as emulate_ms). Timed as phase2_ms.
  • All-whitelisted short-circuit (lib.rs:2953). If every skip_heavy[i] is true, skip all Phase 3 and return a clean verdict (only bomb_dets can make it malicious).
  • Phase 3 — parallel heavy phases (lib.rs:3006). YARA rescan, emulation signal, ML and URL scanning all depend on read-only buffers/dex_scans/module_meta, so they run concurrently inside one std::thread::scope (wall time = slowest phase, not the sum). Threads: (1) module-meta YARA rescan + emulation signal over flagged buffers, (2) ClamAV/YARA content scan over the precomputed scan_items (see below), (3) ML model over zip buffers. Produces clamav_ms, per-engine yara_total_ms, ml_ms; whole scope wall time logged as scope_ms.
  • Scan-item hoist (lib.rs:3039). The eligible-buffer work list (ScanItem { idx, obj_path, data, is_vid }) is computed once up front so ClamAV and YARA passes run on two separate threads over the same read-only slice, overlapping runtimes (wall ≈ max, not sum). Video buffers are re-encoded to metadata via media_scan::extract_metadata; media gating honors SCAN_MEDIA_ENABLED/SCAN_RELEVANT_ONLY.
  • Detection aggregation. detections = bomb_dets + streaming_dets (minus whitelisted) + yara_dets + ml_lineages + severe DEX findings (DEX/{sev}: {msg}).
  • Phase 4 — generated rule + JSON assembly (lib.rs:~3577). 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 (auto-rule generation also gated by the AUTO_RULE_GEN_ENABLED native flag). Builds per-entry entry_md5s maps (capped 1024). Emits the per-stage timing breakdown to logcat (HydraDragon-RustTiming): extract/dex/emulate/phase2/clamav/yara/ml/scope. Final JSON shape:
{"malicious":..,"matches":[..],"detections":[{"name","object_path","hashes"}],
 "permissions":N,"packages":[..],"hashes":[..],"md5":"..",
 "ml":{"malicious":..,"jaccard":..,"anomaly":..,"nearest":..|null},
 "generated_rule":".."|null,"entry_md5s":{..}[,"error":".."]}

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) before scanning. One metadata key is pushed for APK scans, consumed by the single hydradragon YARA-X module (the former separate androguard module is folded into it — there is no androguard key anymore):

hydradragon (built by merge_dex_findings, lib.rs:3978): starts from Java's live-network/HIPS hydradragon JSON (the hydradragonJson arg to nativeScanApk, parsed if it's a JSON object) or an empty {}, merges in the manifest report (built by build_hydradragon_json, lib.rs:4293) as the flat object, then adds:

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

Source: parse_manifest for everything except urls (swept inline from the buffers) and certificate.* (extract_certificateCertInfo). build_hydradragon_json returns None (no manifest fields merged) 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.

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 the module (every field the YARA-X side reads) is in YARA-X-Modules.


hydradragonclamav — ClamAV engine

Pure-Rust reimplementation of ClamAV's non-hash signature loading and scanning engine. Crate root: hydradragonclamav/src/. Dependencies that shape the design: yara-x (fork, features pulley + hydradragon-module + dex-module) for YARA scanning; regex for PCRE subsigs and .cdb/.ftm regex fields; memchr for SIMD literal pre-checks; jdb_xorf (Binary-Fuse16 filters) for the atom prefilter; image + rustdct for the perceptual image fuzzy hash (byte-identical to ClamAV's libclamav_rust).

lib.rs re-exports the headline types: Bytecode, BytecodeSet (from bytecode); ContainerSignature, ContainerType, Database, FileTypeMagic, LoadError, LoadReport, NumSpec, UnsupportedRecord (from database); Engine, ScanMatch, ScanOptions, ScanView, SignatureKind (from scanner); YaraEngine (from yara_scan). All other modules are pub mod (reachable as hydradragonclamav::pattern::Pattern etc.) but not re-exported at the crate root.

Source files (21): atomfilter.rs, atomfilter_build.rs, atomfilter_hash.rs, atomscan.rs, bytecode.rs, bytecode_vm.rs, cert.rs, database.rs, filtering.rs, fuzzy.rs, icon.rs, icon_match.rs, lib.rs, logical.rs, main.rs, pattern.rs, pe.rs, phishing.rs, scanner.rs, version_info.rs, yara_scan.rs (+ phishing/tld.rs).

ENGINE_FLEVEL = 240 (in bytecode_vm.rs) is the crate-wide engine functionality level; database and logical both gate signatures against it.

lib.rs (22 lines)

Crate root. Declares all 14 public modules and re-exports the headline types above. No logic.

pe.rs (112 lines) — PE header parser

Minimal PE/PE32+ header parser used by the scanner and the version-info/icon modules.

  • pub struct PeInfo (:2) — entry_point_offset: Option<usize>, sections: Vec<Section>, vinfo: Vec<u32> (sorted file offsets where VS_VERSION_INFO string entries begin), res_rva: u32 (resource data-directory RVA), size_of_headers: u32.
  • pub struct Section (:16) — raw_start: usize, raw_size: usize, virtual_address: u32, virtual_size: u32.
  • pub fn parse_pe(data: &[u8]) -> Option<PeInfo> (:23) — validates MZ + PE\0\0, reads COFF section count + optional-header size, accepts PE32 (0x10b) or PE32+ (0x20b), builds the section table, resolves the entry-point RVA to a file offset via rva_to_offset, reads SizeOfHeaders and dirs[2], calls version_info::version_info_offsets. None on any bounds/magic failure.
  • pub fn rva_to_offset(rva: u32, sections: &[Section]) -> Option<usize> (:93) — maps an RVA to a raw file offset.

version_info.rs (298 lines) — VS_VERSION_INFO offset extraction

Faithful port of ClamAV's findres(0x10,…) + the VS_VERSION_INFO walk in pe.c. Produces the sorted set of file offsets where version-info string entries begin (ClamAV's peinfo->vinfo, each entry vptr - baseptr + 6), so a VI-anchored signature matches only at one of these.

  • pub fn version_info_offsets(data: &[u8], res_rva: u32, sections: &[Section], hdr_size: u32) -> Vec<u32> (:281) — sorted, deduped file offsets. Empty when res_rva == 0 or no parseable RT_VERSION resource.

cert.rs (238 lines) — .crb Authenticode cert trust/block loader

Loader for .crb (Authenticode certificate trust/block rules), ported from cli_loadcrt. Parses every record byte-exactly; the PKCS#7 verification engine is a separate follow-up, so certs are loaded and counted but not yet matched at scan time.

  • pub struct CertEntry (:33) — name: String, blocked: bool (trusted==0), subject: [u8; 20] (SHA-1), serial: Option<[u8; 20]> (None = ignore_serial), pubkey: Vec<u8>, code_sign: bool, time_sign: bool, cert_sign: bool, not_before: Option<i64>, source: SourceLocation.
  • pub struct CertTrustDb (:54) — certs: Vec<CertEntry>.
    • pub fn is_empty(&self) -> bool (:59); pub fn len(&self) -> usize (:63).
    • pub fn add_line(&mut self, line: &str, source: SourceLocation, flevel: u32) -> Result<bool, String> (:70) — parses name;trusted;subject;serial;pubkey;exp;codesign;timesign;certsign;notbefore;comment[;minFL[;maxFL]]. Returns Ok(false) when f-level gating excludes the engine, Ok(true) when added, Err on malformed.

icon.rs (253 lines) — .idb icon fingerprint loader

Loader for .idb icon signatures (Name:Group1:Group2:Metric), ported from cli_loadidb. Parses the 124-nibble (62-byte) fuzzy-image fingerprint of a PE icon, bucketed by icon size (16/24/32 → bucket (size>>3)-2 = 0/1/2), with interned group names. The matcher (icon_match.rs) is separate.

  • pub struct IconMetric (:23) — name: String, group: [u32; 2], [u32; 3] triples for color_avg/x/y, gray_*, bright_*, dark_*, edge_*, noedge_*, then rsum/gsum/bsum/ccount: u32, source.
  • pub struct IconMatcher (:56) — icons: [Vec<IconMetric>; 3] (size buckets), group_names: [Vec<String>; 2].
    • pub fn is_empty(&self) -> bool (:62); pub fn len(&self) -> usize (:67).
    • pub fn add_line(&mut self, line: &str, source: SourceLocation) -> Result<(), String> (:82) — validates 4 tokens, 124-nibble metric, size byte ∈ {16,24,32}, centroid bounds, color/gray avg ≤ 4072, rsum+gsum+bsum ≤ 103, ccount ≤ 100.

icon_match.rs (1003 lines) — PE icon matcher (matchicon)

Faithful port of ClamAV's pe_icons.c (USE_FLOATS path): walk RT_GROUP_ICON/RT_ICON resources, decode the BMP icon, scale to 16/24/32, compute the getmetrics fingerprint, and compare against loaded .idb fingerprints via matchpoint/matchbwpoint. Evaluates the IconGroup1/IconGroup2 TDB constraint.

  • pub fn matchicon(data: &[u8], sections: &[Section], hdr_size: u32, res_rva: u32, matcher: &IconMatcher, grp1: Option<&str>, grp2: Option<&str>) -> bool (:984) — true iff some PE icon matches an .idb fingerprint in the requested groups. Empty matcher or res_rva == 0false.

Internal load-bearing functions: find_resources (general findres, cap 64), getmetrics (the full fingerprint: HSV color presence, top-3 non-overlapping color/gray/bright/dark areas, Sobel edge detection via labdiff, separable Gaussian blur, edge/no-edge area sums, bwonly fallback), matchpoint/matchbwpoint (centroid-distance + average-difference similarity), icon_confident (per-candidate confidence test), parseicon (BMP decode for depths 1/4/8/16/24/32, palette, AND-mask → alpha, alpha-blend over white, scale modes 0/1/2), groupiconscan, GroupSet, lab/labdiff/hsv.

fuzzy.rs (216 lines) — image perceptual pHash

Faithful port of ClamAV's fuzzy_hash_calculate_image, using the same image + rustdct crates so the 64-bit hash is byte-identical to fuzzy_img#<hash> subsignatures. Only exact (hamming distance 0) matches supported, same as ClamAV.

  • pub fn calculate_image(buffer: &[u8]) -> Option<[u8; 8]> (:74) — fast-rejects non-image magic and >50 MB buffers, decodes under catch_unwind, drops alpha, applies ITU-R 601-2 luma, resizes to 32×32 Lanczos3, runs 2-D DCT-2, takes the top-left 8×8 block, thresholds against the median of 64 values, packs big-endian into 8 bytes.
  • pub fn parse_fuzzy_img(raw: &str) -> Result<[u8; 8], String> (:163) — parses fuzzy_img#<hex>[#<distance>]. Rejects unknown algorithms, non-16-char hex, non-zero hamming distance.

filtering.rs (292 lines) — shift-or 2-gram filter

Rust port of ClamAV's libclamav/filtering.c ("A fast filter for static patterns"). A bit-parallel shift-or automaton over overlapping little-endian 2-grams with a second end table; can false-positive but never false-negative.

  • pub struct Filter (:36) — b: Box<[u8; 65536]>, end: Box<[u8; 65536]>, loaded: bool. 128 KiB, boxed.
    • pub fn new() -> Self (:53) — filter_init: all bits set.
    • pub fn is_loaded(&self) -> bool (:61).
    • pub fn add_static(&mut self, pattern: &[u8]) -> Option<usize> (:93) — filter_add_static: picks the best MAXSOPATLEN (8)-byte subpattern by ClamAV's signed-int scoring, registers every overlapping 2-gram, marks the end. Returns the registered length or None if len < 2. Caps at MAXPATLEN (255).
    • pub fn search(&self, data: &[u8]) -> i64 (:182) — filter_search (__hot__): approximate match start position, or -1.
    • pub fn search_ext(&self, data: &[u8]) -> Option<usize> (:205) — exact position of the first possible match end.

atomfilter_hash.rs (103 lines) — rolling polynomial hash

Rolling polynomial hash over fixed-width byte windows, used to derive atom keys for the Binary-Fuse16 filters. Incrementally updatable per byte, so sweeping every length-len window is O(n).

  • pub const ATOM_LENGTHS: [usize; 5] = [2, 4, 8, 12, 16] (:20) — canonical atom-key window lengths (2..=16).
  • pub fn bucket_len(atom_len: usize) -> Option<usize> (:25) — largest entry in ATOM_LENGTHSatom_len, or None if below 2.
  • pub fn hash_window(data: &[u8]) -> u64 (:32) — from-scratch rolling hash (h = h*ROLL_BASE + b, wrapping mod 2⁶⁴).
  • pub fn roll_windows(data: &[u8], len: usize, emit: impl FnMut(usize, u64)) (:51) — sweeps every length-len window, calling emit(start, hash) incrementally in O(1) amortized per byte.
  • pub(crate) const ROLL_BASE: u64 = 0x0000_0100_0000_01b3 (FNV-1a prime).

atomfilter.rs (161 lines) — atom-filter data model

Data model for the Binary-Fuse16 atom/counter/threshold promotion scanner. A slot's counter reaching threshold is the match — no byte-level re-verification of the atom (Avast-style).

  • pub type SlotId = u32 (:21).
  • pub enum SlotTarget (:24) — Extended { sig_index: u32 } (an .ndb/.db signature) or LogicalSubsig { sig_index: u32, subsig_index: u32 } (one subsig of an .ldb signature, feeding LogicalExpr::eval).
  • pub struct SlotDef (:34) — target: SlotTarget, threshold: u32 (almost always 1).
  • pub struct AtomBucket (:46) — bf: Bf16, slots: HashMap<u64, Box<[SlotId]>>.
    • pub fn resolve(&self, key: u64) -> Option<&[SlotId]> (:61) — returns the slots if bf.has(key) AND key is genuinely in slots (absorbs bare Bf16 false positives).
  • pub enum ExtSlot (:71) — Atom(SlotId) (every pattern had a usable atom; extended sigs match on ANY pattern) or AutoMatch (some pattern fully wildcarded → unconditionally matched).
  • pub enum SubsigSlot (:82) — Atom(SlotId), AutoMatch (a Body subsig with no usable atom), External (non-Body subsig: Pcre/ByteCompare/Fuzzy/Unsupported — left for the scanner's exact-evaluation carve-out).
  • pub struct AtomFilterDb (:101) — buckets: [Option<AtomBucket>; 5], buckets_nocase: [Option<AtomBucket>; 5] (case-sensitive and case-folded, never merged), slots: Vec<SlotDef>, ext_slot: Vec<ExtSlot>, log_subsig_slots: Vec<Box<[SubsigSlot]>>.
    • pub fn empty() -> Self (:120).

atomfilter_build.rs (267 lines) — build the AtomFilterDb

Builds an AtomFilterDb from a loaded Database — the Binary-Fuse16 replacement for ClamAV's Aho-Corasick atom prefilter. Same atom-selection logic (Pattern::required_atom/required_atom_nocase, 2..=16 window, case/nocase split). Every usable atom of every signature/subsignature is indexed.

  • pub struct AtomFilterBuilder; (:124) — unit struct.
    • pub fn build(db: &Database) -> AtomFilterDb (:127) — for each extended signature: if all patterns have usable atoms, allocates one SlotId with threshold: 1 targeting SlotTarget::Extended, registers every pattern's atom, stores ExtSlot::Atom; else ExtSlot::AutoMatch. For each logical signature: per atom-indexable Body subsig, allocates a SlotId targeting SlotTarget::LogicalSubsig; non-Body subsigs become SubsigSlot::External; Body subsigs with any no-atom variant become SubsigSlot::AutoMatch. Finalizes per-bucket Bf16::from(&keys) + the exact key → slot-list map.

atomscan.rs (278 lines) — scan-time atom sweep

Scan-time path for the Binary-Fuse16 atom/counter/threshold scheme. One sliding-window sweep per canonical atom length (both case-sensitive and case-folded) resolves every window's rolling hash against its bucket's Bf16 + exact key map, incrementing hit counters. The sweep is the match for Body subsigs/patterns.

  • pub struct SlotCounts (:28) — per-slot hit counts + last-seen window offset.
    • pub fn get(&self, slot: SlotId) -> u32 (:34); pub fn last_offset(&self, slot: SlotId) -> Option<usize> (:40) — highest-offset resolved window (u32::MAX = never hit).
  • pub struct AtomFilterScanner<'a> (:46) — wraps &'a AtomFilterDb.
    • pub fn new(db: &'a AtomFilterDb) -> Self (:51).
    • pub fn scan(&self, data: &[u8]) -> SlotCounts (:58) — sweeps buckets (case-sensitive), then (only if any nocase bucket exists) sweeps buckets_nocase against a lowercased copy. Single fused pass over all active bucket lengths.
  • pub fn ext_matched(ext_slot: ExtSlot, slots: &[SlotDef], counts: &SlotCounts) -> bool (:151) — AutoMatch → true; Atom(id)counts.get(id) >= threshold.
  • pub fn logical_initial_counts_into(out: &mut [usize], sub_slots: &[SubsigSlot], slots: &[SlotDef], counts: &SlotCounts) -> usize (:165) — fills out for LogicalExpr::eval: Body slot counter, 1 for AutoMatch, 0 for External. Returns elements written.
  • pub fn logical_initial_counts(sub_slots, slots, counts) -> Vec<usize> (:190) — allocating convenience wrapper.
  • pub fn subsig_anchor_offset(trigger: SubsigSlot, counts: &SlotCounts) -> usize (:199) — buffer offset to anchor a ByteCompare subsig's read at.

bytecode.rs (149 lines) — .cbc Stage-1 header/trigger parsing

Stage-1 parsing of .cbc ClamAV bytecode files: the ClamBC… header and the line-2 logical-signature trigger. Does not execute bytecode (that's bytecode_vm).

  • pub struct Bytecode (:26) — name: String, trigger: Option<String> (line 2), min_func_level: Option<u32>, source: String (full .cbc text).
  • pub struct BytecodeLoadReport (:39) — files_seen, loaded, skipped: usize.
  • pub struct BytecodeSet (:47) — bytecodes: Vec<Bytecode>, report: BytecodeLoadReport.
    • pub fn load_from_dir(dir: &Path) -> Self (:54) — scans for *.cbc.
    • pub fn from_bytes_map(files: &HashMap<String, Vec<u8>>) -> Self (:78).
  • pub fn parse_cbc(text: &str) -> Option<Bytecode> (:100) — None if not ClamBC…; parses header + line-2 trigger.

bytecode_vm.rs (2781 lines) — ClamBC VM: decoder + interpreter

The full ClamBC bytecode VM: Phase-1 faithful decoder of the nibble-armored .cbc program format (0x60|nibble), Phase-2a interpreter prep pass (stack-frame layout + operand remapping), Phase-2b interpreter (cli_vm_execute), Phase-3 API table, Phase-4 execution context. Detection signalled via setvirusnamectx.virname.

Opcode constants (:19-69): pub const OP_BC_*: u16 for all 51 opcodes (ADD, SUB, MUL, UDIV, SDIV, UREM, SREM, SHL, LSHR, ASHR, AND, OR, XOR, TRUNC, SEXT, ZEXT, BRANCH, JMP, RET, RET_VOID, ICMP_EQ/NE/UGT/UGE/ULT/ULE/SGT/SGE/SLE/SLT, SELECT, CALL_DIRECT, CALL_API, COPY, GEP1, GEPZ, GEPN, STORE, LOAD, MEMSET, MEMCPY, MEMMOVE, MEMCMP, ISBIGENDIAN, ABORT, BSWAP16/32/64, PTRDIFF32, PTRTOINT64, INVALID). pub const BC_START_TID: u16 = 69 (:88). pub const ENGINE_FLEVEL: u32 = 240 (:208).

  • pub enum TypeKind (:102) — Function, PackedStruct, Struct, Array, Pointer.
  • pub struct BcType (:111) — kind, num_elements, contained: Vec<u16>, size, align.
  • pub enum Ops (:121) — None, Unary(u32), Binop([u32; 2]), Three([u32; 3]), Cast { source, mask: u64, size: u8 }, Branch { condition, br_true: u16, br_false: u16 }, Jump(u16), Call { funcid: u16, is_api: bool, ops: Vec<u32> }, GepN(Vec<u32>).
  • pub struct BcInst (:137) — opcode: u16, ty: u16, dest: u32, interp_op: u8, ops: Ops.
  • pub struct BcBB (:146) — insts: Vec<BcInst>.
  • pub struct BcFunc (:151) — num_args, num_locals, num_insts, num_values, num_constants, return_type: u16, types: Vec<u16> (bit 0x8000 = pointer local), bbs: Vec<BcBB>, constants: Vec<u64>, num_bytes: u32 (stack-frame size).
  • pub struct Bc (:179) — format_level, kind, min_func, max_func, num_types, num_func, start_tid, types, funcs, uses_apis, apis, globals, global_tys, lsig: Option<String>, hook_name: Option<String>, skipped: bool, num_global_bytes, global_bytes, prepared: bool.
    • pub fn prepare_interpreter(&mut self) -> DResult<()> (:1262) — mirrors cli_bytecode_prepare_interpreter: lays out globals, per-function stack frame, remaps operands from value index to byte offset. Sets prepared = true.
    • pub fn run(&self, ctx: &mut BcCtx) -> Result<i64, VmError> (:2523) — registers ctx globals (match counts → GID 1, kind → GID 2, filesize → GID 5, pedata → GID 4, globalBytes → GID 7), then vm.execute(0). Detection is ctx.virname.
  • pub enum DecodeError (:419) — Malformed(String), Skip(String).
  • pub fn decode_bytecode(text: &str) -> DResult<Option<Bc>> (:1098) — top-level decode driver over Lsig → Types → Apis → Globals → MdOptHeader → FuncHeader → Bb. Validates header magic, format level 6 or 7, type start id 69.
  • pub struct PeSection (:1540) — 9× u32 exposed to get_pe_section.
  • pub struct BcCtx<'a> (:1556) — file: &'a [u8], file_size: u32, off: u32, virname: Option<String>, lsigcnt: [u32; 64], lsigoff: [u32; 64], kind: u16, sections: Vec<PeSection>, nsections: u16, hdr_size: u32, ep: u32, max_ops: u64 (DoS bound, default 50,000,000).
    • pub fn new(file: &'a [u8]) -> Self (:1573).
  • pub enum VmError (:1626) — Bytecode(String), Timeout.

Implemented APIs (107-entry API_TABLE at :1607): read/write/seek/setvirusname/pe_rawaddr/file_find/file_byteat/malloc/get_pe_section/read_number/memstr/hex2ui/atoi/file_find_limit/engine_functionality_level/dconf_level/ilog2; debug/tracing APIs no-op; unimplemented APIs safe-default to 0/null.

database.rs (1335 lines) — signature loading pipeline

The non-hash signature loader and the Database container. Dispatches by file extension (mirroring readdb.c's cli_load), parses each per-line text format, and accounts for 100% of files seen (no silent drops). Two entry points: load_dir (filesystem) and from_bytes_map (AAssetManager). Also hosts the ClamAV→Rust regex sanitizer.

  • pub struct NameSpan (:14) — start: u32, len: u32 (interned name in Database::name_arena).
  • pub fn intern_name(arena: &mut String, name: &str) -> NameSpan (:20).
  • pub struct Database (:27) — extended: Vec<ExtendedSignature>, logical: Vec<LogicalSignature>, container: Vec<ContainerSignature>, file_type_magic: Vec<FileTypeMagic>, phishing: PhishingDb, unsupported: Vec<UnsupportedRecord>, bytecode_programs: Vec<Bc>, name_arena: String.
    • pub fn ext_name(&self, sig: &ExtendedSignature) -> &str (:46).
    • pub fn pattern_mem_stats(&self) -> crate::pattern::MemStats (:52).
    • pub fn load_dir(path: impl AsRef<Path>) -> io::Result<(Self, LoadReport)> (:238) — first pass collects .ign/.ign2 ignore names; second pass loads each file. Shrinks-to-fit the main vecs.
    • pub fn from_bytes_map(files: &HashMap<String, Vec<u8>>) -> (Self, LoadReport) (:257).
  • pub struct ExtendedSignature (:81) — name: NameSpan, target: Option<u32>, offset: OffsetSpec, patterns: Box<[Pattern]>, source.
  • pub struct ContainerSignature (:98) — name, container_type, container_size: NumSpec, has_filename: bool, size_in_container: NumSpec, size_real: NumSpec, encrypted: Option<bool>, file_pos: NumSpec, source. Filename/encrypted-constrained sigs are parsed but skipped at scan time.
  • pub enum ContainerType (:115) — Any, Format(&'static str) (zip/gz/xz/7z/tar), Unsupported.
  • pub enum NumSpec (:126) — Any, Exact(u64), Range(Option<u64>, Option<u64>). pub fn parse(raw: &str) -> Result<Self, String> (:855), pub fn matches(&self, value: u64) -> bool (:877), pub fn is_constrained(&self) -> bool (:887).
  • pub struct FileTypeMagic (:134) — offset: OffsetSpec, patterns: Box<[Pattern]>, clamav_type: Box<str>, source.
  • pub struct SourceLocation (:142) — path: Arc<Path>, line: usize.
  • pub struct UnsupportedRecord (:150) — source, reason: String.
  • pub struct LoadError (:156) — source, message: String.
  • pub struct LoadReport (:162) — many counters: files_seen, lines_seen, extended_loaded, db_loaded, logical_loaded, container_loaded, ftm_loaded, hash_files_skipped, unsupported_files, unsupported_records, bytecodes_loaded, ign_entries, ignored_skipped, tdb_attr_skipped, phishing_files/phishing_loaded, icon_files/icon_loaded, cert_files/cert_loaded, ioc_files, config_files, metadata_files, container_db_files, deprecated_files, unknown_files, by_extension: BTreeMap<String, usize>, errors: Vec<LoadError>.
  • pub struct OffsetSpec (:218) — anchor: OffsetAnchor, max_shift: Option<usize>.
    • pub fn any() -> Self (:275); pub fn parse(raw: &str) -> Self (:282) — * → Any; EOF-N → EofMinus; EP/SE/SL/S<n>/VI → various; decimal → Absolute.
    • pub fn scan_ranges(&self, data_len: usize) -> Vec<(usize, usize)> (:310).
  • pub enum OffsetAnchor (:224) — Any, Absolute(usize), EofMinus(usize), EntryPoint(i64), SectionStart { index, delta }, SectionEntire { index }, LastSectionStart { delta }, VersionInfo, MacroGroup(String), Unsupported(String).
  • pub(crate) fn sanitize_clamav_regex(pattern: &str) -> String (:604) — translates ClamAV regex dialect to Rust regex.

Extension dispatch (classify_extension, :1140): ndb/ndu/sdbparse_extended_signature (name:target:offset:hex[:minFL[:maxFL]]); dbparse_db_signature (name=hex); ldb/ldulogical::parse_logical_signature; cdbparse_container_signature; ftmparse_ftm; pdb/gdbphishing.protected.add_line; wdbphishing.allow.add_line; hdb/hsb/hdu/hsu/mdb/msb/mdu/msu/imp/fp/sfp → skipped (hash engine); idb/crb → counted; .cbc handled by the scanner's finish_engine_init.

pattern.rs (1836 lines) — compiled hex-pattern matcher

ClamAV-compatible hex-pattern representation and matcher. Patterns compile to u16 instructions plus a () special table for alternations/boundaries/gaps. Matching uses a recursive match_rec with SIMD literal anchoring (memchr/memmem) and fuel budgets. This is the engine that verifies atom-filter candidates.

Constants (:10): CLI_MATCH_CHAR = 0x0000, CLI_MATCH_NOCASE = 0x0100, CLI_MATCH_IGNORE = 0x0200, CLI_MATCH_NIBBLE_HIGH = 0x0300, CLI_MATCH_NIBBLE_LOW = 0x0400, CLI_MATCH_SPECIAL = 0x0700.

  • pub struct Modifiers (:90) — nocase, wide, ascii, fullword. pub fn parse(raw: &str) -> Result<Self, String> (:98).
  • pub enum Special (:117) — AltChar { bytes, negative } ((2e|2f|40)), AltStrFixed { strs, len, negative } ((dead|beef)), AltStr { branches, min, negative } ((aa|bbbb)), Boundary ((B)/(L)/(W), zero-width), Gap { min, max } (*/{n-m}).
  • pub const INLINE_CAP: usize = 22 (:236).
  • pub enum Instructions (:239) — Inline { buf: [u8; INLINE_CAP], len: u8 } (zero-alloc short literals), Pure(Box<[u8]>), Complex(Box<[u16]>). pub fn pure(bytes: Vec<u8>) -> Self (:255), pub fn len(&self) -> usize (:266), pub fn get_u16(&self, index) -> u16 (:280).
  • pub struct Pattern (:292) — instructions, specials: Box<[Special]>, best_literal_offset: u16, best_literal_len: u16, fullword.
    • pub fn best_literal(&self) -> Option<(usize, usize)> (:317) — the most-selective fixed-byte run.
    • pub fn from_instructions(inst: Vec<u16>, fullword: bool) -> Self (:326); pub fn from_parsed(inst, specials, fullword) -> Self (:332).
    • pub fn parse_hex(hex: &str) -> Result<Vec<u16>, String> (:450).
    • pub fn is_match(&self, data: &[u8]) -> bool (:455).
    • pub fn max_match_len(&self) -> Option<usize> (:483) — None = unbounded.
    • pub fn find_all(&self, data: &[u8], ranges: &[(usize, usize)], limit: usize) -> Vec<MatchRange> (:573) — main scan. Fast path: anchor on best_anchor() via SIMD, verify with match_rec. Else find_any_fixed_byte/find_all_fallback_bruteforce.
    • pub fn find_all_at(&self, data, ranges, limit, hints: &[u32]) -> Vec<MatchRange> (:908) — prefilter-hinted scan.
    • pub fn match_at(&self, data: &[u8], start: usize) -> Option<usize> (:1035) — bounded by MATCH_FUEL_BUDGET = 2000.
    • pub fn mem_stats(&self) -> MemStats (:1257).
    • pub fn required_atom(&self) -> Option<Vec<u8>> (:1282) — the best literal's bytes (prefilter atom).
    • pub fn required_atom_nocase(&self) -> Option<Vec<u8>> (:1291) — longest CHAR|NOCASE run, lowercased.
  • pub struct MemStats (:1356); pub struct MatchRange (:1392) — start: usize, end: usize.
  • pub fn compile_pattern_variants(raw: &str, modifiers: Modifiers) -> Result<Vec<Pattern>, String> (:1398) — emits the ascii variant and the wide variant (if wide).

logical.rs (1487 lines) — .ldb logical signatures + expression engine

Parses .ldb/.ldu logical signatures (name;TDB;expression;subsigs…) and evaluates the boolean/count expression tree over subsignature match counts. Handles Body/Pcre/ByteCompare/Fuzzy/Unsupported subsignatures, the TDB constraint block, PCRE trigger/regex/flags, byte-compare specs, and the lazy-regex memory strategy.

  • pub struct LazyRegex (:23) — source: Arc<str>, compiled: Arc<OnceLock<Option<Regex>>>. PCRE subsig regex compiled on first trigger (dominant memory saving: ~170 MB → few MB resident).
    • pub fn get(&self) -> Option<&Regex> (:37); pub fn is_match(&self, data: &[u8]) -> bool (:43).
  • pub struct LogicalSignature (:49) — name, target, file_size, container, nos, ep, icongrp1, icongrp2, handlertype, intermediates, tdb_unsupported, expression: LogicalExpr, subsignatures: Vec<Subsignature>, source, bytecode: Option<usize>.
  • pub struct PcreSubsig (:92) — trigger: LogicalExpr, regex: LazyRegex, global: bool.
  • pub enum Subsignature (:99) — Body { offset: Option<Box<OffsetSpec>>, patterns: Box<[Pattern]> }, Pcre(Box<PcreSubsig>), ByteCompare(Box<ByteCompareSpec>), Fuzzy([u8; 8]), Unsupported(Box<str>).
  • pub enum ByteReadType (:119) — HexAscii, DecimalAscii, Auto, BinaryLe, BinaryBe.
  • pub struct ByteCompareSpec (:133) — trigger_subsig: usize, offset_sign: i64, offset_value: usize, read_type, exact: bool, num_bytes: usize, comparisons: Vec<(CompareOp, u64)>.
    • pub fn evaluate(&self, data: &[u8], trigger_offset: usize) -> bool (:996).
  • pub struct LogicalExpr (:164) — flat post-order array of ExprNode (Subsig/And/Or/Compare); root = last node.
    • pub fn eval(&self, counts: &[usize]) -> EvalStats (:468) — ClamAV cli_ac_chklsig.
    • pub fn can_still_match(&self, counts: &[usize], evaluated: &[bool]) -> bool (:486) — over-approximate feasibility.
    • pub fn has_nonmonotone_compare(&self) -> bool (:506).
    • pub fn is_definitely_matched(&self, counts: &[usize], evaluated: &[bool]) -> bool (:526).
  • pub enum CompareOp (:227) — Equal, Greater, GreaterEqual, Less, LessEqual.
  • pub struct EvalStats (:236) — matched: bool, hits: usize, ids: u64.
  • pub fn parse_logical_signature(line: &str, source: SourceLocation) -> Result<(LogicalSignature, Vec<String>), String> (:284) — PCRE-aware ldb_tokenize; ExprParser recursive-descent; parse_tdb; parse_subsignature. Returns Err("unrecognised TDB attribute …") for unknown TDB keys.

scanner.rs (1792 lines) — scan pipeline + Engine

The scan entry point and the engine that ties everything together: file-type detection, the atom-filter sweep, extended/logical signature evaluation, PCRE/byte-compare/fuzzy/image-icon subsigs, bytecode execution, YARA delegation, and phishing.

  • pub struct TimingBreakdown (:10) — clamav_ns: u128, yara_per_engine: Vec<(String, u128)>. pub fn accumulate(&mut self, other: TimingBreakdown) (:17).
  • pub struct Engine (:24) — database: Database, private atomfilter_db, pub yara: Vec<YaraEngine>.
  • pub struct ScanOptions (:39) — scan_archives: bool, max_recursion: usize (default 16), max_child_size: usize (default 650 MiB). impl Default.
  • pub struct ScanMatch (:56) — name: String, kind: SignatureKind, source: SourceLocation, object_path: String, view: ScanView.
  • pub enum SignatureKind (:102) — Extended, Logical, Container, Phishing, Yara.
  • pub enum ScanView (:113) — Raw (only variant).

Engine public API:

  • pub fn prefilter_mem_report(&self) -> String (:191).
  • pub fn from_database_dir(path: impl AsRef<Path>) -> io::Result<(Self, LoadReport)> (:202).
  • pub fn from_bytes_map(files: &HashMap<String, Vec<u8>>) -> (Self, LoadReport) (:216).
  • pub fn load_yara_rules(&mut self, path: impl AsRef<Path>) -> Option<()> (:279).
  • pub fn add_yara_source_file(&mut self, path: impl AsRef<Path>) -> Option<()> (:286).
  • pub fn add_compiled_yara_file(&mut self, path: impl AsRef<Path>) -> Option<()> (:294) — add a .yrc.
  • pub fn add_compiled_yara(&mut self, engine: YaraEngine) (:304).
  • pub fn scan_path(&self, path: impl AsRef<Path>, options: ScanOptions) -> io::Result<Vec<ScanMatch>> (:308).
  • pub fn scan_bytes(&self, data: &[u8], options: ScanOptions) -> Vec<ScanMatch> (:318).
  • pub fn scan_bytes_named(&self, data, object_path, options, module_meta: &[(&str, &[u8])]) -> Vec<ScanMatch> (:322) — the main entry.
  • pub fn scan_bytes_named_with_breakdown(&self, data, object_path, options, module_meta) -> (Vec<ScanMatch>, TimingBreakdown) (:338).
  • pub fn detect_target(&self, data: &[u8]) -> Option<u32> (:471).

Scan pipeline (scan_object, :353): (1) size guard; (2) type detection (.ftm + builtin magic: \x7fELF→6, %PDF→10, GIF/PNG/JPEG→5, dex\n→16, PK\x03\x04→17); (3) archive guard (skip RAR); (4) ClamAV engine gate — CLAMAV_ALLOWED_TARGETS = [3,5,6,7,10,16,17] (HTML/Graphics/ELF/text/PDF/DEX/ZIP-APK); (5) scan_context — one AtomFilterScanner::scan sweep → SlotCounts, then scan_extended + scan_logical; (6) YARA per ruleset (Android targets only, module metadata forwarded); (7) phishing for HTML with a protected DB. No ZIP extraction here — the caller (hydradragonandroid's collect_buffers) walks archives via hydradragonextractor.

Extended scan (scan_extended/scan_one_extended, :521/:537): iterates ext_slot; if atomscan::ext_matched, checks target_matches, rejects unsupported offset anchors, runs pattern.find_all across all patterns.

Logical scan (scan_logical/scan_one_logical, :583/:644): per sig — TDB gating; initial counts from atomscan::logical_initial_counts_into; byte-confirmation of Body subsigs with the real Pattern (without this, atom shadows like mira for mirai would fire spuriously); can_still_match short-circuit; Fuzzy via ctx.image_fuzzy_hash()fuzzy::calculate_image; Phase-2 Pcre (lazy LazyRegex::get, capped to first PCRE_MAX_SCAN_BYTES = 10_000_000 bytes) + ByteCompare (ByteCompareSpec::evaluate anchored at trigger's last_offset); LogicalExpr::eval; on match: handlertype suppresses, bytecode runs run_bytecode, else emit ScanMatch.

Bytecode (run_bytecode, :925): builds BcCtx::new(ctx.data), copies up to 64 trigger counts into lsigcnt, calls bc.run; detection is bctx.virname.

yara_scan.rs (151 lines) — YARA-x bridge

Bridges to the yara-x fork. Compiles YARA source or loads pre-compiled .yrc rulesets, caches one yara_x::Scanner per engine per thread (amortizing Scanner::new's setup + WASM runtime instantiation), and scans Android-relevant file types only.

  • pub fn is_target_allowed(target: Option<u32>) -> bool (:38) — true iff target in ALLOWED_TARGETS = [3, 5, 6, 7, 10, 16, 17]. Excludes PE/OLE2/Mail/Mach-O/SWF/Java.
  • pub struct YaraEngine (:47) — id: u64, pub name: String, rules: Box<yara_x::Rules> (boxed so its address is stable across Vec::push reallocations, since thread-local Scanners hold &'static Rules).
    • pub fn from_source_file(path: impl AsRef<Path>) -> Option<Self> (:62).
    • pub fn from_source(source: &str, name: String) -> Option<Self> (:69).
    • pub fn from_compiled(bytes: &[u8], name: String) -> Option<Self> (:80) — Rules::deserialize(bytes) (the fast Android asset path).
    • pub fn from_compiled_file(path: impl AsRef<Path>) -> Option<Self> (:86).
    • pub fn scan(&self, data: &[u8], object_path: &str, module_meta: &[(&str, &[u8])]) -> Vec<ScanMatch> (:93) — reuses a thread-local Scanner keyed by self.id; scanner.fast_scan(true); with module metadata builds ScanOptions and calls scan_with_options (feeding per-module JSON reports); each matching rule becomes ScanMatch named YARA-X.<identifier>.

phishing.rs (677 lines) + phishing/tld.rs (78 lines) — phishing detection

tld.rs: verbatim port of ClamAV's iana_tld.h/iana_cctld.h gperf wordlists, sorted for binary search.

  • pub fn is_tld(s: &str) -> bool (tld.rs:8); pub fn is_cctld(s: &str) -> bool (tld.rs:13).

phishing.rs: HTML phishing detection (protected/allow domain DBs loaded from .pdb/.gdb/.wdb). The protected/allow line parsers and scan_html/get_domain logic mirror ClamAV's phishcheck.c.

main.rs (423 lines) — CLI driver

A hydradragonclamav CLI binary: --database/-d, --scan/-s, --no-archives, --max-recursion, --max-child-size (K/M/G suffixes), --list-unsupported, HDC_MEM_STATS=1 memory profiler, HDC_HOLD steady-state sampler. Loads the DB, prints a full coverage report, trims the working set (K32EmptyWorkingSet on Windows), scans files (exit 1 on detection, 2 on error). Not shipped in the APK — host-side testing tool.


hydradragonml — Burn classifier

A pure-Rust neural-network malware classifier (Burn 0.21 on the burn-wgpu backend) that scores an APK 0.0–1.0 from its tokenized strings plus 26 Android engine features. No ONNX, no tract: weights ship as a Burn .mpk recorder file and inference runs fully in-process. Crate root: hydradragonml/src/.

The model is a Burn neural network, not "ONNX/tract" or "MinHash/LSH + Isolation Forest" as some older docs said. FNV-1a token hashing still exists in this crate, but only for the benign-content DB lookup (features::extract_minhash), not for classification.

src/lib.rs

pub mod features;
pub mod model;
pub const DEFAULT_CONFIDENCE_THRESHOLD: f32 = 0.95;
pub const SUSPICIOUS_THRESHOLD: f32 = 0.90;

pub struct Model {
    classifier: model::ApkClassifier<B>,  // B = Wgpu
    tokenizer: features::Tokenizer,
    confidence_threshold: f32,
    device: WgpuDevice,
}

pub struct ScanResult {
    pub malicious: bool,    // confidence >= confidence_threshold
    pub suspicious: bool,   // !malicious && confidence >= SUSPICIOUS_THRESHOLD
    pub confidence: f32,    // clamped 0.0..1.0
}

impl Model:

  • pub fn load(model_bytes, vocab_bytes, device) -> Result<Self, ...> (lib.rs:27) — parses vocab_bytes via Tokenizer::load_json, writes model_bytes to a temp .mpk file, and ApkClassifier::load_weightss it. Threshold starts at DEFAULT_CONFIDENCE_THRESHOLD (0.95).
  • pub fn load_from_path(model_path, vocab_bytes, device) (lib.rs:48) — same but reads the .mpk directly from a path (used by the scan/training binaries).
  • pub fn set_threshold(&mut self, t: f32) (lib.rs:64) — self.confidence_threshold = t.clamp(0.0, 1.0).
  • pub fn scan(&self, apk: &[u8]) -> Option<ScanResult> (lib.rs:68) — scan_with_features(apk, &EngineFeatures::default()).
  • pub fn scan_with_features(&self, apk, engine_feats) -> Option<ScanResult> (lib.rs:72) — tokenizes the APK, builds the [26] engine-feature tensor, runs classifier.forward, and maps the sigmoid output scalar to malicious/suspicious/confidence. None if the APK isn't a readable ZIP or yields no tokens.

src/model.rs — the network

ApkClassifier<B> (model.rs:14) — a small MLP fused over text + engine features:

Layer Shape → Activation
embedding [VOCAB_SIZE=20000, EMBED_DIM=64], then mean-pool over the token dim
fc_text 64 → 32 (pooled embedding) ReLU
fc_engine 26 → 32 (EngineFeatures) ReLU
fc_combine 64 → 32 (text ⊕ engine concat) ReLU
fc_out 32 → 1 Sigmoid

forward_batch (model.rs:44) is the batched path used by training; forward (model.rs:33) is the single-sample wrapper. Weights are saved/loaded via Burn's NamedMpkFileRecorder (full precision) — save_weights(path) (:57) and load_weights(path, device) (:63).

src/features.rs — tokenizer + engine features

Constants (features.rs:4-10): VOCAB_SIZE = 20000, EMBED_DIM = 64, MIN_STR_LEN = 5, MAX_TOKENS = 120_000, MAX_ENTRY_SCAN = 16 MiB, ENGINE_FEATURE_COUNT = 26.

  • Tokenizer (:87): maps lowercased substrings to vocab ids. load_json(bytes) (:96) parses vocab.json ("<UNK>" → 0). tokenize(apk) (:101) opens the APK as a zip::ZipArchive; sub_tokenize (:146) splits each entry name on . / ; : - \ _ (parts ≥ 2 chars → vocab id, unknown → 0), then harvest_strings (:161) pulls printable runs in ASCII and UTF-16LE (length ≥ 5, up to MAX_ENTRY_SCAN bytes) from androidmanifest.xml, resources.arsc, *.dex, and meta-inf/* entries. None if no content entry or empty token list.
  • EngineFeatures (:13): 26 floats — DEX (dex_class_count, dex_string_count, dex_api_call_count, dex_finding_high, dex_finding_critical), ELF (elf_count, elf_emulated_strings, elf_network_calls, elf_file_calls, elf_exec_calls, elf_anti_debug), manifest (manifest_dangerous_permissions, manifest_total_permissions, manifest_activities, manifest_services, manifest_receivers, manifest_min_sdk, manifest_target_sdk), URL/IP (url_malicious_count, url_phishing_count, ip_malicious_count), a reserved feature pinned to 0.0, is_testkey, benign_jaccard, media_hidden_count, hips_findings. to_vec() (:53) min-max normalizes each to [0,1] (the model input).
  • extract_minhash(apk) (:246) + fnv1a (:224) + token(prefix, s) (:233): FNV-1a hashed string tokens (namespaced name:/dex:/manifest:/res:/perm:/api:/url:) used by the benign-content DB (benign_db.rs), not by the classifier.

src/bin/train.rshydradragonml-train binary

Trains ApkClassifier (autodiff Autodiff<Wgpu>, Adam, binary cross-entropy) over --benign <dir> / --malware <dir> corpora and writes weights to --output model.mpk:

cargo run --release --bin hydradragonml-train -- \
    --benign ./benign --malware ./malware \
    --vocab vocab.json --output model.mpk [--epochs 6] [--lr 0.001] [--batch-size 8]

--vocab must be the same vocab.json shipped in the Android assets (built over the same corpus so training and inference tokenize identically). Samples are tokenized via the DEX feature extractors (dex-core/dex-analysis, git deps); the corpus is split 80/20 train/valid and the learning rate halves each epoch.

src/main.rshydradragonml-scan binary

Dataset evaluation harness. parse_args: --dataset/-d, --model/-m, --vocab/-v, --threshold/-t (default DEFAULT_CONFIDENCE_THRESHOLD) — all three paths are required. Walks the dataset for *.apk (skipping paths containing invalid), reads the model/vocab bytes and loads via Model::load, applies set_threshold, runs model.scan per APK, and prints MALICIOUS/BENIGN + confidence + elapsed time. ground_truth infers labels from path substrings (benign/clean/f-droid vs malware/malicious/malwarebazaar) and prints accuracy/precision/recall/F1.

src/bin/debug_features.rs + dump_features.rs

  • debug-features (declared as [[bin]] in Cargo.toml): hardcoded to two F-Droid APK paths; loads model.mpk + vocab.json, runs the same inference pipeline inline, prints tokens/confidence via Model::load + model.scan.
  • dump_features: prints per-entry declared/compress/actual sizes and writes <apk>.rust_dense.json for byte-for-byte comparison against the Python training pipeline's <apk>.python_dense.json. (Exists on disk but not wired in Cargo.toml.)

On-device consumption

hydradragonandroid/src/lib.rs:23 use hydradragonml::Model;. Assets are read as MODEL_MPK: &str = "model.mpk" (lib.rs:111) and VOCAB_JSON: &str = "vocab.json" (lib.rs:112) — the model ships as a Burn .mpk recorder. The model is loaded once at init into Engine.model: Option<Model> via Model::load(model_bytes, vocab_bytes, NdArrayDevice::default()) (pure-Rust ndarray Burn backend — no GPU/WGPU) and scored via scan_with_features(data, engine_features) on each non-whitelisted APK buffer during run_scan (Phase 3). Training is in-crate (the hydradragonml-train binary) — there is no train_model.py / ONNX export step anymore; the model ships as assets/scan/model.mpk alongside assets/scan/vocab.json.


hydradragonextractor — archive extraction

Detects and extracts Android-relevant archive formats (ZIP/APK, TAR, GZIP, XZ, 7z, RAR, bzip2, LZMA, and more) either to disk or fully in memory, with parallel ZIP decompression, decompression-bomb detection, per-archive entry caps, and path-traversal protection. Crate root: hydradragonextractor/src/. Dependencies: oxiarc-archive v0.3 (multi-format backend), unrar-ng (from the HydraDragonAntivirus/unrar.rs fork), thiserror v2.

src/lib.rs — public API

Error type:

#[derive(Debug, thiserror::Error)]
pub enum ExtractError {                        // lib.rs:27
    Io(#[from] std::io::Error),
    OperationFailed { reason: String },
    DecompressionBomb { format: &'static str },
}

Public structs:

  • pub struct ExtractResult { pub files: Vec<PathBuf>, pub output_dir: PathBuf } (:22).
  • pub struct ZipEntryInfo { pub name: String, pub size: u64, pub compressed_size: u64 } (:118).
  • pub struct EntryInfo { pub name: String, pub size: u64 } (:159).

Top-level functions:

  • pub fn detect_format(data: &[u8]) -> Option<&'static str> (:45) — returns one of "rar", "zip", "gz", "xz", "tar", "7z", "bz2", "zst", "lz4", "br", "snappy", "cab", "lzh", "iso", "lzma", or None. RAR detected first by magic (Rar!\x1a\x07\x00 v1.5 / Rar!\x1a\x07\x01\x00 v5); else oxiarc_archive::detect::ArchiveFormat::from_magic, with ustar TAR at offset 257 (is_tar :75) and raw LZMA ([0x5d, 0x00], is_lzma :80) fallbacks.
  • pub fn extract_archive(path: &Path, output_dir: &Path) -> Result<ExtractResult> (:92) — reads the file, dispatches RAR to rar::extract_to_dir and everything else to extract_to_dir. Creates output_dir. Returns the extracted file paths.
  • pub fn extract_archive_from_bytes(data: &[u8]) -> Result<Vec<(String, Vec<u8>)>> (:111) — the primary in-memory API used on-device. Returns (in_archive_name, decompressed_bytes) pairs so detections report against the real member. RAR → rar::extract_from_bytes; else extract_to_memory.
  • pub fn zip_list_entries(data: &[u8]) -> Result<Vec<ZipEntryInfo>> (:127) — lists ZIP entries (skipping directory entries), capped at MAX_ARCHIVE_ENTRIES (4096).
  • pub fn zip_extract_entry(data: &[u8], name: &str) -> Result<Vec<u8>> (:145).
  • pub fn list_entries(data: &[u8]) -> Result<Vec<EntryInfo>> (:166) — format-agnostic. Single-stream formats (gz/xz/bz2/zst/lz4/br/snappy/lzma) return a single EntryInfo { name: "decompressed", size: 0 }.
  • pub fn extract_entry(data: &[u8], name: &str) -> Result<Vec<u8>> (:206) — format-agnostic. zstd/lz4/br/snappy return OperationFailed (not implemented).

Decompression-bomb guard (public):

  • pub(crate) const MAX_DECOMPRESSED_SIZE: usize = 200_000_000 (:371); const BOMB_RATIO: usize = 1000 (:372); const MIN_RATIO_CHECK_SIZE: usize = 10_000_000 (:373); static DETECT_BOMBS: AtomicBool = AtomicBool::new(true) (:378).
  • pub fn set_bomb_detection_enabled(enabled: bool) (:380).
  • pub(crate) fn is_decompression_bomb(compressed_len, decompressed_len) -> bool (:384) — triggers if decompressed_len > 200 MB, OR decompressed_len >= 10 MB && decompressed_len / compressed_len > 1000.
  • pub fn is_bomb_error(e: &ExtractError) -> bool (:401) — matches DecompressionBomb.

safe_output_path(output_dir, name) (:723) is the path-traversal guard — only Normal and CurDir components are accepted; any ParentDir/RootDir/Prefix yields None and the entry is skipped.

Parallel ZIP extraction (zip_to_memory, :510)

  • pub(crate) const MAX_ARCHIVE_ENTRIES: usize = 4096 (:508).
  • Splits entry names into chunks: n_threads = available_parallelism().min(4), chunk_size = ceil(names.len() / n_threads). std::thread::scope spawns one thread per chunk; each builds its own ZipReader from the shared data slice and extracts its chunk. A shared AtomicBool bomb_found records any per-entry bomb; results flow through an mpsc::channel. After join: if bomb_found, returns DecompressionBomb { format: "zip" }. ~4× wall-clock speedup on a 1000-entry APK on a 4-core device.

src/rar.rs — RAR support (236 lines, private module)

Uses unrar::Archive. Because unrar requires a file path, the in-memory RAR helpers spill to a temp file under std::env::temp_dir().join(format!("hdrartmp_{:x}", crate::rand_byte())) and clean it up on every exit path.

  • pub fn extract_to_dir(path: &Path, output_dir: &Path) -> Result<Vec<PathBuf>> (:6).
  • pub fn extract_from_bytes(data: &[u8]) -> Result<Vec<(String, Vec<u8>)>> (:46) — writes to archive.rar in a temp dir, loops read_header() (stopping at MAX_ARCHIVE_ENTRIES), header.read()(data, rest), bomb check, pushes (entry_name, data).
  • pub fn list_entries(data: &[u8]) -> crate::Result<Vec<crate::EntryInfo>> (:118).
  • pub fn extract_entry(data: &[u8], name: &str) -> crate::Result<Vec<u8>> (:172).

Supported formats

Format detect in-memory extract disk extract single-entry bomb check
ZIP / APK "zip" zip_to_memory (parallel) zip_to_dir zip_extract_entry per-entry + aggregate
TAR "tar" tar_to_memory tar_to_dir tar_extract_entry
GZIP "gz" gzip_to_memory (auto-tar) gzip_to_dir decompress_gzip yes
XZ "xz" xz_to_memory (auto-tar) xz_to_dir decompress_xz yes
bzip2 "bz2" bzip2_to_memory (auto-tar) bzip2_to_dir decompress_bzip2 yes
7z "7z" sz_to_memory sz_to_dir sz_extract_entry per-entry
RAR v1.5 / v5 "rar" rar::extract_from_bytes rar::extract_to_dir rar::extract_entry per-entry
LZMA (raw) "lzma" xz_to_memory xz_to_dir decompress_xz yes
zstd/lz4/br/snappy detected listing-only stub not supported not implemented
CAB / LZH / ISO detected not supported not supported

Recursion limits

The crate itself does not recurse — it extracts one archive level. Recursion is driven by the on-device caller: hydradragonandroid/src/lib.rs guards if item.depth < 16 && fmt.is_some() before calling extract_archive_from_bytes, pushing children with depth: item.depth + 1. So nested-archive recursion is capped at 16 levels on-device. Additionally the on-device worker caps extraction at 4096 buffers total or 2 GB cumulative bytes, independent of the crate's own MAX_ARCHIVE_ENTRIES = 4096 per single archive.

On-device consumption

collect_buffers in hydradragonandroid/src/lib.rs calls extract_archive_from_bytes per work item. On Err(e) if is_bomb_error(&e) it pushes a ("HDR.Bomb.Decompression", obj_path, lineage) detection rather than treating it as an extraction failure. The user toggle is wired through nativeSetDetectZipBombset_bomb_detection_enabled. Extracted (name, bytes) pairs become Bufs scanned by ClamAV/YARA/ML.


hydradragonxorfilter — Binary-Fuse filters

The single source of truth for the .xf on-disk container. Both the offline builder (dev-tools/xorfilter_writer) and the on-device native scanner depend on this crate, so a filter written on x86 is queryable byte-for-byte on arm64. Only BinaryFuse16 (Bf16) is used — ~2.16 bytes/key, the project-wide standard for URL/domain/IP blocklists and the MD5 whitelist. Crate root: hydradragonxorfilter/src/. Dependencies: jdb_xorf v0.13 (with bitcode feature — Bf8/Bf16/Bf32), bitcode v0.6, memmap2 v0.9.

src/lib.rs — public API

const TAG_BF16: u8 = 16;                       // lib.rs:25 — first byte of every .xf

pub fn key(s: &str) -> u64                     // lib.rs:35
pub fn key_bytes(b: &[u8]) -> u64              // lib.rs:49

pub struct XorFilter(Bf16);                    // lib.rs:61

impl XorFilter {
    pub fn contains_key(&self, k: u64) -> bool  // lib.rs:65
    pub fn contains(&self, s: &str) -> bool     // lib.rs:70
    pub fn from_bytes(bytes: &[u8]) -> Option<XorFilter>  // lib.rs:76
    pub fn load(path: &Path) -> Option<XorFilter>        // lib.rs:88
}

pub fn build_from_keys(mut keys: Vec<u64>) -> Result<Vec<u8>, String>  // lib.rs:104

Key derivation:

  • key(s: &str) -> u64 (:35) — FNV-1a 64-bit (offset 0xcbf29ce484222325, prime 0x100000001b3) over the ASCII-lowercased bytes. Used for case-insensitive items — hostnames, URLs, hex digests.
  • key_bytes(b: &[u8]) -> u64 (:49) — same FNV-1a over raw bytes with no lowercasing — for case-sensitive keys (binary signature atoms).

On-disk .xf format: [1 byte tag = 16 (TAG_BF16)] ++ bitcode::encode(&Bf16). Byte 0 is always 0x10; the remainder is the bitcode-serialized jdb_xorf::Bf16 body. bitcode is deterministic and platform-independent, so host-encoded bytes decode identically on arm64.

Query:

  • contains_key(k: u64) (:65) — self.0.has(&k).
  • contains(s: &str) (:70) — self.contains_key(key(s)).

Decode:

  • from_bytes(bytes: &[u8]) -> Option<XorFilter> (:76) — split_first() reads the tag; None on a bad tag (!= 16) or malformed body.
  • load(path: &Path) -> Option<XorFilter> (:88) — File::open, unsafe { memmap2::Mmap::map(&file) }, then catch_unwind(|| XorFilter::from_bytes(&mmap)). The returned filter owns its data (decoded onto the native heap); the mapping is dropped on return.

Build:

  • build_from_keys(mut keys: Vec<u64>) -> Result<Vec<u8>, String> (:104) — sort_unstable + dedup (binary-fuse construction requires distinct keys), rejects empty input, bitcode::encode(&Bf16::from(&keys)) inside catch_unwind, prepends TAG_BF16.

Writer/reader byte-compatibility: guaranteed by construction — both xorfilter_writer and hydradragonandroid call key/key_bytes, build_from_keys, and XorFilter::from_bytes/contains from this one crate. No format constants are duplicated.

On-device consumption

  • hydradragonandroid/src/lib.rs:23 use hydradragonxorfilter::XorFilter;. WHITELIST_XF = "whitelist.xf" (:125) is the NSRL MD5 whitelist, loaded once into Engine.whitelist: Option<XorFilter>.
  • url_scan.rs:21 filter: XorFilter per category; load_from_assets (:43) calls XorFilter::from_bytes for each <stem>.xf; scan calls f.filter.contains(norm). Stems: malwareurl, phishingurl, phishing, malicious, malicious_mail, abuse, spam, mining.
  • ip_scan.rs:4 use XorFilter;; stems ipmalware, ipphishing, ipbruteforce, ipddos, ipspam; f.filter.contains(ip).

The .xf files live in app/src/main/assets/scan/ and are memory-mapped at init. See NSRL-Whitelisting for the whitelist layering and Malicious-URL-Scanning for the URL/domain scan path.


Dev-tools (offline build tools)

The dev-tools build the assets the on-device crates consume. See Data-Pipeline for the orchestration scripts; this section covers the Rust dev-tools.

dev-tools/hydradragon_yara_x_compile — offline .yrc compiler

Same yara-x features as the on-device crate. See YARA-X-Modules#the-yar--to-yrc-build-chain for the full build chain and filtering logic. CLI: hydradragon_yara_x_compile [--check] [--filtered] <source_dir> [<output_dir>]. Output defaults to app/src/main/assets/scan.

dev-tools/xorfilter_writer — offline .xf blob builder

Path-depends on hydradragonxorfilter so it uses the exact same key derivation, width, and on-disk format as the device. Three modes: build (xorfilter_writer <in.txt> <out.xf>), check (--check <file.xf> <item>), false-positive-rate (--fp <file.xf> <count> <hex|dom>, fixed seed 0x9E3779B97F4A7C15 → reproducible xorshift64* PRNG). The build_xfilters.sh/build_xfilters.cmd orchestration invokes this writer for each category stem into app/src/main/assets/scan/. Stems must match the CATS tables in url_scan.rs/ip_scan.rs.

dev-tools/yarGen — vendored YARA rule generator

A vendored copy of Florian Roth's yarGen v0.24.0 — creates YARA rules from strings found in malware files while removing strings that also appear in goodware. Python, build-time only; its .yar output is an input to hydradragon_yara_x_compile. See Data-Pipeline#yara-rule-generation-yargen for the project's actual usage. Companion: convert_cuckoo_to_hydradragon.py rewrites import "cuckoo"import "hydradragon" (whole-token regex) before compilation; truncateruledesc64kb.py truncates rule descriptions to 7 KB.

Offline → on-device invariants

Dev-tool (offline, x86) Output artifact On-device consumer (arm64) Load mechanism
xorfilter_writer <stem>.xf, whitelist.xf hydradragonandroid (url_scan.rs, ip_scan.rs, lib.rs) XorFilter::from_bytes / XorFilter::load (shared hydradragonxorfilter)
hydradragon_yara_x_compile <stem>.yrc hydradragonclamavhydradragonandroid yara_x::Rules::deserialize (YaraEngine::from_compiled)
yarGen (vendored) *.yar source rules input to hydradragon_yara_x_compile
hydradragonml-train (cargo run --release --bin hydradragonml-train -- --benign <dir> --malware <dir> --vocab vocab.json --output model.mpk) model.onnx (a Burn .mpk recorder) hydradragonml::Model::load (with vocab.json) Burn NamedMpkFileRecorder / ApkClassifier::load_weights

Two shared invariants make the offline→on-device flow safe: (1) hydradragonxorfilter is the single source of truth for .xf format and key derivation; (2) the yara-x fork with pulley is the single source of truth for .yrc bytecode — the compiler and the on-device hydradragonclamav/hydradragonandroid must use the same feature set (serialized module is backend-specific).

Clone this wiki locally