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, errors: Vec, } fn main() { let code = match run() { Ok(code) => code, Err(err) => { eprintln!("{err:#}"); 1 } }; std::process::exit(code); } fn run() -> Result { 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 { 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, errors: Vec, ) -> Report { Report { ok, installer: installer.to_path_buf(), wineprefix: wineprefix.to_path_buf(), checked_files, errors, } } fn command_path(name: &str) -> Option { 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")); } }