use std::collections::BTreeMap;
use std::path::Path;
use crate::error::{Error, Result};
use crate::hash::sha256_hex;
use crate::{CompileReport, LinkMode};
pub const SCHEMA: &str = "zic-rs-alias-map-v1";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AliasEntry {
Zone { sha256: String },
Link {
target: String,
target_sha256: String,
materialised: LinkMode,
},
}
impl AliasEntry {
pub fn kind_str(&self) -> &'static str {
match self {
AliasEntry::Zone { .. } => "zone",
AliasEntry::Link { .. } => "link",
}
}
}
#[derive(Debug, Clone)]
pub struct AliasMap {
pub entries: BTreeMap<String, AliasEntry>,
pub identifiers: usize,
pub canonical_zones: usize,
pub links: usize,
pub duplicated_byte_links: usize,
}
pub fn build(report: &CompileReport, _root: &Path) -> Result<AliasMap> {
let mut zone_hash: BTreeMap<String, String> = BTreeMap::new();
for z in &report.zones_compiled {
let bytes = std::fs::read(&z.output_path).map_err(|e| Error::io(&z.output_path, e))?;
zone_hash.insert(z.name.clone(), sha256_hex(&bytes));
}
let mut entries: BTreeMap<String, AliasEntry> = BTreeMap::new();
for z in &report.zones_compiled {
entries.insert(
z.name.clone(),
AliasEntry::Zone {
sha256: zone_hash.get(&z.name).cloned().unwrap_or_default(),
},
);
}
let mut duplicated_byte_links = 0;
for l in &report.links_written {
let materialised = l.mode;
if materialised == LinkMode::Copy {
duplicated_byte_links += 1;
}
let target_sha256 = zone_hash.get(&l.target).cloned().unwrap_or_default();
entries.insert(
l.link_name.clone(),
AliasEntry::Link {
target: l.target.clone(),
target_sha256,
materialised,
},
);
}
let map = AliasMap {
identifiers: entries.len(),
canonical_zones: report.zones_compiled.len(),
links: report.links_written.len(),
duplicated_byte_links,
entries,
};
map.validate()?;
Ok(map)
}
impl AliasMap {
pub fn validate(&self) -> Result<()> {
let (mut zones, mut links) = (0usize, 0usize);
for (name, entry) in &self.entries {
match entry {
AliasEntry::Zone { sha256 } => {
zones += 1;
if sha256.len() != 64 {
return Err(Error::message(format!(
"alias-map: zone {name:?} has a malformed content hash"
)));
}
}
AliasEntry::Link {
target,
target_sha256,
..
} => {
links += 1;
if name == target {
return Err(Error::message(format!(
"alias-map: {name:?} is a self-link"
)));
}
match self.entries.get(target) {
Some(AliasEntry::Zone { sha256 }) => {
if target_sha256 != sha256 {
return Err(Error::message(format!(
"alias-map: link {name:?} records a target hash that does not \
match its zone {target:?}"
)));
}
}
Some(AliasEntry::Link { .. }) => {
return Err(Error::message(format!(
"alias-map: link {name:?} targets another link {target:?}, not a \
canonical zone"
)))
}
None => {
return Err(Error::message(format!(
"alias-map: link {name:?} targets {target:?}, which is not a \
compiled zone in this map"
)))
}
}
}
}
}
if zones != self.canonical_zones || links != self.links || self.identifiers != zones + links
{
return Err(Error::message(
"alias-map: summary counts disagree with the entries".to_string(),
));
}
Ok(())
}
pub fn to_json(&self) -> String {
let mut s = String::new();
s.push_str("{\n");
s.push_str(&format!(" \"schema\": {},\n", json_str(SCHEMA)));
s.push_str(" \"zones\": {");
let mut first = true;
for (name, entry) in &self.entries {
s.push_str(if first { "\n" } else { ",\n" });
first = false;
let kind = entry.kind_str();
match entry {
AliasEntry::Zone { sha256 } => {
s.push_str(&format!(
" {}: {{ \"kind\": {}, \"sha256\": {} }}",
json_str(name),
json_str(kind),
json_str(sha256)
));
}
AliasEntry::Link {
target,
target_sha256,
materialised,
} => {
s.push_str(&format!(
" {}: {{ \"kind\": {}, \"target\": {}, \"target_sha256\": {}, \"materialised\": {} }}",
json_str(name),
json_str(kind),
json_str(target),
json_str(target_sha256),
json_str(materialised.as_str())
));
}
}
}
s.push_str(if self.entries.is_empty() {
"},\n"
} else {
"\n },\n"
});
s.push_str(" \"summary\": {\n");
s.push_str(&format!(" \"identifiers\": {},\n", self.identifiers));
s.push_str(&format!(
" \"canonical_zones\": {},\n",
self.canonical_zones
));
s.push_str(&format!(" \"links\": {},\n", self.links));
s.push_str(&format!(
" \"duplicated_byte_links\": {}\n",
self.duplicated_byte_links
));
s.push_str(" }\n");
s.push_str("}\n");
s
}
pub fn write_to(&self, path: &Path) -> Result<()> {
std::fs::write(path, self.to_json()).map_err(|e| Error::io(path, e))
}
}
pub const COMPILE_SCHEMA: &str = "zic-rs-compile-manifest-v8";
#[derive(Debug, Clone)]
pub struct TzdbProvenance {
pub detected_version: Option<String>,
pub claimed_version: Option<String>,
pub source_path: String,
pub source_sha256: String,
}
impl TzdbProvenance {
pub fn version_status(&self) -> &'static str {
match (&self.detected_version, &self.claimed_version) {
(Some(d), Some(c)) if d == c => "detected_matches_claim",
(Some(_), Some(_)) => "detected_differs_from_claim",
(Some(_), None) => "detected_only",
(None, Some(_)) => "claimed_only",
(None, None) => "unknown",
}
}
}
#[derive(Debug, Clone)]
pub struct SourceFile {
pub logical_name: String,
pub sha256: String,
pub bytes: usize,
pub order_index: usize,
}
#[derive(Debug, Clone)]
pub struct SourceInputs {
pub kind: SourceInputKind,
pub files: Vec<SourceFile>,
pub aggregate_hash: String,
}
#[derive(Debug, Clone)]
pub struct LeapSourceInfo {
pub mode: LeapSourceMode,
pub sha256: Option<String>,
pub entry_count: usize,
pub expires: bool,
pub rolling_entries: usize,
}
#[derive(Debug, Clone)]
pub struct BuildProfile {
pub output_tree: OutputTree,
pub leap_source: LeapSourceInfo,
pub emit_style: crate::EmitStyle,
pub range: Option<(Option<i64>, Option<i64>)>,
pub redundant_until: Option<i64>,
pub link_mode: crate::LinkMode,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutputTree {
Posix,
Right,
}
impl OutputTree {
pub fn as_str(self) -> &'static str {
match self {
OutputTree::Posix => "posix",
OutputTree::Right => "right",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LeapSourceMode {
None,
File,
}
impl LeapSourceMode {
pub fn as_str(self) -> &'static str {
match self {
LeapSourceMode::None => "none",
LeapSourceMode::File => "file",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SourceInputKind {
TzdataZi,
MultiFile,
SingleFile,
Unknown,
}
impl SourceInputKind {
pub fn as_str(self) -> &'static str {
match self {
SourceInputKind::TzdataZi => "tzdata_zi",
SourceInputKind::MultiFile => "multi_file",
SourceInputKind::SingleFile => "single_file",
SourceInputKind::Unknown => "unknown",
}
}
pub const ALL: [SourceInputKind; 4] = [
SourceInputKind::TzdataZi,
SourceInputKind::MultiFile,
SourceInputKind::SingleFile,
SourceInputKind::Unknown,
];
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OracleVerdict {
NotRun,
}
impl OracleVerdict {
pub fn as_str(self) -> &'static str {
match self {
OracleVerdict::NotRun => "not-run",
}
}
}
fn emit_style_str(s: crate::EmitStyle) -> &'static str {
match s {
crate::EmitStyle::Default => "default",
crate::EmitStyle::ZicSlim => "zic-slim",
crate::EmitStyle::ZicFat => "zic-fat",
}
}
#[derive(Debug, Clone)]
pub struct LinkProfile {
pub link_policy: String,
pub zones_compiled_count: usize,
pub links_selected_count: usize,
pub links_materialized_count: usize,
pub links_omitted_count: usize,
pub links_failed_count: usize,
pub alias_map_sha256: String,
pub selected_links_sha256: String,
pub omitted_links_sha256: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BackwardDetected {
Present,
Absent,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BackwardClaim {
Included,
Excluded,
None,
}
#[derive(Debug, Clone)]
pub struct BackwardEvidence {
pub detected: BackwardDetected,
pub claimed: BackwardClaim,
pub evidence_sha256: Option<String>,
}
impl BackwardEvidence {
pub fn reconcile(source_inputs: &SourceInputs, args: &SourceVariantArgs) -> Result<Self> {
let claimed = match args.backward_claim {
Some(true) => BackwardClaim::Included,
Some(false) => BackwardClaim::Excluded,
None => BackwardClaim::None,
};
let (detected, evidence_sha256) = match &args.backward_source {
Some(path) => {
let bytes = std::fs::read(path).map_err(|e| Error::io(path, e))?;
let h = sha256_hex(&bytes);
let present = source_inputs.files.iter().any(|f| f.sha256 == h);
let d = if present {
BackwardDetected::Present
} else {
BackwardDetected::Absent
};
(d, Some(h))
}
None => (BackwardDetected::Unknown, None),
};
Ok(BackwardEvidence {
detected,
claimed,
evidence_sha256,
})
}
pub fn status(&self) -> &'static str {
use BackwardClaim as C;
use BackwardDetected as D;
match (self.detected, self.claimed) {
(D::Present, C::Included) | (D::Absent, C::Excluded) => "detected_matches_claim",
(D::Present, C::Excluded) | (D::Absent, C::Included) => "detected_contradicts_claim",
(D::Present, C::None) => "detected_present",
(D::Absent, C::None) => "detected_absent",
(D::Unknown, C::Included) => "claimed_present_unverified",
(D::Unknown, C::Excluded) => "claimed_absent_unverified",
(D::Unknown, C::None) => "unknown_no_evidence",
}
}
fn detected_str(&self) -> &'static str {
match self.detected {
BackwardDetected::Present => "present",
BackwardDetected::Absent => "absent",
BackwardDetected::Unknown => "unknown",
}
}
fn claimed_str(&self) -> &'static str {
match self.claimed {
BackwardClaim::Included => "included",
BackwardClaim::Excluded => "excluded",
BackwardClaim::None => "none",
}
}
}
pub const REF_2026B_BACKZONE_SHA256: &str =
"63fb39adae0b0d8b2179629725a9dfb694c7a386b99750b636a017d896d28dfa";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BackzoneDetected {
Present,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BackzoneClaim {
Included,
Excluded,
None,
}
#[derive(Debug, Clone)]
pub struct BackzoneEvidence {
pub detected: BackzoneDetected,
pub claimed: BackzoneClaim,
pub evidence_sha256: Option<String>,
}
impl BackzoneEvidence {
pub fn reconcile(
source_inputs: &SourceInputs,
claim: Option<bool>,
reference_backzone_sha256: &str,
) -> Self {
let claimed = match claim {
Some(true) => BackzoneClaim::Included,
Some(false) => BackzoneClaim::Excluded,
None => BackzoneClaim::None,
};
let present = source_inputs
.files
.iter()
.any(|f| f.sha256 == reference_backzone_sha256);
let (detected, evidence_sha256) = if present {
(
BackzoneDetected::Present,
Some(reference_backzone_sha256.to_string()),
)
} else {
(BackzoneDetected::Unknown, None)
};
BackzoneEvidence {
detected,
claimed,
evidence_sha256,
}
}
pub fn status(&self) -> &'static str {
use BackzoneClaim as C;
use BackzoneDetected as D;
match (self.detected, self.claimed) {
(D::Present, C::Included) => "detected_matches_claim",
(D::Present, C::Excluded) => "detected_contradicts_claim",
(D::Present, C::None) => "detected_present",
(D::Unknown, C::Included) => "claimed_present_unverified",
(D::Unknown, C::Excluded) => "claimed_absent_unverified",
(D::Unknown, C::None) => "unknown_no_evidence",
}
}
fn detected_str(&self) -> &'static str {
match self.detected {
BackzoneDetected::Present => "present",
BackzoneDetected::Unknown => "unknown",
}
}
fn claimed_str(&self) -> &'static str {
match self.claimed {
BackzoneClaim::Included => "included",
BackzoneClaim::Excluded => "excluded",
BackzoneClaim::None => "none",
}
}
}
pub const REF_2026B_ZONE_TAB_SHA256: &str =
"4d8e389e5f4b0ec0466d5b14f42e5dfb0308c4376165fcf478339afd9ddcb00c";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PackratlistDetected {
SubsetFromPolicyInput,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PackratlistClaim {
Full,
Subset,
None,
NotClaimed,
}
#[derive(Debug, Clone)]
pub struct PackratlistEvidence {
pub detected: PackratlistDetected,
pub claimed: PackratlistClaim,
pub evidence_sha256: Option<String>,
}
impl PackratlistEvidence {
pub fn reconcile(
claim: Option<&str>,
admitted_policy_input_sha256: Option<&str>,
reference_zone_tab_sha256: &str,
backzone_present: bool,
) -> Self {
let claimed = match claim {
Some("full") => PackratlistClaim::Full,
Some("subset") => PackratlistClaim::Subset,
Some("none") => PackratlistClaim::None,
_ => PackratlistClaim::NotClaimed,
};
let detected_subset =
backzone_present && admitted_policy_input_sha256 == Some(reference_zone_tab_sha256);
let (detected, evidence_sha256) = if detected_subset {
(
PackratlistDetected::SubsetFromPolicyInput,
Some(reference_zone_tab_sha256.to_string()),
)
} else {
(PackratlistDetected::Unknown, None)
};
PackratlistEvidence {
detected,
claimed,
evidence_sha256,
}
}
pub fn status(&self) -> &'static str {
use PackratlistClaim as C;
use PackratlistDetected as D;
match (self.detected, &self.claimed) {
(D::SubsetFromPolicyInput, C::Subset) => "detected_matches_claim",
(D::SubsetFromPolicyInput, C::Full) | (D::SubsetFromPolicyInput, C::None) => {
"detected_contradicts_claim"
}
(D::SubsetFromPolicyInput, C::NotClaimed) => "detected_subset_from_policy_input",
(D::Unknown, C::Full) => "claimed_full_not_hash_backed",
(D::Unknown, C::Subset) => "claimed_subset_not_hash_backed",
(D::Unknown, C::None) => "claimed_none_not_hash_backed",
(D::Unknown, C::NotClaimed) => "unknown_no_evidence",
}
}
fn detected_str(&self) -> &'static str {
match self.detected {
PackratlistDetected::SubsetFromPolicyInput => "subset_from_policy_input",
PackratlistDetected::Unknown => "unknown",
}
}
fn claimed_str(&self) -> &'static str {
match self.claimed {
PackratlistClaim::Full => "full",
PackratlistClaim::Subset => "subset",
PackratlistClaim::None => "none",
PackratlistClaim::NotClaimed => "not_claimed",
}
}
}
pub const REF_2026B_ARCHIVE_SHA256: &str =
"ffad46a04c8d1624197056630af475a35f3556d0887f028ac1bd33b7d47dc653";
pub const REF_2026B_MAKEFILE_SHA256: &str =
"0b4588ea467c969b23fc48335e91eb63f403574b4aac69380b84a00373c7e81d";
pub const REF_2026B_ZIGUARD_AWK_SHA256: &str =
"e4600a2360b692242d6da76666411ece8ada76b61e6f8fb69cec79592b261785";
pub const REF_2026B_DATAFORM_COMMAND: &str = "make vanguard.zi main.zi rearguard.zi";
pub const REF_2026B_DATAFORM_TOOLCHAIN: &str = "GNU Make 4.4.1; GNU Awk 5.4.0";
pub const REF_2026B_DATAFORM_GENERATED_FROM: &str = "tzdb-2026b";
pub const REF_2026B_MAIN_ZI_SHA256: &str =
"e0225823ae0c3a99a016a4afd7e3c48cfd948132b65fbaa596a47c53ae45e4e1";
pub const REF_2026B_VANGUARD_ZI_SHA256: &str =
"49e16da4a6252a2e432fc1f68bf6daac9a6f73507dde3e3bdbcbbf78e86727ce";
pub const REF_2026B_REARGUARD_ZI_SHA256: &str =
"91c4f362a6bb297efd3cd35bce6b62367a4c00a9721a773bae0cbb0d1bf9fe23";
pub fn dataform_recipe_hash(
archive_sha256: &str,
makefile_sha256: &str,
ziguard_awk_sha256: &str,
command: &str,
toolchain: &str,
) -> String {
let recipe = format!(
"archive_sha256={archive_sha256}\nmakefile_sha256={makefile_sha256}\n\
ziguard_awk_sha256={ziguard_awk_sha256}\ncommand={command}\ntoolchain={toolchain}\n"
);
crate::hash::sha256_hex(recipe.as_bytes())
}
#[derive(Debug, Clone, Copy)]
pub struct DataformReference<'a> {
pub main_sha256: &'a str,
pub vanguard_sha256: &'a str,
pub rearguard_sha256: &'a str,
pub recipe_hash: &'a str,
pub generated_from: &'a str,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DataformDetected {
Main,
Vanguard,
Rearguard,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DataformClaim {
Main,
Vanguard,
Rearguard,
None,
}
#[derive(Debug, Clone)]
pub struct DataformEvidence {
pub detected: DataformDetected,
pub claimed: DataformClaim,
pub evidence_sha256: Option<String>,
pub recipe_hash: Option<String>,
pub generated_from: Option<String>,
}
impl DataformEvidence {
pub fn reconcile(
source_inputs: &SourceInputs,
claim: Option<&str>,
reference: &DataformReference,
) -> Self {
let claimed = match claim {
Some("main") => DataformClaim::Main,
Some("vanguard") => DataformClaim::Vanguard,
Some("rearguard") => DataformClaim::Rearguard,
_ => DataformClaim::None,
};
let mut detected = DataformDetected::Unknown;
let mut evidence_sha256 = None;
for f in &source_inputs.files {
if f.sha256 == reference.main_sha256 {
detected = DataformDetected::Main;
} else if f.sha256 == reference.vanguard_sha256 {
detected = DataformDetected::Vanguard;
} else if f.sha256 == reference.rearguard_sha256 {
detected = DataformDetected::Rearguard;
} else {
continue;
}
evidence_sha256 = Some(f.sha256.clone());
break;
}
let (recipe_hash, generated_from) = if evidence_sha256.is_some() {
(
Some(reference.recipe_hash.to_string()),
Some(reference.generated_from.to_string()),
)
} else {
(None, None)
};
DataformEvidence {
detected,
claimed,
evidence_sha256,
recipe_hash,
generated_from,
}
}
pub fn status(&self) -> &'static str {
use DataformClaim as C;
use DataformDetected as D;
let claim_form = match self.claimed {
C::Main => Some(D::Main),
C::Vanguard => Some(D::Vanguard),
C::Rearguard => Some(D::Rearguard),
C::None => None,
};
match (self.detected, claim_form) {
(D::Unknown, None) => "unknown_no_evidence",
(D::Unknown, Some(_)) => "claim_only",
(_, None) => "detected_only",
(d, Some(c)) if d == c => "detected_matches_claim",
(_, Some(_)) => "detected_contradicts_claim",
}
}
fn detected_str(&self) -> &'static str {
match self.detected {
DataformDetected::Main => "main",
DataformDetected::Vanguard => "vanguard",
DataformDetected::Rearguard => "rearguard",
DataformDetected::Unknown => "unknown",
}
}
fn claimed_str(&self) -> &'static str {
match self.claimed {
DataformClaim::Main => "main",
DataformClaim::Vanguard => "vanguard",
DataformClaim::Rearguard => "rearguard",
DataformClaim::None => "none",
}
}
}
#[derive(Debug, Clone)]
pub struct SourceProfile {
pub backward: BackwardEvidence,
pub backzone: BackzoneEvidence,
pub packratlist: PackratlistEvidence,
pub dataform: DataformEvidence,
}
#[derive(Debug, Clone, Default)]
pub struct SourceVariantArgs {
pub backward_claim: Option<bool>,
pub backward_source: Option<std::path::PathBuf>,
pub backzone_claim: Option<bool>,
pub packratlist_claim: Option<String>,
pub packratlist_source: Option<std::path::PathBuf>,
pub dataform_claim: Option<String>,
}
pub const SOURCE_VARIANT_GATE_STATUS: &str = "lifted_for_2026b";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OracleMode {
NotRun,
ReferenceZic,
ReferenceZdump,
StructuralDecode,
Unavailable(String),
}
impl OracleMode {
pub fn mode_str(&self) -> &'static str {
match self {
OracleMode::NotRun => "not_run",
OracleMode::ReferenceZic => "reference_zic",
OracleMode::ReferenceZdump => "reference_zdump",
OracleMode::StructuralDecode => "structural_decode",
OracleMode::Unavailable(_) => "unavailable",
}
}
pub fn skipped_with_reason(&self) -> Option<&str> {
match self {
OracleMode::Unavailable(reason) => Some(reason.as_str()),
_ => None,
}
}
pub fn manifest_str(&self) -> &'static str {
match self {
OracleMode::NotRun => "not-run",
other => other.mode_str(),
}
}
pub fn to_json_field(&self) -> String {
let reason = match self.skipped_with_reason() {
Some(r) => json_str(r),
None => "null".to_string(),
};
format!(
"{{ \"mode\": {}, \"skipped_with_reason\": {} }}",
json_str(self.mode_str()),
reason
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NegativeCapability {
DoesNotClaimAllIanaReleasesWithoutAdmission,
DoesNotClaimArbitraryTzifRoundtrip,
DoesNotClaimFullToctouResistance,
DoesNotClaimFutureCivilTimeAuthority,
DoesNotClaimLeapSmearSemantics,
DoesNotClaimRangeTruncationLeapExpiryInteractionParityWithoutWitness,
DoesNotClaimReportAuthenticityWithoutSignatureOrReproducibleContext,
DoesNotClaimTzifValidatorAsSecuritySandbox,
DoesNotClaimUnadmittedVendorParity,
DoesNotCurateTimeOrDefineDisplayNames,
DoesNotDependOnHostEndianness,
DoesNotInferDataformFromContent,
DoesNotInferSourceVariantFromOutputShape,
DoesNotRequireManifestToReadTzif,
DoesNotShipOrOperateVendorQemuLabsInCoreRepo,
DoesNotTreatManifestAsTzifSemantics,
}
impl NegativeCapability {
pub fn as_str(self) -> &'static str {
use NegativeCapability::*;
match self {
DoesNotClaimAllIanaReleasesWithoutAdmission => {
"does_not_claim_all_iana_releases_without_admission"
}
DoesNotClaimArbitraryTzifRoundtrip => "does_not_claim_arbitrary_tzif_roundtrip",
DoesNotClaimFullToctouResistance => "does_not_claim_full_toctou_resistance",
DoesNotClaimFutureCivilTimeAuthority => "does_not_claim_future_civil_time_authority",
DoesNotClaimLeapSmearSemantics => "does_not_claim_leap_smear_semantics",
DoesNotClaimRangeTruncationLeapExpiryInteractionParityWithoutWitness => {
"does_not_claim_range_truncation_leap_expiry_interaction_parity_without_witness"
}
DoesNotClaimReportAuthenticityWithoutSignatureOrReproducibleContext => {
"does_not_claim_report_authenticity_without_signature_or_reproducible_context"
}
DoesNotClaimTzifValidatorAsSecuritySandbox => {
"does_not_claim_tzif_validator_as_security_sandbox"
}
DoesNotClaimUnadmittedVendorParity => "does_not_claim_unadmitted_vendor_parity",
DoesNotCurateTimeOrDefineDisplayNames => "does_not_curate_time_or_define_display_names",
DoesNotDependOnHostEndianness => "does_not_depend_on_host_endianness",
DoesNotInferDataformFromContent => "does_not_infer_dataform_from_content",
DoesNotInferSourceVariantFromOutputShape => {
"does_not_infer_source_variant_from_output_shape"
}
DoesNotRequireManifestToReadTzif => "does_not_require_manifest_to_read_tzif",
DoesNotShipOrOperateVendorQemuLabsInCoreRepo => {
"does_not_ship_or_operate_vendor_qemu_labs_in_core_repo"
}
DoesNotTreatManifestAsTzifSemantics => "does_not_treat_manifest_as_tzif_semantics",
}
}
pub fn enforced_by(self) -> &'static str {
use NegativeCapability::*;
match self {
DoesNotClaimAllIanaReleasesWithoutAdmission => {
"T12.5a.3 release-admission matrix (only 2026b admitted)"
}
DoesNotClaimArbitraryTzifRoundtrip => {
"T15.4 tzif/rfc9636 (a validator/reader is not a round-trip preservation claim)"
}
DoesNotClaimFullToctouResistance => {
"T14.6 hostile-output-tree ledger (RequiresOpenatStyleHardening)"
}
DoesNotClaimFutureCivilTimeAuthority => {
"docs/tzdb-governance.md + RFC 9557 (tzdb predicts; named-tz rules change; not a legal oracle)"
}
DoesNotClaimLeapSmearSemantics => {
"T11 emits discrete TZif leap-second records (compile::apply_leaps / LeapRecord); no smearing path exists"
}
DoesNotClaimRangeTruncationLeapExpiryInteractionParityWithoutWitness => {
"T11.4 — Rolling-leap-under-`-r` is a hard error (compile/leap.rs); the -r×leap-expiry interaction has no semantic witness"
}
DoesNotClaimReportAuthenticityWithoutSignatureOrReproducibleContext => {
"T15.5 ConformanceStatus.report_provenance (default unsigned_local_report — not an attestation)"
}
DoesNotClaimTzifValidatorAsSecuritySandbox => {
"T15.4 tzif/rfc9636 non-claim (bounds-safe, but not a hardened sandbox for hostile binaries)"
}
DoesNotClaimUnadmittedVendorParity => {
"T13 reference-platform diagnostic matrix (only upstream_iana_2026b admitted)"
}
DoesNotCurateTimeOrDefineDisplayNames => {
"docs/tzdb-governance.md (IANA/CLDR boundary; not zic-rs's role)"
}
DoesNotDependOnHostEndianness => {
"tzif/header.rs + data writers emit big-endian fixed-width fields (to_be_bytes); byte-identical Etc/UTC fixture pins it"
}
DoesNotInferDataformFromContent => {
"T12.5d test (negative-SAVE is not vanguard; hash-backed only)"
}
DoesNotInferSourceVariantFromOutputShape => {
"T12.5 source_variants_not_inferred_* tests"
}
DoesNotRequireManifestToReadTzif => {
"RFC 9636 (a TZif reader needs only the emitted bytes; manifest is a sidecar)"
}
DoesNotShipOrOperateVendorQemuLabsInCoreRepo => {
"T16.5 vendor_oracle — core defines/admits receipts only; no VM images/QEMU orchestration vendored"
}
DoesNotTreatManifestAsTzifSemantics => {
"reports/t12-close-receipt.md §5 (manifest is provenance, not TZif semantics)"
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArtifactCategory {
CompileInput,
PolicyInput,
ReferenceInput,
GeneratedArtifact,
OutputArtifact,
DiagnosticArtifact,
SemanticWitnessArtifact,
StructuralValidationArtifact,
PolicyProse,
ReleaseNoteEvidence,
}
impl ArtifactCategory {
pub fn as_str(self) -> &'static str {
use ArtifactCategory::*;
match self {
CompileInput => "compile_input",
PolicyInput => "policy_input",
ReferenceInput => "reference_input",
GeneratedArtifact => "generated_artifact",
OutputArtifact => "output_artifact",
DiagnosticArtifact => "diagnostic_artifact",
SemanticWitnessArtifact => "semantic_witness_artifact",
StructuralValidationArtifact => "structural_validation_artifact",
PolicyProse => "policy_prose",
ReleaseNoteEvidence => "release_note_evidence",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReportKind {
Support,
Structural,
Manifest,
SemanticWitness,
TzifValidation,
}
impl ReportKind {
pub fn as_str(self) -> &'static str {
match self {
ReportKind::Support => "support",
ReportKind::Structural => "structural",
ReportKind::Manifest => "manifest",
ReportKind::SemanticWitness => "semantic_witness",
ReportKind::TzifValidation => "tzif_validation",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConformanceLevel {
NotEvaluated,
ReleaseAdmittedCompileCoverage,
StructurallyValidatedOnly,
SemanticWitnessedOnly,
KnownDivergencePresent,
OracleUnavailable,
}
impl ConformanceLevel {
pub fn as_str(self) -> &'static str {
match self {
ConformanceLevel::NotEvaluated => "not_evaluated",
ConformanceLevel::ReleaseAdmittedCompileCoverage => "release_admitted_compile_coverage",
ConformanceLevel::StructurallyValidatedOnly => "structurally_validated_only",
ConformanceLevel::SemanticWitnessedOnly => "semantic_witnessed_only",
ConformanceLevel::KnownDivergencePresent => "known_divergence_present",
ConformanceLevel::OracleUnavailable => "oracle_unavailable",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WorkspaceProvenance {
CleanGitTree,
DirtyGitTree,
SourceArchive,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReportProvenance {
UnsignedLocalReport,
ReproducibleCiArtifact,
SignedReleaseArtifact,
}
#[derive(Debug, Clone)]
pub struct CompilerIdentity {
pub zic_rs_version: &'static str,
pub rustc: Option<&'static str>,
pub target: String,
pub profile: &'static str,
pub git_commit: Option<&'static str>,
}
impl CompilerIdentity {
pub fn capture() -> Self {
CompilerIdentity {
zic_rs_version: env!("CARGO_PKG_VERSION"),
rustc: option_env!("ZIC_RS_RUSTC_VERSION"),
target: format!("{}-{}", std::env::consts::ARCH, std::env::consts::OS),
profile: if cfg!(debug_assertions) {
"debug"
} else {
"release"
},
git_commit: option_env!("ZIC_RS_GIT_COMMIT"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReferencePinGate {
Open,
LiftedFor2026b,
}
impl ReferencePinGate {
pub fn as_str(self) -> &'static str {
match self {
ReferencePinGate::Open => "open",
ReferencePinGate::LiftedFor2026b => "lifted_for_2026b",
}
}
pub fn current() -> Self {
ReferencePinGate::LiftedFor2026b
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReferenceLocatorKind {
VersionedArchive,
LiveCurrentDirectory,
LocalCachedCopy,
DistroSourcePackage,
Unknown,
}
impl ReferenceLocatorKind {
pub fn as_str(self) -> &'static str {
match self {
ReferenceLocatorKind::VersionedArchive => "versioned_archive",
ReferenceLocatorKind::LiveCurrentDirectory => "live_current_directory",
ReferenceLocatorKind::LocalCachedCopy => "local_cached_copy",
ReferenceLocatorKind::DistroSourcePackage => "distro_source_package",
ReferenceLocatorKind::Unknown => "unknown",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SignatureTrustModel {
FingerprintAnchored,
WebOfTrustValidated,
PlatformKeyring,
HashOnly,
Unsigned,
Unknown,
}
impl SignatureTrustModel {
pub fn as_str(self) -> &'static str {
match self {
SignatureTrustModel::FingerprintAnchored => "fingerprint_anchored",
SignatureTrustModel::WebOfTrustValidated => "web_of_trust_validated",
SignatureTrustModel::PlatformKeyring => "platform_keyring",
SignatureTrustModel::HashOnly => "hash_only",
SignatureTrustModel::Unsigned => "unsigned",
SignatureTrustModel::Unknown => "unknown",
}
}
pub fn pins_integrity(self) -> bool {
matches!(
self,
SignatureTrustModel::FingerprintAnchored
| SignatureTrustModel::WebOfTrustValidated
| SignatureTrustModel::PlatformKeyring
| SignatureTrustModel::HashOnly
)
}
}
#[derive(Debug, Clone, Copy)]
pub struct ReferenceAdmission {
pub locator: ReferenceLocatorKind,
pub trust: SignatureTrustModel,
}
impl ReferenceAdmission {
pub fn supports_sealed_claim(&self) -> bool {
matches!(self.locator, ReferenceLocatorKind::VersionedArchive)
&& self.trust.pins_integrity()
}
pub fn to_json(&self) -> String {
format!(
"{{ \"locator\": {}, \"signature_trust\": {}, \"supports_sealed_claim\": {} }}",
json_str(self.locator.as_str()),
json_str(self.trust.as_str()),
self.supports_sealed_claim()
)
}
}
pub const ADMITTED_2026B_REFERENCE: ReferenceAdmission = ReferenceAdmission {
locator: ReferenceLocatorKind::VersionedArchive,
trust: SignatureTrustModel::FingerprintAnchored,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ClaimPortability {
ReleaseSpecific,
OracleSpecific,
PlatformSpecific,
ProfileSpecific,
FixtureSpecific,
GeneralProjectPolicy,
}
impl ClaimPortability {
pub fn as_str(self) -> &'static str {
match self {
ClaimPortability::ReleaseSpecific => "release_specific",
ClaimPortability::OracleSpecific => "oracle_specific",
ClaimPortability::PlatformSpecific => "platform_specific",
ClaimPortability::ProfileSpecific => "profile_specific",
ClaimPortability::FixtureSpecific => "fixture_specific",
ClaimPortability::GeneralProjectPolicy => "general_project_policy",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EvidenceAuthorityKind {
NormativeSpec,
ImplementationObservation,
ManpageDocumentation,
PolicyGuidance,
ReleaseNote,
EmpiricalFixture,
ProjectDoctrine,
}
impl EvidenceAuthorityKind {
pub fn as_str(self) -> &'static str {
match self {
EvidenceAuthorityKind::NormativeSpec => "normative_spec",
EvidenceAuthorityKind::ImplementationObservation => "implementation_observation",
EvidenceAuthorityKind::ManpageDocumentation => "manpage_documentation",
EvidenceAuthorityKind::PolicyGuidance => "policy_guidance",
EvidenceAuthorityKind::ReleaseNote => "release_note",
EvidenceAuthorityKind::EmpiricalFixture => "empirical_fixture",
EvidenceAuthorityKind::ProjectDoctrine => "project_doctrine",
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct ClaimBoundary {
pub proves: &'static str,
pub does_not_prove: &'static str,
pub depends_on: &'static str,
}
pub const VALID_DISAMBIGUATION: &[&str] = &[
"structurally_valid: RFC 9636 byte-format integrity (tzif-validate) — NOT behaviour or semantics",
"semantically_witness_matching: offset/is_dst/abbr match zdump for the declared witness set — NOT all instants",
"modern_reader_compatible: no v4/legacy reader hazards — NOT semantic correctness",
"future_projection_matching: the POSIX footer projects like reference — separate from footer parseability",
"release_admitted: the source release is signature-verified + hash-pinned — NOT all IANA releases",
"compile_covered: the admitted release's zones compile — NOT behaviour-matched",
"behaviour_matched: CORE.1 341/341 vs reference zic/zdump over 1900..2040 — the live claim, separate from all above",
];
#[derive(Debug, Clone)]
pub struct ConformanceStatus {
pub report_kind: ReportKind,
pub level: ConformanceLevel,
pub workspace: WorkspaceProvenance,
pub report_provenance: ReportProvenance,
pub compiler: CompilerIdentity,
pub reference_pin_gate: ReferencePinGate,
pub claim_portability: ClaimPortability,
pub evidence_authority: EvidenceAuthorityKind,
pub claim_boundary: ClaimBoundary,
}
impl ConformanceStatus {
pub fn support() -> Self {
ConformanceStatus {
report_kind: ReportKind::Support,
level: ConformanceLevel::ReleaseAdmittedCompileCoverage,
workspace: WorkspaceProvenance::Unknown,
report_provenance: ReportProvenance::UnsignedLocalReport,
compiler: CompilerIdentity::capture(),
reference_pin_gate: ReferencePinGate::current(),
claim_portability: ClaimPortability::ReleaseSpecific,
evidence_authority: EvidenceAuthorityKind::ImplementationObservation,
claim_boundary: ClaimBoundary {
proves: "the admitted release's zones compile (compile-coverage), each accounted in exactly one bucket",
does_not_prove: "behaviour / structural / reader-compatibility parity — those are separate surfaces (semantic-report · structural-report · tzif-validate)",
depends_on: "the signature-verified + hash-pinned 2026b reference set and this zic-rs build",
},
}
}
pub fn declared_scope_hash(&self) -> String {
let mut envelope = String::new();
envelope.push_str(SOURCE_VARIANT_GATE_STATUS);
envelope.push('|');
envelope.push_str(COMPILE_SCHEMA);
envelope.push_str("|zic-rs-support-report-v4|zic-rs-structural-report-v3");
envelope.push_str("|zic-rs-semantic-report-v1|zic-rs-tzif-validation-v1|");
for nc in NEGATIVE_CAPABILITIES {
envelope.push_str(nc.as_str());
envelope.push(',');
}
envelope.push_str("|CORE.1=341/341@1900..2040;0mismatch;0failclosed");
crate::hash::sha256_hex(envelope.as_bytes())
}
pub fn to_json_block(&self) -> String {
let opt = |o: Option<&str>| match o {
Some(v) => json_str(v),
None => "null".to_string(),
};
let mut valid_disambig = String::from("[");
for (i, sense) in VALID_DISAMBIGUATION.iter().enumerate() {
if i > 0 {
valid_disambig.push_str(", ");
}
valid_disambig.push_str(&json_str(sense));
}
valid_disambig.push(']');
format!(
" \"conformance_status\": {{\n\
\"report_kind\": {}, \"conformance_level\": {}, \"declared_scope_hash\": {}, \
\"admitted_release_gate\": {}, \"workspace_provenance\": {}, \"report_provenance\": {}, \
\"claim_portability\": {}, \"evidence_authority\": {}, \
\"claim_boundary\": {{ \"proves\": {}, \"does_not_prove\": {}, \"depends_on\": {} }}, \
\"valid_disambiguation\": {}, \
\"core1_claim\": {}, \
\"available_surfaces\": [\"support-report\", \"structural-report\", \"semantic-report\", \
\"tzif-validation\", \"compile-manifest\"], \
\"compiler_identity\": {{ \"zic_rs_version\": {}, \"rustc\": {}, \"target\": {}, \
\"profile\": {}, \"git_commit\": {} }} }},\n",
json_str(self.report_kind.as_str()),
json_str(self.level.as_str()),
json_str(&self.declared_scope_hash()),
json_str(self.reference_pin_gate.as_str()),
json_str(match self.workspace {
WorkspaceProvenance::CleanGitTree => "clean_git_tree",
WorkspaceProvenance::DirtyGitTree => "dirty_git_tree",
WorkspaceProvenance::SourceArchive => "source_archive",
WorkspaceProvenance::Unknown => "unknown",
}),
json_str(match self.report_provenance {
ReportProvenance::UnsignedLocalReport => "unsigned_local_report",
ReportProvenance::ReproducibleCiArtifact => "reproducible_ci_artifact",
ReportProvenance::SignedReleaseArtifact => "signed_release_artifact",
}),
json_str(self.claim_portability.as_str()),
json_str(self.evidence_authority.as_str()),
json_str(self.claim_boundary.proves),
json_str(self.claim_boundary.does_not_prove),
json_str(self.claim_boundary.depends_on),
valid_disambig,
json_str(
"341/341 canonical zones behaviour-match reference zic/zdump over 1900..2040 \
(0 mismatch, 0 fail-closed)"
),
json_str(self.compiler.zic_rs_version),
opt(self.compiler.rustc),
json_str(&self.compiler.target),
json_str(self.compiler.profile),
opt(self.compiler.git_commit),
)
}
}
pub const NEGATIVE_CAPABILITIES: &[NegativeCapability] = &[
NegativeCapability::DoesNotClaimAllIanaReleasesWithoutAdmission,
NegativeCapability::DoesNotClaimArbitraryTzifRoundtrip,
NegativeCapability::DoesNotClaimFullToctouResistance,
NegativeCapability::DoesNotClaimFutureCivilTimeAuthority,
NegativeCapability::DoesNotClaimLeapSmearSemantics,
NegativeCapability::DoesNotClaimRangeTruncationLeapExpiryInteractionParityWithoutWitness,
NegativeCapability::DoesNotClaimReportAuthenticityWithoutSignatureOrReproducibleContext,
NegativeCapability::DoesNotClaimTzifValidatorAsSecuritySandbox,
NegativeCapability::DoesNotClaimUnadmittedVendorParity,
NegativeCapability::DoesNotCurateTimeOrDefineDisplayNames,
NegativeCapability::DoesNotDependOnHostEndianness,
NegativeCapability::DoesNotInferDataformFromContent,
NegativeCapability::DoesNotInferSourceVariantFromOutputShape,
NegativeCapability::DoesNotRequireManifestToReadTzif,
NegativeCapability::DoesNotShipOrOperateVendorQemuLabsInCoreRepo,
NegativeCapability::DoesNotTreatManifestAsTzifSemantics,
];
pub const SOURCE_VARIANT_BEHAVIOR_IMPLEMENTED: bool = false;
pub const SOURCE_VARIANT_BLOCKED_SUBSTEPS: &[&str] = &[];
pub const SOURCE_VARIANT_UNPINNED_FILES: &[&str] = &[];
pub fn provenance_block_json() -> String {
let arr = |items: &[&str]| -> String {
let inner: Vec<String> = items.iter().map(|i| json_str(i)).collect();
format!("[{}]", inner.join(", "))
};
let mut s = String::new();
s.push_str(" \"provenance\": {\n");
s.push_str(&format!(
" \"manifest_schema\": {},\n",
json_str(COMPILE_SCHEMA)
));
s.push_str(
" \"per_run_profile\": \"see `compile --manifest`: build_profile / source_inputs / \
link_profile / source_profile.backward_evidence\",\n",
);
s.push_str(&format!(
" \"source_variant_reference_pin_gate\": {},\n",
json_str(SOURCE_VARIANT_GATE_STATUS)
));
s.push_str(&format!(
" \"blocked_substeps\": {},\n",
arr(SOURCE_VARIANT_BLOCKED_SUBSTEPS)
));
s.push_str(&format!(
" \"unpinned_required_files\": {},\n",
arr(SOURCE_VARIANT_UNPINNED_FILES)
));
s.push_str(&format!(
" \"source_variant_behavior_implemented\": {},\n",
SOURCE_VARIANT_BEHAVIOR_IMPLEMENTED
));
s.push_str(
" \"note\": \"tzdb 2026b reference set admitted + signature-verified + SHA-256-pinned \
(reports/t12_5a2-reference-admission.md); T12.5b–d source-variant **evidence axes** are \
implemented for that pinned reference, while source-variant **behaviour** remains not \
implemented or claimed. No backzone/PACKRATLIST/DATAFORM/rearguard/vanguard behaviour is \
claimed; never inferred from aliases, filenames, link counts, or output byte shape.\",\n",
);
s.push_str(" \"negative_capabilities\": [");
for (i, nc) in NEGATIVE_CAPABILITIES.iter().enumerate() {
s.push_str(if i == 0 { "\n" } else { ",\n" });
s.push_str(&format!(
" {{ \"capability\": {}, \"enforced_by\": {} }}",
json_str(nc.as_str()),
json_str(nc.enforced_by())
));
}
s.push_str("\n ]\n");
s.push_str(" },\n");
s
}
pub fn provenance_block_text() -> String {
let mut s = String::new();
s.push_str("\nprovenance / capability:\n");
s.push_str(&format!(
" manifest schema: {COMPILE_SCHEMA} (per-run build/source/link/backward profile: see \
`compile --manifest`)\n"
));
s.push_str(&format!(
" source-variant reference-pin gate: {SOURCE_VARIANT_GATE_STATUS} (tzdb 2026b admitted + \
signature-verified + SHA-256-pinned — reports/t12_5a2-reference-admission.md)\n"
));
s.push_str(&format!(
" source-variant behaviour: {} — T12.5b–d unblocked for the pinned reference but not yet \
implemented; backzone/PACKRATLIST/DATAFORM/rearguard/vanguard never inferred from \
aliases/filenames/link counts/output shape\n",
if SOURCE_VARIANT_BEHAVIOR_IMPLEMENTED {
"implemented"
} else {
"NOT implemented or claimed"
}
));
s.push_str(" negative capabilities (non-claims, each enforced):\n");
for nc in NEGATIVE_CAPABILITIES {
s.push_str(&format!(" - {} ({})\n", nc.as_str(), nc.enforced_by()));
}
s
}
#[derive(Debug, Clone)]
pub struct OracleResult {
pub mode: OracleMode,
pub horizon: Option<String>,
pub result: OracleVerdict,
}
impl OracleResult {
pub fn not_run() -> Self {
OracleResult {
mode: OracleMode::NotRun,
horizon: None,
result: OracleVerdict::NotRun,
}
}
}
#[derive(Debug, Clone)]
pub struct CompileManifest {
pub zic_rs_version: String,
pub tzdb: TzdbProvenance,
pub source_inputs: SourceInputs,
pub build_profile: BuildProfile,
pub link_profile: LinkProfile,
pub source_profile: SourceProfile,
pub zones_requested: Vec<String>,
pub zones_compiled: Vec<String>,
pub links_materialized: Vec<String>,
pub unsupported_zones: Vec<String>,
pub oracle: OracleResult,
}
fn build_profile_json(p: &BuildProfile) -> String {
let opt_at = |v: Option<i64>| match v {
Some(n) => format!("\"@{n}\""),
None => "null".to_string(),
};
let mut s = String::new();
s.push_str(" \"build_profile\": {\n");
s.push_str(&format!(
" \"output_tree\": {},\n",
json_str(p.output_tree.as_str())
));
s.push_str(" \"leap_source\": {\n");
s.push_str(&format!(
" \"mode\": {},\n",
json_str(p.leap_source.mode.as_str())
));
match &p.leap_source.sha256 {
Some(h) => s.push_str(&format!(" \"sha256\": {},\n", json_str(h))),
None => s.push_str(" \"sha256\": null,\n"),
}
s.push_str(&format!(
" \"entry_count\": {},\n",
p.leap_source.entry_count
));
s.push_str(&format!(" \"expires\": {},\n", p.leap_source.expires));
s.push_str(&format!(
" \"rolling_entries\": {}\n",
p.leap_source.rolling_entries
));
s.push_str(" },\n");
s.push_str(&format!(
" \"emit_style\": {},\n",
json_str(emit_style_str(p.emit_style))
));
match p.range {
Some((lo, hi)) => s.push_str(&format!(
" \"range\": {{ \"lo\": {}, \"hi\": {} }},\n",
opt_at(lo),
opt_at(hi)
)),
None => s.push_str(" \"range\": null,\n"),
}
s.push_str(&format!(
" \"redundant_until\": {},\n",
opt_at(p.redundant_until)
));
s.push_str(&format!(
" \"link_mode\": {}\n",
json_str(p.link_mode.as_str())
));
s.push_str(" },\n");
s
}
fn source_inputs_json(si: &SourceInputs) -> String {
let mut s = String::new();
s.push_str(" \"source_inputs\": {\n");
s.push_str(&format!(" \"kind\": {},\n", json_str(si.kind.as_str())));
s.push_str(" \"files\": [");
for (i, f) in si.files.iter().enumerate() {
s.push_str(if i == 0 { "\n" } else { ",\n" });
s.push_str(&format!(
" {{ \"order_index\": {}, \"logical_name\": {}, \"sha256\": {}, \"bytes\": {} }}",
f.order_index,
json_str(&f.logical_name),
json_str(&f.sha256),
f.bytes
));
}
s.push_str(if si.files.is_empty() {
"],\n"
} else {
"\n ],\n"
});
s.push_str(&format!(
" \"aggregate_hash\": {}\n",
json_str(&si.aggregate_hash)
));
s.push_str(" },\n");
s
}
fn link_profile_json(lp: &LinkProfile) -> String {
let mut s = String::new();
s.push_str(" \"link_profile\": {\n");
s.push_str(&format!(
" \"link_policy\": {},\n",
json_str(&lp.link_policy)
));
s.push_str(&format!(
" \"zones_compiled_count\": {},\n",
lp.zones_compiled_count
));
s.push_str(&format!(
" \"links_selected_count\": {},\n",
lp.links_selected_count
));
s.push_str(&format!(
" \"links_materialized_count\": {},\n",
lp.links_materialized_count
));
s.push_str(&format!(
" \"links_omitted_count\": {},\n",
lp.links_omitted_count
));
s.push_str(&format!(
" \"links_failed_count\": {},\n",
lp.links_failed_count
));
s.push_str(&format!(
" \"alias_map_sha256\": {},\n",
json_str(&lp.alias_map_sha256)
));
s.push_str(&format!(
" \"selected_links_sha256\": {},\n",
json_str(&lp.selected_links_sha256)
));
s.push_str(&format!(
" \"omitted_links_sha256\": {}\n",
json_str(&lp.omitted_links_sha256)
));
s.push_str(" },\n");
s
}
fn source_profile_json(sp: &SourceProfile) -> String {
let axis =
|key: &str, detected: &str, claimed: &str, status: &str, ev: &Option<String>| -> String {
let mut a = String::new();
a.push_str(&format!(" {}: {{\n", json_str(key)));
a.push_str(&format!(" \"detected\": {},\n", json_str(detected)));
a.push_str(&format!(" \"claimed\": {},\n", json_str(claimed)));
a.push_str(&format!(" \"status\": {},\n", json_str(status)));
match ev {
Some(h) => a.push_str(&format!(" \"evidence_sha256\": {}\n", json_str(h))),
None => a.push_str(" \"evidence_sha256\": null\n"),
}
a.push_str(" }");
a
};
let b = &sp.backward;
let z = &sp.backzone;
let mut s = String::new();
s.push_str(" \"source_profile\": {\n");
s.push_str(&axis(
"backward_evidence",
b.detected_str(),
b.claimed_str(),
b.status(),
&b.evidence_sha256,
));
s.push_str(",\n");
s.push_str(&axis(
"backzone_evidence",
z.detected_str(),
z.claimed_str(),
z.status(),
&z.evidence_sha256,
));
s.push_str(",\n");
let pl = &sp.packratlist;
s.push_str(&axis(
"packratlist_evidence",
pl.detected_str(),
pl.claimed_str(),
pl.status(),
&pl.evidence_sha256,
));
s.push_str(",\n");
let df = &sp.dataform;
let opt = |v: &Option<String>| match v {
Some(h) => json_str(h),
None => "null".to_string(),
};
s.push_str(" \"dataform_evidence\": {\n");
s.push_str(&format!(
" \"detected\": {},\n",
json_str(df.detected_str())
));
s.push_str(&format!(
" \"claimed\": {},\n",
json_str(df.claimed_str())
));
s.push_str(&format!(" \"status\": {},\n", json_str(df.status())));
s.push_str(&format!(
" \"evidence_sha256\": {},\n",
opt(&df.evidence_sha256)
));
s.push_str(&format!(
" \"recipe_hash\": {},\n",
opt(&df.recipe_hash)
));
s.push_str(&format!(
" \"generated_from\": {}\n",
opt(&df.generated_from)
));
s.push_str(" }\n");
s.push_str(" },\n");
s
}
impl CompileManifest {
pub fn to_json(&self) -> String {
let arr = |items: &[String]| -> String {
if items.is_empty() {
"[]".to_string()
} else {
let inner: Vec<String> = items.iter().map(|i| json_str(i)).collect();
format!("[{}]", inner.join(", "))
}
};
let mut s = String::new();
s.push_str("{\n");
s.push_str(&format!(" \"schema\": {},\n", json_str(COMPILE_SCHEMA)));
s.push_str(&format!(
" \"zic_rs_version\": {},\n",
json_str(&self.zic_rs_version)
));
let opt_str = |v: &Option<String>| match v {
Some(x) => json_str(x),
None => "null".to_string(),
};
s.push_str(" \"tzdb\": {\n");
s.push_str(&format!(
" \"detected_version\": {},\n",
opt_str(&self.tzdb.detected_version)
));
s.push_str(&format!(
" \"claimed_version\": {},\n",
opt_str(&self.tzdb.claimed_version)
));
s.push_str(&format!(
" \"version_status\": {},\n",
json_str(self.tzdb.version_status())
));
s.push_str(&format!(
" \"source_path\": {},\n",
json_str(&self.tzdb.source_path)
));
s.push_str(&format!(
" \"source_sha256\": {}\n",
json_str(&self.tzdb.source_sha256)
));
s.push_str(" },\n");
s.push_str(&source_inputs_json(&self.source_inputs));
s.push_str(&build_profile_json(&self.build_profile));
s.push_str(&link_profile_json(&self.link_profile));
s.push_str(&source_profile_json(&self.source_profile));
s.push_str(" \"compile\": {\n");
s.push_str(&format!(
" \"zones_requested\": {},\n",
arr(&self.zones_requested)
));
s.push_str(&format!(
" \"zones_compiled\": {},\n",
arr(&self.zones_compiled)
));
s.push_str(&format!(
" \"links_materialized\": {},\n",
arr(&self.links_materialized)
));
s.push_str(&format!(
" \"unsupported_zones\": {}\n",
arr(&self.unsupported_zones)
));
s.push_str(" },\n");
s.push_str(" \"oracle\": {\n");
s.push_str(&format!(
" \"mode\": {},\n",
json_str(self.oracle.mode.manifest_str())
));
match &self.oracle.horizon {
Some(h) => s.push_str(&format!(" \"horizon\": {},\n", json_str(h))),
None => s.push_str(" \"horizon\": null,\n"),
}
s.push_str(&format!(
" \"result\": {}\n",
json_str(self.oracle.result.as_str())
));
s.push_str(" }\n");
s.push_str("}\n");
s
}
pub fn write_to(&self, path: &Path) -> Result<()> {
std::fs::write(path, self.to_json()).map_err(|e| Error::io(path, e))
}
}
#[allow(clippy::too_many_arguments)]
pub fn build_compile_manifest(
requested: &[String],
source_files: &[std::path::PathBuf],
report: &CompileReport,
config: &crate::CompileConfig,
db: &crate::model::Database,
claimed_version: Option<&str>,
leap_path: Option<&std::path::Path>,
variants: &SourceVariantArgs,
) -> Result<CompileManifest> {
let mut input_files: Vec<SourceFile> = Vec::with_capacity(source_files.len());
for (order_index, f) in source_files.iter().enumerate() {
let bytes = std::fs::read(f).map_err(|e| Error::io(f, e))?;
input_files.push(SourceFile {
logical_name: f
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| f.display().to_string()),
sha256: sha256_hex(&bytes),
bytes: bytes.len(),
order_index,
});
}
let aggregate_seed = input_files
.iter()
.map(|f| f.sha256.as_str())
.collect::<Vec<_>>()
.join("\n");
let aggregate_hash = sha256_hex(aggregate_seed.as_bytes());
let kind = match source_files.len() {
0 => SourceInputKind::Unknown,
1 if source_files[0].extension().and_then(|e| e.to_str()) == Some("zi") => {
SourceInputKind::TzdataZi
}
1 => SourceInputKind::SingleFile,
_ => SourceInputKind::MultiFile,
};
let mut sorted: Vec<&std::path::PathBuf> = source_files.iter().collect();
sorted.sort();
let mut all = Vec::new();
for f in &sorted {
all.extend(std::fs::read(f).map_err(|e| Error::io(f, e))?);
}
let source_sha256 = sha256_hex(&all);
let source_path = sorted
.iter()
.map(|p| p.display().to_string())
.collect::<Vec<_>>()
.join(", ");
let detected_version = crate::report::sniff_tzdb_version(&all);
let source_inputs = SourceInputs {
kind,
files: input_files,
aggregate_hash,
};
let leap_source = match &config.leaps {
None => LeapSourceInfo {
mode: LeapSourceMode::None,
sha256: None,
entry_count: 0,
expires: false,
rolling_entries: 0,
},
Some(table) => LeapSourceInfo {
mode: LeapSourceMode::File,
sha256: match leap_path {
Some(p) => Some(sha256_hex(&std::fs::read(p).map_err(|e| Error::io(p, e))?)),
None => None,
},
entry_count: table.entries.len(),
expires: table.expires.is_some(),
rolling_entries: table.entries.iter().filter(|e| e.rolling).count(),
},
};
let build_profile = BuildProfile {
output_tree: if config.leaps.is_some() {
OutputTree::Right
} else {
OutputTree::Posix
},
leap_source,
emit_style: config.emit_style,
range: config.range.map(|r| (r.lo, r.hi)),
redundant_until: config.redundant_until,
link_mode: config.link_mode,
};
let zones_compiled: Vec<String> = report
.zones_compiled
.iter()
.map(|z| z.name.clone())
.collect();
let links_materialized: Vec<String> = report
.links_written
.iter()
.map(|l| l.link_name.clone())
.collect();
let unsupported_zones: Vec<String> = requested
.iter()
.filter(|r| !zones_compiled.contains(r) && !links_materialized.contains(r))
.cloned()
.collect();
let mut selected_links: Vec<String> = Vec::new();
let mut omitted_links: Vec<String> = Vec::new();
let mut links_failed_count = 0usize;
for link in &db.links {
match crate::resolve_link_target(db, &link.link_name) {
Ok(canonical) if zones_compiled.iter().any(|z| z == canonical) => {
selected_links.push(link.link_name.clone())
}
Ok(_) => omitted_links.push(link.link_name.clone()),
Err(_) => links_failed_count += 1, }
}
selected_links.sort();
selected_links.dedup();
omitted_links.sort();
omitted_links.dedup();
let hash_names = |names: &[String]| sha256_hex(names.join("\n").as_bytes());
let alias_map_sha256 = sha256_hex(build(report, &config.output_dir)?.to_json().as_bytes());
let link_profile = LinkProfile {
link_policy: match config.link_mode {
crate::LinkMode::Copy => "copy",
crate::LinkMode::Symlink => "symlink",
}
.to_string(),
zones_compiled_count: zones_compiled.len(),
links_selected_count: selected_links.len(),
links_materialized_count: report.links_written.len(),
links_omitted_count: omitted_links.len(),
links_failed_count,
alias_map_sha256,
selected_links_sha256: hash_names(&selected_links),
omitted_links_sha256: hash_names(&omitted_links),
};
let backzone = BackzoneEvidence::reconcile(
&source_inputs,
variants.backzone_claim,
REF_2026B_BACKZONE_SHA256,
);
let backzone_present = backzone.detected == BackzoneDetected::Present;
let packratlist_policy_sha = match &variants.packratlist_source {
Some(p) => Some(sha256_hex(&std::fs::read(p).map_err(|e| Error::io(p, e))?)),
None => None,
};
let dataform_recipe = dataform_recipe_hash(
REF_2026B_ARCHIVE_SHA256,
REF_2026B_MAKEFILE_SHA256,
REF_2026B_ZIGUARD_AWK_SHA256,
REF_2026B_DATAFORM_COMMAND,
REF_2026B_DATAFORM_TOOLCHAIN,
);
let dataform_reference = DataformReference {
main_sha256: REF_2026B_MAIN_ZI_SHA256,
vanguard_sha256: REF_2026B_VANGUARD_ZI_SHA256,
rearguard_sha256: REF_2026B_REARGUARD_ZI_SHA256,
recipe_hash: &dataform_recipe,
generated_from: REF_2026B_DATAFORM_GENERATED_FROM,
};
let source_profile = SourceProfile {
backward: BackwardEvidence::reconcile(&source_inputs, variants)?,
packratlist: PackratlistEvidence::reconcile(
variants.packratlist_claim.as_deref(),
packratlist_policy_sha.as_deref(),
REF_2026B_ZONE_TAB_SHA256,
backzone_present,
),
dataform: DataformEvidence::reconcile(
&source_inputs,
variants.dataform_claim.as_deref(),
&dataform_reference,
),
backzone,
};
Ok(CompileManifest {
zic_rs_version: env!("CARGO_PKG_VERSION").to_string(),
tzdb: TzdbProvenance {
detected_version,
claimed_version: claimed_version.map(str::to_string),
source_path,
source_sha256,
},
source_inputs,
build_profile,
link_profile,
source_profile,
zones_requested: requested.to_vec(),
zones_compiled,
links_materialized,
unsupported_zones,
oracle: OracleResult::not_run(),
})
}
use crate::json::escape as json_str;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn source_input_kind_totality_and_literals() {
use std::collections::BTreeSet;
let labels: Vec<&str> = SourceInputKind::ALL.iter().map(|k| k.as_str()).collect();
assert_eq!(
labels,
["tzdata_zi", "multi_file", "single_file", "unknown"]
);
let set: BTreeSet<&str> = labels.iter().copied().collect();
assert_eq!(set.len(), SourceInputKind::ALL.len());
assert!(labels.iter().all(|l| !l.is_empty()));
}
#[test]
fn output_tree_leap_mode_oracle_verdict_literals() {
assert_eq!(OutputTree::Posix.as_str(), "posix");
assert_eq!(OutputTree::Right.as_str(), "right");
assert_eq!(LeapSourceMode::None.as_str(), "none");
assert_eq!(LeapSourceMode::File.as_str(), "file");
assert_eq!(OracleVerdict::NotRun.as_str(), "not-run");
}
#[test]
fn emit_style_boundary_literals_unchanged() {
assert_eq!(emit_style_str(crate::EmitStyle::Default), "default");
assert_eq!(emit_style_str(crate::EmitStyle::ZicSlim), "zic-slim");
assert_eq!(emit_style_str(crate::EmitStyle::ZicFat), "zic-fat");
}
#[test]
fn alias_entry_kind_str() {
let z = AliasEntry::Zone { sha256: "x".into() };
let l = AliasEntry::Link {
target: "t".into(),
target_sha256: "y".into(),
materialised: crate::LinkMode::Copy,
};
assert_eq!(z.kind_str(), "zone");
assert_eq!(l.kind_str(), "link");
}
#[test]
fn json_escaping() {
assert_eq!(json_str("Europe/London"), "\"Europe/London\"");
assert_eq!(json_str("a\\b"), "\"a\\\\b\"");
assert_eq!(json_str("a\"b"), "\"a\\\"b\"");
}
#[test]
fn empty_map_is_valid_json_shape() {
let m = AliasMap {
entries: BTreeMap::new(),
identifiers: 0,
canonical_zones: 0,
links: 0,
duplicated_byte_links: 0,
};
let j = m.to_json();
assert!(j.contains("\"schema\": \"zic-rs-alias-map-v1\""));
assert!(j.contains("\"zones\": {}"));
assert!(j.contains("\"identifiers\": 0"));
}
}