This commit is contained in:
Alexander Medvedev 2026-04-12 21:41:57 +02:00
parent 2c338f468c
commit 5e616a185e
4 changed files with 287 additions and 246 deletions

View File

@ -1,55 +1,93 @@
use std::{ffi::CString, io::Cursor}; use std::{ffi::CString, fmt, io::Cursor};
use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::io::AsyncReadExt;
// Padding bytes are fixed by the vanilla server implementation.
// Some query checkers (incorrectly) depend on these exact bytes.
const PADDING_START: [u8; 11] = [ const PADDING_START: [u8; 11] = [
0x73, 0x70, 0x6C, 0x69, 0x74, 0x6E, 0x75, 0x6D, 0x00, 0x80, 0x00, 0x73, 0x70, 0x6C, 0x69, 0x74, 0x6E, 0x75, 0x6D, 0x00, 0x80, 0x00,
]; ];
// 11 bytes: 10 padding bytes + 1 null terminator for the key-value section.
const PADDING_END: [u8; 11] = [ const PADDING_END: [u8; 11] = [
0x00, 0x01, 0x70, 0x6C, 0x61, 0x79, 0x65, 0x72, 0x5F, 0x00, 0x00, 0x00, 0x01, 0x70, 0x6C, 0x61, 0x79, 0x65, 0x72, 0x5F, 0x00, 0x00,
]; ];
/// The magic number that all Query protocol packets start with.
const MAGIC: u16 = 0xFEFD;
#[derive(Debug, PartialEq, Eq)]
pub enum DecodeError {
/// The byte stream ended before a complete packet could be read.
UnexpectedEof,
/// The first two bytes were not the expected magic value (0xFEFD).
BadMagic,
/// The packet type byte does not correspond to a known type.
UnknownPacketType(u8),
/// The packet was structurally valid but contained an unexpected payload length.
MalformedPayload,
}
impl fmt::Display for DecodeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnexpectedEof => write!(f, "unexpected end of packet"),
Self::BadMagic => write!(f, "bad magic bytes (expected 0xFEFD)"),
Self::UnknownPacketType(t) => write!(f, "unknown packet type: {t:#04x}"),
Self::MalformedPayload => write!(f, "malformed packet payload"),
}
}
}
impl std::error::Error for DecodeError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PacketType { pub enum PacketType {
// There could be other types, but they are not documented.
// Besides, these types are enough to get the server status.
Handshake = 9, Handshake = 9,
Status = 0, Status = 0,
} }
pub struct InvalidPacketType;
impl TryFrom<u8> for PacketType { impl TryFrom<u8> for PacketType {
type Error = InvalidPacketType; type Error = DecodeError;
fn try_from(value: u8) -> Result<Self, Self::Error> { fn try_from(value: u8) -> Result<Self, Self::Error> {
match value { match value {
9 => Ok(Self::Handshake), 9 => Ok(Self::Handshake),
0 => Ok(Self::Status), 0 => Ok(Self::Status),
_ => Err(InvalidPacketType), other => Err(DecodeError::UnknownPacketType(other)),
} }
} }
} }
/// A partially-decoded packet: magic verified, type extracted, payload buffered.
#[derive(Debug, PartialEq, Eq)]
pub struct RawQueryPacket { pub struct RawQueryPacket {
pub packet_type: PacketType, pub packet_type: PacketType,
reader: Cursor<Vec<u8>>, reader: Cursor<Vec<u8>>,
} }
impl RawQueryPacket { impl RawQueryPacket {
pub async fn decode(bytes: Vec<u8>) -> Result<Self, ()> { pub async fn decode(bytes: Vec<u8>) -> Result<Self, DecodeError> {
let mut reader = Cursor::new(bytes); let mut reader = Cursor::new(bytes);
match reader.read_u16().await.map_err(|_| ())? { let magic = reader
// Magic should always equal 65277 since it denotes the protocol being used. .read_u16()
// We should not attempt to decode packets with other magic values. .await
65277 => Ok(Self { .map_err(|_| DecodeError::UnexpectedEof)?;
packet_type: PacketType::try_from(reader.read_u8().await.map_err(|_| ())?) if magic != MAGIC {
.map_err(|_| ())?, return Err(DecodeError::BadMagic);
reader,
}),
_ => Err(()),
} }
let type_byte = reader
.read_u8()
.await
.map_err(|_| DecodeError::UnexpectedEof)?;
let packet_type = PacketType::try_from(type_byte)?;
Ok(Self {
packet_type,
reader,
})
} }
} }
@ -59,10 +97,13 @@ pub struct SHandshake {
} }
impl SHandshake { impl SHandshake {
pub async fn decode(packet: &mut RawQueryPacket) -> Result<Self, ()> { pub async fn decode(packet: &mut RawQueryPacket) -> Result<Self, DecodeError> {
Ok(Self { let session_id = packet
session_id: packet.reader.read_i32().await.map_err(|_| ())?, .reader
}) .read_i32()
.await
.map_err(|_| DecodeError::UnexpectedEof)?;
Ok(Self { session_id })
} }
} }
@ -70,137 +111,119 @@ impl SHandshake {
pub struct SStatusRequest { pub struct SStatusRequest {
pub session_id: i32, pub session_id: i32,
pub challenge_token: i32, pub challenge_token: i32,
// A full status request and a basic status request are pretty similar, /// `true` for a full-status request (payload padded to 8 extra bytes),
// so we might as well just use the same struct. /// `false` for a basic-status request (no padding).
pub is_full_request: bool, pub is_full_request: bool,
} }
impl SStatusRequest { impl SStatusRequest {
pub async fn decode(packet: &mut RawQueryPacket) -> Result<Self, ()> { pub async fn decode(packet: &mut RawQueryPacket) -> Result<Self, DecodeError> {
Ok(Self { let session_id = packet
session_id: packet.reader.read_i32().await.map_err(|_| ())?, .reader
challenge_token: packet.reader.read_i32().await.map_err(|_| ())?, .read_i32()
is_full_request: { .await
let mut buf = [0; 4]; .map_err(|_| DecodeError::UnexpectedEof)?;
let challenge_token = packet
.reader
.read_i32()
.await
.map_err(|_| DecodeError::UnexpectedEof)?;
// If payload is padded to 8 bytes, the client is requesting full status response let mut buf = [0u8; 4];
// In other terms, check if there are 4 extra bytes at the end. let is_full_request = match packet.reader.read(&mut buf).await {
// The extra bytes should be meaningless.
// Otherwise, the client is requesting basic status response.
match packet.reader.read(&mut buf).await {
Ok(0) => false, Ok(0) => false,
Ok(4) => true, Ok(4) => true,
_ => { // Any other byte count is malformed.
// Just ignore malformed packets or errors _ => return Err(DecodeError::MalformedPayload),
return Err(()); };
}
} Ok(Self {
}, session_id,
challenge_token,
is_full_request,
}) })
} }
} }
#[derive(Debug)]
pub struct CHandshake { pub struct CHandshake {
pub session_id: i32, pub session_id: i32,
// For simplicity, use a number type. /// Encoded as a decimal string in the wire format.
// It should be encoded as string here;
// it will be converted in encoding.
pub challenge_token: i32, pub challenge_token: i32,
} }
impl CHandshake { impl CHandshake {
pub async fn encode(&self) -> Vec<u8> { #[must_use]
let mut buf = Vec::new(); pub fn encode(&self) -> Vec<u8> {
let token = CString::new(self.challenge_token.to_string())
.expect("challenge_token string contains no interior nul bytes");
// Packet Type let mut buf = Vec::with_capacity(6 + token.as_bytes_with_nul().len());
buf.write_u8(9).await.unwrap(); buf.push(PacketType::Handshake as u8);
// Session ID buf.extend_from_slice(&self.session_id.to_be_bytes());
buf.write_i32(self.session_id).await.unwrap();
// Challenge token
// Use CString to add null terminator and ensure no null bytes are in the middle of the data
// Unwrap here since there should be no errors with nulls in the middle of data
let token = CString::new(self.challenge_token.to_string()).unwrap();
buf.extend_from_slice(token.as_bytes_with_nul()); buf.extend_from_slice(token.as_bytes_with_nul());
buf buf
} }
} }
#[derive(Debug)]
pub struct CBasicStatus { pub struct CBasicStatus {
pub session_id: i32, pub session_id: i32,
// Use CString, as the protocol requires nul terminated strings
pub motd: CString, pub motd: CString,
// Game type is hardcoded
pub map: CString, pub map: CString,
pub num_players: usize, pub num_players: usize,
pub max_players: usize, pub max_players: usize,
/// Little-endian on the wire (Notchian quirk).
pub host_port: u16, pub host_port: u16,
pub host_ip: CString, pub host_ip: CString,
} }
impl CBasicStatus { impl CBasicStatus {
pub async fn encode(&self) -> Vec<u8> { #[must_use]
let mut buf = Vec::new(); pub fn encode(&self) -> Vec<u8> {
// Packet Type
buf.write_u8(0).await.unwrap();
// Session ID
buf.write_i32(self.session_id).await.unwrap();
// MOTD
buf.extend_from_slice(self.motd.as_bytes_with_nul());
// Game Type
let game_type = CString::new("SMP").unwrap(); let game_type = CString::new("SMP").unwrap();
buf.extend_from_slice(game_type.as_bytes_with_nul());
// Map
buf.extend_from_slice(self.map.as_bytes_with_nul());
// Num players
let num_players = CString::new(self.num_players.to_string()).unwrap(); let num_players = CString::new(self.num_players.to_string()).unwrap();
buf.extend_from_slice(num_players.as_bytes_with_nul());
// Max players
let max_players = CString::new(self.max_players.to_string()).unwrap(); let max_players = CString::new(self.max_players.to_string()).unwrap();
buf.extend_from_slice(max_players.as_bytes_with_nul());
// Port
// No idea why the port needs to be in little endian
buf.write_u16_le(self.host_port).await.unwrap();
// IP
buf.extend_from_slice(self.host_ip.as_bytes_with_nul());
let mut buf = Vec::new();
buf.push(PacketType::Status as u8);
buf.extend_from_slice(&self.session_id.to_be_bytes());
buf.extend_from_slice(self.motd.as_bytes_with_nul());
buf.extend_from_slice(game_type.as_bytes_with_nul());
buf.extend_from_slice(self.map.as_bytes_with_nul());
buf.extend_from_slice(num_players.as_bytes_with_nul());
buf.extend_from_slice(max_players.as_bytes_with_nul());
// The port is written little-endian — this is a known Notchian quirk.
buf.extend_from_slice(&self.host_port.to_le_bytes());
buf.extend_from_slice(self.host_ip.as_bytes_with_nul());
buf buf
} }
} }
#[derive(Debug)]
pub struct CFullStatus { pub struct CFullStatus {
pub session_id: i32, pub session_id: i32,
pub hostname: CString, pub hostname: CString,
// Game type and game id are hardcoded into the protocol.
// They are not here as they cannot be changed.
pub version: CString, pub version: CString,
pub plugins: CString, pub plugins: CString,
pub map: CString, pub map: CString,
pub num_players: usize, pub num_players: usize,
pub max_players: usize, pub max_players: usize,
/// Little-endian on the wire (Notchian quirk).
pub host_port: u16, pub host_port: u16,
pub host_ip: CString, pub host_ip: CString,
pub players: Vec<CString>, pub players: Vec<CString>,
} }
impl CFullStatus { impl CFullStatus {
pub async fn encode(&self) -> Vec<u8> { #[must_use]
pub fn encode(&self) -> Vec<u8> {
let mut buf = Vec::new(); let mut buf = Vec::new();
buf.push(PacketType::Status as u8);
buf.extend_from_slice(&self.session_id.to_be_bytes());
buf.extend_from_slice(&PADDING_START);
// Packet type let kv_pairs: &[(&str, &CString)] = &[
buf.write_u8(0).await.unwrap();
// Session ID
buf.write_i32(self.session_id).await.unwrap();
// Padding (11 bytes, meaningless)
// This is the padding used by vanilla.
// Although meaningless, it seems in testing some query checkers depend on these bytes?
buf.extend_from_slice(PADDING_START.as_slice());
// Key-value pairs
// Keys will not error when encoding as CString
for (key, value) in [
("hostname", &self.hostname), ("hostname", &self.hostname),
("gametype", &CString::new("SMP").unwrap()), ("gametype", &CString::new("SMP").unwrap()),
("game_id", &CString::new("MINECRAFT").unwrap()), ("game_id", &CString::new("MINECRAFT").unwrap()),
@ -220,80 +243,77 @@ impl CFullStatus {
&CString::new(self.host_port.to_string()).unwrap(), &CString::new(self.host_port.to_string()).unwrap(),
), ),
("hostip", &self.host_ip), ("hostip", &self.host_ip),
] { ];
buf.extend_from_slice(CString::new(key).unwrap().as_bytes_with_nul());
for (key, value) in kv_pairs {
buf.extend_from_slice(CString::new(*key).unwrap().as_bytes_with_nul());
buf.extend_from_slice(value.as_bytes_with_nul()); buf.extend_from_slice(value.as_bytes_with_nul());
} }
// Padding (10 bytes, meaningless), with one extra 0x00 for the extra required null terminator after the key-value section // End padding + null terminator for the key-value section.
buf.extend_from_slice(&PADDING_END);
buf.extend_from_slice(PADDING_END.as_slice()); // Player list, each null-terminated, followed by a final null.
// Players
for player in &self.players { for player in &self.players {
buf.extend_from_slice(player.as_bytes_with_nul()); buf.extend_from_slice(player.as_bytes_with_nul());
} }
// Required extra null terminator buf.push(0x00);
buf.write_u8(0).await.unwrap();
buf buf
} }
} }
// All test bytes/packets are from protocol documentation #[cfg(test)]
#[tokio::test] mod tests {
async fn handshake_request() { use super::*;
#[tokio::test]
async fn handshake_request() {
let bytes = vec![0xFE, 0xFD, 0x09, 0x00, 0x00, 0x00, 0x01]; let bytes = vec![0xFE, 0xFD, 0x09, 0x00, 0x00, 0x00, 0x01];
let mut raw_packet = RawQueryPacket::decode(bytes).await.unwrap(); let mut raw = RawQueryPacket::decode(bytes).await.unwrap();
let packet = SHandshake::decode(&mut raw_packet).await.unwrap(); assert_eq!(raw.packet_type, PacketType::Handshake);
let pkt = SHandshake::decode(&mut raw).await.unwrap();
assert_eq!(pkt, SHandshake { session_id: 1 });
}
// What the decoded packet should look like #[tokio::test]
let actual_packet = SHandshake { session_id: 1 }; async fn handshake_response() {
let expected = vec![
assert_eq!(packet, actual_packet);
}
#[tokio::test]
async fn handshake_response() {
let bytes = vec![
0x09, 0x00, 0x00, 0x00, 0x01, 0x39, 0x35, 0x31, 0x33, 0x33, 0x30, 0x37, 0x00, 0x09, 0x00, 0x00, 0x00, 0x01, 0x39, 0x35, 0x31, 0x33, 0x33, 0x30, 0x37, 0x00,
]; ];
let pkt = CHandshake {
let packet = CHandshake {
session_id: 1, session_id: 1,
challenge_token: 9513307, challenge_token: 9513307,
}; };
assert_eq!(pkt.encode(), expected);
}
assert_eq!(bytes, packet.encode().await); #[tokio::test]
} async fn basic_stat_request() {
#[tokio::test]
async fn basic_stat_request() {
let bytes = vec![ let bytes = vec![
0xFE, 0xFD, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x91, 0x29, 0x5B, 0xFE, 0xFD, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x91, 0x29, 0x5B,
]; ];
let mut raw_packet = RawQueryPacket::decode(bytes).await.unwrap(); let mut raw = RawQueryPacket::decode(bytes).await.unwrap();
let packet = SStatusRequest::decode(&mut raw_packet).await.unwrap(); let pkt = SStatusRequest::decode(&mut raw).await.unwrap();
assert_eq!(
let actual_packet = SStatusRequest { pkt,
SStatusRequest {
session_id: 1, session_id: 1,
challenge_token: 9513307, challenge_token: 9513307,
is_full_request: false, is_full_request: false
}; }
);
}
assert_eq!(packet, actual_packet); #[tokio::test]
} async fn basic_stat_response() {
let expected = vec![
#[tokio::test] 0x00, 0x00, 0x00, 0x00, 0x01, 0x41, 0x20, 0x4D, 0x69, 0x6E, 0x65, 0x63, 0x72, 0x61,
async fn basic_stat_response() { 0x66, 0x74, 0x20, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x00, 0x53, 0x4D, 0x50, 0x00,
let bytes = vec![ 0x77, 0x6F, 0x72, 0x6C, 0x64, 0x00, 0x32, 0x00, 0x32, 0x30, 0x00, 0xDD, 0x63, 0x31,
0x00, 0x00, 0x00, 0x00, 0x01, 0x41, 0x20, 0x4D, 0x69, 0x6E, 0x65, 0x63, 0x72, 0x61, 0x66, 0x32, 0x37, 0x2E, 0x30, 0x2E, 0x30, 0x2E, 0x31, 0x00,
0x74, 0x20, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x00, 0x53, 0x4D, 0x50, 0x00, 0x77, 0x6F,
0x72, 0x6C, 0x64, 0x00, 0x32, 0x00, 0x32, 0x30, 0x00, 0xDD, 0x63, 0x31, 0x32, 0x37, 0x2E,
0x30, 0x2E, 0x30, 0x2E, 0x31, 0x00,
]; ];
let pkt = CBasicStatus {
let packet = CBasicStatus {
session_id: 1, session_id: 1,
motd: CString::new("A Minecraft Server").unwrap(), motd: CString::new("A Minecraft Server").unwrap(),
map: CString::new("world").unwrap(), map: CString::new("world").unwrap(),
@ -302,47 +322,48 @@ async fn basic_stat_response() {
host_port: 25565, host_port: 25565,
host_ip: CString::new("127.0.0.1").unwrap(), host_ip: CString::new("127.0.0.1").unwrap(),
}; };
assert_eq!(pkt.encode(), expected);
}
assert_eq!(bytes, packet.encode().await); #[tokio::test]
} async fn full_stat_request() {
#[tokio::test]
async fn full_stat_request() {
let bytes = vec![ let bytes = vec![
0xFE, 0xFD, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x91, 0x29, 0x5B, 0x00, 0x00, 0x00, 0x00, 0xFE, 0xFD, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x91, 0x29, 0x5B, 0x00, 0x00, 0x00,
0x00,
]; ];
let mut raw_packet = RawQueryPacket::decode(bytes).await.unwrap(); let mut raw = RawQueryPacket::decode(bytes).await.unwrap();
let packet = SStatusRequest::decode(&mut raw_packet).await.unwrap(); let pkt = SStatusRequest::decode(&mut raw).await.unwrap();
assert_eq!(
let actual_packet = SStatusRequest { pkt,
SStatusRequest {
session_id: 1, session_id: 1,
challenge_token: 9513307, challenge_token: 9513307,
is_full_request: true, is_full_request: true
}; }
);
}
assert_eq!(packet, actual_packet); #[tokio::test]
} async fn full_stat_response() {
#[tokio::test] let expected = vec![
async fn full_stat_response() { 0x00, 0x00, 0x00, 0x00, 0x01, 0x73, 0x70, 0x6C, 0x69, 0x74, 0x6E, 0x75, 0x6D, 0x00,
let bytes = vec![ 0x80, 0x00, 0x68, 0x6F, 0x73, 0x74, 0x6E, 0x61, 0x6D, 0x65, 0x00, 0x41, 0x20, 0x4D,
0x00, 0x00, 0x00, 0x00, 0x01, 0x73, 0x70, 0x6C, 0x69, 0x74, 0x6E, 0x75, 0x6D, 0x00, 0x80, 0x69, 0x6E, 0x65, 0x63, 0x72, 0x61, 0x66, 0x74, 0x20, 0x53, 0x65, 0x72, 0x76, 0x65,
0x00, 0x68, 0x6F, 0x73, 0x74, 0x6E, 0x61, 0x6D, 0x65, 0x00, 0x41, 0x20, 0x4D, 0x69, 0x6E, 0x72, 0x00, 0x67, 0x61, 0x6D, 0x65, 0x74, 0x79, 0x70, 0x65, 0x00, 0x53, 0x4D, 0x50,
0x65, 0x63, 0x72, 0x61, 0x66, 0x74, 0x20, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x00, 0x67, 0x00, 0x67, 0x61, 0x6D, 0x65, 0x5F, 0x69, 0x64, 0x00, 0x4D, 0x49, 0x4E, 0x45, 0x43,
0x61, 0x6D, 0x65, 0x74, 0x79, 0x70, 0x65, 0x00, 0x53, 0x4D, 0x50, 0x00, 0x67, 0x61, 0x6D, 0x52, 0x41, 0x46, 0x54, 0x00, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x00, 0x42,
0x65, 0x5F, 0x69, 0x64, 0x00, 0x4D, 0x49, 0x4E, 0x45, 0x43, 0x52, 0x41, 0x46, 0x54, 0x00, 0x65, 0x74, 0x61, 0x20, 0x31, 0x2E, 0x39, 0x20, 0x50, 0x72, 0x65, 0x72, 0x65, 0x6C,
0x76, 0x65, 0x72, 0x73, 0x69, 0x6F, 0x6E, 0x00, 0x42, 0x65, 0x74, 0x61, 0x20, 0x31, 0x2E, 0x65, 0x61, 0x73, 0x65, 0x20, 0x34, 0x00, 0x70, 0x6C, 0x75, 0x67, 0x69, 0x6E, 0x73,
0x39, 0x20, 0x50, 0x72, 0x65, 0x72, 0x65, 0x6C, 0x65, 0x61, 0x73, 0x65, 0x20, 0x34, 0x00, 0x00, 0x00, 0x6D, 0x61, 0x70, 0x00, 0x77, 0x6F, 0x72, 0x6C, 0x64, 0x00, 0x6E, 0x75,
0x70, 0x6C, 0x75, 0x67, 0x69, 0x6E, 0x73, 0x00, 0x00, 0x6D, 0x61, 0x70, 0x00, 0x77, 0x6F, 0x6D, 0x70, 0x6C, 0x61, 0x79, 0x65, 0x72, 0x73, 0x00, 0x32, 0x00, 0x6D, 0x61, 0x78,
0x72, 0x6C, 0x64, 0x00, 0x6E, 0x75, 0x6D, 0x70, 0x6C, 0x61, 0x79, 0x65, 0x72, 0x73, 0x00, 0x70, 0x6C, 0x61, 0x79, 0x65, 0x72, 0x73, 0x00, 0x32, 0x30, 0x00, 0x68, 0x6F, 0x73,
0x32, 0x00, 0x6D, 0x61, 0x78, 0x70, 0x6C, 0x61, 0x79, 0x65, 0x72, 0x73, 0x00, 0x32, 0x30, 0x74, 0x70, 0x6F, 0x72, 0x74, 0x00, 0x32, 0x35, 0x35, 0x36, 0x35, 0x00, 0x68, 0x6F,
0x00, 0x68, 0x6F, 0x73, 0x74, 0x70, 0x6F, 0x72, 0x74, 0x00, 0x32, 0x35, 0x35, 0x36, 0x35, 0x73, 0x74, 0x69, 0x70, 0x00, 0x31, 0x32, 0x37, 0x2E, 0x30, 0x2E, 0x30, 0x2E, 0x31,
0x00, 0x68, 0x6F, 0x73, 0x74, 0x69, 0x70, 0x00, 0x31, 0x32, 0x37, 0x2E, 0x30, 0x2E, 0x30, 0x00, 0x00, 0x01, 0x70, 0x6C, 0x61, 0x79, 0x65, 0x72, 0x5F, 0x00, 0x00, 0x62, 0x61,
0x2E, 0x31, 0x00, 0x00, 0x01, 0x70, 0x6C, 0x61, 0x79, 0x65, 0x72, 0x5F, 0x00, 0x00, 0x62, 0x72, 0x6E, 0x65, 0x79, 0x67, 0x61, 0x6C, 0x65, 0x00, 0x56, 0x69, 0x76, 0x61, 0x6C,
0x61, 0x72, 0x6E, 0x65, 0x79, 0x67, 0x61, 0x6C, 0x65, 0x00, 0x56, 0x69, 0x76, 0x61, 0x6C,
0x61, 0x68, 0x65, 0x6C, 0x76, 0x69, 0x67, 0x00, 0x00, 0x61, 0x68, 0x65, 0x6C, 0x76, 0x69, 0x67, 0x00, 0x00,
]; ];
let pkt = CFullStatus {
let packet = CFullStatus {
session_id: 1, session_id: 1,
hostname: CString::new("A Minecraft Server").unwrap(), hostname: CString::new("A Minecraft Server").unwrap(),
version: CString::new("Beta 1.9 Prerelease 4").unwrap(), version: CString::new("Beta 1.9 Prerelease 4").unwrap(),
@ -357,6 +378,33 @@ async fn full_stat_response() {
CString::new("Vivalahelvig").unwrap(), CString::new("Vivalahelvig").unwrap(),
], ],
}; };
assert_eq!(pkt.encode(), expected);
}
assert_eq!(bytes, packet.encode().await); #[tokio::test]
async fn bad_magic_rejected() {
let bytes = vec![0xDE, 0xAD, 0x09, 0x00, 0x00, 0x00, 0x01];
assert_eq!(
RawQueryPacket::decode(bytes).await,
Err(DecodeError::BadMagic)
);
}
#[tokio::test]
async fn unknown_packet_type_rejected() {
let bytes = vec![0xFE, 0xFD, 0xFF];
assert_eq!(
RawQueryPacket::decode(bytes).await,
Err(DecodeError::UnknownPacketType(0xFF))
);
}
#[tokio::test]
async fn truncated_packet_rejected() {
let bytes = vec![0xFE, 0xFD];
assert_eq!(
RawQueryPacket::decode(bytes).await,
Err(DecodeError::UnexpectedEof)
);
}
} }

