Skip to main content

aws_smithy_cbor/
encode.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6use aws_smithy_types::{BigInteger, Blob, DateTime};
7
8/// Macro for delegating method calls to the encoder.
9///
10/// This macro generates wrapper methods for calling specific encoder methods on the encoder
11/// and returning a mutable reference to self for method chaining.
12///
13/// # Example
14///
15/// ```ignore
16/// delegate_method! {
17///     /// Wrapper method for encoding method `encode_str` on the encoder.
18///     encode_str_wrapper => encode_str(data: &str);
19///     /// Wrapper method for encoding method `encode_int` on the encoder.
20///     encode_int_wrapper => encode_int(value: i32);
21/// }
22/// ```
23macro_rules! delegate_method {
24    ($($(#[$meta:meta])* $wrapper_name:ident => $encoder_name:ident($($param_name:ident : $param_type:ty),*);)+) => {
25        $(
26            pub fn $wrapper_name(&mut self, $($param_name: $param_type),*) -> &mut Self {
27                self.encoder.$encoder_name($($param_name)*).expect(INFALLIBLE_WRITE);
28                self
29            }
30        )+
31    };
32}
33
34#[derive(Debug, Clone)]
35pub struct Encoder {
36    encoder: minicbor::Encoder<Vec<u8>>,
37}
38
39/// We always write to a `Vec<u8>`, which is infallible in `minicbor`.
40/// <https://docs.rs/minicbor/latest/minicbor/encode/write/trait.Write.html#impl-Write-for-Vec%3Cu8%3E>
41const INFALLIBLE_WRITE: &str = "write failed";
42
43impl Encoder {
44    pub fn new(writer: Vec<u8>) -> Self {
45        Self {
46            encoder: minicbor::Encoder::new(writer),
47        }
48    }
49
50    delegate_method! {
51        /// Used when it's not cheap to calculate the size, i.e. when the struct has one or more
52        /// `Option`al members.
53        begin_map => begin_map();
54        /// Begins an indefinite-length array.
55        begin_array => begin_array();
56        /// Writes a boolean value.
57        boolean => bool(x: bool);
58        /// Writes a byte value.
59        byte => i8(x: i8);
60        /// Writes a short value.
61        short => i16(x: i16);
62        /// Writes an integer value.
63        integer => i32(x: i32);
64        /// Writes an long value.
65        long => i64(x: i64);
66        /// Writes an float value.
67        float => f32(x: f32);
68        /// Writes an double value.
69        double => f64(x: f64);
70        /// Writes a null tag.
71        null => null();
72        /// Writes an end tag.
73        end => end();
74    }
75
76    /// Maximum size of a CBOR type+length header: 1 byte major type + up to 8 bytes for the length.
77    const MAX_HEADER_LEN: usize = 9;
78
79    /// Writes a CBOR type+length header directly to the writer.
80    ///
81    /// Encodes the "additional information" field per RFC 8949 §3:
82    /// - 0..=23: length is stored directly in the low 5 bits of the initial byte.
83    /// - 24: one-byte uint follows (value 24..=0xff).
84    /// - 25: two-byte big-endian uint follows (value 0x100..=0xffff).
85    /// - 26: four-byte big-endian uint follows (value 0x1_0000..=0xffff_ffff).
86    /// - 27: eight-byte big-endian uint follows (larger values).
87    #[inline]
88    fn write_type_len(writer: &mut Vec<u8>, major: u8, len: usize) {
89        let mut buf = [0u8; Self::MAX_HEADER_LEN];
90        let n = match len {
91            0..=23 => {
92                buf[0] = major | len as u8;
93                1
94            }
95            24..=0xff => {
96                buf[0] = major | 24;
97                buf[1] = len as u8;
98                2
99            }
100            0x100..=0xffff => {
101                buf[0] = major | 25;
102                buf[1..3].copy_from_slice(&(len as u16).to_be_bytes());
103                3
104            }
105            0x1_0000..=0xffff_ffff => {
106                buf[0] = major | 26;
107                buf[1..5].copy_from_slice(&(len as u32).to_be_bytes());
108                5
109            }
110            _ => {
111                buf[0] = major | 27;
112                buf[1..9].copy_from_slice(&(len as u64).to_be_bytes());
113                9
114            }
115        };
116        writer.extend_from_slice(&buf[..n]);
117    }
118
119    /// Writes a definite length string. Collapses header+data into a single reserve+write.
120    pub fn str(&mut self, x: &str) -> &mut Self {
121        let writer = self.encoder.writer_mut();
122        let len = x.len();
123        writer.reserve(Self::MAX_HEADER_LEN + len);
124        Self::write_type_len(writer, 0x60, len);
125        writer.extend_from_slice(x.as_bytes());
126        self
127    }
128
129    /// Writes a `BigInteger` using preferred serialization per RFC 8949 §3.4.3.
130    ///
131    /// Values that fit in a CBOR major type 0 or 1 integer are encoded directly
132    /// (preferred serialization). Larger values use tag 2 (unsigned bignum) or
133    /// tag 3 (negative bignum). For tag 3, the byte string encodes `n` where
134    /// the value is `-1 - n`.
135    pub fn big_integer(&mut self, value: &BigInteger) -> &mut Self {
136        use num_bigint::{BigInt, Sign};
137
138        let n: BigInt = value
139            .as_ref()
140            .parse()
141            .expect("BigInteger contains invalid value");
142        let (sign, magnitude) = n.to_bytes_be();
143
144        match sign {
145            Sign::Plus | Sign::NoSign => {
146                // Try preferred serialization as major type 0.
147                if magnitude.len() <= 8 {
148                    let mut buf = [0u8; 8];
149                    buf[8 - magnitude.len()..].copy_from_slice(&magnitude);
150                    let val = u64::from_be_bytes(buf);
151                    self.encoder.u64(val).expect(INFALLIBLE_WRITE);
152                } else {
153                    self.encoder
154                        .tag(minicbor::data::Tag::new(2))
155                        .expect(INFALLIBLE_WRITE);
156                    // Preferred serialization: strip leading zeroes.
157                    let stripped = strip_leading_zeroes(&magnitude);
158                    self.encoder.bytes(stripped).expect(INFALLIBLE_WRITE);
159                }
160            }
161            Sign::Minus => {
162                // Tag 3 value = -1 - n, so n = -1 - value = |value| - 1.
163                let one = BigInt::from(1u8);
164                let n = (-n) - one;
165                let (_, n_bytes) = n.to_bytes_be();
166
167                // Try preferred serialization as major type 1.
168                if n_bytes.len() <= 8 {
169                    let mut buf = [0u8; 8];
170                    buf[8 - n_bytes.len()..].copy_from_slice(&n_bytes);
171                    let val = u64::from_be_bytes(buf);
172                    // Use i128 to represent -1 - val without overflow, then
173                    // convert to minicbor::data::Int which covers the full
174                    // CBOR major type 1 range.
175                    let neg = -1i128 - (val as i128);
176                    let int_val = minicbor::data::Int::try_from(neg)
177                        .expect("value fits in CBOR integer range");
178                    self.encoder.int(int_val).expect(INFALLIBLE_WRITE);
179                } else {
180                    self.encoder
181                        .tag(minicbor::data::Tag::new(3))
182                        .expect(INFALLIBLE_WRITE);
183                    let stripped = strip_leading_zeroes(&n_bytes);
184                    self.encoder.bytes(stripped).expect(INFALLIBLE_WRITE);
185                }
186            }
187        }
188        self
189    }
190
191    /// Writes a blob from a byte slice. Collapses header+data into a single reserve+write.
192    ///
193    /// Mirrors [`Self::str`]'s slice-input style. Prefer this over [`Self::blob`]
194    /// when the caller already holds a `&[u8]` (e.g. from a schema-serde
195    /// `ShapeSerializer::write_blob(_, &[u8])` call) — it avoids needing to
196    /// wrap the bytes in a [`Blob`] just to satisfy the API.
197    pub fn blob_bytes(&mut self, data: &[u8]) -> &mut Self {
198        let writer = self.encoder.writer_mut();
199        let len = data.len();
200        writer.reserve(Self::MAX_HEADER_LEN + len);
201        Self::write_type_len(writer, 0x40, len);
202        writer.extend_from_slice(data);
203        self
204    }
205
206    /// Writes a blob. Collapses header+data into a single reserve+write.
207    pub fn blob(&mut self, x: &Blob) -> &mut Self {
208        self.blob_bytes(x.as_ref())
209    }
210
211    /// Writes a fixed length array of given length.
212    pub fn array(&mut self, len: usize) -> &mut Self {
213        Self::write_type_len(self.encoder.writer_mut(), 0x80, len);
214        self
215    }
216
217    /// Writes a fixed length map of given length.
218    /// Used when we know the size in advance, i.e.:
219    /// - when a struct has all non-`Option`al members.
220    /// - when serializing `union` shapes (they can only have one member set).
221    /// - when serializing a `map` shape.
222    pub fn map(&mut self, len: usize) -> &mut Self {
223        Self::write_type_len(self.encoder.writer_mut(), 0xa0, len);
224        self
225    }
226
227    pub fn timestamp(&mut self, x: &DateTime) -> &mut Self {
228        self.encoder
229            .tag(minicbor::data::Tag::from(
230                minicbor::data::IanaTag::Timestamp,
231            ))
232            .expect(INFALLIBLE_WRITE);
233        self.encoder.f64(x.as_secs_f64()).expect(INFALLIBLE_WRITE);
234        self
235    }
236
237    pub fn into_writer(self) -> Vec<u8> {
238        self.encoder.into_writer()
239    }
240}
241
242/// Strips leading zero bytes from a big-endian byte slice.
243fn strip_leading_zeroes(bytes: &[u8]) -> &[u8] {
244    let start = bytes.iter().position(|&b| b != 0).unwrap_or(bytes.len());
245    &bytes[start..]
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251    use aws_smithy_types::Blob;
252
253    /// Verify our `str()` produces byte-identical output to minicbor's.
254    #[test]
255    fn str_matches_minicbor() {
256        let cases = [
257            "",                        // len 0
258            "a",                       // len 1 (in 0..=23 range)
259            "hello world!! test str",  // len 22 (still 0..=23)
260            "this is exactly 24 char", // len 24 (0x18, first 1-byte length)
261            &"x".repeat(0xff),         // len 255 (max 1-byte length)
262            &"y".repeat(0x100),        // len 256 (first 2-byte length)
263            &"z".repeat(0x1_0000),     // len 65536 (first 4-byte length)
264        ];
265        for input in &cases {
266            let mut ours = Encoder::new(Vec::new());
267            ours.str(input);
268
269            let mut theirs = minicbor::Encoder::new(Vec::new());
270            theirs.str(input).unwrap();
271
272            assert_eq!(
273                ours.into_writer(),
274                theirs.into_writer(),
275                "str mismatch for input len={}",
276                input.len()
277            );
278        }
279    }
280
281    /// Verify our `blob()` produces byte-identical output to minicbor's.
282    #[test]
283    fn blob_matches_minicbor() {
284        let cases: Vec<Vec<u8>> = vec![
285            vec![],               // empty
286            vec![0x42],           // 1 byte
287            vec![0xAB; 23],       // max inline length
288            vec![0xCD; 24],       // first 1-byte length
289            vec![0xEF; 0xff],     // max 1-byte length
290            vec![0x01; 0x100],    // first 2-byte length
291            vec![0x02; 0x1_0000], // first 4-byte length
292        ];
293        for input in &cases {
294            let mut ours = Encoder::new(Vec::new());
295            ours.blob(&Blob::new(input.clone()));
296
297            let mut theirs = minicbor::Encoder::new(Vec::new());
298            theirs.bytes(input).unwrap();
299
300            assert_eq!(
301                ours.into_writer(),
302                theirs.into_writer(),
303                "blob mismatch for input len={}",
304                input.len()
305            );
306        }
307    }
308
309    /// Verify chained `str()` calls don't corrupt encoder state for subsequent writes.
310    #[test]
311    fn str_chained_matches_minicbor() {
312        let mut ours = Encoder::new(Vec::new());
313        ours.str("key1").str("value1").str("key2").str("value2");
314
315        let mut theirs = minicbor::Encoder::new(Vec::new());
316        theirs
317            .str("key1")
318            .unwrap()
319            .str("value1")
320            .unwrap()
321            .str("key2")
322            .unwrap()
323            .str("value2")
324            .unwrap();
325
326        assert_eq!(ours.into_writer(), theirs.into_writer());
327    }
328
329    /// Verify `str()` works correctly inside a map structure (the real-world hot path).
330    #[test]
331    fn str_inside_map_matches_minicbor() {
332        let mut ours = Encoder::new(Vec::new());
333        ours.begin_map().str("TableName").str("my-table").end();
334
335        let mut theirs = minicbor::Encoder::new(Vec::new());
336        theirs
337            .begin_map()
338            .unwrap()
339            .str("TableName")
340            .unwrap()
341            .str("my-table")
342            .unwrap()
343            .end()
344            .unwrap();
345
346        assert_eq!(ours.into_writer(), theirs.into_writer());
347    }
348
349    /// Verify `str()` handles multi-byte UTF-8 correctly (CBOR text strings must be valid UTF-8).
350    #[test]
351    fn str_utf8_matches_minicbor() {
352        let cases = [
353            "café",          // 2-byte UTF-8
354            "日本語",        // 3-byte UTF-8
355            "🦀🔥",          // 4-byte UTF-8 (emoji)
356            "mixed: aé日🦀", // all byte widths
357        ];
358        for input in &cases {
359            let mut ours = Encoder::new(Vec::new());
360            ours.str(input);
361
362            let mut theirs = minicbor::Encoder::new(Vec::new());
363            theirs.str(input).unwrap();
364
365            assert_eq!(
366                ours.into_writer(),
367                theirs.into_writer(),
368                "str UTF-8 mismatch for {:?}",
369                input
370            );
371        }
372    }
373
374    #[test]
375    fn preferred_serialization_small_positive() {
376        // Small values use major type 0 directly, not tag 2.
377        let mut encoder = Encoder::new(Vec::new());
378        encoder.big_integer(&"0".parse().unwrap());
379        assert_eq!(encoder.into_writer(), vec![0x00]); // major type 0, value 0
380
381        let mut encoder = Encoder::new(Vec::new());
382        encoder.big_integer(&"23".parse().unwrap());
383        assert_eq!(encoder.into_writer(), vec![0x17]); // major type 0, value 23
384
385        let mut encoder = Encoder::new(Vec::new());
386        encoder.big_integer(&"256".parse().unwrap());
387        // major type 0, additional info 25 (2-byte), 0x0100
388        assert_eq!(encoder.into_writer(), vec![0x19, 0x01, 0x00]);
389    }
390
391    #[test]
392    fn preferred_serialization_small_negative() {
393        // Small negatives use major type 1 directly, not tag 3.
394        // Major type 1 value = -1 - argument.
395        let mut encoder = Encoder::new(Vec::new());
396        encoder.big_integer(&"-1".parse().unwrap());
397        assert_eq!(encoder.into_writer(), vec![0x20]); // -1 = -1-0, argument 0
398
399        let mut encoder = Encoder::new(Vec::new());
400        encoder.big_integer(&"-42".parse().unwrap());
401        // -42 = -1-41, argument 41 = 0x29 (major type 1, additional info 24, value 41)
402        assert_eq!(encoder.into_writer(), vec![0x38, 0x29]);
403    }
404
405    #[test]
406    fn preferred_serialization_u64_max() {
407        // u64::MAX = 18446744073709551615 fits in major type 0.
408        let mut encoder = Encoder::new(Vec::new());
409        encoder.big_integer(&"18446744073709551615".parse().unwrap());
410        let bytes = encoder.into_writer();
411        assert_eq!(bytes[0], 0x1b); // major type 0, 8-byte argument
412        assert_eq!(
413            &bytes[1..],
414            &[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]
415        );
416    }
417
418    #[test]
419    fn tag2_for_values_exceeding_u64() {
420        // 2^64 = 18446744073709551616 requires tag 2.
421        // RFC 8949 Appendix A: 0xc249010000000000000000
422        let mut encoder = Encoder::new(Vec::new());
423        encoder.big_integer(&"18446744073709551616".parse().unwrap());
424        let bytes = encoder.into_writer();
425        assert_eq!(
426            bytes,
427            vec![0xc2, 0x49, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
428        );
429    }
430
431    #[test]
432    fn tag3_negative_bignum_rfc8949_example() {
433        // RFC 8949 Appendix A: -18446744073709551617 = 0xc349010000000000000000
434        // value = -1 - n, n = 18446744073709551616 = 2^64
435        let mut encoder = Encoder::new(Vec::new());
436        encoder.big_integer(&"-18446744073709551617".parse().unwrap());
437        let bytes = encoder.into_writer();
438        assert_eq!(
439            bytes,
440            vec![0xc3, 0x49, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
441        );
442    }
443
444    #[test]
445    fn negative_at_major_type_1_boundary() {
446        // -18446744073709551616 = -1 - 18446744073709551615 = -1 - u64::MAX
447        // This fits in major type 1 with 8-byte argument = u64::MAX.
448        let mut encoder = Encoder::new(Vec::new());
449        encoder.big_integer(&"-18446744073709551616".parse().unwrap());
450        let bytes = encoder.into_writer();
451        assert_eq!(bytes[0], 0x3b); // major type 1, 8-byte argument
452        assert_eq!(
453            &bytes[1..],
454            &[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]
455        );
456    }
457
458    #[test]
459    fn tag2_strips_leading_zeroes() {
460        // A large number whose big-endian representation has no leading zeroes.
461        let mut encoder = Encoder::new(Vec::new());
462        let large = "123456789012345678901234567890";
463        encoder.big_integer(&large.parse().unwrap());
464        let bytes = encoder.into_writer();
465        assert_eq!(bytes[0], 0xc2); // tag 2
466                                    // Verify the byte string has no leading zero bytes.
467                                    // bytes[1] is the CBOR byte string length header.
468        let payload_start = if bytes[1] < 0x58 { 2 } else { 3 };
469        assert_ne!(bytes[payload_start], 0x00);
470    }
471}