Skip to main content

deepslate_protocol/
varint.rs

1//! Minecraft `VarInt` encoding and decoding.
2//!
3//! `VarInt`s are variable-length 32-bit integers used throughout the Minecraft
4//! protocol. Each byte uses 7 bits for data and 1 bit (MSB) as a continuation
5//! flag. Values are encoded in little-endian order.
6
7use bytes::{Buf, BufMut};
8
9use crate::types::ProtocolError;
10
11/// Maximum number of bytes a `VarInt` can occupy.
12const MAX_VARINT_BYTES: usize = 5;
13
14/// Precomputed table for O(1) `VarInt` byte length lookup.
15/// Index by `leading_zeros` of the value (0..=32).
16const VAR_INT_LENGTHS: [u8; 33] = [
17    5, 5, 5, 5, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1,
18    1,
19];
20
21/// Returns the number of bytes needed to encode a `VarInt` value.
22#[must_use]
23#[allow(clippy::cast_sign_loss)]
24pub const fn var_int_bytes(value: i32) -> usize {
25    // Treat as unsigned for length calculation
26    let value = value as u32;
27    VAR_INT_LENGTHS[value.leading_zeros() as usize] as usize
28}
29
30/// Read a `VarInt` from a `Buf`, returning the decoded i32.
31///
32/// # Errors
33///
34/// Returns `ProtocolError::VarIntTooLong` if the `VarInt` exceeds 5 bytes.
35/// Returns `ProtocolError::UnexpectedEof` if the buffer runs out.
36pub fn read_var_int(buf: &mut impl Buf) -> Result<i32, ProtocolError> {
37    let mut value: i32 = 0;
38    let mut position: u32 = 0;
39
40    loop {
41        if !buf.has_remaining() {
42            return Err(ProtocolError::UnexpectedEof);
43        }
44
45        let byte = buf.get_u8();
46        value |= i32::from(byte & 0x7F) << position;
47
48        if byte & 0x80 == 0 {
49            return Ok(value);
50        }
51
52        position += 7;
53        if position >= 32 {
54            return Err(ProtocolError::VarIntTooLong);
55        }
56    }
57}
58
59/// Write a `VarInt` to a `BufMut`.
60#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
61pub fn write_var_int(buf: &mut impl BufMut, value: i32) {
62    // Treat as unsigned for encoding
63    let mut unsigned = value as u32;
64
65    // Fast path: single byte (values 0..128)
66    if unsigned < 0x80 {
67        buf.put_u8(unsigned as u8);
68        return;
69    }
70
71    // Fast path: two bytes (values 128..16384)
72    if unsigned < 0x4000 {
73        let encoded = ((unsigned & 0x7F | 0x80) << 8) | (unsigned >> 7);
74        buf.put_u16(encoded as u16);
75        return;
76    }
77
78    // General case
79    loop {
80        #[allow(clippy::cast_possible_truncation)]
81        if unsigned & !0x7F == 0 {
82            buf.put_u8(unsigned as u8);
83            return;
84        }
85        buf.put_u8((unsigned & 0x7F | 0x80) as u8);
86        unsigned >>= 7;
87    }
88}
89
90/// Try to read a `VarInt` from a byte slice without consuming it.
91/// Returns `Ok(Some((value, bytes_read)))` on success,
92/// `Ok(None)` if not enough data is available,
93/// or `Err` if the `VarInt` is malformed.
94///
95/// # Errors
96///
97/// Returns `ProtocolError::VarIntTooLong` if the `VarInt` exceeds 5 bytes.
98pub fn peek_var_int(data: &[u8]) -> Result<Option<(i32, usize)>, ProtocolError> {
99    let mut value: i32 = 0;
100    let mut position: u32 = 0;
101
102    for (i, &byte) in data.iter().enumerate() {
103        if i >= MAX_VARINT_BYTES {
104            return Err(ProtocolError::VarIntTooLong);
105        }
106
107        value |= i32::from(byte & 0x7F) << position;
108
109        if byte & 0x80 == 0 {
110            return Ok(Some((value, i + 1)));
111        }
112
113        position += 7;
114    }
115
116    // Not enough data
117    Ok(None)
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use proptest::prelude::*;
124
125    proptest! {
126        #[test]
127        fn roundtrip_any_i32(v in any::<i32>()) {
128            let mut buf = Vec::new();
129            write_var_int(&mut buf, v);
130            let result = read_var_int(&mut &buf[..]).unwrap();
131            prop_assert_eq!(result, v);
132        }
133
134        #[test]
135        fn var_int_bytes_matches_encoded_length(v in any::<i32>()) {
136            let mut buf = Vec::new();
137            write_var_int(&mut buf, v);
138            prop_assert_eq!(var_int_bytes(v), buf.len());
139        }
140
141        #[test]
142        fn peek_agrees_with_read(v in any::<i32>()) {
143            let mut buf = Vec::new();
144            write_var_int(&mut buf, v);
145            let peeked = peek_var_int(&buf).unwrap().unwrap();
146            let read = read_var_int(&mut &buf[..]).unwrap();
147            prop_assert_eq!(peeked.0, read);
148            prop_assert_eq!(peeked.1, buf.len());
149        }
150    }
151
152    #[test]
153    fn test_roundtrip_zero() {
154        let mut buf = Vec::new();
155        write_var_int(&mut buf, 0);
156        assert_eq!(buf, vec![0x00]);
157        assert_eq!(read_var_int(&mut &buf[..]).unwrap(), 0);
158    }
159
160    #[test]
161    fn test_roundtrip_one() {
162        let mut buf = Vec::new();
163        write_var_int(&mut buf, 1);
164        assert_eq!(buf, vec![0x01]);
165        assert_eq!(read_var_int(&mut &buf[..]).unwrap(), 1);
166    }
167
168    #[test]
169    fn test_roundtrip_127() {
170        let mut buf = Vec::new();
171        write_var_int(&mut buf, 127);
172        assert_eq!(buf, vec![0x7F]);
173        assert_eq!(read_var_int(&mut &buf[..]).unwrap(), 127);
174    }
175
176    #[test]
177    fn test_roundtrip_128() {
178        let mut buf = Vec::new();
179        write_var_int(&mut buf, 128);
180        assert_eq!(buf, vec![0x80, 0x01]);
181        assert_eq!(read_var_int(&mut &buf[..]).unwrap(), 128);
182    }
183
184    #[test]
185    fn test_roundtrip_255() {
186        let mut buf = Vec::new();
187        write_var_int(&mut buf, 255);
188        assert_eq!(buf, vec![0xFF, 0x01]);
189        assert_eq!(read_var_int(&mut &buf[..]).unwrap(), 255);
190    }
191
192    #[test]
193    fn test_roundtrip_25565() {
194        let mut buf = Vec::new();
195        write_var_int(&mut buf, 25565);
196        assert_eq!(buf, vec![0xDD, 0xC7, 0x01]);
197        assert_eq!(read_var_int(&mut &buf[..]).unwrap(), 25565);
198    }
199
200    #[test]
201    fn test_roundtrip_max() {
202        let mut buf = Vec::new();
203        write_var_int(&mut buf, i32::MAX);
204        assert_eq!(read_var_int(&mut &buf[..]).unwrap(), i32::MAX);
205    }
206
207    #[test]
208    fn test_roundtrip_negative_one() {
209        let mut buf = Vec::new();
210        write_var_int(&mut buf, -1);
211        assert_eq!(buf, vec![0xFF, 0xFF, 0xFF, 0xFF, 0x0F]);
212        assert_eq!(read_var_int(&mut &buf[..]).unwrap(), -1);
213    }
214
215    #[test]
216    fn test_var_int_bytes() {
217        assert_eq!(var_int_bytes(0), 1);
218        assert_eq!(var_int_bytes(1), 1);
219        assert_eq!(var_int_bytes(127), 1);
220        assert_eq!(var_int_bytes(128), 2);
221        assert_eq!(var_int_bytes(16383), 2);
222        assert_eq!(var_int_bytes(16384), 3);
223        assert_eq!(var_int_bytes(-1), 5);
224    }
225
226    #[test]
227    fn test_peek_var_int() {
228        let data = [0xDD, 0xC7, 0x01, 0xFF];
229        let result = peek_var_int(&data).unwrap().unwrap();
230        assert_eq!(result, (25565, 3));
231
232        // Incomplete
233        let data = [0x80];
234        assert!(peek_var_int(&data).unwrap().is_none());
235
236        // Empty
237        assert!(peek_var_int(&[]).unwrap().is_none());
238    }
239}