kojacoord-protocol 0.1.0

Minecraft protocol types, packet (de)serialization and version registry for the Kojacoord proxy.
Documentation
use bytes::{Buf, BufMut, Bytes, BytesMut};

use crate::codec::{Decode, Encode, PacketId};
use crate::error::ProtocolError;
use crate::types::VarInt;

fn encode_string(s: &str, dst: &mut BytesMut) -> Result<(), ProtocolError> {
    let bytes = s.as_bytes();
    VarInt(bytes.len() as i32).encode(dst)?;
    dst.put_slice(bytes);
    Ok(())
}

fn decode_string(src: &mut Bytes) -> Result<String, ProtocolError> {
    let len = VarInt::decode(src)?.0 as usize;
    if src.remaining() < len {
        return Err(ProtocolError::Io(std::io::Error::new(
            std::io::ErrorKind::UnexpectedEof,
            "Missing bytes for string",
        )));
    }
    let mut buf = vec![0u8; len];
    src.copy_to_slice(&mut buf);
    String::from_utf8(buf).map_err(|_| {
        ProtocolError::Io(std::io::Error::new(
            std::io::ErrorKind::InvalidData,
            "Invalid UTF-8 in string",
        ))
    })
}

fn encode_byte_array(data: &[u8], dst: &mut BytesMut) -> Result<(), ProtocolError> {
    VarInt(data.len() as i32).encode(dst)?;
    dst.put_slice(data);
    Ok(())
}

fn decode_byte_array(src: &mut Bytes) -> Result<Vec<u8>, ProtocolError> {
    let len = VarInt::decode(src)?.0 as usize;
    if src.remaining() < len {
        return Err(ProtocolError::Io(std::io::Error::new(
            std::io::ErrorKind::UnexpectedEof,
            "Missing bytes for byte array",
        )));
    }
    let mut buf = vec![0u8; len];
    src.copy_to_slice(&mut buf);
    Ok(buf)
}

#[derive(Debug, Clone, PartialEq)]
pub struct ServerboundLoginStart {
    pub username: String,
}

impl PacketId for ServerboundLoginStart {
    fn packet_id(_ver: u32) -> u8 {
        0x00
    }
}

impl Encode for ServerboundLoginStart {
    fn encode(&self, dst: &mut BytesMut) -> Result<(), ProtocolError> {
        encode_string(&self.username, dst)
    }
}

impl Decode for ServerboundLoginStart {
    fn decode(src: &mut Bytes) -> Result<Self, ProtocolError> {
        Ok(Self {
            username: decode_string(src)?,
        })
    }
}

#[derive(Debug, Clone, PartialEq)]
pub struct ClientboundLoginDisconnect {
    pub reason: String,
}

impl PacketId for ClientboundLoginDisconnect {
    fn packet_id(_ver: u32) -> u8 {
        0x00
    }
}

impl Encode for ClientboundLoginDisconnect {
    fn encode(&self, dst: &mut BytesMut) -> Result<(), ProtocolError> {
        encode_string(&self.reason, dst)
    }
}

impl Decode for ClientboundLoginDisconnect {
    fn decode(src: &mut Bytes) -> Result<Self, ProtocolError> {
        Ok(Self {
            reason: decode_string(src)?,
        })
    }
}

#[derive(Debug, Clone, PartialEq)]
pub struct ClientboundEncryptionRequest {
    pub server_id: String,
    pub public_key: Vec<u8>,
    pub verify_token: Vec<u8>,
    pub should_authenticate: bool,
}

impl PacketId for ClientboundEncryptionRequest {
    fn packet_id(_ver: u32) -> u8 {
        0x01
    }
}

impl Encode for ClientboundEncryptionRequest {
    fn encode(&self, dst: &mut BytesMut) -> Result<(), ProtocolError> {
        encode_string(&self.server_id, dst)?;
        encode_byte_array(&self.public_key, dst)?;
        encode_byte_array(&self.verify_token, dst)?;
        dst.put_u8(self.should_authenticate as u8);
        Ok(())
    }
}

