umsh_ulcp/
pui.rs

1//! Packed Unsigned Integers (PUIs).
2//!
3//! Little-endian base-128 encoding with a continuation bit, capped at
4//! three bytes. See "Packed Unsigned Integers" in the minimal
5//! ULCP spec.
6
7/// Largest value encodable in the three-byte PUI limit.
8pub const MAX_VALUE: u32 = 2_097_151;
9
10/// Largest encoded size of a PUI.
11pub const MAX_LEN: usize = 3;
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub enum Error {
15    /// The value exceeds [`MAX_VALUE`].
16    ValueTooLarge,
17    /// The output buffer cannot hold the encoded value.
18    BufferTooSmall,
19    /// The input ended before the final (continuation-clear) byte.
20    Truncated,
21    /// A third byte carried a continuation bit, exceeding the
22    /// three-byte limit.
23    TooLong,
24}
25
26/// Return the encoded size of `value` in bytes.
27pub const fn encoded_len(value: u32) -> usize {
28    if value < 1 << 7 {
29        1
30    } else if value < 1 << 14 {
31        2
32    } else {
33        3
34    }
35}
36
37/// Encode `value` into `out`, returning the number of bytes written.
38pub fn encode(value: u32, out: &mut [u8]) -> Result<usize, Error> {
39    if value > MAX_VALUE {
40        return Err(Error::ValueTooLarge);
41    }
42    let len = encoded_len(value);
43    if out.len() < len {
44        return Err(Error::BufferTooSmall);
45    }
46    let mut remaining = value;
47    for byte in out.iter_mut().take(len - 1) {
48        *byte = (remaining as u8 & 0x7F) | 0x80;
49        remaining >>= 7;
50    }
51    out[len - 1] = remaining as u8;
52    Ok(len)
53}
54
55/// Decode a PUI from the start of `input`.
56///
57/// Returns the value and the number of bytes consumed.
58pub fn decode(input: &[u8]) -> Result<(u32, usize), Error> {
59    let mut value = 0u32;
60    for (index, &byte) in input.iter().enumerate().take(MAX_LEN) {
61        value |= u32::from(byte & 0x7F) << (7 * index);
62        if byte & 0x80 == 0 {
63            return Ok((value, index + 1));
64        }
65        if index + 1 == MAX_LEN {
66            return Err(Error::TooLong);
67        }
68    }
69    Err(Error::Truncated)
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    #[track_caller]
77    fn round_trip(value: u32, expected: &[u8]) {
78        let mut buf = [0u8; MAX_LEN];
79        let len = encode(value, &mut buf).unwrap();
80        assert_eq!(&buf[..len], expected, "encoding of {value}");
81        assert_eq!(decode(expected).unwrap(), (value, expected.len()));
82    }
83
84    #[test]
85    fn spec_example() {
86        // Worked example from the spec: 1337 => [B9 0A].
87        round_trip(1337, &[0xB9, 0x0A]);
88    }
89
90    #[test]
91    fn boundaries() {
92        round_trip(0, &[0x00]);
93        round_trip(127, &[0x7F]);
94        round_trip(128, &[0x80, 0x01]);
95        round_trip(16_383, &[0xFF, 0x7F]);
96        round_trip(16_384, &[0x80, 0x80, 0x01]);
97        round_trip(MAX_VALUE, &[0xFF, 0xFF, 0x7F]);
98    }
99
100    #[test]
101    fn known_identifiers() {
102        // Property ids used by the minimal spec.
103        round_trip(113, &[0x71]);
104        round_trip(4820, &[0xD4, 0x25]);
105        round_trip(4822, &[0xD6, 0x25]);
106    }
107
108    #[test]
109    fn errors() {
110        let mut buf = [0u8; MAX_LEN];
111        assert_eq!(encode(MAX_VALUE + 1, &mut buf), Err(Error::ValueTooLarge));
112        assert_eq!(encode(128, &mut buf[..1]), Err(Error::BufferTooSmall));
113        assert_eq!(decode(&[]), Err(Error::Truncated));
114        assert_eq!(decode(&[0x80]), Err(Error::Truncated));
115        assert_eq!(decode(&[0x80, 0x80]), Err(Error::Truncated));
116        assert_eq!(decode(&[0x80, 0x80, 0x80]), Err(Error::TooLong));
117    }
118
119    #[test]
120    fn trailing_bytes_ignored() {
121        assert_eq!(decode(&[0x7F, 0xAA, 0xBB]).unwrap(), (127, 1));
122    }
123}