1use cloud_sdk_sanitization::sanitize_bytes;
4
5pub struct SnapshotEncoder<'output, E> {
11 destination: Destination<'output>,
12 len: usize,
13 max_len: usize,
14 error: E,
15}
16
17enum Destination<'output> {
18 Measure,
19 Output(&'output mut [u8]),
20 Verify(&'output [u8]),
21}
22
23struct EncodeRollback<'output> {
24 target: &'output mut [u8],
25 armed: bool,
26}
27
28impl EncodeRollback<'_> {
29 fn new(target: &mut [u8]) -> EncodeRollback<'_> {
30 EncodeRollback {
31 target,
32 armed: true,
33 }
34 }
35
36 fn disarm(&mut self) {
37 self.armed = false;
38 }
39}
40
41impl Drop for EncodeRollback<'_> {
42 fn drop(&mut self) {
43 if self.armed {
44 sanitize_bytes(self.target);
45 }
46 }
47}
48
49impl<'output, E: Copy> SnapshotEncoder<'output, E> {
50 fn measuring(max_len: usize, error: E) -> Self {
51 Self {
52 destination: Destination::Measure,
53 len: 0,
54 max_len,
55 error,
56 }
57 }
58
59 fn writing(output: &'output mut [u8], max_len: usize, error: E) -> Self {
60 Self {
61 destination: Destination::Output(output),
62 len: 0,
63 max_len,
64 error,
65 }
66 }
67
68 fn verifying(output: &'output [u8], max_len: usize, error: E) -> Self {
69 Self {
70 destination: Destination::Verify(output),
71 len: 0,
72 max_len,
73 error,
74 }
75 }
76
77 #[must_use]
79 pub const fn len(&self) -> usize {
80 self.len
81 }
82
83 #[must_use]
85 pub const fn is_empty(&self) -> bool {
86 self.len == 0
87 }
88
89 pub fn byte(&mut self, value: u8) -> Result<(), E> {
91 self.bytes(core::slice::from_ref(&value))
92 }
93
94 pub fn bytes(&mut self, value: &[u8]) -> Result<(), E> {
96 let end = self.checked_end(value.len())?;
97 if let Destination::Output(output) = &mut self.destination {
98 let target = output.get_mut(self.len..end).ok_or(self.error)?;
99 target.copy_from_slice(value);
100 } else if let Destination::Verify(output) = &self.destination {
101 let actual = output.get(self.len..end).ok_or(self.error)?;
102 if actual != value {
103 return Err(self.error);
104 }
105 }
106 self.len = end;
107 Ok(())
108 }
109
110 pub fn string(&mut self, value: &str) -> Result<(), E> {
112 self.bytes(value.as_bytes())
113 }
114
115 pub fn u64(&mut self, mut value: u64) -> Result<(), E> {
117 if value == 0 {
118 return self.byte(b'0');
119 }
120
121 let mut digits = [0_u8; 20];
122 let mut cursor = digits.len();
123 while value != 0 {
124 cursor = cursor.checked_sub(1).ok_or(self.error)?;
125 let digit = u8::try_from(value % 10).map_err(|_| self.error)?;
126 let slot = digits.get_mut(cursor).ok_or(self.error)?;
127 *slot = b'0'.checked_add(digit).ok_or(self.error)?;
128 value /= 10;
129 }
130 self.bytes(digits.get(cursor..).ok_or(self.error)?)
131 }
132
133 pub fn percent_encoded(&mut self, value: &str) -> Result<(), E> {
135 for byte in value.bytes() {
136 if super::is_unreserved(byte) {
137 self.byte(byte)?;
138 } else {
139 self.byte(b'%')?;
140 self.byte(super::hex_digit(byte >> 4))?;
141 self.byte(super::hex_digit(byte & 0x0f))?;
142 }
143 }
144 Ok(())
145 }
146
147 pub fn json_string_escaped(&mut self, value: &str) -> Result<(), E> {
149 for byte in value.bytes() {
150 match byte {
151 b'"' => self.string("\\\"")?,
152 b'\\' => self.string("\\\\")?,
153 b'\n' => self.string("\\n")?,
154 b'\r' => self.string("\\r")?,
155 b'\t' => self.string("\\t")?,
156 0x00..=0x1f => {
157 self.string("\\u00")?;
158 self.byte(super::hex_digit(byte >> 4))?;
159 self.byte(super::hex_digit(byte & 0x0f))?;
160 }
161 _ => self.byte(byte)?,
162 }
163 }
164 Ok(())
165 }
166
167 pub fn json_string(&mut self, value: &str) -> Result<(), E> {
169 self.byte(b'"')?;
170 self.json_string_escaped(value)?;
171 self.byte(b'"')
172 }
173
174 pub fn query_pair(&mut self, first: &mut bool, key: &str, value: &str) -> Result<(), E> {
176 self.query_separator(first)?;
177 self.percent_encoded(key)?;
178 self.byte(b'=')?;
179 self.percent_encoded(value)
180 }
181
182 pub fn query_u64(&mut self, first: &mut bool, key: &str, value: u64) -> Result<(), E> {
184 self.query_separator(first)?;
185 self.percent_encoded(key)?;
186 self.byte(b'=')?;
187 self.u64(value)
188 }
189
190 pub fn query_separator(&mut self, first: &mut bool) -> Result<(), E> {
192 if *first {
193 *first = false;
194 Ok(())
195 } else {
196 self.byte(b'&')
197 }
198 }
199
200 fn checked_end(&self, additional: usize) -> Result<usize, E> {
201 let end = self.len.checked_add(additional).ok_or(self.error)?;
202 if end > self.max_len {
203 return Err(self.error);
204 }
205 Ok(end)
206 }
207}
208
209pub fn encode_snapshot<S: Copy, E: Copy>(
226 snapshot: S,
227 output: &mut [u8],
228 error: E,
229 encode: for<'encoder> fn(S, &mut SnapshotEncoder<'encoder, E>) -> Result<(), E>,
230) -> Result<usize, E> {
231 encode_snapshot_bounded(snapshot, output, usize::MAX, error, encode)
232}
233
234pub fn measure_snapshot<S: Copy, E: Copy>(
236 snapshot: S,
237 error: E,
238 encode: for<'encoder> fn(S, &mut SnapshotEncoder<'encoder, E>) -> Result<(), E>,
239) -> Result<usize, E> {
240 measure_snapshot_bounded(snapshot, usize::MAX, error, encode)
241}
242
243pub fn measure_snapshot_bounded<S: Copy, E: Copy>(
245 snapshot: S,
246 max_len: usize,
247 error: E,
248 encode: for<'encoder> fn(S, &mut SnapshotEncoder<'encoder, E>) -> Result<(), E>,
249) -> Result<usize, E> {
250 let mut measure = SnapshotEncoder::measuring(max_len, error);
251 encode(snapshot, &mut measure)?;
252 Ok(measure.len())
253}
254
255pub fn encode_snapshot_bounded<S: Copy, E: Copy>(
260 snapshot: S,
261 output: &mut [u8],
262 max_len: usize,
263 error: E,
264 encode: for<'encoder> fn(S, &mut SnapshotEncoder<'encoder, E>) -> Result<(), E>,
265) -> Result<usize, E> {
266 let required = measure_snapshot_bounded(snapshot, max_len, error, encode)?;
267 let target = output.get_mut(..required).ok_or(error)?;
268 let mut rollback = EncodeRollback::new(target);
269
270 let write_matches = {
271 let mut writer = SnapshotEncoder::writing(&mut *rollback.target, max_len, error);
272 encode(snapshot, &mut writer).is_ok() && writer.len() == required
273 };
274 if !write_matches {
275 return Err(error);
276 }
277
278 let verify_matches = {
279 let mut verifier = SnapshotEncoder::verifying(&*rollback.target, max_len, error);
280 encode(snapshot, &mut verifier).is_ok() && verifier.len() == required
281 };
282 if !verify_matches {
283 return Err(error);
284 }
285 rollback.disarm();
286 Ok(required)
287}
288
289#[cfg(test)]
290mod tests {
291 use core::sync::atomic::{AtomicUsize, Ordering};
292
293 use super::{SnapshotEncoder, encode_snapshot, encode_snapshot_bounded};
294
295 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
296 enum TestError {
297 Rejected,
298 }
299
300 #[test]
301 fn exact_snapshot_encoding_preserves_the_tail() {
302 let mut output = [0xA5_u8; 16];
303 let result = encode_snapshot(
304 ("key", 42_u64),
305 &mut output,
306 TestError::Rejected,
307 |(key, value), encoder| {
308 encoder.string(key)?;
309 encoder.byte(b'=')?;
310 encoder.u64(value)
311 },
312 );
313
314 assert_eq!(result, Ok(6));
315 assert_eq!(output.get(..6), Some(b"key=42".as_slice()));
316 assert!(
317 output
318 .get(6..)
319 .is_some_and(|tail| tail.iter().all(|b| *b == 0xA5))
320 );
321 }
322
323 #[test]
324 fn every_undersized_capacity_is_unchanged() {
325 for capacity in 0..6 {
326 let mut output = [0xA5_u8; 6];
327 let result = encode_snapshot(
328 "secret",
329 output.get_mut(..capacity).unwrap_or_default(),
330 TestError::Rejected,
331 |value, encoder| encoder.string(value),
332 );
333 assert_eq!(result, Err(TestError::Rejected));
334 assert_eq!(output, [0xA5; 6]);
335 }
336 }
337
338 #[test]
339 fn aggregate_cap_is_checked_before_writing() {
340 let mut output = [0xA5_u8; 8];
341 assert_eq!(
342 encode_snapshot_bounded(
343 "1234",
344 &mut output,
345 3,
346 TestError::Rejected,
347 |value, encoder| encoder.string(value),
348 ),
349 Err(TestError::Rejected)
350 );
351 assert_eq!(output, [0xA5; 8]);
352 }
353
354 #[test]
355 fn arithmetic_overflow_is_reported() {
356 let mut encoder = SnapshotEncoder::measuring(usize::MAX, TestError::Rejected);
357 encoder.len = usize::MAX;
358 assert_eq!(encoder.byte(b'x'), Err(TestError::Rejected));
359 }
360
361 #[test]
362 fn nondeterministic_same_length_output_is_rejected_and_cleared() {
363 static PASS: AtomicUsize = AtomicUsize::new(0);
364
365 fn changing(
366 _snapshot: (),
367 encoder: &mut SnapshotEncoder<'_, TestError>,
368 ) -> Result<(), TestError> {
369 let pass = PASS.fetch_add(1, Ordering::Relaxed);
370 encoder.byte(if pass == 2 { b'B' } else { b'A' })
371 }
372
373 PASS.store(0, Ordering::Relaxed);
374 let mut output = [0xA5_u8; 4];
375 assert_eq!(
376 encode_snapshot((), &mut output, TestError::Rejected, changing),
377 Err(TestError::Rejected)
378 );
379 assert_eq!(output, [0, 0xA5, 0xA5, 0xA5]);
380 }
381}