1use super::{
5 DecodeArena, DecodeContext, DecodeDepth, DecodePolicy, ExpandSpec, ResourceDimension,
6 ResourceLimit, SpaceId,
7};
8
9#[derive(Debug, Clone)]
10pub(super) struct BudgetState {
11 pub(super) policy: DecodePolicy,
12 pub(super) input: u64,
13 pub(super) decompressed: u64,
14 pub(super) retained: u64,
15 pub(super) entities: u64,
16 pub(super) depth: u64,
17 pub(super) fuse: Option<ResourceLimit>,
18}
19
20#[derive(Debug, Clone, Copy)]
21pub(super) enum Event {
22 Charge(ResourceDimension, u64, &'static str),
23 Leave,
24 Available(u64),
25 Allowance(u64),
26 AllowanceUse(u64, u64),
27 Expand(ExpandSpec),
28 Space(usize),
29 Batch(Charges),
30}
31
32#[derive(Debug, Clone, Copy, Default)]
33pub(super) struct Charges {
34 amounts: [u64; 3],
35 depth: i128,
36 peak: i128,
37 minimum: i128,
38}
39
40pub(super) fn record(events: &mut Vec<Event>, event: Event) {
41 let mut batch = match events.last() {
42 Some(Event::Batch(batch)) => *batch,
43 _ => Charges::default(),
44 };
45 let compacted = match event {
46 Event::Charge(dimension, amount, _) => {
47 let index = match dimension {
48 ResourceDimension::DecompressedBytes => Some(0),
49 ResourceDimension::RetainedBytes => Some(1),
50 ResourceDimension::Entities => Some(2),
51 ResourceDimension::RecursionDepth => None,
52 ResourceDimension::InputBytes => {
53 events.push(event);
54 return;
55 }
56 };
57 if let Some(index) = index {
58 batch.amounts[index]
59 .checked_add(amount)
60 .map(|total| batch.amounts[index] = total)
61 } else {
62 batch.depth.checked_add(i128::from(amount)).map(|depth| {
63 batch.depth = depth;
64 batch.peak = batch.peak.max(depth);
65 })
66 }
67 }
68 Event::Leave => batch.depth.checked_sub(1).map(|depth| {
69 batch.depth = depth;
70 batch.minimum = batch.minimum.min(depth);
71 }),
72 _ => {
73 events.push(event);
74 return;
75 }
76 };
77 if compacted.is_none() {
78 events.push(event);
79 } else if let Some(Event::Batch(last)) = events.last_mut() {
80 *last = batch;
81 } else {
82 events.push(Event::Batch(batch));
83 }
84}
85
86impl Charges {
87 fn replay(self, candidate: &DecodeContext<'_>) -> bool {
88 let initial = candidate.budget.state().depth;
89 let minimum = i128::from(initial) + self.minimum;
90 let maximum = i128::from(initial) + self.peak;
91 let final_depth = i128::from(initial) + self.depth;
92 if minimum < 0 || maximum > i128::from(candidate.policy().limits.max_recursion_depth) {
93 return false;
94 }
95 let Ok(final_depth) = u64::try_from(final_depth) else {
96 return false;
97 };
98 for (dimension, amount) in [
99 ResourceDimension::DecompressedBytes,
100 ResourceDimension::RetainedBytes,
101 ResourceDimension::Entities,
102 ]
103 .into_iter()
104 .zip(self.amounts)
105 {
106 if candidate
107 .budget
108 .replay_charge(dimension, amount, "replay decode charge group")
109 .is_err()
110 {
111 return false;
112 }
113 }
114 let mut state = candidate.budget.state();
115 state.depth = final_depth;
116 candidate.budget.restore(&state);
117 true
118 }
119}
120
121#[derive(Debug, Clone)]
126pub struct DecodeCheckpoint {
127 state: BudgetState,
128 depth: DecodeDepth,
129 spaces: usize,
130}
131
132#[derive(Debug)]
135pub struct DecodeTrace {
136 events: Vec<Event>,
137 refused: bool,
138}
139
140#[derive(Debug, Clone, Copy)]
142pub struct DecodeSpaceRebinding {
143 old_start: usize,
144 new_start: usize,
145 count: usize,
146}
147
148impl DecodeSpaceRebinding {
149 pub fn is_identity(&self) -> bool {
151 self.count == 0 || self.old_start == self.new_start
152 }
153
154 pub fn rebind(&self, space: &mut SpaceId) {
156 if let Some(relative) = space.index().checked_sub(self.old_start) {
157 if relative < self.count {
158 *space = SpaceId::from_index(self.new_start + relative);
159 }
160 }
161 }
162}
163
164impl DecodeTrace {
165 pub fn retained_bytes_estimate(&self) -> u64 {
167 super::u64_from_index(std::mem::size_of_val(self.events.as_slice()))
168 }
169}
170
171impl DecodeCheckpoint {
172 pub fn resume<'a>(&self, arena: &'a DecodeArena) -> DecodeContext<'a> {
174 let ctx = DecodeContext::new(arena, &self.state.policy, self.depth);
175 ctx.budget.restore(&self.state);
176 ctx.derived_spaces.set(self.spaces);
177 *ctx.budget.trace.borrow_mut() = Some(Vec::new());
178 ctx
179 }
180}
181
182impl DecodeContext<'_> {
183 pub fn with_arena<'b, T>(
187 &self,
188 arena: &'b DecodeArena,
189 work: impl FnOnce(&DecodeContext<'b>) -> T,
190 ) -> T {
191 let local = self.checkpoint().resume(arena);
192 *local.budget.trace.borrow_mut() = self.budget.trace.borrow_mut().take();
193 let result = work(&local);
194 self.budget.restore(&local.budget.state());
195 self.derived_spaces.set(local.derived_spaces.get());
196 *self.budget.trace.borrow_mut() = local.budget.trace.borrow_mut().take();
197 result
198 }
199
200 pub fn retained_allowance(&self) -> u64 {
203 let available = self.budget.retained_available();
204 self.budget.record(Event::Allowance(available));
205 available
206 }
207
208 pub fn observe_retained_allowance(&self, remaining: u64, requested: u64) {
212 self.budget
213 .record(Event::AllowanceUse(remaining, requested));
214 }
215
216 pub fn checkpoint(&self) -> DecodeCheckpoint {
218 DecodeCheckpoint {
219 state: self.budget.state(),
220 depth: self.depth(),
221 spaces: self.derived_spaces.get(),
222 }
223 }
224
225 pub fn take_trace(&self) -> DecodeTrace {
227 let mut events = self.budget.trace.borrow_mut().take().unwrap_or_default();
228 let refused = self.resource_refusal().is_some();
229 if refused {
230 events.clear();
231 }
232 events.shrink_to_fit();
233 DecodeTrace { events, refused }
234 }
235
236 pub fn accept_trace(&self, trace: &DecodeTrace) -> bool {
243 self.accept_trace_rebinding(trace, DecodeSpaceRebinding::is_identity)
244 }
245
246 pub fn accept_trace_rebinding(
253 &self,
254 trace: &DecodeTrace,
255 rebind: impl FnOnce(&DecodeSpaceRebinding) -> bool,
256 ) -> bool {
257 if trace.refused || self.resource_refusal().is_some() {
258 return false;
259 }
260 let arena = DecodeArena::new();
261 let candidate = self.checkpoint().resume(&arena);
262 *candidate.budget.trace.borrow_mut() = None;
263 let mut allowance_delta = 0_i128;
264 let mut exact_observation = false;
265 let mut spaces = DecodeSpaceRebinding {
266 old_start: 0,
267 new_start: 0,
268 count: 0,
269 };
270 for event in &trace.events {
271 let accepted = match *event {
272 Event::Batch(batch) => batch.replay(&candidate),
273 Event::Charge(dimension, amount, operation) => candidate
274 .budget
275 .replay_charge(dimension, amount, operation)
276 .is_ok(),
277 Event::Leave => {
278 candidate.budget.leave();
279 true
280 }
281 Event::Available(available) | Event::Allowance(available) => {
282 if exact_observation {
283 return false;
284 }
285 allowance_delta =
286 i128::from(candidate.budget.retained_available()) - i128::from(available);
287 exact_observation =
288 matches!(event, Event::Available(_)) && allowance_delta != 0;
289 true
290 }
291 Event::AllowanceUse(remaining, requested) => {
292 exact_observation = false;
293 let adjusted = i128::from(remaining) + allowance_delta;
294 (adjusted >= i128::from(requested)) == (remaining >= requested)
295 }
296 Event::Expand(spec) => candidate.admit_expand(spec).is_ok(),
297 Event::Space(expected) => {
298 let Some(old_next) = expected.checked_add(1) else {
299 return false;
300 };
301 let Some(new_next) = candidate.derived_spaces.get().checked_add(1) else {
302 return false;
303 };
304 if spaces.count == 0 {
305 spaces.old_start = old_next;
306 spaces.new_start = new_next;
307 }
308 if old_next.checked_sub(spaces.old_start) != Some(spaces.count) {
309 return false;
310 }
311 spaces.count += 1;
312 candidate.derived_spaces.set(new_next);
313 true
314 }
315 };
316 if !accepted {
317 return false;
318 }
319 }
320 if exact_observation || !rebind(&spaces) {
321 return false;
322 }
323 if let Some(events) = self.budget.trace.borrow_mut().as_mut() {
324 events.extend(trace.events.iter().map(|event| match *event {
325 Event::Space(expected) => {
326 let mut space = SpaceId::from_index(expected + 1);
327 spaces.rebind(&mut space);
328 Event::Space(space.index() - 1)
329 }
330 event => event,
331 }));
332 }
333 self.budget.restore(&candidate.budget.state());
334 self.derived_spaces.set(candidate.derived_spaces.get());
335 true
336 }
337}
338
339#[cfg(test)]
340mod tests;