hopper_core/account/
realloc_guard.rs1use hopper_runtime::error::ProgramError;
26
27#[inline(always)]
28fn checked_size_u32(size: usize) -> Result<u32, ProgramError> {
29 u32::try_from(size).map_err(|_| ProgramError::InvalidRealloc)
30}
31
32pub struct ReallocGuard<const N: usize> {
37 original: [u32; N],
39 current: [u32; N],
41 count: usize,
43 budget: u32,
45 consumed: u32,
47}
48
49impl<const N: usize> ReallocGuard<N> {
50 #[inline(always)]
52 pub const fn new(budget: u32) -> Self {
53 Self {
54 original: [0u32; N],
55 current: [0u32; N],
56 count: 0,
57 budget,
58 consumed: 0,
59 }
60 }
61
62 #[inline(always)]
68 pub fn register(&mut self, slot: usize, size: usize) -> Result<(), ProgramError> {
69 if slot >= N {
70 return Err(ProgramError::InvalidArgument);
71 }
72 let size32 = checked_size_u32(size)?;
73 self.original[slot] = size32;
74 self.current[slot] = size32;
75 if slot >= self.count {
76 self.count = slot + 1;
77 }
78 Ok(())
79 }
80
81 #[inline]
86 pub fn check_growth(&self, slot: usize, new_size: usize) -> Result<(), ProgramError> {
87 if slot >= self.count {
88 return Err(ProgramError::InvalidArgument);
89 }
90 let new_size32 = checked_size_u32(new_size)?;
91 let current = self.current[slot];
92
93 if new_size32 <= current {
94 return Ok(());
96 }
97
98 let delta = new_size32 - current;
99 let new_consumed = self
100 .consumed
101 .checked_add(delta)
102 .ok_or(ProgramError::ArithmeticOverflow)?;
103
104 if new_consumed > self.budget {
105 return Err(ProgramError::InvalidRealloc);
106 }
107
108 Ok(())
109 }
110
111 #[inline(always)]
118 pub fn commit_growth(&mut self, slot: usize, new_size: usize) -> Result<(), ProgramError> {
119 if slot >= self.count {
120 return Err(ProgramError::InvalidArgument);
121 }
122 let new_size32 = checked_size_u32(new_size)?;
123 let current = self.current[slot];
124
125 if new_size32 > current {
126 let delta = new_size32 - current;
127 let new_consumed = self
128 .consumed
129 .checked_add(delta)
130 .ok_or(ProgramError::ArithmeticOverflow)?;
131 if new_consumed > self.budget {
132 return Err(ProgramError::InvalidRealloc);
133 }
134 self.consumed = new_consumed;
135 } else if new_size32 < current {
136 let credit = current - new_size32;
138 self.consumed = self.consumed.saturating_sub(credit);
139 }
140
141 self.current[slot] = new_size32;
142 Ok(())
143 }
144
145 #[inline(always)]
147 pub const fn remaining(&self) -> u32 {
148 self.budget.saturating_sub(self.consumed)
149 }
150
151 #[inline(always)]
153 pub const fn consumed(&self) -> u32 {
154 self.consumed
155 }
156
157 #[inline(always)]
159 pub const fn budget(&self) -> u32 {
160 self.budget
161 }
162
163 #[inline(always)]
165 pub fn slot_growth(&self, slot: usize) -> i64 {
166 if slot >= self.count {
167 return 0;
168 }
169 i64::from(self.current[slot]) - i64::from(self.original[slot])
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[test]
178 fn basic_growth_tracking() {
179 let mut guard = ReallocGuard::<4>::new(1024);
180 guard.register(0, 100).unwrap();
181 guard.register(1, 200).unwrap();
182
183 assert!(guard.check_growth(0, 200).is_ok());
185 guard.commit_growth(0, 200).unwrap();
186 assert_eq!(guard.consumed(), 100);
187 assert_eq!(guard.remaining(), 924);
188
189 assert_eq!(guard.slot_growth(0), 100);
191 assert_eq!(guard.slot_growth(1), 0);
192 }
193
194 #[test]
195 fn budget_exceeded() {
196 let mut guard = ReallocGuard::<4>::new(100);
197 guard.register(0, 50).unwrap();
198
199 assert!(guard.check_growth(0, 200).is_err()); }
202
203 #[test]
204 fn commit_growth_enforces_budget_without_precheck() {
205 let mut guard = ReallocGuard::<4>::new(100);
206 guard.register(0, 50).unwrap();
207
208 assert_eq!(
209 guard.commit_growth(0, 200),
210 Err(ProgramError::InvalidRealloc)
211 );
212 assert_eq!(guard.consumed(), 0);
213 assert_eq!(guard.slot_growth(0), 0);
214 }
215
216 #[test]
217 fn shrink_returns_credit() {
218 let mut guard = ReallocGuard::<4>::new(200);
219 guard.register(0, 100).unwrap();
220
221 guard.commit_growth(0, 200).unwrap();
223 assert_eq!(guard.consumed(), 100);
224
225 guard.commit_growth(0, 150).unwrap();
227 assert_eq!(guard.consumed(), 50);
228 assert_eq!(guard.remaining(), 150);
229 }
230
231 #[test]
232 fn same_size_is_noop() {
233 let mut guard = ReallocGuard::<4>::new(100);
234 guard.register(0, 100).unwrap();
235
236 assert!(guard.check_growth(0, 100).is_ok());
237 guard.commit_growth(0, 100).unwrap();
238 assert_eq!(guard.consumed(), 0);
239 }
240
241 #[test]
242 fn slot_growth_reports_full_u32_range_without_wrapping() {
243 let mut guard = ReallocGuard::<1>::new(u32::MAX);
244 guard.register(0, 0).unwrap();
245 guard
246 .commit_growth(0, usize::try_from(u32::MAX).unwrap())
247 .unwrap();
248
249 assert_eq!(guard.slot_growth(0), i64::from(u32::MAX));
250 }
251
252 #[test]
253 fn register_out_of_bounds() {
254 let mut guard = ReallocGuard::<2>::new(1024);
255 assert!(guard.register(0, 100).is_ok());
256 assert!(guard.register(1, 200).is_ok());
257 assert!(guard.register(2, 300).is_err()); }
259
260 #[test]
261 fn commit_unregistered_slot() {
262 let mut guard = ReallocGuard::<4>::new(1024);
263 guard.register(0, 100).unwrap();
264 assert!(guard.commit_growth(3, 200).is_err());
266 }
267
268 #[test]
269 fn oversized_usize_realloc_is_rejected_explicitly() {
270 let mut guard = ReallocGuard::<2>::new(u32::MAX);
271 let too_large = (u32::MAX as usize).saturating_add(1);
272
273 assert_eq!(
274 guard.register(0, too_large),
275 Err(ProgramError::InvalidRealloc)
276 );
277 guard.register(0, 16).unwrap();
278 assert_eq!(
279 guard.check_growth(0, too_large),
280 Err(ProgramError::InvalidRealloc)
281 );
282 assert_eq!(
283 guard.commit_growth(0, too_large),
284 Err(ProgramError::InvalidRealloc)
285 );
286 }
287}