lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

registry_v7.rs (9739B)


      1 use super::artifact_bundle::{
      2     GeneratedArtifact, read_regular_file, validate_canonical_json_artifact,
      3     validate_sha256_artifact, with_artifact_bundle_transaction,
      4 };
      5 use radroots_event_codec::manifest::registry_v7::{
      6     RADROOTS_EVENT_CONTRACT_REGISTRY_V7_EVENT_COUNT,
      7     RADROOTS_EVENT_CONTRACT_REGISTRY_V7_INVENTORY_SCHEMA_VERSION,
      8     RADROOTS_EVENT_CONTRACT_REGISTRY_V7_KIND_COUNT, RADROOTS_EVENT_CONTRACT_REGISTRY_V7_VERSION,
      9     RadrootsEventContractRegistryV7Inventory, event_contract_registry_v7_inventory,
     10     event_contract_registry_v7_inventory_json, event_contract_registry_v7_inventory_sha256,
     11     parse_event_contract_registry_v7_inventory_json,
     12 };
     13 #[cfg(test)]
     14 use std::fs;
     15 use std::path::Path;
     16 
     17 const INVENTORY_RELATIVE: &str = "contracts/event_store/event_contract_registry_v7.inventory.json";
     18 const INVENTORY_SHA256_RELATIVE: &str =
     19     "contracts/event_store/event_contract_registry_v7.inventory.sha256";
     20 const WRITE_COMMAND: &str = "cargo xtask contract event-contract-registry-v7 --write";
     21 
     22 pub(crate) fn write_event_contract_registry_v7_inventory(
     23     workspace_root: &Path,
     24 ) -> Result<(), String> {
     25     with_artifact_bundle_transaction(workspace_root, |transaction| {
     26         let inventory_json = expected_inventory_json()?;
     27         let inventory_sha256 = expected_inventory_sha256()?;
     28 
     29         transaction.write(vec![
     30             GeneratedArtifact {
     31                 relative: INVENTORY_RELATIVE,
     32                 contents: inventory_json.into_bytes(),
     33             },
     34             GeneratedArtifact {
     35                 relative: INVENTORY_SHA256_RELATIVE,
     36                 contents: format!("{inventory_sha256}\n").into_bytes(),
     37             },
     38         ])?;
     39 
     40         validate_event_contract_registry_v7_inventory_under_lock(workspace_root)
     41     })
     42 }
     43 
     44 pub(crate) fn validate_event_contract_registry_v7_inventory(
     45     workspace_root: &Path,
     46 ) -> Result<(), String> {
     47     with_artifact_bundle_transaction(workspace_root, |_| {
     48         validate_event_contract_registry_v7_inventory_under_lock(workspace_root)
     49     })
     50 }
     51 
     52 pub(super) fn validate_event_contract_registry_v7_inventory_under_lock(
     53     workspace_root: &Path,
     54 ) -> Result<(), String> {
     55     let expected_json = expected_inventory_json()?;
     56     let expected_sha256 = expected_inventory_sha256()?;
     57     let actual_json = read_regular_file(workspace_root, INVENTORY_RELATIVE)?;
     58     let actual_sha256 = read_regular_file(workspace_root, INVENTORY_SHA256_RELATIVE)?;
     59     let actual_json_text = std::str::from_utf8(&actual_json)
     60         .map_err(|error| format!("{INVENTORY_RELATIVE} must be UTF-8 JSON: {error}"))?;
     61     let parsed = parse_event_contract_registry_v7_inventory_json(actual_json_text)
     62         .map_err(|error| format!("parse {INVENTORY_RELATIVE}: {error}"))?;
     63 
     64     validate_inventory_shape(&parsed)?;
     65     validate_canonical_json_artifact(INVENTORY_RELATIVE, &actual_json)?;
     66     validate_sha256_artifact(INVENTORY_SHA256_RELATIVE, &actual_sha256)?;
     67 
     68     if actual_json != expected_json.as_bytes() {
     69         return Err(stale_error(INVENTORY_RELATIVE));
     70     }
     71     if actual_sha256 != format!("{expected_sha256}\n").as_bytes() {
     72         return Err(stale_error(INVENTORY_SHA256_RELATIVE));
     73     }
     74     if parsed != event_contract_registry_v7_inventory() {
     75         return Err(stale_error(INVENTORY_RELATIVE));
     76     }
     77 
     78     Ok(())
     79 }
     80 
     81 fn expected_inventory_json() -> Result<String, String> {
     82     event_contract_registry_v7_inventory_json()
     83         .map_err(|error| format!("serialize event-contract registry-v7 inventory: {error}"))
     84 }
     85 
     86 fn expected_inventory_sha256() -> Result<String, String> {
     87     event_contract_registry_v7_inventory_sha256()
     88         .map_err(|error| format!("hash event-contract registry-v7 inventory: {error}"))
     89 }
     90 
     91 fn validate_inventory_shape(
     92     inventory: &RadrootsEventContractRegistryV7Inventory,
     93 ) -> Result<(), String> {
     94     if inventory.schema_version != RADROOTS_EVENT_CONTRACT_REGISTRY_V7_INVENTORY_SCHEMA_VERSION {
     95         return Err(format!(
     96             "{INVENTORY_RELATIVE} schema_version must be {}",
     97             RADROOTS_EVENT_CONTRACT_REGISTRY_V7_INVENTORY_SCHEMA_VERSION
     98         ));
     99     }
    100     if inventory.event_contract_registry_version != RADROOTS_EVENT_CONTRACT_REGISTRY_V7_VERSION {
    101         return Err(format!(
    102             "{INVENTORY_RELATIVE} event_contract_registry_version must be {}",
    103             RADROOTS_EVENT_CONTRACT_REGISTRY_V7_VERSION
    104         ));
    105     }
    106     if inventory.kind_contracts.len() != RADROOTS_EVENT_CONTRACT_REGISTRY_V7_KIND_COUNT {
    107         return Err(format!(
    108             "{INVENTORY_RELATIVE} must contain {} kind contracts",
    109             RADROOTS_EVENT_CONTRACT_REGISTRY_V7_KIND_COUNT
    110         ));
    111     }
    112     if inventory.event_contracts.len() != RADROOTS_EVENT_CONTRACT_REGISTRY_V7_EVENT_COUNT {
    113         return Err(format!(
    114             "{INVENTORY_RELATIVE} must contain {} event contracts",
    115             RADROOTS_EVENT_CONTRACT_REGISTRY_V7_EVENT_COUNT
    116         ));
    117     }
    118     if inventory
    119         .kind_contracts
    120         .iter()
    121         .enumerate()
    122         .any(|(ordinal, contract)| contract.ordinal != ordinal)
    123     {
    124         return Err(format!(
    125             "{INVENTORY_RELATIVE} kind-contract ordinals must be contiguous from zero"
    126         ));
    127     }
    128     if inventory
    129         .event_contracts
    130         .iter()
    131         .enumerate()
    132         .any(|(ordinal, contract)| contract.ordinal != ordinal)
    133     {
    134         return Err(format!(
    135             "{INVENTORY_RELATIVE} event-contract ordinals must be contiguous from zero"
    136         ));
    137     }
    138     Ok(())
    139 }
    140 
    141 fn stale_error(relative: &str) -> String {
    142     format!("{relative} is stale; run `{WRITE_COMMAND}`")
    143 }
    144 
    145 #[cfg(test)]
    146 mod tests {
    147     use super::*;
    148 
    149     #[test]
    150     fn write_and_validate_are_exactly_fresh() {
    151         let workspace = tempfile::TempDir::new().expect("workspace");
    152 
    153         write_event_contract_registry_v7_inventory(workspace.path()).expect("write inventory");
    154         validate_event_contract_registry_v7_inventory(workspace.path()).expect("fresh inventory");
    155 
    156         let json = fs::read(workspace.path().join(INVENTORY_RELATIVE)).expect("inventory JSON");
    157         let digest =
    158             fs::read(workspace.path().join(INVENTORY_SHA256_RELATIVE)).expect("inventory digest");
    159         assert_eq!(
    160             json,
    161             expected_inventory_json().expect("expected JSON").as_bytes()
    162         );
    163         assert_eq!(
    164             digest,
    165             format!(
    166                 "{}\n",
    167                 expected_inventory_sha256().expect("expected digest")
    168             )
    169             .as_bytes()
    170         );
    171     }
    172 
    173     #[test]
    174     fn validation_rejects_stale_and_noncanonical_artifacts() {
    175         let workspace = tempfile::TempDir::new().expect("workspace");
    176         write_event_contract_registry_v7_inventory(workspace.path()).expect("write inventory");
    177 
    178         fs::write(
    179             workspace.path().join(INVENTORY_SHA256_RELATIVE),
    180             "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n",
    181         )
    182         .expect("write uppercase digest");
    183         let uppercase = validate_event_contract_registry_v7_inventory(workspace.path())
    184             .expect_err("uppercase digest must fail");
    185         assert!(uppercase.contains("lowercase SHA-256"));
    186 
    187         write_event_contract_registry_v7_inventory(workspace.path()).expect("restore inventory");
    188         let inventory_path = workspace.path().join(INVENTORY_RELATIVE);
    189         let mut json = fs::read_to_string(&inventory_path).expect("read inventory");
    190         json.push('\n');
    191         fs::write(&inventory_path, json).expect("write extra LF");
    192         let extra_lf = validate_event_contract_registry_v7_inventory(workspace.path())
    193             .expect_err("extra LF must fail");
    194         assert!(extra_lf.contains("exactly one LF"));
    195     }
    196 
    197     #[test]
    198     fn validation_rejects_unknown_typed_fields_before_freshness() {
    199         let workspace = tempfile::TempDir::new().expect("workspace");
    200         write_event_contract_registry_v7_inventory(workspace.path()).expect("write inventory");
    201         let inventory_path = workspace.path().join(INVENTORY_RELATIVE);
    202         let mut value: serde_json::Value =
    203             serde_json::from_slice(&fs::read(&inventory_path).expect("read inventory"))
    204                 .expect("parse inventory value");
    205         value
    206             .as_object_mut()
    207             .expect("inventory object")
    208             .insert("unknown".to_string(), serde_json::Value::Bool(true));
    209         let mut json = serde_json::to_string_pretty(&value).expect("serialize mutated inventory");
    210         json.push('\n');
    211         fs::write(inventory_path, json).expect("write mutated inventory");
    212 
    213         let error = validate_event_contract_registry_v7_inventory(workspace.path())
    214             .expect_err("unknown field must fail");
    215         assert!(error.contains("unknown field"));
    216     }
    217 
    218     #[cfg(unix)]
    219     #[test]
    220     fn validation_and_write_reject_symlink_artifacts() {
    221         use std::os::unix::fs::symlink;
    222 
    223         let workspace = tempfile::TempDir::new().expect("workspace");
    224         let parent = workspace.path().join("contracts/event_store");
    225         fs::create_dir_all(&parent).expect("create artifact parent");
    226         let target = workspace.path().join("target.json");
    227         fs::write(&target, "{}\n").expect("write symlink target");
    228         symlink(&target, workspace.path().join(INVENTORY_RELATIVE))
    229             .expect("create artifact symlink");
    230 
    231         let validation = validate_event_contract_registry_v7_inventory(workspace.path())
    232             .expect_err("validation rejects symlink");
    233         assert!(validation.contains("symlink component"));
    234         let write = write_event_contract_registry_v7_inventory(workspace.path())
    235             .expect_err("write rejects symlink");
    236         assert!(write.contains("symlink component"));
    237     }
    238 }