1use crate::json::escape;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum ReferencePlatformStatus {
25 Admitted,
26 Unavailable,
27 DocumentationOnly,
28 SkippedWithReason,
29 InadmissibleUnpinned,
31 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#[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#[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#[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#[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#[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#[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
185pub const KNOWN_FIXTURE_SETS: &[&str] = &["diagnostic-fixtures-v1"];
188
189pub const EXTERNAL_JSON_INGESTION: &str =
198 "implemented_t16_5b__no_dep_receipt_scoped_fail_closed_reader__not_a_general_json_library";
199
200#[derive(Debug, Clone, Copy, PartialEq, Eq)]
204pub enum ReceiptAdmission {
205 Admitted,
206 NotAdmittedRunIncomplete,
207 NotAdmittedExitNotSuccess,
208 NotAdmittedPlatformNotAdmissible,
210 NotAdmittedMissingBinaryIdentity,
212 NotAdmittedNonExactArgv,
214 NotAdmittedHashScopeUndeclared,
216 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#[derive(Debug, Clone, PartialEq, Eq)]
256pub struct VendorOracleReceipt {
257 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 pub argv: Vec<String>,
267 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 pub patch_stack: Option<String>,
274 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 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 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 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 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 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 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#[derive(Debug, Clone, PartialEq, Eq)]
453pub enum ReceiptParseError {
454 MalformedJson,
456 WrongSchema,
458 MissingField(&'static str),
460 WrongType(&'static str),
462 UnknownEnumValue(&'static str),
464 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#[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
496struct 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 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 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 0x00..=0x1f => return None,
611 _ => {
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; 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; 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
689const 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 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 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 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 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}