Skip to main content

cloud_sdk/buffer/
encoder.rs

1//! Transactional two-pass encoding over immutable snapshots.
2
3use cloud_sdk_sanitization::sanitize_bytes;
4
5/// Fixed-buffer snapshot encoder used by provider request components.
6///
7/// Instances are created only by [`encode_snapshot`] and
8/// [`encode_snapshot_bounded`]. The first pass measures with checked
9/// arithmetic; the second pass receives an exactly sized destination.
10pub 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    /// Returns the number of bytes measured or written.
78    #[must_use]
79    pub const fn len(&self) -> usize {
80        self.len
81    }
82
83    /// Reports whether no bytes have been measured or written.
84    #[must_use]
85    pub const fn is_empty(&self) -> bool {
86        self.len == 0
87    }
88
89    /// Appends one byte.
90    pub fn byte(&mut self, value: u8) -> Result<(), E> {
91        self.bytes(core::slice::from_ref(&value))
92    }
93
94    /// Appends bytes without escaping.
95    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    /// Appends UTF-8 without escaping.
111    pub fn string(&mut self, value: &str) -> Result<(), E> {
112        self.bytes(value.as_bytes())
113    }
114
115    /// Appends a base-10 unsigned integer.
116    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    /// Appends one RFC 3986 percent-encoded component.
134    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    /// Appends JSON string contents without surrounding quotes.
148    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    /// Appends a complete JSON string.
168    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    /// Appends a percent-encoded query pair.
175    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    /// Appends a percent-encoded query key and base-10 integer.
183    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    /// Appends `&` unless this is the first query pair.
191    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
209/// Encodes one immutable `Copy` snapshot after an exact measurement pass.
210///
211/// `encode` is a function pointer, not a capturing closure. Both passes
212/// therefore receive only the same by-value snapshot. An undersized output is
213/// unchanged. A final compare-only pass checks every emitted byte exactly. If
214/// either later pass violates the measured contract or unwinds, the exact
215/// admitted destination is cleared before the error or unwind leaves this
216/// function.
217///
218/// # Snapshot contract
219///
220/// The snapshot and values it references must remain immutable for the whole
221/// call. Do not read clocks, random sources, globals, atomics, cells, or other
222/// interior-mutable state from `encode`. Exact replay detects observable
223/// drift, but it is not an authorization mechanism for a deliberately
224/// stateful encoder.
225pub 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
234/// Measures one immutable snapshot without writing output.
235pub 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
243/// Measures one immutable snapshot under an aggregate byte cap.
244pub 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
255/// Encodes one immutable snapshot under an aggregate byte cap.
256///
257/// Capacity and aggregate-limit failures occur before the output is modified.
258/// The [`encode_snapshot`] snapshot contract also applies.
259pub 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}