1use std::cell::Cell;
5
6use crate::CodecError;
7
8use super::error::{
9 ErrorContext, LimitScope, ResourceDimension, ResourceFailure, ResourceLimit, SourceLocation,
10};
11use super::policy::{
12 DecodePolicy, DECOMPRESSED_TOTAL_BASE, DECOMPRESSED_TOTAL_PER_INPUT_BYTE, MATERIALIZED_BASE,
13 MATERIALIZED_PER_INPUT_BYTE, RETAINED_BASE, RETAINED_PER_INPUT_BYTE,
14};
15
16#[derive(Debug)]
17pub(crate) struct DecodeBudget {
18 policy: DecodePolicy,
19 input_bytes: u64,
20 decompressed: Cell<u64>,
21 materialized: Cell<u64>,
22 retained: Cell<u64>,
23 entities: Cell<u64>,
24 collection_items: Cell<u64>,
25 recursion_depth: Cell<u64>,
26 work: Cell<u64>,
27 fuse: Cell<Option<ResourceLimit>>,
28}
29
30impl DecodeBudget {
31 pub(crate) fn new(policy: DecodePolicy, input_bytes: u64) -> Self {
32 Self {
33 policy,
34 input_bytes,
35 decompressed: Cell::new(0),
36 materialized: Cell::new(0),
37 retained: Cell::new(0),
38 entities: Cell::new(0),
39 collection_items: Cell::new(0),
40 recursion_depth: Cell::new(0),
41 work: Cell::new(0),
42 fuse: Cell::new(None),
43 }
44 }
45
46 pub(crate) fn fused(&self) -> Option<ResourceLimit> {
47 self.fuse.get()
48 }
49
50 pub(crate) fn decompression_allowance(&self) -> u64 {
51 let proportional = DECOMPRESSED_TOTAL_BASE
52 .saturating_add(DECOMPRESSED_TOTAL_PER_INPUT_BYTE.saturating_mul(self.input_bytes));
53 self.policy
54 .limits
55 .max_decompressed_bytes_total
56 .min(proportional)
57 }
58
59 pub(crate) fn input_bytes(&self) -> u64 {
60 self.input_bytes
61 }
62
63 pub(crate) fn charge_decompressed(
64 &self,
65 amount: u64,
66 operation: &'static str,
67 location: Option<SourceLocation>,
68 ) -> Result<(), CodecError> {
69 self.charge(
70 ResourceDimension::DecompressedBytes,
71 &self.decompressed,
72 self.decompression_allowance(),
73 amount,
74 operation,
75 location,
76 )
77 }
78
79 pub(crate) fn decompressed_used(&self) -> u64 {
80 self.decompressed.get()
81 }
82
83 fn materialized_allowance(&self) -> u64 {
84 self.policy.limits.max_materialized_bytes.min(
85 MATERIALIZED_BASE
86 .saturating_add(MATERIALIZED_PER_INPUT_BYTE.saturating_mul(self.input_bytes)),
87 )
88 }
89
90 fn retained_allowance(&self) -> u64 {
91 self.policy.limits.max_retained_bytes.min(
92 RETAINED_BASE.saturating_add(RETAINED_PER_INPUT_BYTE.saturating_mul(self.input_bytes)),
93 )
94 }
95
96 fn charge(
97 &self,
98 dimension: ResourceDimension,
99 used: &Cell<u64>,
100 limit: u64,
101 amount: u64,
102 operation: &'static str,
103 location: Option<SourceLocation>,
104 ) -> Result<(), CodecError> {
105 if let Some(resource) = self.fuse.get() {
106 return Err(CodecError::ResourceLimit(resource));
107 }
108 let before = used.get();
109 if amount > limit.saturating_sub(before) {
110 return Err(self.refuse(
111 dimension,
112 ResourceFailure::BudgetExceeded,
113 LimitScope::Global,
114 limit,
115 before,
116 amount,
117 operation,
118 location,
119 ));
120 }
121 used.set(before + amount);
122 Ok(())
123 }
124
125 #[allow(
126 clippy::too_many_arguments,
127 reason = "Decode/encode helper keeps one parameter per independent arena, table, or control flag rather than a catch-all context struct."
128 )]
129 pub(crate) fn refuse(
130 &self,
131 dimension: ResourceDimension,
132 reason: ResourceFailure,
133 scope: LimitScope,
134 limit: u64,
135 used: u64,
136 additional: u64,
137 operation: &'static str,
138 location: Option<SourceLocation>,
139 ) -> CodecError {
140 let resource = ResourceLimit {
141 dimension,
142 reason,
143 scope,
144 limit,
145 used,
146 additional,
147 context: ErrorContext {
148 operation,
149 location,
150 },
151 };
152 self.fuse.set(Some(resource));
153 CodecError::ResourceLimit(resource)
154 }
155
156 pub(crate) fn reserve_scoped(
157 &self,
158 bytes: u64,
159 operation: &'static str,
160 location: Option<SourceLocation>,
161 ) -> Result<ScopedReservation<'_>, CodecError> {
162 self.charge(
163 ResourceDimension::MaterializedBytes,
164 &self.materialized,
165 self.materialized_allowance(),
166 bytes,
167 operation,
168 location,
169 )?;
170 Ok(ScopedReservation {
171 budget: self,
172 bytes,
173 operation,
174 location,
175 })
176 }
177
178 pub(crate) fn charge_retained(
179 &self,
180 bytes: u64,
181 operation: &'static str,
182 location: Option<SourceLocation>,
183 ) -> Result<(), CodecError> {
184 self.charge(
185 ResourceDimension::RetainedBytes,
186 &self.retained,
187 self.retained_allowance(),
188 bytes,
189 operation,
190 location,
191 )
192 }
193
194 pub(crate) fn charge_entities(
195 &self,
196 count: u64,
197 operation: &'static str,
198 ) -> Result<(), CodecError> {
199 self.charge(
200 ResourceDimension::Entities,
201 &self.entities,
202 self.policy.limits.max_entities,
203 count,
204 operation,
205 None,
206 )
207 }
208
209 pub(crate) fn charge_collection_items(
210 &self,
211 count: u64,
212 operation: &'static str,
213 ) -> Result<(), CodecError> {
214 self.charge(
215 ResourceDimension::CollectionItems,
216 &self.collection_items,
217 self.policy.limits.max_collection_items,
218 count,
219 operation,
220 None,
221 )
222 }
223
224 pub(crate) fn enter_nested(
225 &self,
226 operation: &'static str,
227 location: Option<SourceLocation>,
228 ) -> Result<DepthGuard<'_>, CodecError> {
229 self.charge(
230 ResourceDimension::RecursionDepth,
231 &self.recursion_depth,
232 self.policy.limits.max_recursion_depth,
233 1,
234 operation,
235 location,
236 )?;
237 Ok(DepthGuard { budget: self })
238 }
239
240 pub(crate) fn charge_work(
241 &self,
242 units: u64,
243 operation: &'static str,
244 ) -> Result<(), CodecError> {
245 self.charge(
246 ResourceDimension::WorkUnits,
247 &self.work,
248 self.policy.limits.max_work_units,
249 units,
250 operation,
251 None,
252 )
253 }
254}
255
256#[derive(Debug)]
258pub struct ScopedReservation<'a> {
259 budget: &'a DecodeBudget,
260 bytes: u64,
261 operation: &'static str,
262 location: Option<SourceLocation>,
263}
264
265impl ScopedReservation<'_> {
266 pub fn grow(&mut self, bytes: u64) -> Result<(), CodecError> {
268 self.budget.charge(
269 ResourceDimension::MaterializedBytes,
270 &self.budget.materialized,
271 self.budget.materialized_allowance(),
272 bytes,
273 self.operation,
274 self.location,
275 )?;
276 self.bytes = self.bytes.saturating_add(bytes);
277 Ok(())
278 }
279
280 pub fn commit(self) -> Result<(), CodecError> {
282 self.budget
283 .charge_retained(self.bytes, self.operation, self.location)
284 }
285
286 pub fn bytes(&self) -> u64 {
288 self.bytes
289 }
290}
291
292impl Drop for ScopedReservation<'_> {
293 fn drop(&mut self) {
294 self.budget
295 .materialized
296 .set(self.budget.materialized.get().saturating_sub(self.bytes));
297 }
298}
299
300#[derive(Debug)]
302pub struct DepthGuard<'a> {
303 budget: &'a DecodeBudget,
304}
305
306impl Drop for DepthGuard<'_> {
307 fn drop(&mut self) {
308 self.budget
309 .recursion_depth
310 .set(self.budget.recursion_depth.get().saturating_sub(1));
311 }
312}
313
314#[derive(Debug)]
316pub struct WorkBudget<'a> {
317 limit: usize,
318 remaining: Cell<usize>,
319 exhausted: Cell<bool>,
320 session: Option<&'a DecodeBudget>,
321}
322
323impl WorkBudget<'static> {
324 pub const fn new(limit: usize) -> Self {
326 Self {
327 limit,
328 remaining: Cell::new(limit),
329 exhausted: Cell::new(false),
330 session: None,
331 }
332 }
333}
334
335impl<'a> WorkBudget<'a> {
336 pub(crate) fn for_session(limit: u64, session: &'a DecodeBudget) -> Self {
337 let limit = if limit > usize::MAX as u64 {
338 usize::MAX
339 } else {
340 limit as usize
341 };
342 Self {
343 limit,
344 remaining: Cell::new(limit),
345 exhausted: Cell::new(false),
346 session: Some(session),
347 }
348 }
349
350 pub fn charge(&self) -> bool {
352 self.charge_by(1)
353 }
354
355 pub fn charge_by(&self, work: usize) -> bool {
357 if self.exhausted.get() {
358 return false;
359 }
360 let remaining = self.remaining.get();
361 if work > remaining {
362 self.exhausted.set(true);
363 false
364 } else {
365 if let Some(session) = self.session {
366 if session.charge_work(work as u64, "work_budget").is_err() {
367 self.exhausted.set(true);
368 return false;
369 }
370 }
371 self.remaining.set(remaining - work);
372 true
373 }
374 }
375
376 pub fn exhausted(&self) -> bool {
378 self.exhausted.get()
379 }
380
381 pub fn exhaust(&self) {
383 self.exhausted.set(true);
384 }
385
386 pub fn remaining(&self) -> usize {
388 self.remaining.get()
389 }
390
391 pub fn consumed(&self) -> usize {
393 self.limit.saturating_sub(self.remaining.get())
394 }
395
396 pub fn child_slice(&self, limit: usize) -> WorkBudget<'static> {
398 WorkBudget::new(limit.min(self.remaining()))
399 }
400
401 pub fn session_child_slice(&self, limit: usize) -> WorkBudget<'_> {
403 WorkBudget {
404 limit: limit.min(self.remaining()),
405 remaining: Cell::new(limit.min(self.remaining())),
406 exhausted: Cell::new(false),
407 session: self.session,
408 }
409 }
410
411 pub fn consume_child(&self, child: &WorkBudget<'_>) -> bool {
413 self.charge_by(child.consumed())
414 }
415
416 pub fn refuse(&self, operation: &'static str) -> CodecError {
418 if let Some(resource) = self.session.and_then(DecodeBudget::fused) {
419 return CodecError::ResourceLimit(resource);
420 }
421 local_limit_error(
422 "work_units",
423 self.limit as u64,
424 self.consumed().saturating_add(1) as u64,
425 operation,
426 None,
427 )
428 }
429}
430
431pub fn refuse_local_limit(
433 what: &'static str,
434 limit: u64,
435 requested: u64,
436 location: Option<SourceLocation>,
437) -> CodecError {
438 local_limit_error(what, limit, requested, what, location)
439}
440
441fn local_limit_error(
442 what: &'static str,
443 limit: u64,
444 requested: u64,
445 operation: &'static str,
446 location: Option<SourceLocation>,
447) -> CodecError {
448 CodecError::ResourceLimit(ResourceLimit {
449 dimension: ResourceDimension::Codec(what),
450 reason: ResourceFailure::BudgetExceeded,
451 scope: LimitScope::Global,
452 limit,
453 used: requested.min(limit),
454 additional: requested.saturating_sub(limit),
455 context: ErrorContext {
456 operation,
457 location,
458 },
459 })
460}