View File

@ -198,11 +198,9 @@ impl BlockBehaviour for PistonBlock {
let extended_pos = pos.offset(dir.to_offset()); let extended_pos = pos.offset(dir.to_offset());
if let Some(block_entity) = world.get_block_entity(&extended_pos).await { if let Some(block_entity) = world.get_block_entity(&extended_pos).await
let piston = block_entity && let Some(piston) = block_entity.as_any().downcast_ref::<PistonBlockEntity>()
.as_any() {
.downcast_ref::<PistonBlockEntity>()
.unwrap();
piston.finish(world.clone()).await; piston.finish(world.clone()).await;
} }
@ -243,17 +241,18 @@ impl BlockBehaviour for PistonBlock {
if sticky { if sticky {
let pull_pos = pos.offset_dir(dir.to_offset(), 2); let pull_pos = pos.offset_dir(dir.to_offset(), 2);
let (block, state) = world.get_block_and_state(&pull_pos).await; let (block, state) = world.get_block_and_state(&pull_pos).await;
let mut bl2 = false; let piston_piece = if block == &Block::MOVING_PISTON
if block == &Block::MOVING_PISTON
&& let Some(entity) = world.get_block_entity(&pull_pos).await && let Some(entity) = world.get_block_entity(&pull_pos).await
&& let Some(piston) = entity.as_any().downcast_ref::<PistonBlockEntity>()
&& piston.facing == dir
&& piston.extending
{ {
let piston = entity.as_any().downcast_ref::<PistonBlockEntity>().unwrap();
if piston.facing == dir && piston.extending {
piston.finish(world.clone()).await; piston.finish(world.clone()).await;
bl2 = true; true
} } else {
} false
if !bl2 { };
if !piston_piece {
if r#type == 1 if r#type == 1
&& !state.is_air() && !state.is_air()
&& Self::is_movable(block, state, dir, false, dir) && Self::is_movable(block, state, dir, false, dir)

View File

@ -106,9 +106,7 @@ async fn handle_packet(
// Ignore all errors since we don't want the query handler to crash // Ignore all errors since we don't want the query handler to crash
// Protocol also ignores all errors and just doesn't respond // Protocol also ignores all errors and just doesn't respond
let _ = socket let _ = socket.send_to(response.encode().as_slice(), addr).await;
.send_to(response.encode().await.as_slice(), addr)
.await;
clients.write().await.insert(challenge_token, addr); clients.write().await.insert(challenge_token, addr);
} }
@ -171,9 +169,7 @@ async fn handle_packet(
players, players,
}; };
let _ = socket let _ = socket.send_to(response.encode().as_slice(), addr).await;
.send_to(response.encode().await.as_slice(), addr)
.await;
} else { } else {
let response = CBasicStatus { let response = CBasicStatus {
session_id: packet.session_id, session_id: packet.session_id,
@ -191,9 +187,7 @@ async fn handle_packet(
host_ip: CString::new(bound_addr.ip().to_string())?, host_ip: CString::new(bound_addr.ip().to_string())?,
}; };
let _ = socket let _ = socket.send_to(response.encode().as_slice(), addr).await;
.send_to(response.encode().await.as_slice(), addr)
.await;
} }
} }
} }

View File

@ -411,7 +411,7 @@ impl DragonFight {
(p.x - cx).abs() < 2.0 && (p.z - cz).abs() < 2.0 (p.x - cx).abs() < 2.0 && (p.z - cz).abs() < 2.0
}); });
if let Some(e) = found { if let Some(e) = found {
ritual_uuids.push(e.get_entity().entity_uuid) ritual_uuids.push(e.get_entity().entity_uuid);
} else { } else {
debug!("Respawn attempt failed – missing crystal near {:?}.", check); debug!("Respawn attempt failed – missing crystal near {:?}.", check);
return; return;