use sha2::{Digest as _, Sha256};
use type_bridge_contract::diagnostic::{Diagnostic, DiagnosticCategory, DiagnosticCode};
use type_bridge_contract::limits::{
MAX_CANONICAL_BYTES, MAX_CANONICAL_COLLECTION_LEN, MAX_CANONICAL_STRING_BYTES,
};
use type_bridge_contract::migration::MigrationId;
use type_bridge_contract::schema::DeclaredSchema;
use type_bridge_schema::ManagedDeltaContext;
use crate::manifest::{
SchemaMigrationDraft, VerifiedSchemaMigrationManifest, build_verified_manifest,
};
pub const LEGACY_CHECKSUM_ALGORITHM: &str = "python-source-sha256/16";
pub const LEGACY_APPLIED_SET_CANONICALIZATION: &str = "typebridge.legacy-applied-set/v1";
pub const LEGACY_APPLIED_SET_ALGORITHM: &str = "sha256";
const LEGACY_CHECKSUM_LEN: usize = 16;
fn validate_legacy_component(value: String, kind: &'static str) -> Result<String, Diagnostic> {
if !value.is_empty() && value.len() <= MAX_CANONICAL_STRING_BYTES {
Ok(value)
} else {
Err(failure(
"migration_legacy_identity_component_invalid",
"legacy migration identity components must be nonempty bounded UTF-8",
)
.with_detail("component_kind", kind))
}
}
macro_rules! legacy_component {
($name:ident, $doc:literal, $kind:literal) => {
#[doc = $doc]
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct $name(String);
impl $name {
pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
Ok(Self(validate_legacy_component(value.into(), $kind)?))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
};
}
legacy_component!(
LegacyMigrationAppLabel,
"A lossless bounded UTF-8 application label from released migration history.",
"app_label"
);
legacy_component!(
LegacyMigrationName,
"A lossless bounded UTF-8 migration name from released migration history.",
"name"
);
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct LegacyMigrationId {
app_label: LegacyMigrationAppLabel,
name: LegacyMigrationName,
}
impl LegacyMigrationId {
pub fn new(app_label: impl Into<String>, name: impl Into<String>) -> Result<Self, Diagnostic> {
Ok(Self {
app_label: LegacyMigrationAppLabel::new(app_label)?,
name: LegacyMigrationName::new(name)?,
})
}
pub const fn app_label(&self) -> &LegacyMigrationAppLabel {
&self.app_label
}
pub const fn name(&self) -> &LegacyMigrationName {
&self.name
}
}
impl From<MigrationId> for LegacyMigrationId {
fn from(id: MigrationId) -> Self {
Self {
app_label: LegacyMigrationAppLabel(id.app_label().as_str().to_owned()),
name: LegacyMigrationName(id.name().as_str().to_owned()),
}
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct LegacyMigrationChecksum(String);
impl LegacyMigrationChecksum {
pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
let value = value.into();
if value.len() != LEGACY_CHECKSUM_LEN
|| !value
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
{
return Err(failure(
"migration_legacy_checksum_invalid",
"legacy checksum must be exactly 16 lowercase hexadecimal characters",
));
}
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub const fn algorithm(&self) -> &'static str {
LEGACY_CHECKSUM_ALGORITHM
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct LegacyMigrationReference {
id: LegacyMigrationId,
checksum: LegacyMigrationChecksum,
}
impl LegacyMigrationReference {
pub fn new(id: impl Into<LegacyMigrationId>, checksum: LegacyMigrationChecksum) -> Self {
Self {
id: id.into(),
checksum,
}
}
pub const fn id(&self) -> &LegacyMigrationId {
&self.id
}
pub const fn checksum(&self) -> &LegacyMigrationChecksum {
&self.checksum
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct LegacyAppliedSetDigest(String);
impl LegacyAppliedSetDigest {
pub fn compute(
references: impl IntoIterator<Item = LegacyMigrationReference>,
) -> Result<Self, Diagnostic> {
let mut collected = Vec::new();
let mut preimage_bytes = LEGACY_APPLIED_SET_CANONICALIZATION
.len()
.checked_add(1 + std::mem::size_of::<u64>())
.ok_or_else(applied_set_too_large)?;
for reference in references {
if collected.len() == MAX_CANONICAL_COLLECTION_LEN {
return Err(failure(
"migration_legacy_applied_set_too_many_rows",
"legacy applied-set digest exceeds the canonical collection ceiling",
));
}
for field in [
reference.id().app_label().as_str().as_bytes(),
reference.id().name().as_str().as_bytes(),
reference.checksum().as_str().as_bytes(),
] {
preimage_bytes = preimage_bytes
.checked_add(std::mem::size_of::<u64>())
.and_then(|bytes| bytes.checked_add(field.len()))
.ok_or_else(applied_set_too_large)?;
}
if preimage_bytes > MAX_CANONICAL_BYTES {
return Err(applied_set_too_large());
}
collected.push(reference);
}
let mut references = collected;
references.sort();
if references.is_empty() {
return Err(failure(
"migration_legacy_applied_set_empty",
"legacy applied-set digest requires at least one migration",
));
}
if references
.windows(2)
.any(|pair| pair[0].id() == pair[1].id())
{
return Err(failure(
"migration_legacy_applied_set_duplicate",
"legacy applied-set digest contains a duplicate migration identity",
));
}
let mut hasher = Sha256::new();
hasher.update(LEGACY_APPLIED_SET_CANONICALIZATION.as_bytes());
hasher.update([0]);
let row_count = u64::try_from(references.len()).map_err(|_| applied_set_too_large())?;
hasher.update(row_count.to_be_bytes());
for reference in &references {
hash_field(&mut hasher, reference.id().app_label().as_str().as_bytes());
hash_field(&mut hasher, reference.id().name().as_str().as_bytes());
hash_field(&mut hasher, reference.checksum().as_str().as_bytes());
}
Ok(Self(hex_digest(hasher.finalize())))
}
pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
let value = value.into();
if value.len() != 64
|| !value
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
{
return Err(failure(
"migration_legacy_applied_set_digest_invalid",
"legacy applied-set digest must be 64 lowercase hexadecimal characters",
));
}
Ok(Self(value))
}
pub const fn algorithm(&self) -> &'static str {
LEGACY_APPLIED_SET_ALGORITHM
}
pub const fn canonicalization(&self) -> &'static str {
LEGACY_APPLIED_SET_CANONICALIZATION
}
pub fn as_str(&self) -> &str {
&self.0
}
}
pub fn build_legacy_frontier_bridge(
id: MigrationId,
legacy_frontier: Vec<LegacyMigrationReference>,
legacy_applied_set: LegacyAppliedSetDigest,
reconstructed_head: &DeclaredSchema,
context: &ManagedDeltaContext,
) -> Result<VerifiedSchemaMigrationManifest, Diagnostic> {
build_verified_manifest(
SchemaMigrationDraft::legacy_bridge(id, legacy_frontier, legacy_applied_set)?,
(reconstructed_head, context),
)
}
fn hash_field(hasher: &mut Sha256, value: &[u8]) {
hasher.update(u64::try_from(value.len()).unwrap_or(u64::MAX).to_be_bytes());
hasher.update(value);
}
fn applied_set_too_large() -> Diagnostic {
failure(
"migration_legacy_applied_set_too_large",
"legacy applied-set digest exceeds the canonical byte ceiling",
)
}
fn hex_digest(bytes: impl AsRef<[u8]>) -> String {
bytes
.as_ref()
.iter()
.map(|byte| format!("{byte:02x}"))
.collect()
}
fn failure(code: &'static str, message: &'static str) -> Diagnostic {
Diagnostic::new(
DiagnosticCategory::InvalidContract,
DiagnosticCode::new(code).expect("static legacy diagnostic code is canonical"),
message,
)
}