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