1use crate::{
2 capability::ValidatedAllocations,
3 declaration::DeclarationSnapshot,
4 key::StableKey,
5 ledger::{AllocationLedger, ClaimConflict, RecoveredLedger, validate_declaration_claim},
6 policy::AllocationPolicy,
7 slot::MemoryManagerSlot,
8};
9
10#[non_exhaustive]
19#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
20pub enum AllocationValidationError<P> {
21 #[error("allocation policy rejected a declaration")]
23 Policy(P),
24 #[error("stable key '{stable_key}' was historically bound to a different allocation slot")]
26 StableKeySlotConflict {
27 stable_key: StableKey,
29 historical_slot: MemoryManagerSlot,
31 declared_slot: MemoryManagerSlot,
33 },
34 #[error("allocation slot '{slot:?}' was historically bound to stable key '{historical_key}'")]
36 SlotStableKeyConflict {
37 slot: MemoryManagerSlot,
39 historical_key: StableKey,
41 declared_key: StableKey,
43 },
44 #[error("stable key '{stable_key}' was explicitly retired and cannot be redeclared")]
46 RetiredAllocation {
47 stable_key: StableKey,
49 slot: MemoryManagerSlot,
51 },
52}
53
54pub fn validate_allocations<P: AllocationPolicy>(
62 recovered: &RecoveredLedger,
63 snapshot: DeclarationSnapshot,
64 policy: &P,
65) -> Result<ValidatedAllocations, AllocationValidationError<P::Error>> {
66 check_allocations(recovered, &snapshot, policy)?;
67 let (declarations, runtime_fingerprint) = snapshot.into_parts();
68
69 Ok(ValidatedAllocations::new(
70 recovered.current_generation(),
71 declarations,
72 runtime_fingerprint,
73 ))
74}
75
76pub fn check_allocations<P: AllocationPolicy>(
79 recovered: &RecoveredLedger,
80 snapshot: &DeclarationSnapshot,
81 policy: &P,
82) -> Result<(), AllocationValidationError<P::Error>> {
83 let ledger = recovered.ledger();
84
85 for declaration in snapshot.declarations() {
86 policy
87 .validate_key(&declaration.stable_key)
88 .map_err(AllocationValidationError::Policy)?;
89 policy
90 .validate_slot(&declaration.stable_key, &declaration.slot)
91 .map_err(AllocationValidationError::Policy)?;
92
93 validate_declaration_history(ledger, declaration)?;
94 }
95
96 Ok(())
97}
98
99fn validate_declaration_history<P>(
100 ledger: &AllocationLedger,
101 declaration: &crate::declaration::AllocationDeclaration,
102) -> Result<(), AllocationValidationError<P>> {
103 validate_declaration_claim(ledger, declaration)
104 .map(|_| ())
105 .map_err(|conflict| map_validation_claim_conflict(declaration, conflict))
106}
107
108fn map_validation_claim_conflict<P>(
109 declaration: &crate::declaration::AllocationDeclaration,
110 conflict: ClaimConflict<'_>,
111) -> AllocationValidationError<P> {
112 match conflict {
113 ClaimConflict::StableKeyMoved { record } => {
114 AllocationValidationError::StableKeySlotConflict {
115 stable_key: declaration.stable_key.clone(),
116 historical_slot: record.slot.clone(),
117 declared_slot: declaration.slot.clone(),
118 }
119 }
120 ClaimConflict::SlotReused { record } => AllocationValidationError::SlotStableKeyConflict {
121 slot: declaration.slot.clone(),
122 historical_key: record.stable_key.clone(),
123 declared_key: declaration.stable_key.clone(),
124 },
125 ClaimConflict::Tombstoned { record } => AllocationValidationError::RetiredAllocation {
126 stable_key: declaration.stable_key.clone(),
127 slot: record.slot.clone(),
128 },
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use crate::{
136 declaration::AllocationDeclaration,
137 ledger::{AllocationHistory, AllocationRecord, AllocationState, GenerationRecord},
138 schema::SchemaMetadata,
139 slot::MemoryManagerSlot,
140 };
141
142 #[derive(Debug, Eq, PartialEq)]
143 struct TestPolicy;
144
145 impl AllocationPolicy for TestPolicy {
146 type Error = &'static str;
147
148 fn validate_key(&self, key: &StableKey) -> Result<(), Self::Error> {
149 if key.as_str().starts_with("bad.") {
150 return Err("bad key");
151 }
152 Ok(())
153 }
154
155 fn validate_slot(
156 &self,
157 _key: &StableKey,
158 _slot: &MemoryManagerSlot,
159 ) -> Result<(), Self::Error> {
160 Ok(())
161 }
162
163 fn validate_reserved_slot(
164 &self,
165 _key: &StableKey,
166 _slot: &MemoryManagerSlot,
167 ) -> Result<(), Self::Error> {
168 Ok(())
169 }
170 }
171
172 fn ledger(records: Vec<AllocationRecord>) -> AllocationLedger {
173 let generations = (1..=7)
174 .map(|generation| {
175 GenerationRecord::new(
176 generation,
177 if generation == 1 { 0 } else { generation - 1 },
178 None,
179 0,
180 None,
181 )
182 .expect("generation record")
183 })
184 .collect();
185
186 AllocationLedger {
187 current_generation: 7,
188 allocation_history: AllocationHistory::from_parts(records, generations),
189 }
190 }
191
192 fn declaration(key: &str, id: u8) -> AllocationDeclaration {
193 AllocationDeclaration::new(
194 key,
195 MemoryManagerSlot::new(id).expect("usable slot"),
196 None,
197 SchemaMetadata::default(),
198 )
199 .expect("declaration")
200 }
201
202 fn active_record(key: &str, id: u8) -> AllocationRecord {
203 AllocationRecord::active(1, &declaration(key, id))
204 }
205
206 fn recovered(records: Vec<AllocationRecord>) -> RecoveredLedger {
207 RecoveredLedger::from_trusted_ledger(ledger(records))
208 }
209
210 #[test]
211 fn accepts_matching_historical_owner() {
212 let snapshot =
213 DeclarationSnapshot::new(vec![declaration("app.users.v1", 100)]).expect("snapshot");
214
215 let validated = validate_allocations(
216 &recovered(vec![active_record("app.users.v1", 100)]),
217 snapshot,
218 &TestPolicy,
219 )
220 .expect("validated");
221
222 assert_eq!(validated.base_generation(), 7);
223 }
224
225 #[test]
226 fn omitted_historical_records_do_not_fail_validation() {
227 let snapshot =
228 DeclarationSnapshot::new(vec![declaration("app.users.v1", 100)]).expect("snapshot");
229
230 validate_allocations(
231 &recovered(vec![
232 active_record("app.users.v1", 100),
233 active_record("app.orders.v1", 101),
234 ]),
235 snapshot,
236 &TestPolicy,
237 )
238 .expect("omitted records are preserved, not retired");
239 }
240
241 #[test]
242 fn rejects_same_key_different_slot() {
243 let snapshot =
244 DeclarationSnapshot::new(vec![declaration("app.users.v1", 101)]).expect("snapshot");
245
246 let err = validate_allocations(
247 &recovered(vec![active_record("app.users.v1", 100)]),
248 snapshot,
249 &TestPolicy,
250 )
251 .expect_err("conflict");
252
253 assert!(matches!(
254 err,
255 AllocationValidationError::StableKeySlotConflict { .. }
256 ));
257 }
258
259 #[test]
260 fn rejects_same_slot_different_key() {
261 let snapshot =
262 DeclarationSnapshot::new(vec![declaration("app.orders.v1", 100)]).expect("snapshot");
263
264 let err = validate_allocations(
265 &recovered(vec![active_record("app.users.v1", 100)]),
266 snapshot,
267 &TestPolicy,
268 )
269 .expect_err("conflict");
270
271 assert!(matches!(
272 err,
273 AllocationValidationError::SlotStableKeyConflict { .. }
274 ));
275 }
276
277 #[test]
278 fn rejects_retired_redeclaration() {
279 let mut record = active_record("app.users.v1", 100);
280 record.state = AllocationState::Retired { generation: 3 };
281 let snapshot =
282 DeclarationSnapshot::new(vec![declaration("app.users.v1", 100)]).expect("snapshot");
283
284 let err = validate_allocations(&recovered(vec![record]), snapshot, &TestPolicy)
285 .expect_err("retired");
286
287 assert!(matches!(
288 err,
289 AllocationValidationError::RetiredAllocation { .. }
290 ));
291 }
292
293 #[test]
294 fn policy_rejections_fail_before_validation_succeeds() {
295 let snapshot =
296 DeclarationSnapshot::new(vec![declaration("bad.users.v1", 100)]).expect("snapshot");
297
298 let err = validate_allocations(&recovered(Vec::new()), snapshot, &TestPolicy)
299 .expect_err("policy failure");
300
301 assert_eq!(err, AllocationValidationError::Policy("bad key"));
302 }
303
304 #[test]
305 fn full_slot_domain_validates_stages_and_commits_through_public_boundaries() {
306 let mut store = crate::LedgerCommitStore::default();
307 let genesis = AllocationLedger::new(0, AllocationHistory::default()).unwrap();
308 let recovered = store.recover_or_initialize(&genesis).unwrap();
309 let snapshot = DeclarationSnapshot::new(
310 (0..=crate::MEMORY_MANAGER_MAX_ID)
311 .map(|id| declaration(&format!("app.store{id}.v1"), id))
312 .collect(),
313 )
314 .unwrap();
315 let validated = validate_allocations(&recovered, snapshot, &TestPolicy).unwrap();
316 let staged = recovered
317 .ledger()
318 .stage_validated_generation(&validated, None)
319 .unwrap();
320 assert_eq!(staged.allocation_history().records().len(), 255);
321 assert_eq!(
322 staged.allocation_history().generations()[0].declaration_count(),
323 255
324 );
325 let committed = store.commit(&staged).unwrap();
326 assert_eq!(committed.current_generation(), 1);
327 assert_eq!(store.recover().unwrap(), committed);
328 }
329}