use sha2::{Digest, Sha256}; use super::*; fn sha256_hex(bytes: &[u8]) -> String { format!("{:x}", Sha256::digest(bytes)) } #[cfg(test)] pub(super) fn baseline_source_digest() -> String { let source = include_str!("../baseline_v1.rs"); let (_, baseline) = source .split_once("// baseline-v1:start\n") .expect("baseline start marker must exist"); let (baseline, _) = baseline .split_once("// baseline-v1:end") .expect("baseline end marker must exist"); sha256_hex(baseline.as_bytes()) } pub(super) fn validate_descriptors( descriptors: &[MigrationDescriptor], ) -> Result<(), MigrationError> { if descriptors.is_empty() || descriptors.len() > 1_024 { return Err(MigrationError::new( "invalid_contract", "contract.sequence", None, "contact_operator", )); } for (index, descriptor) in descriptors.iter().enumerate() { let expected_version = i64::try_from(index + 1).unwrap_or(i64::MAX); let valid_name = !descriptor.name.is_empty() && descriptor.name.len() <= 128 && descriptor .name .bytes() .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-') && !descriptors[..index] .iter() .any(|prior| prior.name == descriptor.name); let valid_source_digest = descriptor.source_digest.len() == 64 && descriptor .source_digest .bytes() .all(|byte| byte.is_ascii_hexdigit() && !byte.is_ascii_uppercase()); let valid_checksum = if descriptor.version == 1 { descriptor.checksum == BASELINE_CHECKSUM } else { descriptor.checksum == descriptor.source_digest }; let valid_window = descriptor.readable_schema_min >= 1 && descriptor.readable_schema_min <= descriptor.readable_schema_max && descriptor.readable_schema_max <= descriptor.version; let valid_phase = match descriptor.phase { "expand" => descriptor.backfill == BackfillPolicy::None, "migrate" => matches!( descriptor.backfill, BackfillPolicy::Bounded { max_batch_rows: 1..=10_000, max_batch_ms: 1..=60_000, resumable: true, } ), "contract" => { descriptor.compatibility == "window-closed" && descriptor.contract_evidence.is_some_and(|evidence| { !evidence.is_empty() && evidence.len() <= 256 && !evidence.starts_with('/') && !evidence.contains("..") }) && descriptor.backfill == BackfillPolicy::None } _ => false, }; let valid_compatibility = matches!( descriptor.compatibility, "legacy-baseline" | "n-minus-one-readable" | "window-open" | "window-closed" ); let valid_owner = !descriptor.owner.is_empty() && descriptor.owner.len() <= 128 && descriptor .owner .bytes() .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-'); if descriptor.version != expected_version || !valid_name || !valid_source_digest || !valid_checksum || !valid_phase || !valid_compatibility || !valid_owner || !valid_window || !descriptor.transactional { return Err(MigrationError::new( "invalid_contract", "contract.sequence", Some(descriptor.version), "contact_operator", )); } } if descriptors.len() != usize::try_from(CURRENT_VERSION).unwrap_or_default() || descriptors .iter() .map(|descriptor| descriptor.version) .ne(IMPLEMENTED_VERSIONS.iter().copied()) || sha256_hex(CONSOLIDATION_SOURCE.as_bytes()) != CONSOLIDATION_SOURCE_SHA256 || sha256_hex(REQUEST_TRACE_IDENTITY_SOURCE.as_bytes()) != REQUEST_TRACE_IDENTITY_SOURCE_SHA256 || sha256_hex(OPERATION_LIFECYCLE_SOURCE.as_bytes()) != OPERATION_LIFECYCLE_SOURCE_SHA256 || sha256_hex(execution_outcome_v5::SOURCE.as_bytes()) != execution_outcome_v5::SOURCE_SHA256 || sha256_hex(platform_key_name_reuse_v6::SOURCE.as_bytes()) != platform_key_name_reuse_v6::SOURCE_SHA256 || sha256_hex(master_key_identity_v7::SOURCE.as_bytes()) != master_key_identity_v7::SOURCE_SHA256 || sha256_hex(admin_auth_lifecycle_v8::SOURCE.as_bytes()) != admin_auth_lifecycle_v8::SOURCE_SHA256 || sha256_hex(agent_catalog_lifecycle_v9::SOURCE.as_bytes()) != agent_catalog_lifecycle_v9::SOURCE_SHA256 || sha256_hex(approval_side_effects_v10::SOURCE.as_bytes()) != approval_side_effects_v10::SOURCE_SHA256 || sha256_hex(onboarding_product_events_v11::SOURCE.as_bytes()) != onboarding_product_events_v11::SOURCE_SHA256 || sha256_hex(artifact_metadata_v12::SOURCE.as_bytes()) != artifact_metadata_v12::SOURCE_SHA256 { return Err(MigrationError::new( "invalid_contract", "contract.implementation", Some(CURRENT_VERSION), "contact_operator", )); } Ok(()) }