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