use bytes::{Buf, BufMut};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::varint;
const MAX_STRING_LENGTH: usize = 32767;
const MAX_PROPERTIES: usize = 16;
#[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("zlib decompression failed: {0}")]
DecompressionFailed(#[from] libdeflater::DecompressionError),
#[error("zlib compression failed: {0}")]
CompressionFailed(#[from] libdeflater::CompressionError),
#[error("uncompressed size {size} exceeds maximum {max}")]
UncompressedSizeTooLarge {
size: usize,
max: usize,
},
#[error("frame too large: {size} bytes (max {max})")]
FrameTooLarge {
size: usize,
max: usize,
},
#[error("read buffer overflow: {size} bytes (max {max})")]
ReadBufferOverflow {
size: usize,
max: usize,
},
#[error("property count too large: {count} (max {max})")]
PropertyCountExceeded {
count: i32,
max: usize,
},
#[error("byte array too long: {length} > {max}")]
ByteArrayTooLong {
length: usize,
max: usize,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GameProfile {
pub id: Uuid,
pub name: String,
pub properties: Vec<ProfileProperty>,
}
#[derive(Debug, Clone, PartialEq, Eq, 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);
}
if buf.chunk().len() >= length {
let s = std::str::from_utf8(&buf.chunk()[..length])
.map_err(|_| ProtocolError::InvalidUtf8)?
.to_owned();
buf.advance(length);
return Ok(s);
}
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); }
}
}
pub fn read_properties(buf: &mut impl Buf) -> Result<Vec<ProfileProperty>, ProtocolError> {
let raw_count = varint::read_var_int(buf)?;
let count = usize::try_from(raw_count)
.ok()
.filter(|&n| n <= MAX_PROPERTIES)
.ok_or(ProtocolError::PropertyCountExceeded {
count: raw_count,
max: MAX_PROPERTIES,
})?;
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::*;
use proptest::prelude::*;
proptest! {
#[test]
fn roundtrip_any_string(s in ".{0,1024}") {
let mut buf = Vec::new();
write_string(&mut buf, &s);
let result = read_string(&mut &buf[..]).unwrap();
prop_assert_eq!(result, s);
}
#[test]
fn roundtrip_any_uuid(u in any::<u128>()) {
let uuid = Uuid::from_u128(u);
let mut buf = Vec::new();
write_uuid(&mut buf, uuid);
let result = read_uuid(&mut &buf[..]).unwrap();
prop_assert_eq!(result, uuid);
}
#[test]
fn roundtrip_any_properties(
props in prop::collection::vec(
(
".{0,32}",
".{0,1024}",
prop::option::weighted(0.5, ".{0,1024}")
).prop_map(|(name, value, signature)| ProfileProperty { name, value, signature }),
0..4
)
) {
let mut buf = Vec::new();
write_properties(&mut buf, &props);
let result = read_properties(&mut &buf[..]).unwrap();
prop_assert_eq!(result, props);
}
}
#[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_properties_rejects_huge_count() {
let mut buf = Vec::new();
varint::write_var_int(&mut buf, i32::MAX);
let err = read_properties(&mut &buf[..]).unwrap_err();
assert!(
matches!(err, ProtocolError::PropertyCountExceeded { count, max } if count == i32::MAX && max == MAX_PROPERTIES),
"expected PropertyCountExceeded, got {err:?}"
);
}
#[test]
fn test_properties_rejects_negative_count() {
let mut buf = Vec::new();
varint::write_var_int(&mut buf, -1);
let err = read_properties(&mut &buf[..]).unwrap_err();
assert!(
matches!(err, ProtocolError::PropertyCountExceeded { count, max } if count == -1 && max == MAX_PROPERTIES),
"expected PropertyCountExceeded, got {err:?}"
);
}
#[test]
fn test_properties_rejects_count_above_max() {
let mut buf = Vec::new();
varint::write_var_int(&mut buf, i32::try_from(MAX_PROPERTIES + 1).unwrap());
let err = read_properties(&mut &buf[..]).unwrap_err();
assert!(
matches!(err, ProtocolError::PropertyCountExceeded { .. }),
"expected PropertyCountExceeded, got {err:?}"
);
}
#[test]
fn test_properties_accepts_empty() {
let mut buf = Vec::new();
varint::write_var_int(&mut buf, 0);
let result = read_properties(&mut &buf[..]).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_properties_accepts_count_at_max() {
let props: Vec<ProfileProperty> = (0..MAX_PROPERTIES)
.map(|i| ProfileProperty {
name: format!("prop{i}"),
value: format!("val{i}"),
signature: None,
})
.collect();
let mut buf = Vec::new();
write_properties(&mut buf, &props);
let result = read_properties(&mut &buf[..]).unwrap();
assert_eq!(result.len(), MAX_PROPERTIES);
}
#[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);
}
}