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