use bytes::{Buf, BufMut};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::varint;
const MAX_STRING_LENGTH: usize = 32767;
#[derive(Debug, thiserror::Error)]
pub enum ProtocolError {
#[error("VarInt is too long (exceeded 5 bytes)")]
VarIntTooLong,
#[error("unexpected end of data")]
UnexpectedEof,
#[error("string too long: {length} > {max}")]
StringTooLong {
length: usize,
max: usize,
},
#[error("invalid UTF-8 in string")]
InvalidUtf8,
#[error("unknown packet ID {id:#04x} in state {state}")]
UnknownPacket {
id: i32,
state: String,
},
#[error("invalid packet data: {0}")]
InvalidData(String),
#[error("compression error: {0}")]
CompressionError(String),
#[error("frame too large: {size} bytes (max {max})")]
FrameTooLarge {
size: usize,
max: usize,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GameProfile {
pub id: Uuid,
pub name: String,
pub properties: Vec<ProfileProperty>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProfileProperty {
pub name: String,
pub value: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub signature: Option<String>,
}
pub fn read_string(buf: &mut impl Buf) -> Result<String, ProtocolError> {
read_string_max(buf, MAX_STRING_LENGTH)
}
#[allow(clippy::cast_sign_loss)]
pub fn read_string_max(buf: &mut impl Buf, max_len: usize) -> Result<String, ProtocolError> {
let length = varint::read_var_int(buf)? as usize;
if length > max_len * 4 {
return Err(ProtocolError::StringTooLong {
length,
max: max_len * 4,
});
}
if buf.remaining() < length {
return Err(ProtocolError::UnexpectedEof);
}
let mut data = vec![0u8; length];
buf.copy_to_slice(&mut data);
String::from_utf8(data).map_err(|_| ProtocolError::InvalidUtf8)
}
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
pub fn write_string(buf: &mut impl BufMut, value: &str) {
let bytes = value.as_bytes();
varint::write_var_int(buf, bytes.len() as i32);
buf.put_slice(bytes);
}
pub fn read_uuid(buf: &mut impl Buf) -> Result<Uuid, ProtocolError> {
if buf.remaining() < 16 {
return Err(ProtocolError::UnexpectedEof);
}
let most = buf.get_u64();
let least = buf.get_u64();
Ok(Uuid::from_u64_pair(most, least))
}
pub fn write_uuid(buf: &mut impl BufMut, uuid: Uuid) {
let (most, least) = uuid.as_u64_pair();
buf.put_u64(most);
buf.put_u64(least);
}
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
pub fn write_properties(buf: &mut impl BufMut, properties: &[ProfileProperty]) {
varint::write_var_int(buf, properties.len() as i32);
for prop in properties {
write_string(buf, &prop.name);
write_string(buf, &prop.value);
if let Some(sig) = &prop.signature {
buf.put_u8(1); write_string(buf, sig);
} else {
buf.put_u8(0); }
}
}
#[allow(clippy::cast_sign_loss)]
pub fn read_properties(buf: &mut impl Buf) -> Result<Vec<ProfileProperty>, ProtocolError> {
let count = varint::read_var_int(buf)? as usize;
let mut properties = Vec::with_capacity(count);
for _ in 0..count {
let name = read_string(buf)?;
let value = read_string(buf)?;
let has_signature = if buf.remaining() < 1 {
return Err(ProtocolError::UnexpectedEof);
} else {
buf.get_u8() != 0
};
let signature = if has_signature {
Some(read_string(buf)?)
} else {
None
};
properties.push(ProfileProperty {
name,
value,
signature,
});
}
Ok(properties)
}
#[allow(clippy::cast_possible_truncation)]
fn write_nbt_string_tag(buf: &mut impl BufMut, name: &str, value: &str) {
buf.put_u8(0x08); buf.put_u16(name.len() as u16);
buf.put_slice(name.as_bytes());
buf.put_u16(value.len() as u16);
buf.put_slice(value.as_bytes());
}
pub fn write_nbt_text_component(buf: &mut impl BufMut, text: &str, color: Option<&str>) {
buf.put_u8(0x0A); write_nbt_string_tag(buf, "text", text);
if let Some(color) = color {
write_nbt_string_tag(buf, "color", color);
}
buf.put_u8(0x00); }
#[must_use]
pub fn undashed_uuid(uuid: Uuid) -> String {
uuid.as_simple().to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_string_roundtrip() {
let mut buf = Vec::new();
write_string(&mut buf, "Hello, Minecraft!");
let result = read_string(&mut &buf[..]).unwrap();
assert_eq!(result, "Hello, Minecraft!");
}
#[test]
fn test_string_empty() {
let mut buf = Vec::new();
write_string(&mut buf, "");
let result = read_string(&mut &buf[..]).unwrap();
assert_eq!(result, "");
}
#[test]
fn test_uuid_roundtrip() {
let uuid = Uuid::parse_str("069a79f4-44e9-4726-a5be-fca90e38aaf5").unwrap();
let mut buf = Vec::new();
write_uuid(&mut buf, uuid);
assert_eq!(buf.len(), 16);
let result = read_uuid(&mut &buf[..]).unwrap();
assert_eq!(result, uuid);
}
#[test]
fn test_properties_roundtrip() {
let props = 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 = Vec::new();
write_properties(&mut buf, &props);
let result = read_properties(&mut &buf[..]).unwrap();
assert_eq!(result.len(), 2);
assert_eq!(result[0].name, "textures");
assert_eq!(result[0].signature.as_deref(), Some("sig"));
assert_eq!(result[1].signature, None);
}
#[test]
fn test_nbt_text_component_simple() {
let mut buf = Vec::new();
write_nbt_text_component(&mut buf, "hello", None);
assert_eq!(buf[0], 0x0A); assert_eq!(buf[1], 0x08); assert_eq!(&buf[2..4], &[0x00, 0x04]); assert_eq!(&buf[4..8], b"text");
assert_eq!(&buf[8..10], &[0x00, 0x05]); assert_eq!(&buf[10..15], b"hello");
assert_eq!(buf[15], 0x00); assert_eq!(buf.len(), 16);
}
#[test]
fn test_nbt_text_component_with_color() {
let mut buf = Vec::new();
write_nbt_text_component(&mut buf, "hi", Some("yellow"));
assert_eq!(buf[0], 0x0A); assert_eq!(buf[1], 0x08);
assert_eq!(&buf[2..4], &[0x00, 0x04]);
assert_eq!(&buf[4..8], b"text");
assert_eq!(&buf[8..10], &[0x00, 0x02]);
assert_eq!(&buf[10..12], b"hi");
assert_eq!(buf[12], 0x08);
assert_eq!(&buf[13..15], &[0x00, 0x05]);
assert_eq!(&buf[15..20], b"color");
assert_eq!(&buf[20..22], &[0x00, 0x06]);
assert_eq!(&buf[22..28], b"yellow");
assert_eq!(buf[28], 0x00);
assert_eq!(buf.len(), 29);
}
}