impl Decode for ClientboundEncryptionRequest {
    fn decode(src: &mut Bytes) -> Result<Self, ProtocolError> {
        let server_id = decode_string(src)?;
        let public_key = decode_byte_array(src)?;
        let verify_token = decode_byte_array(src)?;
        let should_authenticate = src.get_u8() != 0;
        Ok(Self {
            server_id,
            public_key,
            verify_token,
            should_authenticate,
        })
    }
}

#[derive(Debug, Clone, PartialEq)]
pub struct ProfileProperty {
    pub name: String,
    pub value: String,
    pub signature: Option<String>,
}

impl Encode for ProfileProperty {
    fn encode(&self, dst: &mut BytesMut) -> Result<(), ProtocolError> {
        encode_string(&self.name, dst)?;
        encode_string(&self.value, dst)?;

        if let Some(sig) = &self.signature {
            dst.put_u8(1);
            encode_string(sig, dst)?;
        } else {
            dst.put_u8(0);
        }
        Ok(())
    }
}

impl Decode for ProfileProperty {
    fn decode(src: &mut Bytes) -> Result<Self, ProtocolError> {
        let name = decode_string(src)?;
        let value = decode_string(src)?;
        if src.remaining() < 1 {
            return Err(ProtocolError::Io(std::io::Error::new(
                std::io::ErrorKind::UnexpectedEof,
                "Missing boolean flag for ProfileProperty signature",
            )));
        }
        let signature = if src.get_u8() != 0 {
            Some(decode_string(src)?)
        } else {
            None
        };
        Ok(Self {
            name,
            value,
            signature,
        })
    }
}

#[derive(Debug, Clone, PartialEq)]
pub struct ClientboundLoginSuccess {
    pub uuid: uuid::Uuid,
    pub username: String,
    pub properties: Vec<ProfileProperty>,
}

impl PacketId for ClientboundLoginSuccess {
    fn packet_id(_ver: u32) -> u8 {
        0x02
    }
}

impl Encode for ClientboundLoginSuccess {
    fn encode(&self, dst: &mut BytesMut) -> Result<(), ProtocolError> {
        let (hi, lo) = self.uuid.as_u64_pair();
        dst.put_i64(hi as i64);
        dst.put_i64(lo as i64);
        encode_string(&self.username, dst)?;
        VarInt(self.properties.len() as i32).encode(dst)?;
        for prop in &self.properties {
            prop.encode(dst)?;
        }
        Ok(())
    }
}

impl Decode for ClientboundLoginSuccess {
    fn decode(src: &mut Bytes) -> Result<Self, ProtocolError> {
        if src.remaining() < 16 {
            return Err(ProtocolError::Io(std::io::Error::new(
                std::io::ErrorKind::UnexpectedEof,
                "Missing bytes for ClientboundLoginSuccess uuid",
            )));
        }
        let hi = src.get_i64() as u64;
        let lo = src.get_i64() as u64;
        let uuid = uuid::Uuid::from_u64_pair(hi, lo);
        let username = decode_string(src)?;
        let prop_count = VarInt::decode(src)?.0 as usize;
        let mut properties = Vec::with_capacity(prop_count);
        for _ in 0..prop_count {
            properties.push(ProfileProperty::decode(src)?);
        }
        Ok(Self {
            uuid,
            username,
            properties,
        })
    }
}

#[derive(Debug, Clone, PartialEq)]
pub struct ClientboundSetCompression {
    pub threshold: VarInt,
}

impl PacketId for ClientboundSetCompression {
    fn packet_id(_ver: u32) -> u8 {
        0x03
    }
}

impl Encode for ClientboundSetCompression {
    fn encode(&self, dst: &mut BytesMut) -> Result<(), ProtocolError> {
        self.threshold.encode(dst)
    }
}

impl Decode for ClientboundSetCompression {
    fn decode(src: &mut Bytes) -> Result<Self, ProtocolError> {
        Ok(Self {
            threshold: VarInt::decode(src)?,
        })
    }
}

#[derive(Debug, Clone, PartialEq)]
pub struct ServerboundEncryptionResponse {
    pub shared_secret: Vec<u8>,
    pub verify_token: Vec<u8>,
}

