1use crate::{
2 declaration::DeclarationSnapshot,
3 key::StableKey,
4 ledger::{
5 AllocationLedger,
6 claim::{ClaimConflict, validate_declaration_claim},
7 },
8 policy::AllocationPolicy,
9 session::ValidatedAllocations,
10 slot::AllocationSlotDescriptor,
11};
12
13#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
18pub enum AllocationValidationError<P> {
19 #[error("allocation policy rejected a declaration")]
21 Policy(P),
22 #[error("stable key '{stable_key}' was historically bound to a different allocation slot")]
24 StableKeySlotConflict {
25 stable_key: StableKey,
27 historical_slot: Box<AllocationSlotDescriptor>,
29 declared_slot: Box<AllocationSlotDescriptor>,
31 },
32 #[error("allocation slot '{slot:?}' was historically bound to stable key '{historical_key}'")]
34 SlotStableKeyConflict {
35 slot: Box<AllocationSlotDescriptor>,
37 historical_key: StableKey,
39 declared_key: StableKey,
41 },
42 #[error("stable key '{stable_key}' was explicitly retired and cannot be redeclared")]
44 RetiredAllocation {
45 stable_key: StableKey,
47 slot: Box<AllocationSlotDescriptor>,
49 },
50}
51
52pub fn validate_allocations<P: AllocationPolicy>(
57 ledger: &AllocationLedger,
58 snapshot: DeclarationSnapshot,
59 policy: &P,
60) -> Result<ValidatedAllocations, AllocationValidationError<P::Error>> {
61 for declaration in snapshot.declarations() {
62 policy
63 .validate_key(&declaration.stable_key)
64 .map_err(AllocationValidationError::Policy)?;
65 policy
66 .validate_slot(&declaration.stable_key, &declaration.slot)
67 .map_err(AllocationValidationError::Policy)?;
68
69 validate_declaration_history(ledger, declaration)?;
70 }
71
72 let (declarations, runtime_fingerprint) = snapshot.into_parts();
73
74 Ok(ValidatedAllocations::new(
75 ledger.current_generation,
76 declarations,
77 runtime_fingerprint,
78 ))
79}
80
81fn validate_declaration_history<P>(
82 ledger: &AllocationLedger,
83 declaration: &crate::declaration::AllocationDeclaration,
84) -> Result<(), AllocationValidationError<P>> {
85 validate_declaration_claim(ledger, declaration)
86 .map(|_| ())
87 .map_err(|conflict| map_validation_claim_conflict(ledger, declaration, conflict))
88}
89
90fn map_validation_claim_conflict<P>(
91 ledger: &AllocationLedger,
92 declaration: &crate::declaration::AllocationDeclaration,
93 conflict: ClaimConflict,
94) -> AllocationValidationError<P> {
95 match conflict {
96 ClaimConflict::StableKeyMoved { record_index } => {
97 let record = &ledger.allocation_history().records()[record_index];
98 AllocationValidationError::StableKeySlotConflict {
99 stable_key: declaration.stable_key.clone(),
100 historical_slot: Box::new(record.slot.clone()),
101 declared_slot: Box::new(declaration.slot.clone()),
102 }
103 }
104 ClaimConflict::SlotReused { record_index } => {
105 let record = &ledger.allocation_history().records()[record_index];
106 AllocationValidationError::SlotStableKeyConflict {
107 slot: Box::new(declaration.slot.clone()),
108 historical_key: record.stable_key.clone(),
109 declared_key: declaration.stable_key.clone(),
110 }
111 }
112 ClaimConflict::Tombstoned { record_index } => {
113 let record = &ledger.allocation_history().records()[record_index];
114 AllocationValidationError::RetiredAllocation {
115 stable_key: declaration.stable_key.clone(),
116 slot: Box::new(record.slot.clone()),
117 }
118 }
119 ClaimConflict::ActiveAllocation { .. } => {
120 unreachable!("active allocation conflicts are reservation-only")
121 }
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128 use crate::{
129 declaration::AllocationDeclaration,
130 ledger::{AllocationHistory, AllocationRecord, AllocationState},
131 schema::SchemaMetadata,
132 slot::AllocationSlotDescriptor,
133 };
134
135 #[derive(Debug, Eq, PartialEq)]
136 struct TestPolicy;
137
138 impl AllocationPolicy for TestPolicy {
139 type Error = &'static str;
140
141 fn validate_key(&self, key: &StableKey) -> Result<(), Self::Error> {
142 if key.as_str().starts_with("bad.") {
143 return Err("bad key");
144 }
145 Ok(())
146 }
147
148 fn validate_slot(
149 &self,
150 _key: &StableKey,
151 slot: &AllocationSlotDescriptor,
152 ) -> Result<(), Self::Error> {
153 if slot == &AllocationSlotDescriptor::memory_manager_unchecked(255) {
154 return Err("bad slot");
155 }
156 Ok(())
157 }
158
159 fn validate_reserved_slot(
160 &self,
161 _key: &StableKey,
162 _slot: &AllocationSlotDescriptor,
163 ) -> Result<(), Self::Error> {
164 Ok(())
165 }
166 }
167
168 fn ledger(records: Vec<AllocationRecord>) -> AllocationLedger {
169 AllocationLedger {
170 ledger_schema_version: 1,
171 physical_format_id: 1,
172 current_generation: 7,
173 allocation_history: AllocationHistory::from_parts(records, Vec::new()),
174 }
175 }
176
177 fn declaration(key: &str, id: u8) -> AllocationDeclaration {
178 AllocationDeclaration::new(
179 key,
180 AllocationSlotDescriptor::memory_manager(id).expect("usable slot"),
181 None,
182 SchemaMetadata::default(),
183 )
184 .expect("declaration")
185 }
186
187 fn active_record(key: &str, id: u8) -> AllocationRecord {
188 AllocationRecord::from_declaration(1, declaration(key, id), AllocationState::Active)
189 }
190
191 #[test]
192 fn accepts_matching_historical_owner() {
193 let snapshot =
194 DeclarationSnapshot::new(vec![declaration("app.users.v1", 100)]).expect("snapshot");
195
196 let validated = validate_allocations(
197 &ledger(vec![active_record("app.users.v1", 100)]),
198 snapshot,
199 &TestPolicy,
200 )
201 .expect("validated");
202
203 assert_eq!(validated.generation(), 7);
204 }
205
206 #[test]
207 fn omitted_historical_records_do_not_fail_validation() {
208 let snapshot =
209 DeclarationSnapshot::new(vec![declaration("app.users.v1", 100)]).expect("snapshot");
210
211 validate_allocations(
212 &ledger(vec![
213 active_record("app.users.v1", 100),
214 active_record("app.orders.v1", 101),
215 ]),
216 snapshot,
217 &TestPolicy,
218 )
219 .expect("omitted records are preserved, not retired");
220 }
221
222 #[test]
223 fn rejects_same_key_different_slot() {
224 let snapshot =
225 DeclarationSnapshot::new(vec![declaration("app.users.v1", 101)]).expect("snapshot");
226
227 let err = validate_allocations(
228 &ledger(vec![active_record("app.users.v1", 100)]),
229 snapshot,
230 &TestPolicy,
231 )
232 .expect_err("conflict");
233
234 assert!(matches!(
235 err,
236 AllocationValidationError::StableKeySlotConflict { .. }
237 ));
238 }
239
240 #[test]
241 fn rejects_same_slot_different_key() {
242 let snapshot =
243 DeclarationSnapshot::new(vec![declaration("app.orders.v1", 100)]).expect("snapshot");
244
245 let err = validate_allocations(
246 &ledger(vec![active_record("app.users.v1", 100)]),
247 snapshot,
248 &TestPolicy,
249 )
250 .expect_err("conflict");
251
252 assert!(matches!(
253 err,
254 AllocationValidationError::SlotStableKeyConflict { .. }
255 ));
256 }
257
258 #[test]
259 fn rejects_retired_redeclaration() {
260 let mut record = active_record("app.users.v1", 100);
261 record.state = AllocationState::Retired;
262 record.retired_generation = Some(3);
263 let snapshot =
264 DeclarationSnapshot::new(vec![declaration("app.users.v1", 100)]).expect("snapshot");
265
266 let err = validate_allocations(&ledger(vec![record]), snapshot, &TestPolicy)
267 .expect_err("retired");
268
269 assert!(matches!(
270 err,
271 AllocationValidationError::RetiredAllocation { .. }
272 ));
273 }
274
275 #[test]
276 fn policy_rejections_fail_before_validation_succeeds() {
277 let snapshot =
278 DeclarationSnapshot::new(vec![declaration("bad.users.v1", 100)]).expect("snapshot");
279
280 let err = validate_allocations(&ledger(Vec::new()), snapshot, &TestPolicy)
281 .expect_err("policy failure");
282
283 assert_eq!(err, AllocationValidationError::Policy("bad key"));
284 }
285}