Skip to main content

tzcompile/
vendor_oracle.rs

1//! T16.5 — **external vendor-oracle receipt admission**.
2//!
3//! Doctrine (load-bearing): ***the core repo admits evidence; the external lab generates it.*** The
4//! QEMU/VM/OS-install orchestration that runs a vendor `zic`/`zdump` against the T13/T14 diagnostic
5//! fixture corpus lives **outside** this repository (an external `zic-rs-vendor-oracle-lab`). What lives
6//! here is only: the typed **receipt contract** (`vendor-oracle-receipt-v1`), the **admission rules**
7//! (`VendorOracleReceipt::admit`), canonical JSON **emission**, a minimal **sample** receipt, and the
8//! enforcing non-claim. The core repo never runs a VM.
9//!
10//! **Scope (T16.5-core + T16.5b):** the core *defines* the typed receipt contract, *emits* its canonical
11//! JSON, *ingests* external v1 receipts via a **no-dep, receipt-scoped, fail-closed** reader
12//! ([`VendorOracleReceipt::from_json`] — T16.5b), and *admits* them with strict rules. The reader is
13//! **not** a general JSON library: v1 object shape + known string enums only, unknown fields/values
14//! rejected. **Ingestion is parsing, never execution** — a parse failure ([`ReceiptParseError`]) is *not*
15//! an inadmissible receipt, and the reader runs no platforms. The seal is *"the core can admit a
16//! receipt"*, never *"all vendor platforms are tested"* — platform rows are admitted only by evidence.
17
18use crate::json::escape;
19
20/// The admission status of a vendor/platform reference (T16.5) — never `Admitted` by assumption. A real
21/// platform row reaches `Admitted` only via an admitted receipt that passes the rules; everything else is
22/// an honest non-admitted state.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum ReferencePlatformStatus {
25    Admitted,
26    Unavailable,
27    DocumentationOnly,
28    SkippedWithReason,
29    /// A binary exists but cannot be identified sufficiently to admit (the inadmissibility disposition).
30    InadmissibleUnpinned,
31    /// A receipt is expected from the external lab but has not been admitted yet.
32    PendingExternalReceipt,
33}
34
35impl ReferencePlatformStatus {
36    pub fn as_str(self) -> &'static str {
37        match self {
38            ReferencePlatformStatus::Admitted => "admitted",
39            ReferencePlatformStatus::Unavailable => "unavailable",
40            ReferencePlatformStatus::DocumentationOnly => "documentation_only",
41            ReferencePlatformStatus::SkippedWithReason => "skipped_with_reason",
42            ReferencePlatformStatus::InadmissibleUnpinned => "inadmissible_unpinned",
43            ReferencePlatformStatus::PendingExternalReceipt => "pending_external_receipt",
44        }
45    }
46}
47
48/// How the oracle *run* concluded operationally (T16.5). A **timeout is neither "unavailable" nor
49/// "mismatch"** — it is its own operational result, so it can never be silently read as a verdict.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum OracleRunStatus {
52    Completed,
53    SkippedUnavailable,
54    TimedOut,
55    Killed,
56    FailedToStart,
57}
58
59impl OracleRunStatus {
60    pub fn as_str(self) -> &'static str {
61        match self {
62            OracleRunStatus::Completed => "completed",
63            OracleRunStatus::SkippedUnavailable => "skipped_unavailable",
64            OracleRunStatus::TimedOut => "timed_out",
65            OracleRunStatus::Killed => "killed",
66            OracleRunStatus::FailedToStart => "failed_to_start",
67        }
68    }
69}
70
71/// The oracle process's **exit disposition** (T16.5) — a parsed output with a nonzero exit is **not**
72/// admitted the same way as a clean success.
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum OracleExitDisposition {
75    Success,
76    NonzeroWithOutput,
77    NonzeroNoOutput,
78    TerminatedBySignal,
79    TimedOut,
80    NotRun,
81}
82
83impl OracleExitDisposition {
84    pub fn as_str(self) -> &'static str {
85        match self {
86            OracleExitDisposition::Success => "success",
87            OracleExitDisposition::NonzeroWithOutput => "nonzero_with_output",
88            OracleExitDisposition::NonzeroNoOutput => "nonzero_no_output",
89            OracleExitDisposition::TerminatedBySignal => "terminated_by_signal",
90            OracleExitDisposition::TimedOut => "timed_out",
91            OracleExitDisposition::NotRun => "not_run",
92        }
93    }
94}
95
96/// Encoding metadata for any captured stderr (T16.5) — class/location comparison is primary, but raw
97/// captures still need encoding so a text mismatch is never confused with a semantic one.
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub enum StderrEncoding {
100    Utf8,
101    LocaleDependent,
102    BytesOnly,
103    Unknown,
104}
105
106impl StderrEncoding {
107    pub fn as_str(self) -> &'static str {
108        match self {
109            StderrEncoding::Utf8 => "utf8",
110            StderrEncoding::LocaleDependent => "locale_dependent",
111            StderrEncoding::BytesOnly => "bytes_only",
112            StderrEncoding::Unknown => "unknown",
113        }
114    }
115}
116
117/// What a receipt hash covers (T16.5) — the **claim identity** (stable verdict content) is separated from
118/// the **artifact identity** (full receipt including volatile run metadata like timestamps), so a claim
119/// hash does not churn on every run.
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121pub enum ReceiptHashScope {
122    StableVerdictOnly,
123    FullReceiptIncludingRunMetadata,
124    Unknown,
125}
126
127impl ReceiptHashScope {
128    pub fn as_str(self) -> &'static str {
129        match self {
130            ReceiptHashScope::StableVerdictOnly => "stable_verdict_only",
131            ReceiptHashScope::FullReceiptIncludingRunMetadata => {
132                "full_receipt_including_run_metadata"
133            }
134            ReceiptHashScope::Unknown => "unknown",
135        }
136    }
137}
138
139/// How the oracle was invoked (T16.5) — prefer **exact argv** over a command-string *template*, so
140/// quoting/shell-interpretation ambiguity cannot creep into conformance-grade evidence.
141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
142pub enum OracleInvocationIdentity {
143    ArgvExact,
144    TemplateOnly,
145    ShellExpanded,
146    Unknown,
147}
148
149impl OracleInvocationIdentity {
150    pub fn as_str(self) -> &'static str {
151        match self {
152            OracleInvocationIdentity::ArgvExact => "argv_exact",
153            OracleInvocationIdentity::TemplateOnly => "template_only",
154            OracleInvocationIdentity::ShellExpanded => "shell_expanded",
155            OracleInvocationIdentity::Unknown => "unknown",
156        }
157    }
158}
159
160/// The class/location comparison verdict for one diagnostic fixture (T16.5) — wording compared **last**,
161/// per the T13 rule; this is the class+location axis.
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163pub enum ClassLocationVerdict {
164    ClassLocationMatch,
165    ClassMatchLocationDiff,
166    Divergence,
167    ReferenceOnly,
168    CandidateOnly,
169    NotCompared,
170}
171
172impl ClassLocationVerdict {
173    pub fn as_str(self) -> &'static str {
174        match self {
175            ClassLocationVerdict::ClassLocationMatch => "class_location_match",
176            ClassLocationVerdict::ClassMatchLocationDiff => "class_match_location_diff",
177            ClassLocationVerdict::Divergence => "divergence",
178            ClassLocationVerdict::ReferenceOnly => "reference_only",
179            ClassLocationVerdict::CandidateOnly => "candidate_only",
180            ClassLocationVerdict::NotCompared => "not_compared",
181        }
182    }
183}
184
185/// The fixture sets the core admits receipts *for* (T16.5). A receipt naming any other set is
186/// inadmissible — the core never admits a receipt against an unknown corpus.
187pub const KNOWN_FIXTURE_SETS: &[&str] = &["diagnostic-fixtures-v1"];
188
189/// **The external-JSON ingestion scope (T16.5-core defined the contract; T16.5b shipped the reader).** The
190/// core now ingests v1 receipts via a **no-dep, receipt-scoped, fail-closed** reader
191/// ([`VendorOracleReceipt::from_json`]) — it is **not** a general JSON library (v1 object shape + known
192/// string enums only; unknown fields/values rejected). Two load-bearing doctrine lines:
193/// - *The core repository admits vendor-oracle evidence by typed receipt contract + a scoped reader. It
194///   does not generate the vendor evidence and does not ship the VM lab.*
195/// - *Ingestion is **parsing**, never execution: a parse failure is not an inadmissible receipt, and the
196///   reader runs no platforms.*
197pub const EXTERNAL_JSON_INGESTION: &str =
198    "implemented_t16_5b__no_dep_receipt_scoped_fail_closed_reader__not_a_general_json_library";
199
200/// The verifier's decision on a receipt (T16.5). `Admitted` is reached **only** when the platform is
201/// named, the fixture set is known, the platform status is admissible, the run completed, and the exit
202/// was a clean success — never by assumption.
203#[derive(Debug, Clone, Copy, PartialEq, Eq)]
204pub enum ReceiptAdmission {
205    Admitted,
206    NotAdmittedRunIncomplete,
207    NotAdmittedExitNotSuccess,
208    /// Platform status is `unavailable`/`documentation_only`/`skipped` — not admitted by assumption.
209    NotAdmittedPlatformNotAdmissible,
210    /// No `zic_binary_sha256` — the reference binary is not identified.
211    NotAdmittedMissingBinaryIdentity,
212    /// Invocation is not `argv_exact` (a shell template/expansion is not conformance-grade).
213    NotAdmittedNonExactArgv,
214    /// `receipt_hash_scope` is `unknown` — the claim/artifact hash scope was not declared.
215    NotAdmittedHashScopeUndeclared,
216    /// A class/location verdict is non-matching and not covered by a declared known-divergence.
217    NotAdmittedUnresolvedVerdict,
218    PendingExternalReceipt,
219    InadmissibleMissingPlatform,
220    InadmissibleUnknownFixtureSet,
221    InadmissibleUnpinnedPlatform,
222}
223
224impl ReceiptAdmission {
225    pub fn as_str(self) -> &'static str {
226        match self {
227            ReceiptAdmission::Admitted => "admitted",
228            ReceiptAdmission::NotAdmittedRunIncomplete => "not_admitted_run_incomplete",
229            ReceiptAdmission::NotAdmittedExitNotSuccess => "not_admitted_exit_not_success",
230            ReceiptAdmission::NotAdmittedPlatformNotAdmissible => {
231                "not_admitted_platform_not_admissible"
232            }
233            ReceiptAdmission::NotAdmittedMissingBinaryIdentity => {
234                "not_admitted_missing_binary_identity"
235            }
236            ReceiptAdmission::NotAdmittedNonExactArgv => "not_admitted_non_exact_argv",
237            ReceiptAdmission::NotAdmittedHashScopeUndeclared => {
238                "not_admitted_hash_scope_undeclared"
239            }
240            ReceiptAdmission::NotAdmittedUnresolvedVerdict => "not_admitted_unresolved_verdict",
241            ReceiptAdmission::PendingExternalReceipt => "pending_external_receipt",
242            ReceiptAdmission::InadmissibleMissingPlatform => "inadmissible_missing_platform",
243            ReceiptAdmission::InadmissibleUnknownFixtureSet => "inadmissible_unknown_fixture_set",
244            ReceiptAdmission::InadmissibleUnpinnedPlatform => "inadmissible_unpinned_platform",
245        }
246    }
247    pub fn is_admitted(self) -> bool {
248        self == ReceiptAdmission::Admitted
249    }
250}
251
252/// One externally-generated vendor/platform diagnostic-oracle receipt (`vendor-oracle-receipt-v1`). The
253/// external lab fills this in from a real vendor `zic`/`zdump` run against the fixture corpus; the core
254/// repo only **admits** it (verifies the contract + rules) and emits its canonical JSON form.
255#[derive(Debug, Clone, PartialEq, Eq)]
256pub struct VendorOracleReceipt {
257    /// e.g. `freebsd_14_x86_64` (empty ⇒ inadmissible).
258    pub platform: String,
259    pub platform_status: ReferencePlatformStatus,
260    pub run_status: OracleRunStatus,
261    pub exit_disposition: OracleExitDisposition,
262    pub stderr_encoding: StderrEncoding,
263    pub hash_scope: ReceiptHashScope,
264    pub invocation: OracleInvocationIdentity,
265    /// The **exact argv** of the oracle invocation (preferred over a shell string).
266    pub argv: Vec<String>,
267    /// Which fixture corpus was run (must be in [`KNOWN_FIXTURE_SETS`]).
268    pub fixture_set: String,
269    pub zic_binary_sha256: Option<String>,
270    pub zic_version_output: Option<String>,
271    pub tzdb_source_release: String,
272    /// Distro patch-stack identity, or `None` = not claimed (pristine/unknown).
273    pub patch_stack: Option<String>,
274    /// Per-fixture class/location verdicts (the T13 comparison axis).
275    pub class_location_verdicts: Vec<(String, ClassLocationVerdict)>,
276    pub known_divergences: Vec<String>,
277    pub skipped_with_reason: Option<String>,
278    pub inadmissible_reason: Option<String>,
279}
280
281impl VendorOracleReceipt {
282    /// Verify the receipt against the **strict** admission rules (T16.5-core). `Admitted` is reached
283    /// **only** when *every* condition holds — never by assumption. Precedence (first failing wins):
284    /// missing platform → unknown fixture set → platform status (unpinned / pending / not-admissible) →
285    /// run incomplete → exit not success → missing binary identity → non-exact argv → hash scope
286    /// undeclared → an unresolved (non-match, not-known-divergence) class/location verdict.
287    pub fn admit(&self) -> ReceiptAdmission {
288        if self.platform.trim().is_empty() {
289            return ReceiptAdmission::InadmissibleMissingPlatform;
290        }
291        if !KNOWN_FIXTURE_SETS.contains(&self.fixture_set.as_str()) {
292            return ReceiptAdmission::InadmissibleUnknownFixtureSet;
293        }
294        match self.platform_status {
295            ReferencePlatformStatus::InadmissibleUnpinned => {
296                return ReceiptAdmission::InadmissibleUnpinnedPlatform
297            }
298            ReferencePlatformStatus::PendingExternalReceipt => {
299                return ReceiptAdmission::PendingExternalReceipt
300            }
301            // Never admitted by assumption: `unavailable`/`documentation_only`/`skipped` cannot seal.
302            ReferencePlatformStatus::Unavailable
303            | ReferencePlatformStatus::DocumentationOnly
304            | ReferencePlatformStatus::SkippedWithReason => {
305                return ReceiptAdmission::NotAdmittedPlatformNotAdmissible
306            }
307            ReferencePlatformStatus::Admitted => {}
308        }
309        if self.run_status != OracleRunStatus::Completed {
310            return ReceiptAdmission::NotAdmittedRunIncomplete;
311        }
312        if self.exit_disposition != OracleExitDisposition::Success {
313            return ReceiptAdmission::NotAdmittedExitNotSuccess;
314        }
315        // Conformance-grade evidence: identified binary + exact argv + a declared hash scope.
316        if self.zic_binary_sha256.is_none() {
317            return ReceiptAdmission::NotAdmittedMissingBinaryIdentity;
318        }
319        if self.invocation != OracleInvocationIdentity::ArgvExact {
320            return ReceiptAdmission::NotAdmittedNonExactArgv;
321        }
322        if self.hash_scope == ReceiptHashScope::Unknown {
323            return ReceiptAdmission::NotAdmittedHashScopeUndeclared;
324        }
325        // Every class/location verdict must match, or be a fixture explicitly listed as a known divergence.
326        let all_resolved = self.class_location_verdicts.iter().all(|(fixture, v)| {
327            *v == ClassLocationVerdict::ClassLocationMatch
328                || self.known_divergences.iter().any(|d| d == fixture)
329        });
330        if !all_resolved {
331            return ReceiptAdmission::NotAdmittedUnresolvedVerdict;
332        }
333        ReceiptAdmission::Admitted
334    }
335
336    /// Emit the canonical `vendor-oracle-receipt-v1` JSON (deterministic; the interchange form).
337    pub fn to_json(&self) -> String {
338        let opt = |o: &Option<String>| match o {
339            Some(v) => escape(v),
340            None => "null".to_string(),
341        };
342        let mut argv = String::from("[");
343        for (i, a) in self.argv.iter().enumerate() {
344            if i > 0 {
345                argv.push_str(", ");
346            }
347            argv.push_str(&escape(a));
348        }
349        argv.push(']');
350        let mut verdicts = String::from("[");
351        for (i, (fixture, v)) in self.class_location_verdicts.iter().enumerate() {
352            if i > 0 {
353                verdicts.push_str(", ");
354            }
355            verdicts.push_str(&format!(
356                "{{ \"fixture\": {}, \"verdict\": {} }}",
357                escape(fixture),
358                escape(v.as_str())
359            ));
360        }
361        verdicts.push(']');
362        let mut divergences = String::from("[");
363        for (i, d) in self.known_divergences.iter().enumerate() {
364            if i > 0 {
365                divergences.push_str(", ");
366            }
367            divergences.push_str(&escape(d));
368        }
369        divergences.push(']');
370        format!(
371            "{{\n  \"schema\": \"vendor-oracle-receipt-v1\",\n  \
372             \"admission\": {},\n  \"platform\": {},\n  \"platform_status\": {},\n  \
373             \"oracle_run_status\": {},\n  \"oracle_exit_disposition\": {},\n  \
374             \"stderr_encoding\": {},\n  \"receipt_hash_scope\": {},\n  \
375             \"oracle_invocation_identity\": {},\n  \"argv\": {},\n  \"fixture_set\": {},\n  \
376             \"zic_binary_sha256\": {},\n  \"zic_version_output\": {},\n  \"tzdb_source_release\": {},\n  \
377             \"patch_stack_identity\": {},\n  \"class_location_verdicts\": {},\n  \
378             \"known_divergences\": {},\n  \"skipped_with_reason\": {},\n  \"inadmissible_reason\": {}\n}}\n",
379            escape(self.admit().as_str()),
380            escape(&self.platform),
381            escape(self.platform_status.as_str()),
382            escape(self.run_status.as_str()),
383            escape(self.exit_disposition.as_str()),
384            escape(self.stderr_encoding.as_str()),
385            escape(self.hash_scope.as_str()),
386            escape(self.invocation.as_str()),
387            argv,
388            escape(&self.fixture_set),
389            opt(&self.zic_binary_sha256),
390            opt(&self.zic_version_output),
391            escape(&self.tzdb_source_release),
392            opt(&self.patch_stack),
393            verdicts,
394            divergences,
395            opt(&self.skipped_with_reason),
396            opt(&self.inadmissible_reason),
397        )
398    }
399
400    /// A minimal **admissible** sample receipt (the canonical schema example). Used by the shape test and
401    /// emittable as documentation. Represents a clean FreeBSD run that the core would admit.
402    pub fn minimal_sample() -> Self {
403        VendorOracleReceipt {
404            platform: "freebsd_14_x86_64".to_string(),
405            platform_status: ReferencePlatformStatus::Admitted,
406            run_status: OracleRunStatus::Completed,
407            exit_disposition: OracleExitDisposition::Success,
408            stderr_encoding: StderrEncoding::Utf8,
409            hash_scope: ReceiptHashScope::StableVerdictOnly,
410            invocation: OracleInvocationIdentity::ArgvExact,
411            argv: vec![
412                "zic".to_string(),
413                "-v".to_string(),
414                "-d".to_string(),
415                "out".to_string(),
416                "fixture.zi".to_string(),
417            ],
418            fixture_set: "diagnostic-fixtures-v1".to_string(),
419            zic_binary_sha256: Some("0".repeat(64)),
420            zic_version_output: Some("zic (FreeBSD) 2026b".to_string()),
421            tzdb_source_release: "2026b".to_string(),
422            patch_stack: None,
423            class_location_verdicts: vec![
424                (
425                    "unknown_line_type".to_string(),
426                    ClassLocationVerdict::ClassLocationMatch,
427                ),
428                (
429                    "duplicate_zone".to_string(),
430                    ClassLocationVerdict::ClassLocationMatch,
431                ),
432            ],
433            known_divergences: Vec::new(),
434            skipped_with_reason: None,
435            inadmissible_reason: None,
436        }
437    }
438}
439
440// ─────────────────────────────────────────────────────────────────────────────────────────────────
441// T16.5b — external receipt JSON **ingestion** (a no-dep reader, scoped to `vendor-oracle-receipt-v1`).
442//
443// This is **not** a general JSON library: it parses exactly the receipt's known object shape and known
444// string enums, and **fails closed** on anything else (malformed JSON, missing/extra fields, wrong types,
445// unknown enum values). The load-bearing distinction: a **parse failure** ([`ReceiptParseError`]) is *not*
446// the same as **inadmissible evidence** — a receipt may parse cleanly and still be non-admitted by
447// [`VendorOracleReceipt::admit`]. QEMU/VM orchestration remains entirely outside the core repo.
448// ─────────────────────────────────────────────────────────────────────────────────────────────────
449
450/// Why an external receipt could not be turned into a typed [`VendorOracleReceipt`] (T16.5b). Distinct
451/// from admission: parse errors mean "this is not a well-formed v1 receipt", never "the evidence is bad".
452#[derive(Debug, Clone, PartialEq, Eq)]
453pub enum ReceiptParseError {
454    /// The bytes are not well-formed JSON, or not a top-level object.
455    MalformedJson,
456    /// `schema` is missing or not `vendor-oracle-receipt-v1`.
457    WrongSchema,
458    /// A required field is absent.
459    MissingField(&'static str),
460    /// A field has the wrong JSON type (e.g. a string where an array was required).
461    WrongType(&'static str),
462    /// A string enum carried a value outside its finite vocabulary (fail-closed; never coerced).
463    UnknownEnumValue(&'static str),
464    /// An unrecognised top-level field (strict v1: we refuse receipts carrying claims we don't model).
465    UnknownField,
466}
467
468impl std::fmt::Display for ReceiptParseError {
469    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
470        match self {
471            ReceiptParseError::MalformedJson => {
472                write!(f, "malformed JSON (not a v1 receipt object)")
473            }
474            ReceiptParseError::WrongSchema => write!(f, "schema is not vendor-oracle-receipt-v1"),
475            ReceiptParseError::MissingField(n) => write!(f, "missing required field: {n}"),
476            ReceiptParseError::WrongType(n) => write!(f, "field has wrong type: {n}"),
477            ReceiptParseError::UnknownEnumValue(n) => write!(f, "unknown enum value in field: {n}"),
478            ReceiptParseError::UnknownField => write!(f, "unrecognised field (strict v1 receipt)"),
479        }
480    }
481}
482
483/// A minimal JSON value — the *only* shapes a receipt uses. Numbers are kept as raw text (receipts have
484/// no numeric fields, but the grammar accepts them so a stray number fails at the *field* layer, not the
485/// tokenizer). This type is private and receipt-scoped on purpose.
486#[derive(Debug, Clone, PartialEq)]
487enum Json {
488    Null,
489    Bool(bool),
490    Num(String),
491    Str(String),
492    Arr(Vec<Json>),
493    Obj(Vec<(String, Json)>),
494}
495
496/// A tiny recursive-descent JSON parser (RFC 8259 subset; fail-closed). Scoped to receipt ingestion — it
497/// is intentionally *not* exposed as a general JSON facility.
498struct JsonReader<'a> {
499    b: &'a [u8],
500    i: usize,
501}
502
503impl<'a> JsonReader<'a> {
504    fn new(s: &'a str) -> Self {
505        JsonReader {
506            b: s.as_bytes(),
507            i: 0,
508        }
509    }
510    fn ws(&mut self) {
511        while self.i < self.b.len() && matches!(self.b[self.i], b' ' | b'\t' | b'\n' | b'\r') {
512            self.i += 1;
513        }
514    }
515    fn peek(&self) -> Option<u8> {
516        self.b.get(self.i).copied()
517    }
518    fn parse(&mut self) -> Option<Json> {
519        self.ws();
520        let v = self.value()?;
521        self.ws();
522        // Reject trailing junk (fail-closed): the whole input must be exactly one value.
523        if self.i == self.b.len() {
524            Some(v)
525        } else {
526            None
527        }
528    }
529    fn value(&mut self) -> Option<Json> {
530        self.ws();
531        match self.peek()? {
532            b'{' => self.object(),
533            b'[' => self.array(),
534            b'"' => self.string().map(Json::Str),
535            b't' => self.lit(b"true").then_some(Json::Bool(true)),
536            b'f' => self.lit(b"false").then_some(Json::Bool(false)),
537            b'n' => self.lit(b"null").then_some(Json::Null),
538            b'-' | b'0'..=b'9' => self.number(),
539            _ => None,
540        }
541    }
542    fn lit(&mut self, kw: &[u8]) -> bool {
543        if self.b[self.i..].starts_with(kw) {
544            self.i += kw.len();
545            true
546        } else {
547            false
548        }
549    }
550    fn number(&mut self) -> Option<Json> {
551        let start = self.i;
552        if self.peek() == Some(b'-') {
553            self.i += 1;
554        }
555        let mut saw_digit = false;
556        while let Some(c) = self.peek() {
557            if c.is_ascii_digit() || matches!(c, b'.' | b'e' | b'E' | b'+' | b'-') {
558                saw_digit |= c.is_ascii_digit();
559                self.i += 1;
560            } else {
561                break;
562            }
563        }
564        if !saw_digit {
565            return None;
566        }
567        Some(Json::Num(
568            std::str::from_utf8(&self.b[start..self.i])
569                .ok()?
570                .to_string(),
571        ))
572    }
573    fn string(&mut self) -> Option<String> {
574        if self.peek() != Some(b'"') {
575            return None;
576        }
577        self.i += 1;
578        let mut out = String::new();
579        loop {
580            let c = self.peek()?;
581            self.i += 1;
582            match c {
583                b'"' => return Some(out),
584                b'\\' => {
585                    let e = self.peek()?;
586                    self.i += 1;
587                    match e {
588                        b'"' => out.push('"'),
589                        b'\\' => out.push('\\'),
590                        b'/' => out.push('/'),
591                        b'b' => out.push('\u{0008}'),
592                        b'f' => out.push('\u{000c}'),
593                        b'n' => out.push('\n'),
594                        b'r' => out.push('\r'),
595                        b't' => out.push('\t'),
596                        b'u' => {
597                            // Exactly 4 hex digits → a BMP scalar (receipts emit only `\u00XX` controls).
598                            if self.i + 4 > self.b.len() {
599                                return None;
600                            }
601                            let hex = std::str::from_utf8(&self.b[self.i..self.i + 4]).ok()?;
602                            let cp = u32::from_str_radix(hex, 16).ok()?;
603                            out.push(char::from_u32(cp)?);
604                            self.i += 4;
605                        }
606                        _ => return None,
607                    }
608                }
609                // A raw control byte inside a string is invalid JSON (fail-closed).
610                0x00..=0x1f => return None,
611                // Otherwise copy the UTF-8 byte(s) through. `c` is the lead byte; copy continuation bytes.
612                _ => {
613                    let lead = c;
614                    let extra = match lead {
615                        0x00..=0x7f => 0,
616                        0xc0..=0xdf => 1,
617                        0xe0..=0xef => 2,
618                        0xf0..=0xf7 => 3,
619                        _ => return None,
620                    };
621                    let begin = self.i - 1;
622                    if begin + 1 + extra > self.b.len() {
623                        return None;
624                    }
625                    self.i = begin + 1 + extra;
626                    out.push_str(std::str::from_utf8(&self.b[begin..self.i]).ok()?);
627                }
628            }
629        }
630    }
631    fn array(&mut self) -> Option<Json> {
632        self.i += 1; // '['
633        let mut items = Vec::new();
634        self.ws();
635        if self.peek() == Some(b']') {
636            self.i += 1;
637            return Some(Json::Arr(items));
638        }
639        loop {
640            items.push(self.value()?);
641            self.ws();
642            match self.peek()? {
643                b',' => {
644                    self.i += 1;
645                    continue;
646                }
647                b']' => {
648                    self.i += 1;
649                    return Some(Json::Arr(items));
650                }
651                _ => return None,
652            }
653        }
654    }
655    fn object(&mut self) -> Option<Json> {
656        self.i += 1; // '{'
657        let mut pairs = Vec::new();
658        self.ws();
659        if self.peek() == Some(b'}') {
660            self.i += 1;
661            return Some(Json::Obj(pairs));
662        }
663        loop {
664            self.ws();
665            let key = self.string()?;
666            self.ws();
667            if self.peek()? != b':' {
668                return None;
669            }
670            self.i += 1;
671            let val = self.value()?;
672            pairs.push((key, val));
673            self.ws();
674            match self.peek()? {
675                b',' => {
676                    self.i += 1;
677                    continue;
678                }
679                b'}' => {
680                    self.i += 1;
681                    return Some(Json::Obj(pairs));
682                }
683                _ => return None,
684            }
685        }
686    }
687}
688
689/// The set of field names a v1 receipt may carry (strict: any other key fails closed). `admission` is
690/// accepted but **ignored** on ingest — the producer's self-assessment is never trusted; we recompute it.
691const RECEIPT_FIELDS: &[&str] = &[
692    "schema",
693    "admission",
694    "platform",
695    "platform_status",
696    "oracle_run_status",
697    "oracle_exit_disposition",
698    "stderr_encoding",
699    "receipt_hash_scope",
700    "oracle_invocation_identity",
701    "argv",
702    "fixture_set",
703    "zic_binary_sha256",
704    "zic_version_output",
705    "tzdb_source_release",
706    "patch_stack_identity",
707    "class_location_verdicts",
708    "known_divergences",
709    "skipped_with_reason",
710    "inadmissible_reason",
711];
712
713impl VendorOracleReceipt {
714    /// Ingest an external `vendor-oracle-receipt-v1` JSON document into the typed contract (T16.5b),
715    /// **fail-closed**. Returns a [`ReceiptParseError`] for anything that is not a well-formed v1 receipt
716    /// — *distinct* from the admission verdict (call [`Self::admit`] on the parsed receipt to decide that).
717    /// The reader is receipt-scoped and never relaxes: unknown fields and unknown enum values are rejected.
718    pub fn from_json(input: &str) -> Result<Self, ReceiptParseError> {
719        let root = JsonReader::new(input)
720            .parse()
721            .ok_or(ReceiptParseError::MalformedJson)?;
722        let obj = match root {
723            Json::Obj(pairs) => pairs,
724            _ => return Err(ReceiptParseError::MalformedJson),
725        };
726        // Strict v1: reject any unrecognised field (we refuse to silently ignore unmodelled claims).
727        for (k, _) in &obj {
728            if !RECEIPT_FIELDS.contains(&k.as_str()) {
729                return Err(ReceiptParseError::UnknownField);
730            }
731        }
732        let get = |name: &'static str| obj.iter().find(|(k, _)| k == name).map(|(_, v)| v);
733        let req = |name: &'static str| get(name).ok_or(ReceiptParseError::MissingField(name));
734
735        // schema must be exactly the v1 id.
736        match req("schema")? {
737            Json::Str(s) if s == "vendor-oracle-receipt-v1" => {}
738            _ => return Err(ReceiptParseError::WrongSchema),
739        }
740
741        let as_str = |name: &'static str, v: &Json| -> Result<String, ReceiptParseError> {
742            match v {
743                Json::Str(s) => Ok(s.clone()),
744                _ => Err(ReceiptParseError::WrongType(name)),
745            }
746        };
747        let opt_str = |name: &'static str| -> Result<Option<String>, ReceiptParseError> {
748            match get(name) {
749                None | Some(Json::Null) => Ok(None),
750                Some(Json::Str(s)) => Ok(Some(s.clone())),
751                Some(_) => Err(ReceiptParseError::WrongType(name)),
752            }
753        };
754        let str_array = |name: &'static str| -> Result<Vec<String>, ReceiptParseError> {
755            match req(name)? {
756                Json::Arr(items) => items.iter().map(|it| as_str(name, it)).collect(),
757                _ => Err(ReceiptParseError::WrongType(name)),
758            }
759        };
760
761        let platform = as_str("platform", req("platform")?)?;
762        let platform_status =
763            parse_platform_status(&as_str("platform_status", req("platform_status")?)?)
764                .ok_or(ReceiptParseError::UnknownEnumValue("platform_status"))?;
765        let run_status = parse_run_status(&as_str("oracle_run_status", req("oracle_run_status")?)?)
766            .ok_or(ReceiptParseError::UnknownEnumValue("oracle_run_status"))?;
767        let exit_disposition = parse_exit_disposition(&as_str(
768            "oracle_exit_disposition",
769            req("oracle_exit_disposition")?,
770        )?)
771        .ok_or(ReceiptParseError::UnknownEnumValue(
772            "oracle_exit_disposition",
773        ))?;
774        let stderr_encoding =
775            parse_stderr_encoding(&as_str("stderr_encoding", req("stderr_encoding")?)?)
776                .ok_or(ReceiptParseError::UnknownEnumValue("stderr_encoding"))?;
777        let hash_scope =
778            parse_hash_scope(&as_str("receipt_hash_scope", req("receipt_hash_scope")?)?)
779                .ok_or(ReceiptParseError::UnknownEnumValue("receipt_hash_scope"))?;
780        let invocation = parse_invocation(&as_str(
781            "oracle_invocation_identity",
782            req("oracle_invocation_identity")?,
783        )?)
784        .ok_or(ReceiptParseError::UnknownEnumValue(
785            "oracle_invocation_identity",
786        ))?;
787        let argv = str_array("argv")?;
788        let fixture_set = as_str("fixture_set", req("fixture_set")?)?;
789
790        // class_location_verdicts: array of { fixture, verdict } (verdict from the finite vocab).
791        let mut class_location_verdicts = Vec::new();
792        match req("class_location_verdicts")? {
793            Json::Arr(items) => {
794                for it in items {
795                    let pairs = match it {
796                        Json::Obj(p) => p,
797                        _ => return Err(ReceiptParseError::WrongType("class_location_verdicts")),
798                    };
799                    let fixture = pairs
800                        .iter()
801                        .find(|(k, _)| k == "fixture")
802                        .map(|(_, v)| as_str("class_location_verdicts", v))
803                        .ok_or(ReceiptParseError::MissingField(
804                            "class_location_verdicts.fixture",
805                        ))??;
806                    let verdict_s = pairs
807                        .iter()
808                        .find(|(k, _)| k == "verdict")
809                        .map(|(_, v)| as_str("class_location_verdicts", v))
810                        .ok_or(ReceiptParseError::MissingField(
811                            "class_location_verdicts.verdict",
812                        ))??;
813                    let verdict = parse_class_location_verdict(&verdict_s).ok_or(
814                        ReceiptParseError::UnknownEnumValue("class_location_verdicts.verdict"),
815                    )?;
816                    class_location_verdicts.push((fixture, verdict));
817                }
818            }
819            _ => return Err(ReceiptParseError::WrongType("class_location_verdicts")),
820        }
821
822        Ok(VendorOracleReceipt {
823            platform,
824            platform_status,
825            run_status,
826            exit_disposition,
827            stderr_encoding,
828            hash_scope,
829            invocation,
830            argv,
831            fixture_set,
832            zic_binary_sha256: opt_str("zic_binary_sha256")?,
833            zic_version_output: opt_str("zic_version_output")?,
834            tzdb_source_release: as_str("tzdb_source_release", req("tzdb_source_release")?)?,
835            patch_stack: opt_str("patch_stack_identity")?,
836            class_location_verdicts,
837            known_divergences: str_array("known_divergences")?,
838            skipped_with_reason: opt_str("skipped_with_reason")?,
839            inadmissible_reason: opt_str("inadmissible_reason")?,
840        })
841    }
842}
843
844fn parse_platform_status(s: &str) -> Option<ReferencePlatformStatus> {
845    Some(match s {
846        "admitted" => ReferencePlatformStatus::Admitted,
847        "unavailable" => ReferencePlatformStatus::Unavailable,
848        "documentation_only" => ReferencePlatformStatus::DocumentationOnly,
849        "skipped_with_reason" => ReferencePlatformStatus::SkippedWithReason,
850        "inadmissible_unpinned" => ReferencePlatformStatus::InadmissibleUnpinned,
851        "pending_external_receipt" => ReferencePlatformStatus::PendingExternalReceipt,
852        _ => return None,
853    })
854}
855fn parse_run_status(s: &str) -> Option<OracleRunStatus> {
856    Some(match s {
857        "completed" => OracleRunStatus::Completed,
858        "skipped_unavailable" => OracleRunStatus::SkippedUnavailable,
859        "timed_out" => OracleRunStatus::TimedOut,
860        "killed" => OracleRunStatus::Killed,
861        "failed_to_start" => OracleRunStatus::FailedToStart,
862        _ => return None,
863    })
864}
865fn parse_exit_disposition(s: &str) -> Option<OracleExitDisposition> {
866    Some(match s {
867        "success" => OracleExitDisposition::Success,
868        "nonzero_with_output" => OracleExitDisposition::NonzeroWithOutput,
869        "nonzero_no_output" => OracleExitDisposition::NonzeroNoOutput,
870        "terminated_by_signal" => OracleExitDisposition::TerminatedBySignal,
871        "timed_out" => OracleExitDisposition::TimedOut,
872        "not_run" => OracleExitDisposition::NotRun,
873        _ => return None,
874    })
875}
876fn parse_stderr_encoding(s: &str) -> Option<StderrEncoding> {
877    Some(match s {
878        "utf8" => StderrEncoding::Utf8,
879        "locale_dependent" => StderrEncoding::LocaleDependent,
880        "bytes_only" => StderrEncoding::BytesOnly,
881        "unknown" => StderrEncoding::Unknown,
882        _ => return None,
883    })
884}
885fn parse_hash_scope(s: &str) -> Option<ReceiptHashScope> {
886    Some(match s {
887        "stable_verdict_only" => ReceiptHashScope::StableVerdictOnly,
888        "full_receipt_including_run_metadata" => ReceiptHashScope::FullReceiptIncludingRunMetadata,
889        "unknown" => ReceiptHashScope::Unknown,
890        _ => return None,
891    })
892}
893fn parse_invocation(s: &str) -> Option<OracleInvocationIdentity> {
894    Some(match s {
895        "argv_exact" => OracleInvocationIdentity::ArgvExact,
896        "template_only" => OracleInvocationIdentity::TemplateOnly,
897        "shell_expanded" => OracleInvocationIdentity::ShellExpanded,
898        "unknown" => OracleInvocationIdentity::Unknown,
899        _ => return None,
900    })
901}
902fn parse_class_location_verdict(s: &str) -> Option<ClassLocationVerdict> {
903    Some(match s {
904        "class_location_match" => ClassLocationVerdict::ClassLocationMatch,
905        "class_match_location_diff" => ClassLocationVerdict::ClassMatchLocationDiff,
906        "divergence" => ClassLocationVerdict::Divergence,
907        "reference_only" => ClassLocationVerdict::ReferenceOnly,
908        "candidate_only" => ClassLocationVerdict::CandidateOnly,
909        "not_compared" => ClassLocationVerdict::NotCompared,
910        _ => return None,
911    })
912}