mirror of
https://github.com/Momoko-Ayase/Senbei.git
synced 2026-11-04 10:31:29 -05:00
fix(cli): fail suspect runs with --strict-integrity
This commit is contained in:
1 parent
312a70b7c6
commit
303aaa6283
2 files changed
+259
-2
No files matched your search
+66
-2
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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"));
|
||||||
|
}
|
||||||
Reference in new issue
Block a user