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
163 fn ledger(records: Vec<AllocationRecord>) -> AllocationLedger {
164 AllocationLedger {
165 current_generation: 7,
166 records,
167 }
168 }
169
170 fn declaration(key: &str, id: u8) -> AllocationDeclaration {
171 AllocationDeclaration::new(
172 key,
173 MemoryManagerSlot::new(id).expect("usable slot"),
174 None,
175 SchemaMetadata::default(),
176 )
177 .expect("declaration")
178 }
179
180 fn active_record(key: &str, id: u8) -> AllocationRecord {
181 AllocationRecord::active(&declaration(key, id))
182 }
183
184 fn recovered(records: Vec<AllocationRecord>) -> RecoveredLedger {
185 RecoveredLedger::from_trusted_ledger(ledger(records))
186 }
187
188 #[test]
189 fn accepts_matching_historical_owner() {
190 let snapshot =
191 DeclarationSnapshot::new(vec![declaration("app.users.v1", 100)]).expect("snapshot");
192
193 let validated = validate_allocations(
194 &recovered(vec![active_record("app.users.v1", 100)]),
195 snapshot,
196 &TestPolicy,
197 )
198 .expect("validated");
199
200 assert_eq!(validated.base_generation(), 7);
201 }
202
203 #[test]
204 fn omitted_historical_records_do_not_fail_validation() {
205 let snapshot =
206 DeclarationSnapshot::new(vec![declaration("app.users.v1", 100)]).expect("snapshot");
207
208 validate_allocations(
209 &recovered(vec![
210 active_record("app.users.v1", 100),
211 active_record("app.orders.v1", 101),
212 ]),
213 snapshot,
214 &TestPolicy,
215 )
216 .expect("omitted records are preserved, not retired");
217 }
218
219 #[test]
220 fn rejects_same_key_different_slot() {
221 let snapshot =
222 DeclarationSnapshot::new(vec![declaration("app.users.v1", 101)]).expect("snapshot");
223
224 let err = validate_allocations(
225 &recovered(vec![active_record("app.users.v1", 100)]),
226 snapshot,
227 &TestPolicy,
228 )
229 .expect_err("conflict");
230
231 assert!(matches!(
232 err,
233 AllocationValidationError::StableKeySlotConflict { .. }
234 ));
235 }
236
237 #[test]
238 fn rejects_same_slot_different_key() {
239 let snapshot =
240 DeclarationSnapshot::new(vec![declaration("app.orders.v1", 100)]).expect("snapshot");
241
242 let err = validate_allocations(
243 &recovered(vec![active_record("app.users.v1", 100)]),
244 snapshot,
245 &TestPolicy,
246 )
247 .expect_err("conflict");
248
249 assert!(matches!(
250 err,
251 AllocationValidationError::SlotStableKeyConflict { .. }
252 ));
253 }
254
255 #[test]
256 fn rejects_retired_redeclaration() {
257 let mut record = active_record("app.users.v1", 100);
258 record.state = AllocationState::Retired;
259 let snapshot =
260 DeclarationSnapshot::new(vec![declaration("app.users.v1", 100)]).expect("snapshot");
261
262 let err = validate_allocations(&recovered(vec![record]), snapshot, &TestPolicy)
263 .expect_err("retired");
264
265 assert!(matches!(
266 err,
267 AllocationValidationError::RetiredAllocation { .. }
268 ));
269 }
270
271 #[test]
272 fn policy_rejections_fail_before_validation_succeeds() {
273 let snapshot =
274 DeclarationSnapshot::new(vec![declaration("bad.users.v1", 100)]).expect("snapshot");
275
276 let err = validate_allocations(&recovered(Vec::new()), snapshot, &TestPolicy)
277 .expect_err("policy failure");
278
279 assert_eq!(err, AllocationValidationError::Policy("bad key"));
280 }
281
282 #[test]
283 fn full_slot_domain_validates_stages_and_commits_through_public_boundaries() {
284 let mut store = crate::LedgerCommitStore::default();
285 let genesis = AllocationLedger::new(0, Vec::new()).unwrap();
286 let recovered = store.recover_or_initialize(&genesis).unwrap();
287 let snapshot = DeclarationSnapshot::new(
288 (0..=crate::MEMORY_MANAGER_MAX_ID)
289 .map(|id| declaration(&format!("app.store{id}.v1"), id))
290 .collect(),
291 )
292 .unwrap();
293 let validated = validate_allocations(&recovered, snapshot, &TestPolicy).unwrap();
294 let staged = recovered
295 .ledger()
296 .stage_validated_generation(&validated)
297 .unwrap();
298 assert_eq!(staged.records().len(), 255);
299
300 let committed = store.commit(&staged).unwrap();
301 assert_eq!(committed.current_generation(), 1);
302 assert_eq!(store.recover().unwrap(), committed);
303 }
304}