Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions protocol/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
uuid = { version = "0.7", features = ["v4", "serde"] }
named-binary-tag = "0.2"
flate2 = "1.0.22"
1 change: 1 addition & 0 deletions protocol/src/data/chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,7 @@ impl Message {

impl_json_encoder_decoder!(Message);

#[derive(Debug)]
pub struct MessageBuilder {
current: Message,
root: Option<Message>,
Expand Down
2 changes: 2 additions & 0 deletions protocol/src/data/server_status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ pub struct ServerStatus {
pub version: ServerVersion,
pub players: OnlinePlayers,
pub description: Message,
#[serde(skip_serializing_if = "Option::is_none")]
pub favicon: Option<String>,
}

#[derive(Clone, Serialize, Deserialize, Debug)]
Expand Down
14 changes: 10 additions & 4 deletions protocol/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ pub enum EncodeError {
/// Max string length.
max_length: u16,
},
IOError {
IoError {
io_error: IoError,
},
JsonError {
Expand All @@ -24,7 +24,7 @@ pub enum EncodeError {

impl From<IoError> for EncodeError {
fn from(io_error: IoError) -> Self {
EncodeError::IOError { io_error }
EncodeError::IoError { io_error }
}
}

Expand All @@ -37,6 +37,11 @@ impl From<JsonError> for EncodeError {
/// Possible errors while decoding packet.
#[derive(Debug)]
pub enum DecodeError {
/// Packet is incomplete.
Incomplete {
/// Minimum number of bytes needed to complete the packet.
bytes_needed: usize,
},
/// Packet was not recognized. Invalid data or wrong protocol version.
UnknownPacketType {
type_id: u8,
Expand All @@ -48,7 +53,7 @@ pub enum DecodeError {
/// Max string length.
max_length: u16,
},
IOError {
IoError {
io_error: IoError,
},
JsonError {
Expand All @@ -73,11 +78,12 @@ pub enum DecodeError {
VarIntTooLong {
max_bytes: usize,
},
DecompressionError,
}

impl From<IoError> for DecodeError {
fn from(io_error: IoError) -> Self {
DecodeError::IOError { io_error }
DecodeError::IoError { io_error }
}
}

Expand Down
3 changes: 3 additions & 0 deletions protocol/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
//! This crate implements Minecraft protocol.
//!
//! Information about protocol can be found at https://wiki.vg/Protocol.
#![warn(missing_debug_implementations)]

pub mod data;
pub mod decoder;
pub mod encoder;
pub mod error;
pub mod packet;
pub mod version;

/// Protocol limits maximum string length.
Expand Down
190 changes: 190 additions & 0 deletions protocol/src/packet.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
use std::io::{self, Read, Write};

use flate2::read::ZlibDecoder;
use flate2::write::ZlibEncoder;
use flate2::Compression;
use minecraft_protocol_derive::{Decoder, Encoder};

use crate::decoder::{Decoder, DecoderReadExt};
use crate::encoder::{Encoder, EncoderWriteExt};
use crate::error::{DecodeError, EncodeError};

#[derive(Debug, Clone)]
pub struct Packet {
Copy link
Member

@vaIgarashi vaIgarashi Dec 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When you will intergrate protocol in a network library like tokio this structure would not be necessary. There are more steps in packet codec. Like encryption and compression.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When you will intergrate protocol in a network library like tokio this structure would not be necessary.

Do you have an example for how this works?

I am currently converting a tokio::net::TcpStream into std::net::TcpStream so I can use Packet::decode with it, so this would definitely be an improvement over what I have now.

pub id: i32,
pub data: Vec<u8>,
}

impl Packet {
pub fn encode<W: Write>(
self,
writer: &mut W,
compression_threshold: Option<i32>,
) -> Result<(), EncodeError> {
let mut buf = Vec::new();
let packet = RawPacket {
id: self.id,
data: self.data,
};
if let Some(threshold) = compression_threshold {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nth: match are more preferable when you use else branch

CompressedRawPacket { packet, threshold }.encode(&mut buf)?;
} else {
packet.encode(&mut buf)?;
}

writer.write_var_i32(buf.len() as i32)?;
writer.write_all(&buf)?;

Ok(())
}

pub fn decode<R: Read>(reader: &mut R, compressed: bool) -> Result<Packet, DecodeError> {
let len = match reader.read_var_i32() {
Ok(len) => len as usize,
Err(DecodeError::IoError { io_error })
if io_error.kind() == io::ErrorKind::UnexpectedEof =>
{
return Err(DecodeError::Incomplete { bytes_needed: 1 })
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not correct because read var int may vary from 1 to 4 bytes

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is used only as an indication, so it should be read as “at least 1”. If more are needed, it will happen in the next iteration.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it should be an Option so it works similar to https://docs.rs/nom/latest/nom/enum.Needed.html.

}
Err(err) => return Err(err.into()),
};

let mut buf = Vec::with_capacity(len);
let bytes_read = reader.take(len as u64).read_to_end(&mut buf)?;

if bytes_read != len {
return Err(DecodeError::Incomplete {
bytes_needed: len - bytes_read,
});
}

let RawPacket { id, data } = if compressed {
CompressedRawPacket::decode(&mut buf.as_slice())?
} else {
RawPacket::decode(&mut buf.as_slice())?
};

Ok(Self { id, data })
}
}

#[derive(Debug, Clone, Encoder, Decoder)]
struct RawPacket {
#[data_type(with = "var_int")]
pub id: i32,
#[data_type(with = "rest")]
pub data: Vec<u8>,
}

#[derive(Debug, Clone)]
struct CompressedRawPacket {
packet: RawPacket,
threshold: i32,
}

impl Encoder for CompressedRawPacket {
fn encode<W: Write>(&self, writer: &mut W) -> Result<(), EncodeError> {
let mut packet_buf = Vec::new();
self.packet.encode(&mut packet_buf)?;

let data_len = packet_buf.len() as i32;
if self.threshold >= 0 && data_len > self.threshold {
writer.write_var_i32(data_len)?;
let mut encoder = ZlibEncoder::new(writer, Compression::default());
encoder.write_all(&packet_buf)?;
encoder.finish()?;
} else {
writer.write_var_i32(0)?;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why zero?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://wiki.vg/Protocol#With_compression

If the size of the buffer containing the packet data and ID (as a VarInt) is smaller than the threshold specified in the packet Set Compression. It will be sent as uncompressed. This is done by setting the data length as 0. (Comparable to sending a non-compressed format with an extra 0 between the length, and packet data).

writer.write_all(&packet_buf)?;
};

Ok(())
}
}

impl Decoder for CompressedRawPacket {
type Output = RawPacket;

fn decode<R: Read>(reader: &mut R) -> Result<Self::Output, DecodeError> {
let data_len = reader.read_var_i32()? as usize;
let packet = if data_len == 0 {
RawPacket::decode(reader)?
} else {
let mut decompressed = Vec::with_capacity(data_len);
ZlibDecoder::new(reader).read_to_end(&mut decompressed)?;

if decompressed.len() != data_len {
return Err(DecodeError::DecompressionError);
}

RawPacket::decode(&mut decompressed.as_slice())?
};

Ok(packet)
}
}

#[cfg(test)]
mod tests {
use std::convert::TryInto;

use super::*;

use crate::version::v1_14_4::status::*;

const PING_REQUEST_BYTES: &'static [u8] =
include_bytes!("../test/packet/status/ping_request.dat");

fn ping_request_packet_bytes() -> Vec<u8> {
let len = (1 + PING_REQUEST_BYTES.len()).try_into().unwrap();
let mut vec = vec![len, 1];
vec.extend(PING_REQUEST_BYTES);
vec
}

#[test]
fn test_uncompressed_packet_encode() {
let ping_request = PingRequest {
time: 1577735845610,
};

let mut data = Vec::new();
ping_request.encode(&mut data).unwrap();

let packet = Packet { id: 1, data };

let mut vec = Vec::new();
packet.encode(&mut vec, None).unwrap();

assert_eq!(vec, ping_request_packet_bytes());
}

#[test]
fn test_uncompressed_packet_decode() {
let vec = ping_request_packet_bytes();
let packet = Packet::decode(&mut vec.as_slice(), false).unwrap();

assert_eq!(packet.id, 1);
assert_eq!(packet.data, PING_REQUEST_BYTES);
}

#[test]
fn test_compressed_packet_encode_decode() {
let ping_request = PingRequest {
time: 1577735845610,
};

let mut data = Vec::new();
ping_request.encode(&mut data).unwrap();

let packet = Packet { id: 1, data };

let mut vec = Vec::new();
packet.encode(&mut vec, Some(0)).unwrap();

let packet = Packet::decode(&mut vec.as_slice(), true).unwrap();

assert_eq!(packet.id, 1);
assert_eq!(packet.data, PING_REQUEST_BYTES);
}
}
6 changes: 4 additions & 2 deletions protocol/src/version/v1_14_4/game.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ use nbt::CompoundTag;
use std::io::Read;
use uuid::Uuid;

#[derive(Debug)]
pub enum GameServerBoundPacket {
ServerBoundChatMessage(ServerBoundChatMessage),
ServerBoundKeepAlive(ServerBoundKeepAlive),
ServerBoundAbilities(ServerBoundAbilities),
}

#[derive(Debug)]
pub enum GameClientBoundPacket {
ClientBoundChatMessage(ClientBoundChatMessage),
JoinGame(JoinGame),
Expand Down Expand Up @@ -177,7 +179,7 @@ impl JoinGame {
}
}

#[derive(Encoder, Decoder)]
#[derive(Encoder, Decoder, Debug)]
pub struct ServerBoundKeepAlive {
pub id: u64,
}
Expand All @@ -190,7 +192,7 @@ impl ServerBoundKeepAlive {
}
}

#[derive(Encoder, Decoder)]
#[derive(Encoder, Decoder, Debug)]
pub struct ClientBoundKeepAlive {
pub id: u64,
}
Expand Down
1 change: 1 addition & 0 deletions protocol/src/version/v1_14_4/handshake.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::error::DecodeError;
use minecraft_protocol_derive::{Decoder, Encoder};
use std::io::Read;

#[derive(Debug)]
pub enum HandshakeServerBoundPacket {
Handshake(Handshake),
}
Expand Down
2 changes: 2 additions & 0 deletions protocol/src/version/v1_14_4/login.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ use crate::decoder::Decoder;
use crate::error::DecodeError;
use minecraft_protocol_derive::{Decoder, Encoder};

#[derive(Debug)]
pub enum LoginServerBoundPacket {
LoginStart(LoginStart),
EncryptionResponse(EncryptionResponse),
LoginPluginResponse(LoginPluginResponse),
}

#[derive(Debug)]
pub enum LoginClientBoundPacket {
LoginDisconnect(LoginDisconnect),
EncryptionRequest(EncryptionRequest),
Expand Down
3 changes: 3 additions & 0 deletions protocol/src/version/v1_14_4/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ use crate::error::DecodeError;
use minecraft_protocol_derive::{Decoder, Encoder};
use std::io::Read;

#[derive(Debug)]
pub enum StatusServerBoundPacket {
StatusRequest,
PingRequest(PingRequest),
}

#[derive(Debug)]
pub enum StatusClientBoundPacket {
StatusResponse(StatusResponse),
PingResponse(PingResponse),
Expand Down Expand Up @@ -162,6 +164,7 @@ mod tests {
version,
description: Message::new(Payload::text("Description")),
players,
favicon: None,
};

let status_response = StatusResponse { server_status };
Expand Down