use droidsaw_apk::Apk;
use droidsaw_common::corpus::{ExtractorError, KvPair};
use droidsaw_dex::DexFile;
use crate::context::HbcOwned;
pub struct ApkContext<'a> {
pub apk_sha256: &'a str,
pub apk_path: &'a str,
pub apk: Option<&'a Apk>,
pub dex_files: &'a [DexFile],
pub hbc: Option<&'a HbcOwned>,
}
impl<'a> ApkContext<'a> {
#[must_use]
pub fn from_layers(
apk_sha256: &'a str,
layers: &'a crate::context::CrossLayerContext,
) -> Self {
Self {
apk_sha256,
apk_path: layers.path.as_str(),
apk: layers.apk.as_ref(),
dex_files: layers.dex.as_slice(),
hbc: layers.hbc.as_ref(),
}
}
}
pub trait CorpusExtractor: Sync + Send {
type Cache: Send + Sync;
fn extractor_id(&self) -> &'static str;
fn prepare(&self, ctx: &ApkContext<'_>) -> Result<Self::Cache, ExtractorError>;
fn extract(
&self,
ctx: &ApkContext<'_>,
cache: &Self::Cache,
) -> Result<Vec<KvPair>, ExtractorError>;
}
pub trait DynCorpusExtractor: Sync + Send {
fn extractor_id(&self) -> &'static str;
fn run(&self, ctx: &ApkContext<'_>) -> Result<Vec<KvPair>, ExtractorError>;
}
impl<T> DynCorpusExtractor for T
where
T: CorpusExtractor + Sync + Send,
{
fn extractor_id(&self) -> &'static str {
CorpusExtractor::extractor_id(self)
}
fn run(&self, ctx: &ApkContext<'_>) -> Result<Vec<KvPair>, ExtractorError> {
let cache = self.prepare(ctx)?;
self.extract(ctx, &cache)
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use droidsaw_common::corpus::ExtractorValue;
use super::*;
struct MockSimple;
impl CorpusExtractor for MockSimple {
type Cache = ();
fn extractor_id(&self) -> &'static str {
"mock_simple"
}
fn prepare(&self, _ctx: &ApkContext<'_>) -> Result<Self::Cache, ExtractorError> {
Ok(())
}
fn extract(
&self,
_ctx: &ApkContext<'_>,
_cache: &Self::Cache,
) -> Result<Vec<KvPair>, ExtractorError> {
Ok(vec![
KvPair::new("mock_count", ExtractorValue::Int(7)),
KvPair::new("mock_ok", ExtractorValue::Bool(true)),
])
}
}
struct MockCached;
struct CountCache {
precomputed_count: i64,
}
impl CorpusExtractor for MockCached {
type Cache = CountCache;
fn extractor_id(&self) -> &'static str {
"mock_cached"
}
fn prepare(&self, ctx: &ApkContext<'_>) -> Result<Self::Cache, ExtractorError> {
let count = i64::try_from(ctx.dex_files.len())
.map_err(|e| ExtractorError::Internal(format!("dex count overflow: {e}")))?;
Ok(CountCache {
precomputed_count: count,
})
}
fn extract(
&self,
_ctx: &ApkContext<'_>,
cache: &Self::Cache,
) -> Result<Vec<KvPair>, ExtractorError> {
Ok(vec![KvPair::new(
"cached_dex_count",
ExtractorValue::Int(cache.precomputed_count),
)])
}
}
struct MockPrepareFails;
impl CorpusExtractor for MockPrepareFails {
type Cache = ();
fn extractor_id(&self) -> &'static str {
"mock_prepare_fails"
}
fn prepare(&self, _ctx: &ApkContext<'_>) -> Result<Self::Cache, ExtractorError> {
Err(ExtractorError::MissingState("nothing here"))
}
fn extract(
&self,
_ctx: &ApkContext<'_>,
_cache: &Self::Cache,
) -> Result<Vec<KvPair>, ExtractorError> {
unreachable!("must not be called when prepare errored")
}
}
fn mock_ctx() -> (String, String) {
(
String::from(
"0000000000000000000000000000000000000000000000000000000000000000",
),
String::from("/test/synthetic.apk"),
)
}
fn build_ctx<'a>(sha: &'a str, path: &'a str) -> ApkContext<'a> {
ApkContext {
apk_sha256: sha,
apk_path: path,
apk: None,
dex_files: &[],
hbc: None,
}
}
#[test]
fn simple_extractor_dispatch_returns_expected_facts() {
let (sha, path) = mock_ctx();
let ctx = build_ctx(&sha, &path);
let ex = MockSimple;
let facts = DynCorpusExtractor::run(&ex, &ctx).expect("run ok");
assert_eq!(facts.len(), 2);
assert_eq!(facts[0].key, "mock_count");
assert_eq!(facts[0].value, ExtractorValue::Int(7));
assert_eq!(facts[1].key, "mock_ok");
assert_eq!(facts[1].value, ExtractorValue::Bool(true));
}
#[test]
fn cached_extractor_prepare_runs_before_extract() {
let (sha, path) = mock_ctx();
let ctx = build_ctx(&sha, &path);
let ex = MockCached;
let facts = DynCorpusExtractor::run(&ex, &ctx).expect("run ok");
assert_eq!(facts.len(), 1);
assert_eq!(facts[0].key, "cached_dex_count");
assert_eq!(facts[0].value, ExtractorValue::Int(0));
}
#[test]
fn prepare_error_short_circuits_extract() {
let (sha, path) = mock_ctx();
let ctx = build_ctx(&sha, &path);
let ex = MockPrepareFails;
let err = DynCorpusExtractor::run(&ex, &ctx).unwrap_err();
assert!(
matches!(err, ExtractorError::MissingState("nothing here")),
"expected MissingState, got {err:?}",
);
}
#[test]
fn extractor_id_stable_across_calls() {
let ex = MockSimple;
let id1 = DynCorpusExtractor::extractor_id(&ex);
let id2 = DynCorpusExtractor::extractor_id(&ex);
let id3 = DynCorpusExtractor::extractor_id(&ex);
assert_eq!(id1, id2);
assert_eq!(id2, id3);
assert_eq!(id1, "mock_simple");
}
#[test]
fn heterogeneous_registry_dispatches_all_extractors() {
let (sha, path) = mock_ctx();
let ctx = build_ctx(&sha, &path);
let registry: Vec<Arc<dyn DynCorpusExtractor>> = vec![
Arc::new(MockSimple),
Arc::new(MockCached),
];
let mut all_facts: Vec<(String, KvPair)> = Vec::new();
for ex in ®istry {
let id = ex.extractor_id().to_owned();
let facts = ex.run(&ctx).expect("run ok");
for f in facts {
all_facts.push((id.clone(), f));
}
}
assert_eq!(all_facts.len(), 3);
assert_eq!(all_facts[0].0, "mock_simple");
assert_eq!(all_facts[0].1.key, "mock_count");
assert_eq!(all_facts[1].0, "mock_simple");
assert_eq!(all_facts[1].1.key, "mock_ok");
assert_eq!(all_facts[2].0, "mock_cached");
assert_eq!(all_facts[2].1.key, "cached_dex_count");
}
#[test]
fn one_extractor_failure_does_not_abort_others_in_loop() {
let (sha, path) = mock_ctx();
let ctx = build_ctx(&sha, &path);
let registry: Vec<Arc<dyn DynCorpusExtractor>> = vec![
Arc::new(MockSimple),
Arc::new(MockPrepareFails),
Arc::new(MockCached),
];
let mut successes = 0_usize;
let mut failures = 0_usize;
for ex in ®istry {
match ex.run(&ctx) {
Ok(facts) => {
assert!(!facts.is_empty(), "{} produced empty facts", ex.extractor_id());
successes = successes.checked_add(1).unwrap_or(successes);
}
Err(_) => {
failures = failures.checked_add(1).unwrap_or(failures);
}
}
}
assert_eq!(successes, 2);
assert_eq!(failures, 1);
}
}