Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 75 additions & 1 deletion crates/flutterdec-core/src/pipeline/runners.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use runners_symbols::{
build_pool_value_hints, canonical_standard_model_name, collect_pool_metadata_stats,
collect_symbol_quality_counts, infer_symbol_name_quality, merge_symbol_name,
symbol_name_quality_from_name_kind, SymbolMergeStats, SymbolNameQuality,
SymbolQualityCounts,
};
#[cfg(test)]
use runners_symbols::{is_generic_symbol_name, normalize_external_symbol_name};
Expand Down Expand Up @@ -144,6 +145,67 @@ fn backend_label(value: Option<AdapterBackend>) -> &'static str {
}
}

fn format_quality_gate_failure_message(
report: &QualityReport,
quality_path: &Path,
report_path: &Path,
input_path: &Path,
resolved_backend: Option<AdapterBackend>,
loaded_adapter_kind: &str,
symbol_quality_counts: &SymbolQualityCounts,
) -> String {
let mut out = String::new();
let _ = writeln!(
out,
"quality gate failed after artifact generation. see {} and {}",
quality_path.display(),
report_path.display()
);
if !report.failures.is_empty() {
let _ = writeln!(out, "reasons: {}", report.failures.join("; "));
}
let _ = writeln!(
out,
"summary: placeholder_ifs={} unresolved_cf={} indirect_call_ratio={:.3} disassembly_ratio={:.3}",
report.placeholder_ifs,
report.unresolved_cf,
report.indirect_call_ratio,
report.disassembly_ratio
);

let mut notes: Vec<String> = Vec::new();
if !is_apk_input_path(input_path) {
notes.push("input is not an APK, so manifest/startup evidence is unavailable".to_string());
}
if resolved_backend == Some(AdapterBackend::Internal) {
notes.push("resolved backend is internal".to_string());
}
if loaded_adapter_kind == "dynamic_snapshot_string_model_v1" {
notes.push("adapter kind is dynamic_snapshot_string_model_v1".to_string());
}
if symbol_quality_counts.placeholder > 0
&& symbol_quality_counts.exact == 0
&& symbol_quality_counts.external == 0
&& symbol_quality_counts.heuristic == 0
{
notes.push("all recovered function names are still placeholders".to_string());
}
if !notes.is_empty() {
let _ = writeln!(out, "context: {}", notes.join("; "));
}

out.push_str(
"artifacts were still written. for exploratory runs while flutterdec is still maturing, you can relax the gates, for example:\n",
);
out.push_str(
" flutterdec decompile <input> -o <out> \\\n --max-placeholder-ifs 999999 \\\n --max-unresolved-cf 999999 \\\n --max-indirect-call-ratio 1.0 \\\n --min-disassembly-ratio 0.0\n",
);
out.push_str(
"you can also improve recovery by decompiling the APK instead of raw libapp.so, using a stronger backend, or providing matched engine symbols.",
);
out
}

fn is_apk_input_path(input_path: &Path) -> bool {
input_path
.extension()
Expand Down Expand Up @@ -2001,7 +2063,19 @@ pub fn run_decompile(
)?;

if !report.passed {
bail!("quality gate failed. see {}", quality_path.display());
let report_path = opt.out_dir.join("report.json");
bail!(
"{}",
format_quality_gate_failure_message(
&report,
&quality_path,
&report_path,
input_path,
resolved_backend,
&loaded_adapter_kind,
&symbol_quality_counts,
)
);
}

Ok(report)
Expand Down
54 changes: 54 additions & 0 deletions crates/flutterdec-core/src/pipeline/runners/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,60 @@
assert_eq!(line, "0x613468: 94000001 bl 0x61346c ; call");
}

#[test]
fn quality_gate_failure_message_explains_strict_placeholder_rejection() {
let report = QualityReport {
mode: "strict".to_string(),
passed: false,
failures: vec!["placeholder if-count exceeded threshold".to_string()],
function_count: 5394,
disassembled_function_count: 5394,
disassembly_ratio: 1.0,
total_calls: 77037,
indirect_calls: 9674,
indirect_call_ratio: 0.12557602191154899,
placeholder_ifs: 1691,
unresolved_cf: 0,
raw_register_calls: 9674,
semantic_direct_calls: 37,
semantic_indirect_calls: 10,
dispatch_selector_calls: 1132,
target_va_symbol_calls: 0,
block_helper_refs: 0,
raw_arg_name_refs: 0,
raw_register_name_refs: 0,
placeholder_cond_markers: 1618,
omitted_path_markers: 827,
loop_backedge_markers: 1,
};
let symbol_quality_counts = SymbolQualityCounts {
placeholder: 5394,
heuristic: 0,
external: 0,
exact: 0,
};

let msg = format_quality_gate_failure_message(
&report,
std::path::Path::new("./out/quality.json"),
std::path::Path::new("./out/report.json"),
std::path::Path::new("libapp.so"),
Some(AdapterBackend::Internal),
"dynamic_snapshot_string_model_v1",
&symbol_quality_counts,
);

assert!(msg.contains("quality gate failed after artifact generation"));
assert!(msg.contains("./out/quality.json"));
assert!(msg.contains("./out/report.json"));
assert!(msg.contains("placeholder if-count exceeded threshold"));
assert!(msg.contains("input is not an APK"));
assert!(msg.contains("resolved backend is internal"));
assert!(msg.contains("all recovered function names are still placeholders"));
assert!(msg.contains("--max-placeholder-ifs 999999"));
assert!(msg.contains("artifacts were still written"));
}

#[test]
fn builds_ghidra_symbol_script_with_sorted_entries_and_escaped_names() {
let mut symbols = HashMap::new();
Expand Down
Loading