751 lines
28 KiB
Rust
751 lines
28 KiB
Rust
use aes::cipher::KeyIvInit;
|
|
use bytes::Bytes;
|
|
use flate2::{Compress, Compression, FlushCompress, Status};
|
|
use thiserror::Error;
|
|
use tokio::io::{AsyncWrite, AsyncWriteExt};
|
|
|
|
use crate::{
|
|
Aes128Cfb8Enc, CompressionLevel, CompressionThreshold, MAX_PACKET_DATA_SIZE, MAX_PACKET_SIZE,
|
|
PacketEncodeError, StreamEncryptor, VarInt,
|
|
};
|
|
|
|
// raw -> compress -> encrypt
|
|
|
|
pub enum EncryptionWriter<W: AsyncWrite + Unpin> {
|
|
Encrypt(Box<StreamEncryptor<W>>),
|
|
None(W),
|
|
}
|
|
|
|
impl<W: AsyncWrite + Unpin> EncryptionWriter<W> {
|
|
#[must_use]
|
|
pub fn upgrade(self, cipher: Aes128Cfb8Enc) -> Self {
|
|
match self {
|
|
Self::None(stream) => Self::Encrypt(Box::new(StreamEncryptor::new(cipher, stream))),
|
|
Self::Encrypt(_) => self,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<W: AsyncWrite + Unpin> AsyncWrite for EncryptionWriter<W> {
|
|
fn poll_write(
|
|
self: std::pin::Pin<&mut Self>,
|
|
cx: &mut std::task::Context<'_>,
|
|
buf: &[u8],
|
|
) -> std::task::Poll<Result<usize, std::io::Error>> {
|
|
match self.get_mut() {
|
|
Self::Encrypt(writer) => {
|
|
let writer = std::pin::Pin::new(writer);
|
|
writer.poll_write(cx, buf)
|
|
}
|
|
Self::None(writer) => {
|
|
let writer = std::pin::Pin::new(writer);
|
|
writer.poll_write(cx, buf)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn poll_flush(
|
|
self: std::pin::Pin<&mut Self>,
|
|
cx: &mut std::task::Context<'_>,
|
|
) -> std::task::Poll<Result<(), std::io::Error>> {
|
|
match self.get_mut() {
|
|
Self::Encrypt(writer) => {
|
|
let writer = std::pin::Pin::new(writer);
|
|
writer.poll_flush(cx)
|
|
}
|
|
Self::None(writer) => {
|
|
let writer = std::pin::Pin::new(writer);
|
|
writer.poll_flush(cx)
|
|
}
|
|
}
|
|
}
|
|
|
|
fn poll_shutdown(
|
|
self: std::pin::Pin<&mut Self>,
|
|
cx: &mut std::task::Context<'_>,
|
|
) -> std::task::Poll<Result<(), std::io::Error>> {
|
|
match self.get_mut() {
|
|
Self::Encrypt(writer) => {
|
|
let writer = std::pin::Pin::new(writer);
|
|
writer.poll_shutdown(cx)
|
|
}
|
|
Self::None(writer) => {
|
|
let writer = std::pin::Pin::new(writer);
|
|
writer.poll_shutdown(cx)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Encoder: Server -> Client
|
|
/// Supports `ZLib` endecoding/compression
|
|
/// Supports Aes128 Encryption
|
|
pub struct TCPNetworkEncoder<W: AsyncWrite + Unpin> {
|
|
writer: Option<EncryptionWriter<W>>,
|
|
// compression and compression threshold
|
|
compression: Option<(CompressionThreshold, CompressionLevel)>,
|
|
// Reused compressor to avoid constructing zlib state per packet.
|
|
compressor: Option<(CompressionLevel, Compress)>,
|
|
// Reused compression buffer to avoid allocating a new Vec for each packet.
|
|
compression_scratch: Vec<u8>,
|
|
}
|
|
|
|
impl<W: AsyncWrite + Unpin> TCPNetworkEncoder<W> {
|
|
pub const fn new(writer: W) -> Self {
|
|
Self {
|
|
writer: Some(EncryptionWriter::None(writer)),
|
|
compression: None,
|
|
compressor: None,
|
|
compression_scratch: Vec::new(),
|
|
}
|
|
}
|
|
|
|
pub const fn set_compression(
|
|
&mut self,
|
|
compression_info: (CompressionThreshold, CompressionLevel),
|
|
) {
|
|
self.compression = Some(compression_info);
|
|
}
|
|
|
|
/// NOTE: Encryption can only be set; a minecraft stream cannot go back to being unencrypted
|
|
pub fn set_encryption(&mut self, key: &[u8; 16]) -> Result<(), PacketEncodeError> {
|
|
if matches!(self.writer, Some(EncryptionWriter::Encrypt(_))) {
|
|
return Err(PacketEncodeError::Message(
|
|
"Encryption already enabled".into(),
|
|
));
|
|
}
|
|
let cipher = Aes128Cfb8Enc::new_from_slices(key, key)
|
|
.map_err(|_| PacketEncodeError::Message("Invalid key".into()))?;
|
|
|
|
if let Some(writer) = self.writer.take() {
|
|
self.writer = Some(writer.upgrade(cipher));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn compress_packet_data(
|
|
&mut self,
|
|
packet_data: &[u8],
|
|
compression_level: CompressionLevel,
|
|
) -> Result<(), PacketEncodeError> {
|
|
self.compression_scratch.clear();
|
|
let reserve_hint = packet_data
|
|
.len()
|
|
.saturating_add(packet_data.len() / 16)
|
|
.saturating_add(64);
|
|
let current_capacity = self.compression_scratch.capacity();
|
|
if reserve_hint > current_capacity {
|
|
self.compression_scratch
|
|
.reserve(reserve_hint.saturating_sub(current_capacity));
|
|
}
|
|
|
|
let needs_new_compressor = match self.compressor.as_ref() {
|
|
Some((level, _)) => *level != compression_level,
|
|
None => true,
|
|
};
|
|
if needs_new_compressor {
|
|
self.compressor = Some((
|
|
compression_level,
|
|
Compress::new(Compression::new(compression_level), true),
|
|
));
|
|
}
|
|
|
|
let (_, compressor) = self.compressor.as_mut().ok_or_else(|| {
|
|
PacketEncodeError::Message("compressor must be present after initialization".into())
|
|
})?;
|
|
compressor.reset();
|
|
let status = compressor
|
|
.compress_vec(
|
|
packet_data,
|
|
&mut self.compression_scratch,
|
|
FlushCompress::Finish,
|
|
)
|
|
.map_err(|err| PacketEncodeError::CompressionFailed(err.to_string()))?;
|
|
|
|
if !matches!(status, Status::StreamEnd) {
|
|
return Err(PacketEncodeError::CompressionFailed(format!(
|
|
"Unexpected compressor status: {status:?}"
|
|
)));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Appends a Clientbound `ClientPacket` to the internal buffer and applies compression when needed.
|
|
///
|
|
/// If compression is enabled and the packet size exceeds the threshold, the packet is compressed.
|
|
/// The packet is prefixed with its length and, if compressed, the uncompressed data length.
|
|
/// The packet format is as follows:
|
|
///
|
|
/// **Uncompressed:**
|
|
/// |-----------------------|
|
|
/// | Packet Length (`VarInt`)|
|
|
/// |-----------------------|
|
|
/// | Packet ID (`VarInt`) |
|
|
/// |-----------------------|
|
|
/// | Data (Byte Array) |
|
|
/// |-----------------------|
|
|
///
|
|
/// **Compressed:**
|
|
/// |------------------------|
|
|
/// | Packet Length (`VarInt`) |
|
|
/// |------------------------|
|
|
/// | Data Length (`VarInt`) |
|
|
/// |------------------------|
|
|
/// | Packet ID (`VarInt`) |
|
|
/// |------------------------|
|
|
/// | Data (Byte Array) |
|
|
/// |------------------------|
|
|
///
|
|
/// - `Packet Length`: The total length of the packet *excluding* the `Packet Length` field itself.
|
|
/// - `Data Length`: (Only present in compressed packets) The length of the uncompressed `Packet ID` and `Data`.
|
|
/// - `Packet ID`: The ID of the packet.
|
|
/// - `Data`: The packet's data.
|
|
///
|
|
/// NOTE: This method does not flush. Call [`Self::flush`] to flush buffered data.
|
|
#[allow(clippy::too_many_lines)]
|
|
pub async fn write_packet(&mut self, packet_data: Bytes) -> Result<(), PacketEncodeError> {
|
|
// We need to know the length of the compressed buffer and serde is not async :(
|
|
// We need to write to a buffer here 😔
|
|
|
|
let data_len = packet_data.len();
|
|
if data_len > MAX_PACKET_DATA_SIZE {
|
|
return Err(PacketEncodeError::TooLong(data_len));
|
|
}
|
|
|
|
let data_len_var_int: VarInt = data_len.try_into().map_err(|_| {
|
|
PacketEncodeError::Message(format!(
|
|
"Packet data length is too large to fit in VarInt! ({data_len})"
|
|
))
|
|
})?;
|
|
|
|
if let Some((compression_threshold, compression_level)) = self.compression {
|
|
if data_len >= compression_threshold {
|
|
// Pushed before data:
|
|
// Length of (Data Length) + length of compressed (Packet ID + Data)
|
|
// Length of uncompressed (Packet ID + Data)
|
|
|
|
// TODO: We need the compressed length at the beginning of the packet so we need to write to
|
|
// buf here :( Is there a magic way to find a compressed length?
|
|
self.compress_packet_data(packet_data.as_ref(), compression_level)?;
|
|
debug_assert!(!self.compression_scratch.is_empty());
|
|
|
|
let full_packet_len_var_int: VarInt = (data_len_var_int.written_size()
|
|
+ self.compression_scratch.len())
|
|
.try_into()
|
|
.map_err(|_| {
|
|
PacketEncodeError::Message(format!(
|
|
"Full packet length is too large to fit in VarInt! ({data_len})"
|
|
))
|
|
})?;
|
|
|
|
let complete_serialization_length =
|
|
full_packet_len_var_int.written_size() + full_packet_len_var_int.0 as usize;
|
|
if complete_serialization_length > MAX_PACKET_SIZE as usize {
|
|
return Err(PacketEncodeError::TooLong(complete_serialization_length));
|
|
}
|
|
|
|
full_packet_len_var_int
|
|
.encode_async(self.writer.as_mut().unwrap())
|
|
.await
|
|
.map_err(|err| PacketEncodeError::Message(err.to_string()))?;
|
|
data_len_var_int
|
|
.encode_async(self.writer.as_mut().unwrap())
|
|
.await
|
|
.map_err(|err| PacketEncodeError::Message(err.to_string()))?;
|
|
self.writer
|
|
.as_mut()
|
|
.unwrap()
|
|
.write_all(&self.compression_scratch)
|
|
.await
|
|
.map_err(|err| PacketEncodeError::Message(err.to_string()))?;
|
|
} else {
|
|
// Pushed before data:
|
|
// Length of (Data Length) + length of compressed (Packet ID + Data)
|
|
// 0 to indicate uncompressed
|
|
|
|
let data_len_var_int: VarInt = 0.into();
|
|
let full_packet_len_var_int: VarInt = (data_len_var_int.written_size() + data_len)
|
|
.try_into()
|
|
.map_err(|_| {
|
|
PacketEncodeError::Message(format!(
|
|
"Full packet length is too large to fit in VarInt! ({data_len})"
|
|
))
|
|
})?;
|
|
|
|
let complete_serialization_length =
|
|
full_packet_len_var_int.written_size() + full_packet_len_var_int.0 as usize;
|
|
if complete_serialization_length > MAX_PACKET_SIZE as usize {
|
|
return Err(PacketEncodeError::TooLong(complete_serialization_length));
|
|
}
|
|
|
|
full_packet_len_var_int
|
|
.encode_async(self.writer.as_mut().unwrap())
|
|
.await
|
|
.map_err(|err| PacketEncodeError::Message(err.to_string()))?;
|
|
data_len_var_int
|
|
.encode_async(self.writer.as_mut().unwrap())
|
|
.await
|
|
.map_err(|err| PacketEncodeError::Message(err.to_string()))?;
|
|
self.writer
|
|
.as_mut()
|
|
.unwrap()
|
|
.write_all(&packet_data)
|
|
.await
|
|
.map_err(|err| PacketEncodeError::Message(err.to_string()))?;
|
|
}
|
|
} else {
|
|
// Pushed before data:
|
|
// Length of Packet ID + Data
|
|
|
|
let full_packet_len_var_int: VarInt = data_len_var_int;
|
|
|
|
let complete_serialization_length =
|
|
full_packet_len_var_int.written_size() + full_packet_len_var_int.0 as usize;
|
|
if complete_serialization_length > MAX_PACKET_SIZE as usize {
|
|
return Err(PacketEncodeError::TooLong(complete_serialization_length));
|
|
}
|
|
|
|
full_packet_len_var_int
|
|
.encode_async(self.writer.as_mut().unwrap())
|
|
.await
|
|
.map_err(|err| PacketEncodeError::Message(err.to_string()))?;
|
|
self.writer
|
|
.as_mut()
|
|
.unwrap()
|
|
.write_all(&packet_data)
|
|
.await
|
|
.map_err(|err| PacketEncodeError::Message(err.to_string()))?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn flush(&mut self) -> Result<(), PacketEncodeError> {
|
|
self.writer
|
|
.as_mut()
|
|
.unwrap()
|
|
.flush()
|
|
.await
|
|
.map_err(|err| PacketEncodeError::Message(err.to_string()))
|
|
}
|
|
}
|
|
|
|
#[derive(Error, Debug)]
|
|
#[error("Invalid compression Level")]
|
|
pub struct CompressionLevelError;
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::io::Read;
|
|
|
|
use super::*;
|
|
use crate::java::client::status::CStatusResponse;
|
|
use crate::packet::MultiVersionJavaPacket;
|
|
use crate::ser::{NetworkReadExt, NetworkWriteExt};
|
|
use crate::{ClientPacket, ReadingError};
|
|
use aes::Aes128;
|
|
use cfb8::Decryptor as Cfb8Decryptor;
|
|
use flate2::read::ZlibDecoder;
|
|
use pumpkin_data::packet::clientbound::STATUS_STATUS_RESPONSE;
|
|
use pumpkin_macros::java_packet;
|
|
use pumpkin_util::version::JavaMinecraftVersion;
|
|
use serde::Serialize;
|
|
|
|
/// Define a custom packet for testing maximum packet size
|
|
#[derive(Serialize)]
|
|
#[java_packet(STATUS_STATUS_RESPONSE)]
|
|
pub struct MaxSizePacket {
|
|
data: Vec<u8>,
|
|
}
|
|
|
|
impl MaxSizePacket {
|
|
pub fn new(size: usize) -> Self {
|
|
Self {
|
|
data: vec![0xAB; size], // Fill with arbitrary data
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Helper function to decode a `VarInt` from bytes
|
|
fn decode_varint(buffer: &mut &[u8]) -> Result<i32, ReadingError> {
|
|
Ok(buffer.get_var_int()?.0)
|
|
}
|
|
|
|
/// Helper function to decompress data using libdeflater's Zlib decompressor
|
|
fn decompress_zlib(data: &[u8], expected_size: usize) -> Result<Vec<u8>, std::io::Error> {
|
|
assert!(!data.is_empty());
|
|
let mut decompressed = vec![0u8; expected_size];
|
|
ZlibDecoder::new(data).read_exact(&mut decompressed)?;
|
|
Ok(decompressed)
|
|
}
|
|
|
|
/// Helper function to decrypt data using AES-128 CFB-8 mode
|
|
fn decrypt_aes128(
|
|
encrypted_data: &mut [u8],
|
|
key: &[u8; 16],
|
|
iv: &[u8; 16],
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut decryptor =
|
|
Cfb8Decryptor::<Aes128>::new_from_slices(key, iv).map_err(|_| "Invalid key/iv")?;
|
|
decryptor.decrypt(encrypted_data);
|
|
Ok(())
|
|
}
|
|
|
|
/// Helper function to build a packet with optional compression and encryption
|
|
async fn build_packet_with_encoder<T: ClientPacket>(
|
|
packet: &T,
|
|
compression_info: Option<(CompressionThreshold, CompressionLevel)>,
|
|
key: Option<&[u8; 16]>,
|
|
) -> Result<Box<[u8]>, Box<dyn std::error::Error>> {
|
|
let mut buf = Vec::new();
|
|
let mut encoder = TCPNetworkEncoder::new(&mut buf);
|
|
if let Some(compression_info) = compression_info {
|
|
encoder.set_compression(compression_info);
|
|
}
|
|
|
|
if let Some(key) = key {
|
|
encoder.set_encryption(key).map_err(|e| e.to_string())?;
|
|
}
|
|
|
|
let mut packet_buf = Vec::new();
|
|
let writer = &mut packet_buf;
|
|
writer.write_var_int(&VarInt(T::to_id(JavaMinecraftVersion::V_1_21_11)))?;
|
|
packet.write_packet_data(writer, &JavaMinecraftVersion::V_1_21_11)?;
|
|
|
|
encoder
|
|
.write_packet(packet_buf.into())
|
|
.await
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
Ok(buf.into_boxed_slice())
|
|
}
|
|
|
|
/// Test encoding without compression and encryption
|
|
#[tokio::test]
|
|
async fn encode_without_compression_and_encryption() -> Result<(), Box<dyn std::error::Error>> {
|
|
// Create a CStatusResponse packet
|
|
let packet =
|
|
CStatusResponse::new(String::from("{\"description\": \"A Minecraft Server\"}"));
|
|
|
|
// Build the packet without compression and encryption
|
|
let packet_bytes = build_packet_with_encoder(&packet, None, None).await?;
|
|
|
|
// Decode the packet manually to verify correctness
|
|
let mut buffer = &packet_bytes[..];
|
|
|
|
// Read packet length VarInt
|
|
let packet_length = decode_varint(&mut buffer).map_err(|e| e.to_string())?;
|
|
assert_eq!(
|
|
packet_length as usize,
|
|
buffer.len(),
|
|
"Packet length mismatch"
|
|
);
|
|
|
|
// Read packet ID VarInt
|
|
let decoded_packet_id = decode_varint(&mut buffer).map_err(|e| e.to_string())?;
|
|
assert_eq!(
|
|
decoded_packet_id,
|
|
CStatusResponse::to_id(JavaMinecraftVersion::V_1_21_11)
|
|
);
|
|
|
|
// Remaining buffer is the payload
|
|
// We need to obtain the expected payload
|
|
let mut expected_payload = Vec::new();
|
|
packet.write_packet_data(&mut expected_payload, &JavaMinecraftVersion::V_1_21_11)?;
|
|
|
|
assert_eq!(buffer, expected_payload);
|
|
Ok(())
|
|
}
|
|
|
|
/// Test encoding with compression
|
|
#[tokio::test]
|
|
async fn encode_with_compression() -> Result<(), Box<dyn std::error::Error>> {
|
|
// Create a CStatusResponse packet
|
|
let packet = CStatusResponse::new("{\"description\": \"A Minecraft Server\"}".to_string());
|
|
|
|
// Build the packet with compression enabled
|
|
let packet_bytes = build_packet_with_encoder(&packet, Some((0, 6)), None).await?;
|
|
|
|
// Decode the packet manually to verify correctness
|
|
let mut buffer = &packet_bytes[..];
|
|
|
|
// Read packet length VarInt
|
|
let packet_length = decode_varint(&mut buffer).map_err(|e| e.to_string())?;
|
|
assert_eq!(
|
|
packet_length as usize,
|
|
buffer.len(),
|
|
"Packet length mismatch"
|
|
);
|
|
|
|
// Read data length VarInt (uncompressed data length)
|
|
let data_length = decode_varint(&mut buffer).map_err(|e| e.to_string())?;
|
|
let mut expected_payload = Vec::new();
|
|
packet.write_packet_data(&mut expected_payload, &JavaMinecraftVersion::V_1_21_11)?;
|
|
let uncompressed_data_length =
|
|
VarInt(CStatusResponse::to_id(JavaMinecraftVersion::V_1_21_11)).written_size()
|
|
+ expected_payload.len();
|
|
assert_eq!(data_length as usize, uncompressed_data_length);
|
|
|
|
// Remaining buffer is the compressed data
|
|
let compressed_data = buffer;
|
|
|
|
// Decompress the data
|
|
let decompressed_data = decompress_zlib(compressed_data, data_length as usize)?;
|
|
|
|
// Verify packet ID and payload
|
|
let mut decompressed_buffer = &decompressed_data[..];
|
|
|
|
// Read packet ID VarInt
|
|
let decoded_packet_id =
|
|
decode_varint(&mut decompressed_buffer).map_err(|e| e.to_string())?;
|
|
assert_eq!(
|
|
decoded_packet_id,
|
|
CStatusResponse::to_id(JavaMinecraftVersion::V_1_21_11)
|
|
);
|
|
|
|
// Remaining buffer is the payload
|
|
assert_eq!(decompressed_buffer, expected_payload);
|
|
Ok(())
|
|
}
|
|
|
|
/// Test encoding with encryption
|
|
#[tokio::test]
|
|
async fn encode_with_encryption() -> Result<(), Box<dyn std::error::Error>> {
|
|
// Create a CStatusResponse packet
|
|
let packet = CStatusResponse::new("{\"description\": \"A Minecraft Server\"}".to_string());
|
|
|
|
// Encryption key and IV (IV is the same as key in this case)
|
|
let key = [0x00u8; 16]; // Example key
|
|
|
|
// Build the packet with encryption enabled (no compression)
|
|
let mut packet_bytes = build_packet_with_encoder(&packet, None, Some(&key)).await?;
|
|
|
|
// Decrypt the packet
|
|
decrypt_aes128(&mut packet_bytes, &key, &key)?;
|
|
|
|
// Decode the packet manually to verify correctness
|
|
let mut buffer = &packet_bytes[..];
|
|
|
|
// Read packet length VarInt
|
|
let packet_length = decode_varint(&mut buffer).map_err(|e| e.to_string())?;
|
|
assert_eq!(
|
|
packet_length as usize,
|
|
buffer.len(),
|
|
"Packet length mismatch"
|
|
);
|
|
|
|
// Read packet ID VarInt
|
|
let decoded_packet_id = decode_varint(&mut buffer).map_err(|e| e.to_string())?;
|
|
assert_eq!(
|
|
decoded_packet_id,
|
|
CStatusResponse::to_id(JavaMinecraftVersion::V_1_21_11)
|
|
);
|
|
|
|
// Remaining buffer is the payload
|
|
let mut expected_payload = Vec::new();
|
|
packet.write_packet_data(&mut expected_payload, &JavaMinecraftVersion::V_1_21_11)?;
|
|
assert_eq!(buffer, expected_payload);
|
|
Ok(())
|
|
}
|
|
|
|
/// Test encoding with both compression and encryption
|
|
#[tokio::test]
|
|
async fn encode_with_compression_and_encryption() -> Result<(), Box<dyn std::error::Error>> {
|
|
// Create a CStatusResponse packet
|
|
let packet = CStatusResponse::new("{\"description\": \"A Minecraft Server\"}".to_string());
|
|
|
|
// Encryption key and IV (IV is the same as key in this case)
|
|
let key = [0x01u8; 16]; // Example key
|
|
|
|
// Build the packet with both compression and encryption enabled
|
|
// Compression threshold is set to 0 to force compression
|
|
let mut packet_bytes = build_packet_with_encoder(&packet, Some((0, 6)), Some(&key)).await?;
|
|
|
|
// Decrypt the packet
|
|
decrypt_aes128(&mut packet_bytes, &key, &key)?;
|
|
|
|
// Decode the packet manually to verify correctness
|
|
let mut buffer = &packet_bytes[..];
|
|
|
|
// Read packet length VarInt
|
|
let packet_length = decode_varint(&mut buffer).map_err(|e| e.to_string())?;
|
|
assert_eq!(
|
|
packet_length as usize,
|
|
buffer.len(),
|
|
"Packet length mismatch"
|
|
);
|
|
|
|
// Read data length VarInt (uncompressed data length)
|
|
let data_length = decode_varint(&mut buffer).map_err(|e| e.to_string())?;
|
|
let mut expected_payload = Vec::new();
|
|
packet.write_packet_data(&mut expected_payload, &JavaMinecraftVersion::V_1_21_11)?;
|
|
let uncompressed_data_length =
|
|
VarInt(CStatusResponse::to_id(JavaMinecraftVersion::V_1_21_11)).written_size()
|
|
+ expected_payload.len();
|
|
assert_eq!(data_length as usize, uncompressed_data_length);
|
|
|
|
// Remaining buffer is the compressed data
|
|
let compressed_data = buffer;
|
|
|
|
// Decompress the data
|
|
let decompressed_data = decompress_zlib(compressed_data, data_length as usize)?;
|
|
|
|
// Verify packet ID and payload
|
|
let mut decompressed_buffer = &decompressed_data[..];
|
|
|
|
// Read packet ID VarInt
|
|
let decoded_packet_id =
|
|
decode_varint(&mut decompressed_buffer).map_err(|e| e.to_string())?;
|
|
assert_eq!(
|
|
decoded_packet_id,
|
|
CStatusResponse::to_id(JavaMinecraftVersion::V_1_21_11)
|
|
);
|
|
|
|
// Remaining buffer is the payload
|
|
assert_eq!(decompressed_buffer, expected_payload);
|
|
Ok(())
|
|
}
|
|
|
|
/// Test encoding with zero-length payload
|
|
#[tokio::test]
|
|
async fn encode_with_zero_length_payload() -> Result<(), Box<dyn std::error::Error>> {
|
|
// Create a CStatusResponse packet with empty payload
|
|
let packet = CStatusResponse::new(String::new());
|
|
|
|
// Build the packet without compression and encryption
|
|
let packet_bytes = build_packet_with_encoder(&packet, None, None).await?;
|
|
|
|
// Decode the packet manually to verify correctness
|
|
let mut buffer = &packet_bytes[..];
|
|
|
|
// Read packet length VarInt
|
|
let packet_length = decode_varint(&mut buffer).map_err(|e| e.to_string())?;
|
|
assert_eq!(
|
|
packet_length as usize,
|
|
buffer.len(),
|
|
"Packet length mismatch"
|
|
);
|
|
|
|
// Read packet ID VarInt
|
|
let decoded_packet_id = decode_varint(&mut buffer).map_err(|e| e.to_string())?;
|
|
assert_eq!(
|
|
decoded_packet_id,
|
|
CStatusResponse::to_id(JavaMinecraftVersion::V_1_21_11)
|
|
);
|
|
|
|
// Remaining buffer is the payload (empty)
|
|
let mut expected_payload = Vec::new();
|
|
packet.write_packet_data(&mut expected_payload, &JavaMinecraftVersion::V_1_21_11)?;
|
|
|
|
assert_eq!(
|
|
buffer.len(),
|
|
expected_payload.len(),
|
|
"Payload length mismatch"
|
|
);
|
|
assert_eq!(buffer, expected_payload);
|
|
Ok(())
|
|
}
|
|
|
|
/// Test encoding with maximum length payload
|
|
#[tokio::test]
|
|
async fn encode_with_maximum_string_length() -> Result<(), Box<dyn std::error::Error>> {
|
|
// Maximum allowed string length is 32767 bytes
|
|
let max_string_length = 32767;
|
|
let payload_str = "A".repeat(max_string_length);
|
|
let packet = CStatusResponse::new(payload_str);
|
|
|
|
// Build the packet without compression and encryption
|
|
let packet_bytes = build_packet_with_encoder(&packet, None, None).await?;
|
|
|
|
// Verify that the packet size does not exceed MAX_PACKET_SIZE as usize
|
|
assert!(
|
|
packet_bytes.len() <= MAX_PACKET_SIZE as usize,
|
|
"Packet size exceeds maximum allowed size"
|
|
);
|
|
|
|
// Decode the packet manually to verify correctness
|
|
let mut buffer = &packet_bytes[..];
|
|
|
|
// Read packet length VarInt
|
|
let packet_length = decode_varint(&mut buffer).map_err(|e| e.to_string())?;
|
|
assert_eq!(
|
|
packet_length as usize,
|
|
buffer.len(),
|
|
"Packet length mismatch"
|
|
);
|
|
|
|
// Read packet ID VarInt
|
|
let decoded_packet_id = decode_varint(&mut buffer).map_err(|e| e.to_string())?;
|
|
// Assume packet ID is 0 for CStatusResponse
|
|
assert_eq!(
|
|
decoded_packet_id,
|
|
CStatusResponse::to_id(JavaMinecraftVersion::V_1_21_11)
|
|
);
|
|
|
|
// Remaining buffer is the payload
|
|
let mut expected_payload = Vec::new();
|
|
packet.write_packet_data(&mut expected_payload, &JavaMinecraftVersion::V_1_21_11)?;
|
|
|
|
assert_eq!(buffer, expected_payload);
|
|
Ok(())
|
|
}
|
|
|
|
/// Test encoding a packet that exceeds `MAX_PACKET_SIZE` as usize
|
|
#[tokio::test]
|
|
async fn encode_packet_exceeding_maximum_size() -> Result<(), Box<dyn std::error::Error>> {
|
|
// Create a custom packet with data exceeding MAX_PACKET_SIZE as usize
|
|
let data_size = MAX_PACKET_SIZE as usize + 1; // Exceed by 1 byte
|
|
let packet = MaxSizePacket::new(data_size);
|
|
|
|
// Build the packet without compression and encryption
|
|
// This should return PacketEncodeError::TooLong
|
|
let result = build_packet_with_encoder(&packet, None, None).await;
|
|
assert!(result.is_err());
|
|
// We can't easily check for TooLong since it's boxed, but we verified it returns an error
|
|
Ok(())
|
|
}
|
|
|
|
/// Test encoding with a small payload that should not be compressed
|
|
#[tokio::test]
|
|
async fn encode_small_payload_no_compression() -> Result<(), Box<dyn std::error::Error>> {
|
|
// Create a CStatusResponse packet with small payload
|
|
let packet = CStatusResponse::new(String::from("Hi"));
|
|
|
|
// Build the packet with compression enabled
|
|
// Compression threshold is set to a value higher than payload length
|
|
let packet_bytes = build_packet_with_encoder(&packet, Some((10, 6)), None).await?;
|
|
|
|
// Decode the packet manually to verify that it was not compressed
|
|
let mut buffer = &packet_bytes[..];
|
|
|
|
// Read packet length VarInt
|
|
let packet_length = decode_varint(&mut buffer).map_err(|e| e.to_string())?;
|
|
assert_eq!(
|
|
packet_length as usize,
|
|
buffer.len(),
|
|
"Packet length mismatch"
|
|
);
|
|
|
|
// Read data length VarInt (should be 0 indicating no compression)
|
|
let data_length = decode_varint(&mut buffer).map_err(|e| e.to_string())?;
|
|
assert_eq!(
|
|
data_length, 0,
|
|
"Data length should be 0 indicating no compression"
|
|
);
|
|
|
|
// Read packet ID VarInt
|
|
let decoded_packet_id = decode_varint(&mut buffer).map_err(|e| e.to_string())?;
|
|
assert_eq!(
|
|
decoded_packet_id,
|
|
CStatusResponse::to_id(JavaMinecraftVersion::V_1_21_11)
|
|
);
|
|
|
|
// Remaining buffer is the payload
|
|
let mut expected_payload = Vec::new();
|
|
packet.write_packet_data(&mut expected_payload, &JavaMinecraftVersion::V_1_21_11)?;
|
|
|
|
assert_eq!(buffer, expected_payload);
|
|
Ok(())
|
|
}
|
|
}
|