fix(cli): fail suspect runs with --strict-integrity

This commit is contained in:
Momoko-Ayase committed 2026-11-04 12:35:22 +08:00
1 parent 312a70b7c6
commit 303aaa6283
2 files changed
+259 -2

No files matched your search

+66 -2
View File
@@ -10,6 +10,7 @@ fn main() -> std::process::ExitCode {
let mut no_log = false; let mut no_log = false;
let mut verbose = false; let mut verbose = false;
let mut scan_all = false; let mut scan_all = false;
let mut strict_integrity = false;
while let Some(a) = args.next() { while let Some(a) = args.next() {
match a.as_str() { match a.as_str() {
@@ -26,6 +27,7 @@ fn main() -> std::process::ExitCode {
"--no-pause" => no_pause = true, "--no-pause" => no_pause = true,
"--no-log" => no_log = true, "--no-log" => no_log = true,
"--scan-all" => scan_all = true, "--scan-all" => scan_all = true,
"--strict-integrity" => strict_integrity = true,
"--out" => match args.next() { "--out" => match args.next() {
Some(v) if !v.starts_with('-') => out = Some(v), Some(v) if !v.starts_with('-') => out = Some(v),
_ => { _ => {
@@ -77,7 +79,7 @@ fn main() -> std::process::ExitCode {
println!("{}", summary.line()); println!("{}", summary.line());
println!("done in {} ms", summary.duration_ms); println!("done in {} ms", summary.duration_ms);
} }
if summary.errors > 0 { 1 } else { 0 } exit_code(&summary, strict_integrity)
} }
Err(error) => { Err(error) => {
if quiet < 2 { if quiet < 2 {
@@ -93,9 +95,22 @@ fn main() -> std::process::ExitCode {
std::process::ExitCode::from(code as u8) std::process::ExitCode::from(code as u8)
} }
/// Process status for a finished run.
///
/// Suspect outputs are still written. By default a suspect file is a warning
/// and the process stays successful; strict mode treats it like an error so a
/// caller can fail the run. An error fails the run either way.
fn exit_code(summary: &job::Summary, strict_integrity: bool) -> u8 {
if summary.errors > 0 || (strict_integrity && summary.suspect > 0) {
1
} else {
0
}
}
fn print_help() { fn print_help() {
println!( println!(
"senbei <file|folder> [--out DIR] [-v|--verbose] [-q|--quiet]... [--scan-all] [--no-log] [--no-pause] [-V|--version] [-h|--help]" "senbei <file|folder> [--out DIR] [-v|--verbose] [-q|--quiet]... [--scan-all] [--strict-integrity] [--no-log] [--no-pause] [-V|--version] [-h|--help]"
); );
println!( println!(
" input a Crackproof PE (.exe/.dll), an il2cpp global-metadata.dat,\n\ " input a Crackproof PE (.exe/.dll), an il2cpp global-metadata.dat,\n\
@@ -107,4 +122,53 @@ fn print_help() {
" --scan-all probe selected .exe/.dll/.so/metadata names below the\n\ " --scan-all probe selected .exe/.dll/.so/metadata names below the\n\
\x20 size floor; other filenames remain excluded." \x20 size floor; other filenames remain excluded."
); );
println!(
" --strict-integrity\n\
\x20 exit non-zero when any output fails the static integrity\n\
\x20 check (a suspect) or a file errors. Off by default: a\n\
\x20 suspect is still written and the process exits 0 unless\n\
\x20 a file errors. Quiet levels are unchanged."
);
}
#[cfg(test)]
mod strict_integrity {
use super::exit_code;
use senbei_io::job::Summary;
fn summary(errors: usize, suspect: usize) -> Summary {
Summary {
errors,
suspect,
..Summary::default()
}
}
#[test]
fn clean_run_exits_0_in_both_modes() {
let summary = summary(0, 0);
assert_eq!(exit_code(&summary, false), 0);
assert_eq!(exit_code(&summary, true), 0);
}
#[test]
fn errors_exit_1_whether_or_not_strict() {
let summary = summary(2, 0);
assert_eq!(exit_code(&summary, false), 1);
assert_eq!(exit_code(&summary, true), 1);
}
#[test]
fn suspect_exits_0_by_default_and_1_when_strict() {
let summary = summary(0, 1);
assert_eq!(exit_code(&summary, false), 0);
assert_eq!(exit_code(&summary, true), 1);
}
#[test]
fn errors_with_suspects_exit_1_in_both_modes() {
let summary = summary(1, 3);
assert_eq!(exit_code(&summary, false), 1);
assert_eq!(exit_code(&summary, true), 1);
}
} }
+193
View File
@@ -0,0 +1,193 @@
//! CLI behavior of `--strict-integrity`.
//!
//! The flag is off by default and only changes the exit status (see the unit
//! tests in `src/main.rs` for the errors-versus-suspect matrix). These tests
//! drive the binary: help text, flag parsing, the error exit, and the quiet
//! contract. The suspect-only exit policy is covered separately by unit tests
//! so these process tests do not depend on the optional protected corpus.
use std::path::Path;
use std::process::Command;
fn senbei() -> Command {
Command::new(env!("CARGO_BIN_EXE_senbei"))
}
fn run(args: &[&str]) -> std::process::Output {
senbei().args(args).output().expect("spawn senbei")
}
fn assert_silent(output: &std::process::Output) {
assert!(
output.stdout.is_empty() && output.stderr.is_empty(),
"quiet>=2 must stay silent, stdout={:?} stderr={:?}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
fn write_plain(dir: &Path) -> std::path::PathBuf {
let path = dir.join("plain.bin");
std::fs::write(&path, b"not a protected image").unwrap();
path
}
fn list_logs(dir: &Path) -> Vec<std::path::PathBuf> {
std::fs::read_dir(dir)
.into_iter()
.flatten()
.filter_map(|entry| entry.ok())
.map(|entry| entry.path())
.filter(|path| {
path.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name.starts_with("senbei-") && name.ends_with(".log"))
})
.collect()
}
#[test]
fn help_documents_strict_integrity_behavior() {
let output = run(&["--help"]);
assert!(output.status.success());
let text = String::from_utf8(output.stdout).unwrap();
assert!(text.contains("--strict-integrity"));
assert!(text.contains("--quiet"));
assert!(text.contains("exit non-zero"));
assert!(text.contains("suspect"));
assert!(text.contains("Off by default"));
assert!(text.contains("still written"));
assert!(text.contains("exits 0 unless"));
assert!(text.contains("a file errors"));
assert!(text.contains("Quiet levels are unchanged"));
}
#[test]
fn short_help_and_version_ignore_the_flag_position() {
let help = run(&["-h", "--strict-integrity"]);
assert!(help.status.success());
assert!(String::from_utf8_lossy(&help.stdout).contains("--strict-integrity"));
let version = run(&["--version"]);
assert!(version.status.success());
assert!(String::from_utf8_lossy(&version.stdout).starts_with("Senbei "));
}
#[test]
fn unknown_flag_still_exits_2() {
let output = run(&["--strict"]);
assert_eq!(output.status.code(), Some(2));
let err = String::from_utf8_lossy(&output.stderr);
assert!(err.contains("unknown option '--strict'"));
assert!(String::from_utf8_lossy(&output.stdout).contains("--strict-integrity"));
}
#[test]
fn missing_input_exits_2_with_the_flag() {
let output = run(&["--strict-integrity", "--no-pause"]);
assert_eq!(output.status.code(), Some(2));
assert!(String::from_utf8_lossy(&output.stdout).contains("--strict-integrity"));
}
#[test]
fn errors_exit_1_by_default_and_when_strict_and_write_nothing() {
let td = tempfile::tempdir().unwrap();
let input = write_plain(td.path());
for extra in [None, Some("--strict-integrity")] {
let out = td
.path()
.join(if extra.is_some() { "strict" } else { "default" });
let mut args = vec![
input.to_str().unwrap(),
"--out",
out.to_str().unwrap(),
"--no-log",
"--no-pause",
];
if let Some(flag) = extra {
args.push(flag);
}
let output = run(&args);
assert_eq!(output.status.code(), Some(1), "{output:?}");
assert!(
!out.join("plain.unpack.bin").exists(),
"an error must not leave an artifact"
);
let text = String::from_utf8_lossy(&output.stdout);
assert!(text.contains("0 unpacked"));
assert!(text.contains("1 errors"));
assert!(text.contains("0 suspect"));
}
}
#[test]
fn double_quiet_stays_silent_for_errors_in_both_modes() {
let td = tempfile::tempdir().unwrap();
let input = write_plain(td.path());
let out = td.path().join("out");
for extra in [None, Some("--strict-integrity")] {
let mut args = vec![
input.to_str().unwrap(),
"--out",
out.to_str().unwrap(),
"--no-log",
"--no-pause",
"-q",
"-q",
];
if let Some(flag) = extra {
args.push(flag);
}
let output = run(&args);
assert_eq!(output.status.code(), Some(1));
assert_silent(&output);
}
}
#[test]
fn single_quiet_keeps_the_summary_and_the_error() {
let td = tempfile::tempdir().unwrap();
let input = write_plain(td.path());
let out = td.path().join("out");
let output = run(&[
input.to_str().unwrap(),
"--out",
out.to_str().unwrap(),
"--no-log",
"--no-pause",
"-q",
"--strict-integrity",
]);
assert_eq!(output.status.code(), Some(1));
let stdout = String::from_utf8_lossy(&output.stdout);
// One -q only suppresses per-file lines. The banner and the summary still
// print; that is the existing quiet contract and strict mode must not
// change it.
assert!(stdout.contains("Senbei "), "{stdout}");
assert!(stdout.contains("1 errors"), "{stdout}");
assert!(stdout.contains("0 suspect"), "{stdout}");
}
#[test]
fn strict_does_not_change_what_the_log_records() {
let td = tempfile::tempdir().unwrap();
let input = write_plain(td.path());
let out = td.path().join("out");
let output = run(&[
input.to_str().unwrap(),
"--out",
out.to_str().unwrap(),
"--strict-integrity",
"--no-pause",
"-q",
"-q",
]);
assert_eq!(output.status.code(), Some(1));
assert_silent(&output);
let logs = list_logs(&out);
assert_eq!(logs.len(), 1);
let text = std::fs::read_to_string(&logs[0]).unwrap();
assert!(text.contains("summary:"));
assert!(text.contains("1 errors"));
assert!(text.contains("0 suspect"));
}