deepslate-protocol 0.1.0

Minecraft protocol primitives for the Deepslate proxy.
Documentation
//! Minecraft `VarInt` encoding and decoding.
//!
//! `VarInt`s are variable-length 32-bit integers used throughout the Minecraft
//! protocol. Each byte uses 7 bits for data and 1 bit (MSB) as a continuation
//! flag. Values are encoded in little-endian order.

use bytes::{Buf, BufMut};

use crate::types::ProtocolError;

/// Maximum number of bytes a `VarInt` can occupy.
const MAX_VARINT_BYTES: usize = 5;

/// Precomputed table for O(1) `VarInt` byte length lookup.
/// Index by `leading_zeros` of the value (0..=32).
const VAR_INT_LENGTHS: [u8; 33] = [
    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,
    1,
];

/// Returns the number of bytes needed to encode a `VarInt` value.
#[must_use]
#[allow(clippy::cast_sign_loss)]
pub const fn var_int_bytes(value: i32) -> usize {
    // Treat as unsigned for length calculation
    let value = value as u32;
    VAR_INT_LENGTHS[value.leading_zeros() as usize] as usize
}

/// Read a `VarInt` from a `Buf`, returning the decoded i32.
///
/// # Errors
///
/// Returns `ProtocolError::VarIntTooLong` if the `VarInt` exceeds 5 bytes.
/// Returns `ProtocolError::UnexpectedEof` if the buffer runs out.
pub fn read_var_int(buf: &mut impl Buf) -> Result<i32, ProtocolError> {
    let mut value: i32 = 0;
    let mut position: u32 = 0;

    loop {
        if !buf.has_remaining() {
            return Err(ProtocolError::UnexpectedEof);
        }

        let byte = buf.get_u8();
        value |= i32::from(byte & 0x7F) << position;

        if byte & 0x80 == 0 {
            return Ok(value);
        }

        position += 7;
        if position >= 32 {
            return Err(ProtocolError::VarIntTooLong);
        }
    }
}

/// Write a `VarInt` to a `BufMut`.
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
pub fn write_var_int(buf: &mut impl BufMut, value: i32) {
    // Treat as unsigned for encoding
    let mut unsigned = value as u32;

    // Fast path: single byte (values 0..128)
    if unsigned < 0x80 {
        buf.put_u8(unsigned as u8);
        return;
    }

    // Fast path: two bytes (values 128..16384)
    if unsigned < 0x4000 {
        let encoded = ((unsigned & 0x7F | 0x80) << 8) | (unsigned >> 7);
        buf.put_u16(encoded as u16);
        return;
    }

    // General case
    loop {
        #[allow(clippy::cast_possible_truncation)]
        if unsigned & !0x7F == 0 {
            buf.put_u8(unsigned as u8);
            return;
        }
        buf.put_u8((unsigned & 0x7F | 0x80) as u8);
        unsigned >>= 7;
    }
}

/// Try to read a `VarInt` from a byte slice without consuming it.
/// Returns `Ok(Some((value, bytes_read)))` on success,
/// `Ok(None)` if not enough data is available,
/// or `Err` if the `VarInt` is malformed.
///
/// # Errors
///
/// Returns `ProtocolError::VarIntTooLong` if the `VarInt` exceeds 5 bytes.
pub fn peek_var_int(data: &[u8]) -> Result<Option<(i32, usize)>, ProtocolError> {
    let mut value: i32 = 0;
    let mut position: u32 = 0;

    for (i, &byte) in data.iter().enumerate() {
        if i >= MAX_VARINT_BYTES {
            return Err(ProtocolError::VarIntTooLong);
        }

        value |= i32::from(byte & 0x7F) << position;

        if byte & 0x80 == 0 {
            return Ok(Some((value, i + 1)));
        }

        position += 7;
    }

    // Not enough data
    Ok(None)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_roundtrip_zero() {
        let mut buf = Vec::new();
        write_var_int(&mut buf, 0);
        assert_eq!(buf, vec![0x00]);
        assert_eq!(read_var_int(&mut &buf[..]).unwrap(), 0);
    }

    #[test]
    fn test_roundtrip_one() {
        let mut buf = Vec::new();
        write_var_int(&mut buf, 1);
        assert_eq!(buf, vec![0x01]);
        assert_eq!(read_var_int(&mut &buf[..]).unwrap(), 1);
    }

    #[test]
    fn test_roundtrip_127() {
        let mut buf = Vec::new();
        write_var_int(&mut buf, 127);
        assert_eq!(buf, vec![0x7F]);
        assert_eq!(read_var_int(&mut &buf[..]).unwrap(), 127);
    }

    #[test]
    fn test_roundtrip_128() {
        let mut buf = Vec::new();
        write_var_int(&mut buf, 128);
        assert_eq!(buf, vec![0x80, 0x01]);
        assert_eq!(read_var_int(&mut &buf[..]).unwrap(), 128);
    }

    #[test]
    fn test_roundtrip_255() {
        let mut buf = Vec::new();
        write_var_int(&mut buf, 255);
        assert_eq!(buf, vec![0xFF, 0x01]);
        assert_eq!(read_var_int(&mut &buf[..]).unwrap(), 255);
    }

    #[test]
    fn test_roundtrip_25565() {
        let mut buf = Vec::new();
        write_var_int(&mut buf, 25565);
        assert_eq!(buf, vec![0xDD, 0xC7, 0x01]);
        assert_eq!(read_var_int(&mut &buf[..]).unwrap(), 25565);
    }

    #[test]
    fn test_roundtrip_max() {
        let mut buf = Vec::new();
        write_var_int(&mut buf, i32::MAX);
        assert_eq!(read_var_int(&mut &buf[..]).unwrap(), i32::MAX);
    }

    #[test]
    fn test_roundtrip_negative_one() {
        let mut buf = Vec::new();
        write_var_int(&mut buf, -1);
        assert_eq!(buf, vec![0xFF, 0xFF, 0xFF, 0xFF, 0x0F]);
        assert_eq!(read_var_int(&mut &buf[..]).unwrap(), -1);
    }

    #[test]
    fn test_var_int_bytes() {
        assert_eq!(var_int_bytes(0), 1);
        assert_eq!(var_int_bytes(1), 1);
        assert_eq!(var_int_bytes(127), 1);
        assert_eq!(var_int_bytes(128), 2);
        assert_eq!(var_int_bytes(16383), 2);
        assert_eq!(var_int_bytes(16384), 3);
        assert_eq!(var_int_bytes(-1), 5);
    }

    #[test]
    fn test_peek_var_int() {
        let data = [0xDD, 0xC7, 0x01, 0xFF];
        let result = peek_var_int(&data).unwrap().unwrap();
        assert_eq!(result, (25565, 3));

        // Incomplete
        let data = [0x80];
        assert!(peek_var_int(&data).unwrap().is_none());

        // Empty
        assert!(peek_var_int(&[]).unwrap().is_none());
    }
}