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