Files
AWatch-rus/adk-rust/crates/verify-innosetup-installer/src/main.rs
T

271 lines
8.0 KiB
Rust

use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use anyhow::{Context, Result, bail};
use clap::Parser;
use serde::Serialize;
const DEFAULT_INSTALLER: &str = "windows/installkit/innosetup/AWatch-rus-InstallKit.exe";
const DEFAULT_WINEPREFIX: &str = "/tmp/aw-inno-verify-wineprefix";
const INSTALL_DIR_WIN: &str = r"C:\AWatchRusExtract";
const REQUIRED_FILES: &[&str] = &[
"windows/AWatchRusCollectorGuardService.cs",
"windows/aw-collector-guard.ps1",
"windows/install-collector-guard-service.ps1",
"windows/aw-windows-telemetry.exe",
"windows/dlp-policy.native-cross-os.example.json",
];
const WINDOWS_TELEMETRY_EXE_SOURCE: &str =
"adk-rust/target/x86_64-pc-windows-gnu/release/aw-windows-telemetry.exe";
const GUARD_MARKER: &str = "collector guard self-test OK";
#[derive(Debug, Parser)]
#[command(about = "Verify AWatch-rus InnoSetup installer payload through Wine")]
struct Cli {
#[arg(default_value = DEFAULT_INSTALLER)]
installer: PathBuf,
#[arg(long, default_value = ".")]
root: PathBuf,
#[arg(long, env = "WINEPREFIX_VERIFY", default_value = DEFAULT_WINEPREFIX)]
wineprefix: PathBuf,
#[arg(long, default_value_t = false)]
json: bool,
}
#[derive(Debug, Serialize)]
struct Report {
ok: bool,
installer: PathBuf,
wineprefix: PathBuf,
checked_files: Vec<String>,
errors: Vec<String>,
}
fn main() {
let code = match run() {
Ok(code) => code,
Err(err) => {
eprintln!("{err:#}");
1
}
};
std::process::exit(code);
}
fn run() -> Result<i32> {
let cli = Cli::parse();
let root = cli
.root
.canonicalize()
.with_context(|| format!("canonicalize root {}", cli.root.display()))?;
let installer = resolve_path(&root, cli.installer);
let report = verify(&root, &installer, &cli.wineprefix);
print_report(&report, cli.json)?;
Ok(if report.ok { 0 } else { 1 })
}
fn verify(root: &Path, installer: &Path, wineprefix: &Path) -> Report {
let mut errors = Vec::new();
if !installer.is_file() {
errors.push(format!("Installer not found: {}", installer.display()));
return report(false, installer, wineprefix, Vec::new(), errors);
}
if command_path("wine").is_none() {
errors.push("wine not found".to_string());
return report(false, installer, wineprefix, Vec::new(), errors);
}
if command_path("wineboot").is_none() {
errors.push("wineboot not found".to_string());
return report(false, installer, wineprefix, Vec::new(), errors);
}
if command_path("wineserver").is_none() {
errors.push("wineserver not found".to_string());
return report(false, installer, wineprefix, Vec::new(), errors);
}
if wineprefix.exists()
&& let Err(err) = fs::remove_dir_all(wineprefix)
{
errors.push(format!(
"failed to remove WINEPREFIX {}: {err}",
wineprefix.display()
));
return report(false, installer, wineprefix, Vec::new(), errors);
}
if let Err(err) = fs::create_dir_all(wineprefix) {
errors.push(format!(
"failed to create WINEPREFIX {}: {err}",
wineprefix.display()
));
return report(false, installer, wineprefix, Vec::new(), errors);
}
if let Err(err) = run_quiet(
Command::new("wineboot")
.arg("-u")
.env("WINEPREFIX", wineprefix)
.env(
"WINEDEBUG",
env::var("WINEDEBUG").unwrap_or_else(|_| "-all".to_string()),
),
) {
errors.push(err.to_string());
return report(false, installer, wineprefix, Vec::new(), errors);
}
if let Err(err) = run_quiet(
Command::new("wine")
.arg(installer)
.arg("/VERYSILENT")
.arg("/SUPPRESSMSGBOXES")
.arg("/NORESTART")
.arg("/SP-")
.arg("/TASKS=")
.arg(format!(r#"/DIR={INSTALL_DIR_WIN}"#))
.env("WINEPREFIX", wineprefix)
.env(
"WINEDEBUG",
env::var("WINEDEBUG").unwrap_or_else(|_| "-all".to_string()),
),
) {
errors.push(err.to_string());
return report(false, installer, wineprefix, Vec::new(), errors);
}
if let Err(err) = run_quiet(
Command::new("wineserver")
.arg("-w")
.env("WINEPREFIX", wineprefix),
) {
errors.push(err.to_string());
return report(false, installer, wineprefix, Vec::new(), errors);
}
let install_dir = wineprefix.join("drive_c/AWatchRusExtract");
let mut checked = Vec::new();
for rel in REQUIRED_FILES {
let extracted = install_dir.join(rel);
let repo = if *rel == "windows/aw-windows-telemetry.exe" {
root.join(WINDOWS_TELEMETRY_EXE_SOURCE)
} else {
root.join(rel)
};
if !extracted.is_file() {
errors.push(format!("Missing extracted file: {rel}"));
continue;
}
match files_equal(&repo, &extracted) {
Ok(true) => checked.push((*rel).to_string()),
Ok(false) => errors.push(format!("Extracted file differs from repo: {rel}")),
Err(err) => errors.push(err.to_string()),
}
}
let guard = install_dir.join("windows/aw-collector-guard.ps1");
match fs::read_to_string(&guard) {
Ok(text) if text.contains(GUARD_MARKER) => {}
Ok(_) => {
errors.push("Guard self-test marker missing in extracted installer payload".to_string())
}
Err(err) => errors.push(format!(
"read extracted guard script {}: {err}",
guard.display()
)),
}
report(errors.is_empty(), installer, wineprefix, checked, errors)
}
fn run_quiet(command: &mut Command) -> Result<()> {
let program = command.get_program().to_string_lossy().to_string();
let status = command
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.with_context(|| format!("run {program}"))?;
if !status.success() {
bail!("{program} failed with status {status}");
}
Ok(())
}
fn files_equal(left: &Path, right: &Path) -> Result<bool> {
let left = fs::read(left).with_context(|| format!("read {}", left.display()))?;
let right = fs::read(right).with_context(|| format!("read {}", right.display()))?;
Ok(left == right)
}
fn print_report(report: &Report, json: bool) -> Result<()> {
if json {
println!("{}", serde_json::to_string_pretty(report)?);
return Ok(());
}
for err in &report.errors {
eprintln!("{err}");
}
if report.ok {
println!("verify_innosetup_installer: OK");
}
Ok(())
}
fn report(
ok: bool,
installer: &Path,
wineprefix: &Path,
checked_files: Vec<String>,
errors: Vec<String>,
) -> Report {
Report {
ok,
installer: installer.to_path_buf(),
wineprefix: wineprefix.to_path_buf(),
checked_files,
errors,
}
}
fn command_path(name: &str) -> Option<PathBuf> {
let path = env::var_os("PATH")?;
env::split_paths(&path)
.map(|dir| dir.join(name))
.find(|candidate| candidate.is_file())
}
fn resolve_path(root: &Path, path: PathBuf) -> PathBuf {
if path.is_absolute() {
path
} else {
root.join(path)
}
}
#[cfg(test)]
mod tests {
use std::fs;
use tempfile::tempdir;
use super::{files_equal, resolve_path};
#[test]
fn compares_files_by_bytes() {
let tmp = tempdir().unwrap();
let a = tmp.path().join("a");
let b = tmp.path().join("b");
fs::write(&a, b"same").unwrap();
fs::write(&b, b"same").unwrap();
assert!(files_equal(&a, &b).unwrap());
fs::write(&b, b"different").unwrap();
assert!(!files_equal(&a, &b).unwrap());
}
#[test]
fn resolves_relative_path_against_root() {
let root = std::path::Path::new("/tmp/root");
assert_eq!(resolve_path(root, "a/b".into()), root.join("a/b"));
}
}