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;

pub use clientbound::{ClientboundPongResponse, ClientboundStatusResponse};
pub use serverbound::{ServerboundPingRequest, ServerboundStatusRequest};

pub const PROTOCOL_VERSION: u32 = 47;

mod serverbound {
    use super::*;

    #[derive(Debug, Clone, PartialEq)]
    pub struct ServerboundStatusRequest;

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

    impl Encode for ServerboundStatusRequest {
        fn encode(&self, _dst: &mut BytesMut) -> Result<(), ProtocolError> {
            Ok(())
        }
    }

    impl Decode for ServerboundStatusRequest {
        fn decode(_src: &mut Bytes) -> Result<Self, ProtocolError> {
            Ok(Self)
        }
    }

    #[derive(Debug, Clone, PartialEq)]
    pub struct ServerboundPingRequest {
        pub payload: i64,
    }

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

    impl Encode for ServerboundPingRequest {
        fn encode(&self, dst: &mut BytesMut) -> Result<(), ProtocolError> {
            dst.put_i64(self.payload);
            Ok(())
        }
    }

    impl Decode for ServerboundPingRequest {
        fn decode(src: &mut Bytes) -> Result<Self, ProtocolError> {
            Ok(Self {
                payload: src.get_i64(),
            })
        }
    }
}

mod clientbound {
    use super::*;

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

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

    impl Encode for ClientboundStatusResponse {
        fn encode(&self, dst: &mut BytesMut) -> Result<(), ProtocolError> {
            let json_bytes = self.json_response.as_bytes();
            VarInt(json_bytes.len() as i32).encode(dst)?;
            dst.put_slice(json_bytes);
            Ok(())
        }
    }

    impl Decode for ClientboundStatusResponse {
        fn decode(src: &mut Bytes) -> Result<Self, ProtocolError> {
            let json_len = VarInt::decode(src)?.0 as usize;

            if src.remaining() < json_len {
                return Err(ProtocolError::Io(std::io::Error::new(
                    std::io::ErrorKind::UnexpectedEof,
                    "Missing bytes while reading ClientboundStatusResponse JSON payload",
                )));
            }

            let mut json_bytes = vec![0u8; json_len];
            src.copy_to_slice(&mut json_bytes);

            let json_response = String::from_utf8(json_bytes).map_err(|_| {
                ProtocolError::Io(std::io::Error::new(
                    std::io::ErrorKind::InvalidData,
                    "Invalid UTF-8 string in ClientboundStatusResponse",
                ))
            })?;

            Ok(Self { json_response })
        }
    }

    #[derive(Debug, Clone, PartialEq)]
    pub struct ClientboundPongResponse {
        pub payload: i64,
    }

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

    impl Encode for ClientboundPongResponse {
        fn encode(&self, dst: &mut BytesMut) -> Result<(), ProtocolError> {
            dst.put_i64(self.payload);
            Ok(())
        }
    }

    impl Decode for ClientboundPongResponse {
        fn decode(src: &mut Bytes) -> Result<Self, ProtocolError> {
            if src.remaining() < 8 {
                return Err(ProtocolError::Io(std::io::Error::new(
                    std::io::ErrorKind::UnexpectedEof,
                    "Missing bytes for ClientboundPongResponse payload",
                )));
            }
            Ok(Self {
                payload: src.get_i64(),
            })
        }
    }
}

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

    #[test]
    fn status_request_roundtrip() {
        let mut buf = BytesMut::new();
        ServerboundStatusRequest.encode(&mut buf).unwrap();
        let mut bytes = buf.freeze();
        let _ = ServerboundStatusRequest::decode(&mut bytes).unwrap();
    }

    #[test]
    fn status_response_roundtrip() {
        let p = ClientboundStatusResponse {
            json_response: r#"{"version":{"name":"1.8","protocol":47},"players":{"max":20,"online":0},"description":{"text":"A Minecraft 1.8 server"}}"#.to_string(),
        };
        let mut buf = BytesMut::new();
        p.encode(&mut buf).unwrap();
        let mut bytes = buf.freeze();
        assert_eq!(ClientboundStatusResponse::decode(&mut bytes).unwrap(), p);
    }

    #[test]
    fn ping_roundtrip() {
        let p = ServerboundPingRequest { payload: 123456789 };
        let mut buf = BytesMut::new();
        p.encode(&mut buf).unwrap();
        let mut bytes = buf.freeze();
        assert_eq!(ServerboundPingRequest::decode(&mut bytes).unwrap(), p);
    }

    #[test]
    fn pong_roundtrip() {
        let p = ClientboundPongResponse { payload: -99 };
        let mut buf = BytesMut::new();
        p.encode(&mut buf).unwrap();
        let mut bytes = buf.freeze();
        assert_eq!(ClientboundPongResponse::decode(&mut bytes).unwrap(), p);
    }

    #[test]
    fn packet_ids_are_correct_for_1_8() {
        assert_eq!(ServerboundStatusRequest::packet_id(PROTOCOL_VERSION), 0x00);
        assert_eq!(ServerboundPingRequest::packet_id(PROTOCOL_VERSION), 0x01);
        assert_eq!(ClientboundStatusResponse::packet_id(PROTOCOL_VERSION), 0x00);
        assert_eq!(ClientboundPongResponse::packet_id(PROTOCOL_VERSION), 0x01);
    }
}