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