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 }