Skip to main content

krabka_protocol/primitives/
array.rs

1//! Wire-level helpers for `[]<elem>` and nullable `[]<elem>` arrays.
2//!
3//! Non-flexible arrays use a 4-byte big-endian `INT32` length prefix, and −1
4//! means null. Flexible, compact arrays use an unsigned varint whose value is
5//! `len + 1`, and 0 means null.
6
7use bytes::{Buf, BufMut};
8
9use crate::{
10    ProtocolError,
11    primitives::{
12        fixed::{get_i32, put_i32},
13        varint::{compact_length, get_uvarint, put_uvarint, usize_uvarint_len, uvarint_len},
14    },
15};
16
17/// Writes a non-nullable array-length prefix.
18/// # Errors
19/// Returns [`ProtocolError::LengthOverflow`] when `n` does not fit the prefix.
20pub fn put_array_len<B: BufMut>(
21    buf: &mut B,
22    n: usize,
23    flexible: bool,
24) -> Result<(), ProtocolError> {
25    if flexible {
26        put_uvarint(buf, compact_length(n, "array")?);
27    } else {
28        put_i32(buf, int32_length(n)?);
29    }
30    Ok(())
31}
32
33/// The non-flexible `INT32` prefix value for an array of `n` elements.
34fn int32_length(n: usize) -> Result<i32, ProtocolError> {
35    i32::try_from(n).map_err(|_| ProtocolError::LengthOverflow {
36        what: "array",
37        len: n,
38    })
39}
40
41/// Writes a nullable array-length prefix. `None` encodes as −1 for
42/// non-flexible versions, or 0 for flexible versions.
43/// # Errors
44/// Returns [`ProtocolError::LengthOverflow`] when the length does not fit the
45/// prefix.
46pub fn put_nullable_array_len<B: BufMut>(
47    buf: &mut B,
48    len: Option<usize>,
49    flexible: bool,
50) -> Result<(), ProtocolError> {
51    match (flexible, len) {
52        (false, None) => put_i32(buf, -1),
53        (false, Some(n)) => put_i32(buf, int32_length(n)?),
54        (true, None) => put_uvarint(buf, 0),
55        (true, Some(n)) => put_uvarint(buf, compact_length(n, "array")?),
56    }
57    Ok(())
58}
59
60/// Number of bytes consumed by a non-nullable array-length prefix.
61#[must_use]
62pub fn array_len_prefix_len(n: usize, flexible: bool) -> usize {
63    if flexible {
64        usize_uvarint_len(n.saturating_add(1))
65    } else {
66        4
67    }
68}
69
70/// Number of bytes consumed by a nullable array-length prefix.
71#[must_use]
72pub fn nullable_array_len_prefix_len(len: Option<usize>, flexible: bool) -> usize {
73    match (flexible, len) {
74        (false, _) => 4,
75        (true, None) => uvarint_len(0),
76        (true, Some(n)) => usize_uvarint_len(n.saturating_add(1)),
77    }
78}
79
80/// Reads a non-nullable array length. Returns an error if the encoded value is
81/// null, which is −1 or 0.
82/// # Errors
83/// Returns the underlying protocol error when input is truncated, contains an invalid length or tag, or cannot be encoded for the selected version.
84pub fn get_array_len<B: Buf>(buf: &mut B, flexible: bool) -> Result<usize, ProtocolError> {
85    let n = if flexible {
86        let raw = get_uvarint(buf)?;
87        if raw == 0 {
88            return Err(ProtocolError::InvalidValue(
89                "non-nullable array was null (compact encoding)",
90            ));
91        }
92        (raw - 1) as usize
93    } else {
94        let Ok(n) = usize::try_from(get_i32(buf)?) else {
95            return Err(ProtocolError::InvalidValue(
96                "non-nullable array had negative length",
97            ));
98        };
99        n
100    };
101    // Every array element occupies at least one byte on the wire, so a
102    // legitimate array of `n` elements always has at least `n` bytes left in
103    // the buffer. Any larger `n` is impossible and indicates a malformed or
104    // hostile frame; reject before a caller can `Vec::with_capacity(n)`.
105    if n > buf.remaining() {
106        return Err(ProtocolError::InvalidValue(
107            "array length exceeds remaining buffer",
108        ));
109    }
110    Ok(n)
111}
112
113/// Reads a nullable array length. Returns `None` when the encoded value is
114/// null, which is −1 for non-flexible versions and 0 for flexible versions.
115/// # Errors
116/// Returns the underlying protocol error when input is truncated, contains an invalid length or tag, or cannot be encoded for the selected version.
117pub fn get_nullable_array_len<B: Buf>(
118    buf: &mut B,
119    flexible: bool,
120) -> Result<Option<usize>, ProtocolError> {
121    let n = if flexible {
122        let raw = get_uvarint(buf)?;
123        if raw == 0 {
124            return Ok(None);
125        }
126        (raw - 1) as usize
127    } else {
128        // A negative length is null.
129        let Ok(n) = usize::try_from(get_i32(buf)?) else {
130            return Ok(None);
131        };
132        n
133    };
134    // See `get_array_len`: a length larger than the remaining bytes cannot
135    // describe a real array and must be rejected before pre-allocation.
136    if n > buf.remaining() {
137        return Err(ProtocolError::InvalidValue(
138            "array length exceeds remaining buffer",
139        ));
140    }
141    Ok(Some(n))
142}
143
144#[cfg(test)]
145mod tests {
146
147    use bytes::BytesMut;
148
149    use super::*;
150
151    #[test]
152    fn array_roundtrip_cases() {
153        for (_case, len, flexible, expected_prefix) in [
154            ("non-flex empty", 0, false, &[0, 0, 0, 0][..]),
155            ("non-flex three", 3, false, &[0, 0, 0, 3][..]),
156            ("flex empty", 0, true, &[0x01][..]),
157            ("flex three", 3, true, &[0x04][..]),
158        ] {
159            let mut buf = BytesMut::new();
160            put_array_len(&mut buf, len, flexible).unwrap();
161            assert2::assert!(&buf[..] == expected_prefix);
162            buf.extend_from_slice(&vec![0; len]);
163            let mut cur = &buf[..];
164            assert2::assert!((get_array_len(&mut cur, flexible).unwrap(), cur.len()) == (len, len));
165        }
166    }
167
168    #[test]
169    fn nullable_array_roundtrip_cases() {
170        for (_case, len, flexible, expected_prefix) in [
171            ("non-flex null", None, false, &[0xFF, 0xFF, 0xFF, 0xFF][..]),
172            ("non-flex some", Some(3), false, &[0, 0, 0, 3][..]),
173            ("flex null", None, true, &[0x00][..]),
174            ("flex some", Some(3), true, &[0x04][..]),
175        ] {
176            let mut buf = BytesMut::new();
177            put_nullable_array_len(&mut buf, len, flexible).unwrap();
178            assert2::assert!(&buf[..] == expected_prefix);
179            let payload_len = len.unwrap_or(0);
180            buf.extend_from_slice(&vec![0; payload_len]);
181            let mut cur = &buf[..];
182            assert2::assert!(
183                (
184                    get_nullable_array_len(&mut cur, flexible).unwrap(),
185                    cur.len()
186                ) == (len, payload_len)
187            );
188        }
189    }
190
191    // --- prefix_len helpers -------------------------------------------------
192
193    #[test]
194    fn array_len_prefix_len_non_flex() {
195        for (_case, len) in [("empty", 0), ("populated", 100)] {
196            assert2::assert!(array_len_prefix_len(len, false) == 4);
197        }
198    }
199
200    #[test]
201    fn array_len_prefix_len_flex() {
202        // len=0 → varint(1) = 1 byte; len=126 → varint(127) = 1 byte;
203        // len=127 → varint(128) = 2 bytes.
204        for (len, want) in [(0, 1), (126, 1), (127, 2)] {
205            assert2::assert!(array_len_prefix_len(len, true) == want);
206        }
207    }
208
209    #[test]
210    fn nullable_prefix_len_non_flex_always_4() {
211        for (_case, len) in [("null", None), ("some", Some(3))] {
212            assert2::assert!(nullable_array_len_prefix_len(len, false) == 4);
213        }
214    }
215
216    #[test]
217    fn nullable_prefix_len_flex_null_is_1() {
218        // null → varint(0) = 1 byte
219        assert2::assert!(nullable_array_len_prefix_len(None, true) == 1);
220    }
221
222    // --- error cases --------------------------------------------------------
223
224    #[test]
225    fn non_nullable_rejects_null_cases() {
226        for (_case, bytes, flexible) in [
227            ("non-flex", &(-1i32).to_be_bytes()[..], false),
228            ("flex", &[0x00][..], true),
229        ] {
230            let mut cur = bytes;
231            assert2::assert!(matches!(
232                get_array_len(&mut cur, flexible),
233                Err(ProtocolError::InvalidValue(_))
234            ));
235        }
236    }
237
238    // --- pre-allocation DoS bound (length > remaining buffer) ----------------
239
240    #[test]
241    fn rejects_length_exceeding_remaining_cases() {
242        for (_case, nullable, flexible) in [
243            ("non-nullable non-flex", false, false),
244            ("non-nullable flex", false, true),
245            ("nullable non-flex", true, false),
246            ("nullable flex", true, true),
247        ] {
248            let mut buf = BytesMut::new();
249            if nullable {
250                put_nullable_array_len(&mut buf, Some(2_000_000_000), flexible).unwrap();
251            } else {
252                put_array_len(&mut buf, 2_000_000_000, flexible).unwrap();
253            }
254            let mut cur = &buf[..];
255            let result = if nullable {
256                get_nullable_array_len(&mut cur, flexible).map(|value| value.unwrap_or(0))
257            } else {
258                get_array_len(&mut cur, flexible)
259            };
260            assert2::assert!(matches!(
261                result,
262                Err(ProtocolError::InvalidValue(
263                    "array length exceeds remaining buffer"
264                ))
265            ));
266        }
267    }
268
269    #[test]
270    fn length_equal_to_remaining_is_accepted() {
271        for (_case, flexible) in [("non-flex", false), ("flex", true)] {
272            let mut buf = BytesMut::new();
273            put_array_len(&mut buf, 5, flexible).unwrap();
274            buf.extend_from_slice(&[0u8; 5]);
275            let mut cur = &buf[..];
276            assert2::assert!(get_array_len(&mut cur, flexible).unwrap() == 5);
277        }
278    }
279}