//! Shared SIGINT-guard conformance vectors //! (`docs/gpu-probe-sigint-framing.md` §7c). //! //! # Why these live here rather than in each suite //! //! The contract is that the **shell** consumer (`scripts/gate`) and the //! **Rust** consumer (R-d's `sigint_diagnosis`) agree on every case. //! Two independently written copies of the list can drift while both //! still report "45 cases" — the same-length-different-content //! divergence this matrix exists to rule out. One generator, two //! consumers. /// What a consumer must do with a given `(status, stdout)` pair. /// /// `ValidatedError` and `Boundary` **both exit 2**, so a test comparing /// only exit codes cannot separate them — and a validator that accepted /// *every* status 2 would pass. They are told apart by whether the /// child's stderr is surfaced: a validated verdict speaks with the /// helper's voice; a boundary failure must not. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Outcome { Safe, ValidatedIgnored, ValidatedError, Boundary, } pub const TOKEN_SAFE: &[u8] = b"pmacs-sigint-v1:safe"; pub const TOKEN_IGNORED: &[u8] = b"pmacs-sigint-v1:ignored"; pub const TOKEN_ERROR: &[u8] = b"pmacs-sigint-v1:error"; /// Emitted on stderr by the **branch-discriminating** cases — the /// cross-product rows and X1 — so a consumer's output can be searched /// for it: present ⇒ the child's stderr was surfaced. /// /// **X3 and X4 deliberately carry their own payloads instead**, which /// is what makes them distinct inputs rather than duplicates of /// `1/E/empty` and `0/V/safe/bare`. An earlier revision gave every case /// this same sentinel and so shipped 45 entries over 43 distinct /// inputs; [`shared_cases`] now asserts uniqueness so that cannot /// recur silently. pub const SENTINEL: &str = "PMACS-CONFORMANCE-SENTINEL"; /// The canonical wording the helper uses for `ignored`. X3 emits it /// **without** a valid token, so a consumer that surfaced untrusted /// stderr would repeat it — the defect A6b forbids. pub const CANONICAL_IGNORED: &str = "pmacs: SIGINT is ignored; run this command with SIGINT deliverable"; pub struct Case { pub name: String, pub status: i32, pub stdout: Vec, /// Exact stderr this stub emits. Most cases use [`SENTINEL`]; X3 /// and X4 carry their own payloads, which is what makes them /// distinct inputs rather than duplicates of other rows. pub stderr: String, pub expect: Outcome, } /// A `/bin/sh` stub reproducing one case exactly: its stdout bytes, its /// own stderr payload, and its status. #[must_use] pub fn stub_script(case: &Case) -> String { let octal = case.stdout.iter().fold(String::new(), |mut acc, b| { use std::fmt::Write as _; let _ = write!(acc, "\\{b:03o}"); acc }); format!( "#!/bin/sh\nprintf '{octal}'\necho '{}' >&2\nexit {}\n", case.stderr, case.status ) } /// The shared set: ten classes × encodings × three statuses, plus /// X1/X3/X4. Only the diagonal validates. /// /// X2 — a spawn error with no status — is deliberately absent: the /// shell boundary cannot represent it, because an `exec` failure there /// becomes a status. Rust exercises it separately. #[must_use] #[allow( clippy::too_many_lines, reason = "the bulk is the generated vector list; splitting it would \ separate a case from the outcome it encodes, which is the \ one thing this file exists to keep together" )] pub fn shared_cases() -> Vec { let toks: [(&str, &[u8]); 3] = [ ("safe", TOKEN_SAFE), ("ignored", TOKEN_IGNORED), ("error", TOKEN_ERROR), ]; let mut out = Vec::new(); for (idx, (name, correct)) in toks.iter().enumerate() { let status = i32::try_from(idx).expect("0..=2"); let diagonal = match status { 0 => Outcome::Safe, 1 => Outcome::ValidatedIgnored, _ => Outcome::ValidatedError, }; let mut lf = correct.to_vec(); lf.push(b'\n'); out.push(Case { name: format!("{status}/V/{name}/bare"), status, stdout: correct.to_vec(), stderr: SENTINEL.to_owned(), expect: diagonal, }); out.push(Case { name: format!("{status}/V/{name}/lf"), status, stdout: lf, stderr: SENTINEL.to_owned(), expect: diagonal, }); for (other, bytes) in &toks { if other == name { continue; } let mut olf = bytes.to_vec(); olf.push(b'\n'); out.push(Case { name: format!("{status}/M/{other}/bare"), status, stdout: bytes.to_vec(), stderr: SENTINEL.to_owned(), expect: Outcome::Boundary, }); out.push(Case { name: format!("{status}/M/{other}/lf"), status, stdout: olf, stderr: SENTINEL.to_owned(), expect: Outcome::Boundary, }); } let mut leading = vec![b'\n']; leading.extend_from_slice(correct); let mut extra = correct.to_vec(); extra.extend_from_slice(b"\n\n"); let mut spaces = b" ".to_vec(); spaces.extend_from_slice(correct); spaces.push(b' '); let mut crlf = correct.to_vec(); crlf.extend_from_slice(b"\r\n"); let mut doubled = correct.to_vec(); doubled.extend_from_slice(correct); let mut nul = correct.to_vec(); nul.push(0); for (cls, bytes) in [ ("E/empty", Vec::new()), ("U/unknown", b"pmacs-sigint-v2:safe".to_vec()), ("L/leading-lf", leading), ("X/extra-lf", extra), ("S/spaces", spaces), ("C/crlf", crlf), ("D/doubled", doubled), ("N/nul", nul), ] { out.push(Case { name: format!("{status}/{cls}"), status, stdout: bytes, stderr: SENTINEL.to_owned(), expect: Outcome::Boundary, }); } } out.push(Case { name: "X1/status-126".to_owned(), status: 126, stdout: TOKEN_SAFE.to_vec(), stderr: SENTINEL.to_owned(), expect: Outcome::Boundary, }); out.push(Case { name: "X3/ignored-text-no-token".to_owned(), status: 1, stdout: Vec::new(), // The canonical ignored wording WITHOUT a token: a consumer // that surfaced untrusted stderr would repeat it. stderr: CANONICAL_IGNORED.to_owned(), expect: Outcome::Boundary, }); out.push(Case { name: "X4/stderr-noise".to_owned(), status: 0, stdout: TOKEN_SAFE.to_vec(), // Noise on stderr must not affect classification --- and this // payload is what distinguishes X4 from 0/V/safe/bare. stderr: "unrelated chatter on stderr".to_owned(), expect: Outcome::Safe, }); // The set must be 45 DISTINCT inputs, not merely 45 entries. A // previous revision gave every case the same stderr, which silently // collapsed X3 into `1/E/empty` and X4 into `0/V/safe/bare` — 45 // entries, 43 inputs, and two framing-specified cases quietly not // exercised. Asserted here rather than in each suite so no consumer // can forget it. let mut seen = std::collections::HashSet::new(); for case in &out { assert!( seen.insert((case.status, case.stdout.clone(), case.stderr.clone())), "duplicate conformance input at {}: (status, stdout, stderr) already present", case.name ); } assert_eq!(seen.len(), out.len(), "every case must be a distinct input"); out }