Skip to main content

paralock_core/
encoding.rs

1//! Byte encodings shared by the whole crate: hex (with or without `0x`), Bitcoin-style CompactSize lengths and a
2//! bounds-checked little-endian [`Reader`].
3//!
4//! Everything here is total: malformed input produces an [`EncodingError`], never a panic or an out-of-bounds index.
5
6use core::fmt;
7
8/// Largest CompactSize value accepted when reading (zcashd's `MAX_SIZE`, 32 MiB). Larger lengths cannot describe a
9/// valid transaction field and are rejected before any allocation.
10pub const MAX_COMPACT_SIZE: u64 = 0x0200_0000;
11
12/// A malformed encoding.
13#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
14#[non_exhaustive]
15pub enum EncodingError {
16    /// The string is not valid hex (odd length or a non-hex character).
17    #[error("invalid hex")]
18    InvalidHex,
19    /// A value has the wrong length.
20    #[error("{what} must be {expected} bytes, got {actual}")]
21    InvalidLength {
22        /// What was being decoded.
23        what: &'static str,
24        /// Required length in bytes.
25        expected: usize,
26        /// Actual length in bytes.
27        actual: usize,
28    },
29    /// The input ended before the value being read.
30    #[error("unexpected end of data")]
31    UnexpectedEnd,
32    /// A CompactSize was not minimally encoded (consensus rejects these).
33    #[error("non-canonical compactSize: {value} encoded in {len} bytes")]
34    NonCanonicalCompactSize {
35        /// The value read.
36        value: u64,
37        /// Bytes used (3 or 5).
38        len: usize,
39    },
40    /// A CompactSize exceeded [`MAX_COMPACT_SIZE`].
41    #[error("CompactSize too large")]
42    CompactSizeTooLarge,
43    /// A CompactSize used the 64-bit (`0xff`) form.
44    #[error("compactSize > 32 bits not supported")]
45    CompactSize64,
46}
47
48/// Decodes hex, with or without a `0x` prefix (either case of hex digits).
49pub fn decode_hex(s: &str) -> Result<Vec<u8>, EncodingError> {
50    let digits = s.strip_prefix("0x").unwrap_or(s);
51    hex::decode(digits).map_err(|_| EncodingError::InvalidHex)
52}
53
54/// Decodes hex (with or without `0x`) into exactly `N` bytes.
55pub fn decode_hex_array<const N: usize>(s: &str, what: &'static str) -> Result<[u8; N], EncodingError> {
56    let bytes = decode_hex(s)?;
57    to_array(&bytes, what)
58}
59
60/// Copies a slice into an `N`-byte array, or reports the length mismatch.
61pub fn to_array<const N: usize>(bytes: &[u8], what: &'static str) -> Result<[u8; N], EncodingError> {
62    <[u8; N]>::try_from(bytes).map_err(|_| EncodingError::InvalidLength { what, expected: N, actual: bytes.len() })
63}
64
65/// Lowercase hex without a prefix.
66pub fn to_hex(bytes: &[u8]) -> String {
67    hex::encode(bytes)
68}
69
70/// Lowercase hex with a `0x` prefix (the TypeScript `Hex` type).
71pub fn to_hex_0x(bytes: &[u8]) -> String {
72    let mut out = String::with_capacity(2 + bytes.len() * 2);
73    out.push_str("0x");
74    out.push_str(&hex::encode(bytes));
75    out
76}
77
78/// Appends a CompactSize length prefix (little-endian, minimal).
79pub fn write_compact_size(out: &mut Vec<u8>, n: u64) {
80    if n < 0xfd {
81        out.push(n as u8);
82    } else if n <= 0xffff {
83        out.push(0xfd);
84        out.extend_from_slice(&(n as u16).to_le_bytes());
85    } else if n <= 0xffff_ffff {
86        out.push(0xfe);
87        out.extend_from_slice(&(n as u32).to_le_bytes());
88    } else {
89        out.push(0xff);
90        out.extend_from_slice(&n.to_le_bytes());
91    }
92}
93
94/// The CompactSize encoding of `n`.
95pub fn compact_size(n: u64) -> Vec<u8> {
96    let mut out = Vec::with_capacity(9);
97    write_compact_size(&mut out, n);
98    out
99}
100
101/// Number of bytes of the CompactSize encoding of `n`.
102pub const fn compact_size_len(n: u64) -> usize {
103    if n < 0xfd {
104        1
105    } else if n <= 0xffff {
106        3
107    } else if n <= 0xffff_ffff {
108        5
109    } else {
110        9
111    }
112}
113
114/// Appends `data` prefixed with its CompactSize length.
115pub fn write_var_bytes(out: &mut Vec<u8>, data: &[u8]) {
116    write_compact_size(out, data.len() as u64);
117    out.extend_from_slice(data);
118}
119
120/// Reads little-endian integers and CompactSize-prefixed fields from a byte slice. Every read is bounds-checked.
121#[derive(Clone)]
122pub struct Reader<'a> {
123    bytes: &'a [u8],
124    pos: usize,
125}
126
127impl fmt::Debug for Reader<'_> {
128    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129        f.debug_struct("Reader").field("len", &self.bytes.len()).field("pos", &self.pos).finish()
130    }
131}
132
133impl<'a> Reader<'a> {
134    /// A reader positioned at the start of `bytes`.
135    pub const fn new(bytes: &'a [u8]) -> Self {
136        Self { bytes, pos: 0 }
137    }
138
139    /// Current offset.
140    pub const fn position(&self) -> usize {
141        self.pos
142    }
143
144    /// Bytes not read yet.
145    pub fn remaining(&self) -> usize {
146        self.bytes.len().saturating_sub(self.pos)
147    }
148
149    /// True once every byte has been read.
150    pub fn is_done(&self) -> bool {
151        self.remaining() == 0
152    }
153
154    /// The unread bytes (without consuming them).
155    pub fn rest(&self) -> &'a [u8] {
156        self.bytes.get(self.pos..).unwrap_or(&[])
157    }
158
159    /// The next `n` bytes.
160    pub fn take(&mut self, n: usize) -> Result<&'a [u8], EncodingError> {
161        let end = self.pos.checked_add(n).ok_or(EncodingError::UnexpectedEnd)?;
162        let out = self.bytes.get(self.pos..end).ok_or(EncodingError::UnexpectedEnd)?;
163        self.pos = end;
164        Ok(out)
165    }
166
167    /// The next `N` bytes as an array.
168    pub fn array<const N: usize>(&mut self) -> Result<[u8; N], EncodingError> {
169        let bytes = self.take(N)?;
170        <[u8; N]>::try_from(bytes).map_err(|_| EncodingError::UnexpectedEnd)
171    }
172
173    /// One byte.
174    pub fn u8(&mut self) -> Result<u8, EncodingError> {
175        Ok(self.array::<1>()?[0])
176    }
177
178    /// 2-byte little-endian unsigned integer.
179    pub fn u16(&mut self) -> Result<u16, EncodingError> {
180        Ok(u16::from_le_bytes(self.array()?))
181    }
182
183    /// 4-byte little-endian unsigned integer.
184    pub fn u32(&mut self) -> Result<u32, EncodingError> {
185        Ok(u32::from_le_bytes(self.array()?))
186    }
187
188    /// 8-byte little-endian signed integer.
189    pub fn i64(&mut self) -> Result<i64, EncodingError> {
190        Ok(i64::from_le_bytes(self.array()?))
191    }
192
193    /// A minimally encoded CompactSize of at most [`MAX_COMPACT_SIZE`].
194    pub fn compact_size(&mut self) -> Result<u64, EncodingError> {
195        let first = self.u8()?;
196        let n = match first {
197            0..=0xfc => u64::from(first),
198            0xfd => {
199                let n = u64::from(self.u16()?);
200                if n < 0xfd {
201                    return Err(EncodingError::NonCanonicalCompactSize { value: n, len: 3 });
202                }
203                n
204            }
205            0xfe => {
206                let n = u64::from(self.u32()?);
207                if n <= 0xffff {
208                    return Err(EncodingError::NonCanonicalCompactSize { value: n, len: 5 });
209                }
210                n
211            }
212            0xff => return Err(EncodingError::CompactSize64),
213        };
214        if n > MAX_COMPACT_SIZE {
215            return Err(EncodingError::CompactSizeTooLarge);
216        }
217        Ok(n)
218    }
219
220    /// A CompactSize used as a length or count, checked against the bytes left (each item at least `min_item_size`
221    /// bytes), so a hostile count can't trigger a huge allocation.
222    pub fn count(&mut self, min_item_size: usize) -> Result<usize, EncodingError> {
223        let n = usize::try_from(self.compact_size()?).map_err(|_| EncodingError::CompactSizeTooLarge)?;
224        let needed = n.checked_mul(min_item_size.max(1)).ok_or(EncodingError::UnexpectedEnd)?;
225        if needed > self.remaining() {
226            return Err(EncodingError::UnexpectedEnd);
227        }
228        Ok(n)
229    }
230
231    /// A CompactSize-prefixed byte string.
232    pub fn var_bytes(&mut self) -> Result<&'a [u8], EncodingError> {
233        let n = self.count(1)?;
234        self.take(n)
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
241    use super::*;
242
243    #[test]
244    fn compact_size_round_trips_little_endian() {
245        assert_eq!(to_hex(&compact_size(0x1234)), "fd3412");
246        assert_eq!(Reader::new(&compact_size(0x1234)).compact_size().unwrap(), 0x1234);
247        assert_eq!(Reader::new(&compact_size(70_000)).compact_size().unwrap(), 70_000);
248        assert_eq!(Reader::new(&[0xfd, 1]).compact_size(), Err(EncodingError::UnexpectedEnd));
249        for n in [0u64, 1, 0xfc, 0xfd, 0xffff, 0x1_0000, MAX_COMPACT_SIZE] {
250            let enc = compact_size(n);
251            assert_eq!(enc.len(), compact_size_len(n));
252            assert_eq!(Reader::new(&enc).compact_size().unwrap(), n);
253        }
254    }
255
256    #[test]
257    fn compact_size_rejects_non_canonical_and_huge() {
258        assert_eq!(
259            Reader::new(&[0xfd, 0x01, 0x00]).compact_size(),
260            Err(EncodingError::NonCanonicalCompactSize { value: 1, len: 3 })
261        );
262        assert_eq!(
263            Reader::new(&[0xfe, 0xff, 0xff, 0, 0]).compact_size(),
264            Err(EncodingError::NonCanonicalCompactSize { value: 0xffff, len: 5 })
265        );
266        assert_eq!(Reader::new(&[0xff, 0, 0, 0, 0, 1, 0, 0, 0]).compact_size(), Err(EncodingError::CompactSize64));
267        assert_eq!(
268            Reader::new(&compact_size(MAX_COMPACT_SIZE + 1)).compact_size(),
269            Err(EncodingError::CompactSizeTooLarge)
270        );
271    }
272
273    #[test]
274    fn counts_are_bounded_by_the_remaining_bytes() {
275        let mut data = compact_size(1000);
276        data.extend_from_slice(&[0u8; 10]);
277        assert_eq!(Reader::new(&data).count(1), Err(EncodingError::UnexpectedEnd));
278    }
279
280    #[test]
281    fn hex_accepts_an_optional_prefix() {
282        assert_eq!(decode_hex("0xABcd").unwrap(), vec![0xab, 0xcd]);
283        assert_eq!(decode_hex("abcd").unwrap(), vec![0xab, 0xcd]);
284        assert_eq!(decode_hex("abc"), Err(EncodingError::InvalidHex));
285        assert_eq!(decode_hex("zz"), Err(EncodingError::InvalidHex));
286        assert_eq!(to_hex_0x(&[1, 2]), "0x0102");
287    }
288}