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