impl PacketId for ServerboundEncryptionResponse {
    fn packet_id(_ver: u32) -> u8 {
        0x01
    }
}

impl Encode for ServerboundEncryptionResponse {
    fn encode(&self, dst: &mut BytesMut) -> Result<(), ProtocolError> {
        encode_byte_array(&self.shared_secret, dst)?;
        encode_byte_array(&self.verify_token, dst)
    }
}

impl Decode for ServerboundEncryptionResponse {
    fn decode(src: &mut Bytes) -> Result<Self, ProtocolError> {
        let shared_secret = decode_byte_array(src)?;
        let verify_token = decode_byte_array(src)?;
        Ok(Self {
            shared_secret,
            verify_token,
        })
    }
}

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

    #[test]
    fn login_start_roundtrip() {
        let p = ServerboundLoginStart {
            username: "Alex".to_string(),
        };
        let mut buf = BytesMut::new();
        p.encode(&mut buf).unwrap();
        let mut b = buf.freeze();
        assert_eq!(ServerboundLoginStart::decode(&mut b).unwrap(), p);
    }

    #[test]
    fn login_disconnect_roundtrip() {
        let p = ClientboundLoginDisconnect {
            reason: r#"{"text":"Not whitelisted."}"#.to_string(),
        };
        let mut buf = BytesMut::new();
        p.encode(&mut buf).unwrap();
        let mut b = buf.freeze();
        assert_eq!(ClientboundLoginDisconnect::decode(&mut b).unwrap(), p);
    }

    #[test]
    fn encryption_request_roundtrip() {
        let p = ClientboundEncryptionRequest {
            server_id: String::new(),
            public_key: vec![0xDE, 0xAD, 0xBE, 0xEF],
            verify_token: vec![0x01, 0x02, 0x03, 0x04],
            should_authenticate: true,
        };
        let mut buf = BytesMut::new();
        p.encode(&mut buf).unwrap();
        let mut b = buf.freeze();
        assert_eq!(ClientboundEncryptionRequest::decode(&mut b).unwrap(), p);
    }

    #[test]
    fn login_success_uuid_binary() {
        let p = ClientboundLoginSuccess {
            uuid: uuid::Uuid::new_v4(),
            username: "Alex".to_string(),
            properties: Vec::new(),
        };
        let mut buf = BytesMut::new();
        p.encode(&mut buf).unwrap();
        let mut b = buf.freeze();
        let d = ClientboundLoginSuccess::decode(&mut b).unwrap();
        assert_eq!(d.uuid, p.uuid);
        assert_eq!(d.username, p.username);
        assert_eq!(d.properties, p.properties);
    }

    #[test]
    fn login_success_with_properties() {
        let p = ClientboundLoginSuccess {
            uuid: uuid::Uuid::new_v4(),
            username: "Alex".to_string(),
            properties: vec![
                ProfileProperty {
                    name: "textures".to_string(),
                    value: "base64data==".to_string(),
                    signature: Some("sig".to_string()),
                },
                ProfileProperty {
                    name: "other".to_string(),
                    value: "val".to_string(),
                    signature: None,
                },
            ],
        };
        let mut buf = BytesMut::new();
        p.encode(&mut buf).unwrap();
        let mut b = buf.freeze();
        assert_eq!(ClientboundLoginSuccess::decode(&mut b).unwrap(), p);
    }

    #[test]
    fn set_compression_roundtrip() {
        let p = ClientboundSetCompression {
            threshold: VarInt(256),
        };
        let mut buf = BytesMut::new();
        p.encode(&mut buf).unwrap();
        let mut b = buf.freeze();
        assert_eq!(ClientboundSetCompression::decode(&mut b).unwrap(), p);
    }

    #[test]
    fn encryption_response_roundtrip() {
        let p = ServerboundEncryptionResponse {
            shared_secret: vec![0u8; 128],
            verify_token: vec![0xAA; 128],
        };
        let mut buf = BytesMut::new();
        p.encode(&mut buf).unwrap();
        let mut b = buf.freeze();
        assert_eq!(ServerboundEncryptionResponse::decode(&mut b).unwrap(), p);
    }
}