lib

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

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 }