conformance.rs (14369B)
1 #![cfg(feature = "serde")] 2 3 use radroots_blossom::{BlobDescriptor, BlobUrl, MediaType, Sha256, hash::HashPath}; 4 use serde::Deserialize; 5 use serde_json::Value; 6 use std::{borrow::Cow, collections::BTreeSet, fs, path::Path}; 7 8 const PACKAGED_VECTORS: &str = include_str!("fixtures/hash_path_and_descriptor.v1.json"); 9 const WORKSPACE_VECTOR_PATH: &str = 10 "../../contracts/conformance/vectors/blossom/hash_path_and_descriptor.v1.json"; 11 const WORKSPACE_CONTRACT_MARKER_PATH: &str = "../../contracts/manifest.toml"; 12 const SUPPORTED_VECTOR_KINDS: [&str; 17] = [ 13 "blossom.sha256.digest", 14 "blossom.sha256.parse.valid", 15 "blossom.sha256.parse.invalid", 16 "blossom.hash_path.parse.valid", 17 "blossom.hash_path.parse.invalid", 18 "blossom.blob_url.parse.valid", 19 "blossom.blob_url.parse.invalid", 20 "blossom.reference_policy.valid", 21 "blossom.reference_policy.invalid", 22 "blossom.media_type.parse.valid", 23 "blossom.media_type.parse.invalid", 24 "blossom.descriptor.parse.valid", 25 "blossom.descriptor.parse.invalid", 26 "blossom.descriptor.approve_reference.valid", 27 "blossom.descriptor.approve_reference.invalid", 28 "blossom.descriptor.verify_bytes.valid", 29 "blossom.descriptor.verify_bytes.invalid", 30 ]; 31 32 #[derive(Debug, Deserialize)] 33 #[serde(deny_unknown_fields)] 34 struct Suite { 35 suite: String, 36 contract_version: String, 37 vectors: Vec<Vector>, 38 } 39 40 #[derive(Debug, Deserialize)] 41 #[serde(deny_unknown_fields)] 42 struct Vector { 43 id: String, 44 kind: String, 45 input: Value, 46 expected: Value, 47 } 48 49 #[test] 50 fn checked_in_vectors_execute_against_public_api() { 51 let vectors = conformance_vectors(); 52 let suite: Suite = serde_json::from_str(&vectors).expect("Blossom vectors must parse"); 53 assert_eq!(suite.suite, "blossom_hash_path_and_descriptor"); 54 assert_eq!(suite.contract_version, "1.0.0"); 55 assert!(!suite.vectors.is_empty()); 56 assert_vector_inventory(&suite.vectors); 57 58 for vector in &suite.vectors { 59 execute(vector); 60 } 61 } 62 63 fn assert_vector_inventory(vectors: &[Vector]) { 64 let mut ids = BTreeSet::new(); 65 let mut kinds = BTreeSet::new(); 66 for vector in vectors { 67 assert!(!vector.id.trim().is_empty(), "vector id must be nonblank"); 68 assert!( 69 ids.insert(vector.id.as_str()), 70 "duplicate vector id {}", 71 vector.id 72 ); 73 assert!( 74 vector.input.is_object(), 75 "{} input must be an object", 76 vector.id 77 ); 78 assert!( 79 vector.expected.is_object(), 80 "{} expected must be an object", 81 vector.id 82 ); 83 kinds.insert(vector.kind.as_str()); 84 } 85 assert_eq!(kinds, BTreeSet::from(SUPPORTED_VECTOR_KINDS)); 86 } 87 88 fn conformance_vectors() -> Cow<'static, str> { 89 let workspace_path = Path::new(env!("CARGO_MANIFEST_DIR")).join(WORKSPACE_VECTOR_PATH); 90 match fs::read_to_string(&workspace_path) { 91 Ok(canonical) => { 92 assert_eq!( 93 canonical, 94 PACKAGED_VECTORS, 95 "packaged Blossom vectors must match {}", 96 workspace_path.display() 97 ); 98 Cow::Owned(canonical) 99 } 100 Err(error) 101 if error.kind() == std::io::ErrorKind::NotFound 102 && !Path::new(env!("CARGO_MANIFEST_DIR")) 103 .join(WORKSPACE_CONTRACT_MARKER_PATH) 104 .is_file() => 105 { 106 Cow::Borrowed(PACKAGED_VECTORS) 107 } 108 Err(error) => panic!("failed to read {}: {error}", workspace_path.display()), 109 } 110 } 111 112 fn execute(vector: &Vector) { 113 match vector.kind.as_str() { 114 "blossom.sha256.digest" => sha256_digest(vector), 115 "blossom.sha256.parse.valid" => sha256_parse_valid(vector), 116 "blossom.sha256.parse.invalid" => sha256_parse_invalid(vector), 117 "blossom.hash_path.parse.valid" => hash_path_parse_valid(vector), 118 "blossom.hash_path.parse.invalid" => hash_path_parse_invalid(vector), 119 "blossom.blob_url.parse.valid" => blob_url_parse_valid(vector), 120 "blossom.blob_url.parse.invalid" => blob_url_parse_invalid(vector), 121 "blossom.reference_policy.valid" => reference_policy_valid(vector), 122 "blossom.reference_policy.invalid" => reference_policy_invalid(vector), 123 "blossom.media_type.parse.valid" => media_type_parse_valid(vector), 124 "blossom.media_type.parse.invalid" => media_type_parse_invalid(vector), 125 "blossom.descriptor.parse.valid" => descriptor_parse_valid(vector), 126 "blossom.descriptor.parse.invalid" => descriptor_parse_invalid(vector), 127 "blossom.descriptor.approve_reference.valid" => descriptor_approval_valid(vector), 128 "blossom.descriptor.approve_reference.invalid" => descriptor_approval_invalid(vector), 129 "blossom.descriptor.verify_bytes.valid" => descriptor_verify_valid(vector), 130 "blossom.descriptor.verify_bytes.invalid" => descriptor_verify_invalid(vector), 131 kind => panic!("{} uses unsupported vector kind {kind}", vector.id), 132 } 133 } 134 135 fn sha256_digest(vector: &Vector) { 136 let bytes = input_bytes(vector); 137 assert_eq!( 138 Sha256::digest(&bytes).to_string(), 139 expected_str(vector, "sha256"), 140 "{}", 141 vector.id 142 ); 143 } 144 145 fn sha256_parse_valid(vector: &Vector) { 146 let parsed = Sha256::from_hex(input_str(vector, "sha256")) 147 .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id)); 148 assert_eq!( 149 parsed.to_string(), 150 expected_str(vector, "sha256"), 151 "{}", 152 vector.id 153 ); 154 } 155 156 fn sha256_parse_invalid(vector: &Vector) { 157 let error = Sha256::from_hex(input_str(vector, "sha256")) 158 .expect_err("invalid SHA-256 vector must fail"); 159 assert_error(vector, error.code()); 160 } 161 162 fn hash_path_parse_valid(vector: &Vector) { 163 let parsed = HashPath::parse(input_str(vector, "path")) 164 .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id)); 165 assert_eq!( 166 parsed.hash().to_string(), 167 expected_str(vector, "sha256"), 168 "{}", 169 vector.id 170 ); 171 assert_eq!( 172 parsed.extension().map(|extension| extension.as_str()), 173 optional_expected_str(vector, "extension"), 174 "{}", 175 vector.id 176 ); 177 } 178 179 fn hash_path_parse_invalid(vector: &Vector) { 180 let error = 181 HashPath::parse(input_str(vector, "path")).expect_err("invalid hash-path vector must fail"); 182 assert_error(vector, error.code()); 183 } 184 185 fn blob_url_parse_valid(vector: &Vector) { 186 let parsed = parse_blob_url(vector); 187 assert_eq!( 188 parsed.scheme(), 189 expected_str(vector, "scheme"), 190 "{}", 191 vector.id 192 ); 193 assert_eq!(parsed.host(), expected_str(vector, "host"), "{}", vector.id); 194 assert_eq!( 195 parsed.port().map(u64::from), 196 vector.expected.get("port").and_then(Value::as_u64), 197 "{}", 198 vector.id 199 ); 200 assert_eq!( 201 parsed.hash_path().hash().to_string(), 202 expected_str(vector, "sha256"), 203 "{}", 204 vector.id 205 ); 206 assert_eq!( 207 parsed 208 .hash_path() 209 .extension() 210 .map(|extension| extension.as_str()), 211 optional_expected_str(vector, "extension"), 212 "{}", 213 vector.id 214 ); 215 } 216 217 fn blob_url_parse_invalid(vector: &Vector) { 218 let error = 219 BlobUrl::parse(input_str(vector, "url")).expect_err("invalid blob-URL vector must fail"); 220 assert_error(vector, error.code()); 221 } 222 223 fn reference_policy_valid(vector: &Vector) { 224 let parsed = parse_blob_url(vector); 225 let transport = if parsed.is_https() { 226 "https" 227 } else if parsed.is_loopback_http() { 228 "loopback_http" 229 } else { 230 panic!("{} is not an approved reference", vector.id); 231 }; 232 let approved = parsed 233 .approve() 234 .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id)); 235 assert_eq!(vector.expected["approved"], true, "{}", vector.id); 236 assert_eq!( 237 transport, 238 expected_str(vector, "transport"), 239 "{}", 240 vector.id 241 ); 242 assert!(!approved.as_str().is_empty()); 243 } 244 245 fn reference_policy_invalid(vector: &Vector) { 246 let error = parse_blob_url(vector) 247 .approve() 248 .expect_err("insecure reference vector must fail"); 249 assert_error(vector, error.code()); 250 } 251 252 fn media_type_parse_valid(vector: &Vector) { 253 let parsed = MediaType::parse(input_str(vector, "media_type")) 254 .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id)); 255 assert_eq!( 256 parsed.as_str(), 257 expected_str(vector, "media_type"), 258 "{}", 259 vector.id 260 ); 261 } 262 263 fn media_type_parse_invalid(vector: &Vector) { 264 let error = MediaType::parse(input_str(vector, "media_type")) 265 .expect_err("invalid media-type vector must fail"); 266 assert_error(vector, error.code()); 267 } 268 269 fn descriptor_parse_valid(vector: &Vector) { 270 let descriptor = parse_descriptor(vector); 271 let actual = serde_json::to_value(descriptor).expect("descriptor must serialize"); 272 assert_eq!(actual, vector.expected["descriptor"], "{}", vector.id); 273 } 274 275 fn descriptor_parse_invalid(vector: &Vector) { 276 let input = &vector.input["descriptor"]; 277 serde_json::from_value::<BlobDescriptor>(input.clone()) 278 .expect_err("invalid descriptor vector must fail"); 279 let code = classify_descriptor_input(input); 280 assert_error(vector, code); 281 if expected_error(vector) == "missing_descriptor_field" { 282 let field = expected_str(vector, "field"); 283 assert!(input.get(field).is_none(), "{} retains {field}", vector.id); 284 } 285 } 286 287 fn descriptor_approval_valid(vector: &Vector) { 288 let approved = parse_descriptor(vector) 289 .approve_reference() 290 .unwrap_or_else(|error| panic!("{} approval failed: {error}", vector.id)); 291 assert_eq!( 292 approved.url().as_str(), 293 expected_str(vector, "approved_url"), 294 "{}", 295 vector.id 296 ); 297 } 298 299 fn descriptor_approval_invalid(vector: &Vector) { 300 let error = parse_descriptor(vector) 301 .approve_reference() 302 .expect_err("invalid descriptor approval vector must fail"); 303 assert_error(vector, error.code()); 304 } 305 306 fn descriptor_verify_valid(vector: &Vector) { 307 let bytes = input_bytes(vector); 308 let media_type = approved_media_type(vector); 309 let verified = parse_descriptor(vector) 310 .approve_reference() 311 .unwrap_or_else(|error| panic!("{} approval failed: {error}", vector.id)) 312 .verify_bytes(&bytes, &media_type) 313 .unwrap_or_else(|error| panic!("{} verification failed: {error}", vector.id)); 314 let expected = &vector.expected["verified"]; 315 assert_eq!( 316 verified.sha256().to_string(), 317 expected["sha256"].as_str().expect("expected sha256"), 318 "{}", 319 vector.id 320 ); 321 assert_eq!( 322 verified.size(), 323 expected["size"].as_u64().expect("expected size"), 324 "{}", 325 vector.id 326 ); 327 assert_eq!( 328 verified.media_type().as_str(), 329 expected["media_type"] 330 .as_str() 331 .expect("expected media_type"), 332 "{}", 333 vector.id 334 ); 335 } 336 337 fn descriptor_verify_invalid(vector: &Vector) { 338 let bytes = input_bytes(vector); 339 let media_type = approved_media_type(vector); 340 let error = parse_descriptor(vector) 341 .approve_reference() 342 .unwrap_or_else(|error| panic!("{} approval failed: {error}", vector.id)) 343 .verify_bytes(&bytes, &media_type) 344 .expect_err("invalid descriptor verification vector must fail"); 345 assert_error(vector, error.code()); 346 } 347 348 fn parse_blob_url(vector: &Vector) -> BlobUrl { 349 BlobUrl::parse(input_str(vector, "url")) 350 .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id)) 351 } 352 353 fn parse_descriptor(vector: &Vector) -> BlobDescriptor { 354 serde_json::from_value(vector.input["descriptor"].clone()) 355 .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id)) 356 } 357 358 fn approved_media_type(vector: &Vector) -> MediaType { 359 MediaType::parse(input_str(vector, "approved_media_type")) 360 .unwrap_or_else(|error| panic!("{} media type failed: {error}", vector.id)) 361 } 362 363 fn input_bytes(vector: &Vector) -> Vec<u8> { 364 hex::decode(input_str(vector, "bytes_hex")) 365 .unwrap_or_else(|error| panic!("{} bytes_hex failed: {error}", vector.id)) 366 } 367 368 fn input_str<'a>(vector: &'a Vector, field: &str) -> &'a str { 369 vector.input[field] 370 .as_str() 371 .unwrap_or_else(|| panic!("{} input.{field} must be a string", vector.id)) 372 } 373 374 fn expected_str<'a>(vector: &'a Vector, field: &str) -> &'a str { 375 vector.expected[field] 376 .as_str() 377 .unwrap_or_else(|| panic!("{} expected.{field} must be a string", vector.id)) 378 } 379 380 fn optional_expected_str<'a>(vector: &'a Vector, field: &str) -> Option<&'a str> { 381 vector.expected.get(field).and_then(Value::as_str) 382 } 383 384 fn expected_error(vector: &Vector) -> &str { 385 expected_str(vector, "error") 386 } 387 388 fn assert_error(vector: &Vector, actual: &str) { 389 assert_eq!(actual, expected_error(vector), "{}", vector.id); 390 } 391 392 fn classify_descriptor_input(input: &Value) -> &'static str { 393 for field in ["url", "sha256", "size", "type", "uploaded"] { 394 if input.get(field).is_none() { 395 return "missing_descriptor_field"; 396 } 397 } 398 399 let url = match BlobUrl::parse( 400 input["url"] 401 .as_str() 402 .expect("descriptor url must be a string"), 403 ) { 404 Ok(url) => url, 405 Err(error) => return error.code(), 406 }; 407 let sha256 = match Sha256::from_hex( 408 input["sha256"] 409 .as_str() 410 .expect("descriptor sha256 must be a string"), 411 ) { 412 Ok(sha256) => sha256, 413 Err(error) => return error.code(), 414 }; 415 let Some(size) = input["size"].as_u64() else { 416 return "invalid_descriptor_size"; 417 }; 418 let media_type = match MediaType::parse( 419 input["type"] 420 .as_str() 421 .expect("descriptor type must be a string"), 422 ) { 423 Ok(media_type) => media_type, 424 Err(error) => return error.code(), 425 }; 426 let Some(uploaded) = input["uploaded"].as_u64() else { 427 return "invalid_descriptor_uploaded"; 428 }; 429 430 BlobDescriptor::new(url, sha256, size, media_type, uploaded) 431 .expect_err("invalid descriptor vector must fail structured construction") 432 .code() 433 }