type_bridge_schema_migration/
legacy.rs1use sha2::{Digest as _, Sha256};
12use type_bridge_contract::diagnostic::{Diagnostic, DiagnosticCategory, DiagnosticCode};
13use type_bridge_contract::limits::{
14 MAX_CANONICAL_BYTES, MAX_CANONICAL_COLLECTION_LEN, MAX_CANONICAL_STRING_BYTES,
15};
16use type_bridge_contract::migration::MigrationId;
17use type_bridge_contract::schema::DeclaredSchema;
18use type_bridge_schema::ManagedDeltaContext;
19
20use crate::manifest::{
21 SchemaMigrationDraft, VerifiedSchemaMigrationManifest, build_verified_manifest,
22};
23
24pub const LEGACY_CHECKSUM_ALGORITHM: &str = "python-source-sha256/16";
27pub const LEGACY_APPLIED_SET_CANONICALIZATION: &str = "typebridge.legacy-applied-set/v1";
29pub const LEGACY_APPLIED_SET_ALGORITHM: &str = "sha256";
31
32const LEGACY_CHECKSUM_LEN: usize = 16;
33
34fn validate_legacy_component(value: String, kind: &'static str) -> Result<String, Diagnostic> {
35 if !value.is_empty() && value.len() <= MAX_CANONICAL_STRING_BYTES {
36 Ok(value)
37 } else {
38 Err(failure(
39 "migration_legacy_identity_component_invalid",
40 "legacy migration identity components must be nonempty bounded UTF-8",
41 )
42 .with_detail("component_kind", kind))
43 }
44}
45
46macro_rules! legacy_component {
47 ($name:ident, $doc:literal, $kind:literal) => {
48 #[doc = $doc]
49 #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
50 pub struct $name(String);
51
52 impl $name {
53 pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
55 Ok(Self(validate_legacy_component(value.into(), $kind)?))
56 }
57
58 pub fn as_str(&self) -> &str {
60 &self.0
61 }
62 }
63 };
64}
65
66legacy_component!(
67 LegacyMigrationAppLabel,
68 "A lossless bounded UTF-8 application label from released migration history.",
69 "app_label"
70);
71legacy_component!(
72 LegacyMigrationName,
73 "A lossless bounded UTF-8 migration name from released migration history.",
74 "name"
75);
76
77#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
83pub struct LegacyMigrationId {
84 app_label: LegacyMigrationAppLabel,
85 name: LegacyMigrationName,
86}
87
88impl LegacyMigrationId {
89 pub fn new(app_label: impl Into<String>, name: impl Into<String>) -> Result<Self, Diagnostic> {
91 Ok(Self {
92 app_label: LegacyMigrationAppLabel::new(app_label)?,
93 name: LegacyMigrationName::new(name)?,
94 })
95 }
96
97 pub const fn app_label(&self) -> &LegacyMigrationAppLabel {
99 &self.app_label
100 }
101
102 pub const fn name(&self) -> &LegacyMigrationName {
104 &self.name
105 }
106}
107
108impl From<MigrationId> for LegacyMigrationId {
109 fn from(id: MigrationId) -> Self {
110 Self {
111 app_label: LegacyMigrationAppLabel(id.app_label().as_str().to_owned()),
112 name: LegacyMigrationName(id.name().as_str().to_owned()),
113 }
114 }
115}
116
117#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
123pub struct LegacyMigrationChecksum(String);
124
125impl LegacyMigrationChecksum {
126 pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
128 let value = value.into();
129 if value.len() != LEGACY_CHECKSUM_LEN
130 || !value
131 .bytes()
132 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
133 {
134 return Err(failure(
135 "migration_legacy_checksum_invalid",
136 "legacy checksum must be exactly 16 lowercase hexadecimal characters",
137 ));
138 }
139 Ok(Self(value))
140 }
141
142 pub fn as_str(&self) -> &str {
144 &self.0
145 }
146
147 pub const fn algorithm(&self) -> &'static str {
149 LEGACY_CHECKSUM_ALGORITHM
150 }
151}
152
153#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
155pub struct LegacyMigrationReference {
156 id: LegacyMigrationId,
157 checksum: LegacyMigrationChecksum,
158}
159
160impl LegacyMigrationReference {
161 pub fn new(id: impl Into<LegacyMigrationId>, checksum: LegacyMigrationChecksum) -> Self {
163 Self {
164 id: id.into(),
165 checksum,
166 }
167 }
168
169 pub const fn id(&self) -> &LegacyMigrationId {
171 &self.id
172 }
173
174 pub const fn checksum(&self) -> &LegacyMigrationChecksum {
176 &self.checksum
177 }
178}
179
180#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
187pub struct LegacyAppliedSetDigest(String);
188
189impl LegacyAppliedSetDigest {
190 pub fn compute(
192 references: impl IntoIterator<Item = LegacyMigrationReference>,
193 ) -> Result<Self, Diagnostic> {
194 let mut collected = Vec::new();
195 let mut preimage_bytes = LEGACY_APPLIED_SET_CANONICALIZATION
196 .len()
197 .checked_add(1 + std::mem::size_of::<u64>())
198 .ok_or_else(applied_set_too_large)?;
199 for reference in references {
200 if collected.len() == MAX_CANONICAL_COLLECTION_LEN {
201 return Err(failure(
202 "migration_legacy_applied_set_too_many_rows",
203 "legacy applied-set digest exceeds the canonical collection ceiling",
204 ));
205 }
206 for field in [
207 reference.id().app_label().as_str().as_bytes(),
208 reference.id().name().as_str().as_bytes(),
209 reference.checksum().as_str().as_bytes(),
210 ] {
211 preimage_bytes = preimage_bytes
212 .checked_add(std::mem::size_of::<u64>())
213 .and_then(|bytes| bytes.checked_add(field.len()))
214 .ok_or_else(applied_set_too_large)?;
215 }
216 if preimage_bytes > MAX_CANONICAL_BYTES {
217 return Err(applied_set_too_large());
218 }
219 collected.push(reference);
220 }
221 let mut references = collected;
222 references.sort();
223 if references.is_empty() {
224 return Err(failure(
225 "migration_legacy_applied_set_empty",
226 "legacy applied-set digest requires at least one migration",
227 ));
228 }
229 if references
230 .windows(2)
231 .any(|pair| pair[0].id() == pair[1].id())
232 {
233 return Err(failure(
234 "migration_legacy_applied_set_duplicate",
235 "legacy applied-set digest contains a duplicate migration identity",
236 ));
237 }
238 let mut hasher = Sha256::new();
239 hasher.update(LEGACY_APPLIED_SET_CANONICALIZATION.as_bytes());
240 hasher.update([0]);
241 let row_count = u64::try_from(references.len()).map_err(|_| applied_set_too_large())?;
242 hasher.update(row_count.to_be_bytes());
243 for reference in &references {
244 hash_field(&mut hasher, reference.id().app_label().as_str().as_bytes());
245 hash_field(&mut hasher, reference.id().name().as_str().as_bytes());
246 hash_field(&mut hasher, reference.checksum().as_str().as_bytes());
247 }
248 Ok(Self(hex_digest(hasher.finalize())))
249 }
250
251 pub fn new(value: impl Into<String>) -> Result<Self, Diagnostic> {
253 let value = value.into();
254 if value.len() != 64
255 || !value
256 .bytes()
257 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
258 {
259 return Err(failure(
260 "migration_legacy_applied_set_digest_invalid",
261 "legacy applied-set digest must be 64 lowercase hexadecimal characters",
262 ));
263 }
264 Ok(Self(value))
265 }
266
267 pub const fn algorithm(&self) -> &'static str {
269 LEGACY_APPLIED_SET_ALGORITHM
270 }
271
272 pub const fn canonicalization(&self) -> &'static str {
274 LEGACY_APPLIED_SET_CANONICALIZATION
275 }
276
277 pub fn as_str(&self) -> &str {
279 &self.0
280 }
281}
282
283pub fn build_legacy_frontier_bridge(
290 id: MigrationId,
291 legacy_frontier: Vec<LegacyMigrationReference>,
292 legacy_applied_set: LegacyAppliedSetDigest,
293 reconstructed_head: &DeclaredSchema,
294 context: &ManagedDeltaContext,
295) -> Result<VerifiedSchemaMigrationManifest, Diagnostic> {
296 build_verified_manifest(
297 SchemaMigrationDraft::legacy_bridge(id, legacy_frontier, legacy_applied_set)?,
298 (reconstructed_head, context),
299 )
300}
301
302fn hash_field(hasher: &mut Sha256, value: &[u8]) {
303 hasher.update(u64::try_from(value.len()).unwrap_or(u64::MAX).to_be_bytes());
304 hasher.update(value);
305}
306
307fn applied_set_too_large() -> Diagnostic {
308 failure(
309 "migration_legacy_applied_set_too_large",
310 "legacy applied-set digest exceeds the canonical byte ceiling",
311 )
312}
313
314fn hex_digest(bytes: impl AsRef<[u8]>) -> String {
315 bytes
316 .as_ref()
317 .iter()
318 .map(|byte| format!("{byte:02x}"))
319 .collect()
320}
321
322fn failure(code: &'static str, message: &'static str) -> Diagnostic {
323 Diagnostic::new(
324 DiagnosticCategory::InvalidContract,
325 DiagnosticCode::new(code).expect("static legacy diagnostic code is canonical"),
326 message,
327 )
328}