141 lines
5.6 KiB
Rust
141 lines
5.6 KiB
Rust
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(())
|
|
}
|