chore: more plugin api work

also added bedrock fuzzing
This commit is contained in:
Alexander Medvedev 2026-04-01 19:37:01 +02:00
parent 7bf248d16a
commit c15ebf181d
15 changed files with 625 additions and 137 deletions

15
Cargo.lock generated
View File

@ -2735,7 +2735,7 @@ dependencies = [
"tracing-subscriber",
"ureq",
"uuid",
"wasmparser 0.245.1",
"wasmparser 0.246.1",
"wasmtime",
"wasmtime-wasi",
]
@ -4352,6 +4352,19 @@ dependencies = [
"serde",
]
[[package]]
name = "wasmparser"
version = "0.246.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d991c35d79bf8336dc1cd632ed4aacf0dc5fac4bc466c670625b037b972bb9c"
dependencies = [
"bitflags",
"hashbrown 0.16.1",
"indexmap",
"semver",
"serde",
]
[[package]]
name = "wasmprinter"
version = "0.245.1"

View File

@ -181,7 +181,7 @@ ureq = "3.3.0"
wasmtime = "43.0"
wasmtime-wasi = "43.0"
wit-bindgen = "0.54"
wasmparser = "0.245"
wasmparser = "0.246"
postcard = { version = "1.1", features = ["alloc"] }
tracing-serde-structured = "0.4"

View File

@ -4,6 +4,16 @@ interface player {
use permission.{permission-level};
use text.{text-component};
record player-abilities {
invulnerable: bool,
flying: bool,
allow-flying: bool,
creative: bool,
allow-modify-world: bool,
fly-speed: f32,
walk-speed: f32,
}
resource player {
get-id: func () -> result<string, string>;
get-name: func () -> result<string, string>;
@ -45,8 +55,23 @@ interface player {
get-max-health: func () -> result<f32, string>;
get-food-level: func () -> result<u8, string>;
get-saturation: func () -> result<f32, string>;
get-experience-level: func () -> result<s32, string>;
get-experience-progress: func () -> result<f32, string>;
get-experience-points: func () -> result<s32, string>;
get-experience-level: func() -> result<s32, string>;
get-experience-progress: func() -> result<f32, string>;
get-experience-points: func() -> result<s32, string>;
set-experience-level: func(level: s32) -> result<_, string>;
set-experience-progress: func(progress: f32) -> result<_, string>;
set-experience-points: func(points: s32) -> result<_, string>;
add-experience-levels: func(levels: s32) -> result<_, string>;
add-experience-points: func(points: s32) -> result<_, string>;
is-sneaking: func() -> result<bool, string>;
is-sprinting: func() -> result<bool, string>;
is-on-ground: func() -> result<bool, string>;
is-flying: func() -> result<bool, string>;
set-flying: func(flying: bool) -> result<_, string>;
get-abilities: func() -> result<player-abilities, string>;
set-abilities: func(abilities: player-abilities) -> result<_, string>;
}
}

View File

@ -7,6 +7,16 @@ interface server {
}
resource server {
/// Returns the current difficulty level of the server
get-difficulty: func() -> difficulty;
/// Returns the total number of players connected across all worlds.
get-player-count: func() -> u32;
/// Returns the rolling average Milliseconds Per Tick (MSPT) over the last 100 ticks
get-mspt: func() -> f64;
/// Returns the effective Ticks Per Second (TPS)
get-tps: func() -> f64;
}
}

View File

@ -1,5 +1,49 @@
interface %world {
resource %world {
get-id: func () -> string;
enum piston-behavior {
normal,
destroy,
block,
ignore,
push-only
}
}
flags block-flags {
notify-neighbors,
notify-listeners,
force-state,
skip-drops,
moved,
skip-redstone-wire-state-replacement,
skip-block-entity-replaced-callback,
skip-block-added-callback,
}
record block-state {
id: u16,
luminance: u8,
opacity: u8,
hardness: f32,
is-air: bool,
is-liquid: bool,
is-solid: bool,
is-full-cube: bool,
piston-behavior: piston-behavior,
has-random-ticks: bool,
}
record block-pos {
x: s32,
y: s32,
z: s32,
}
resource %world {
get-id: func() -> string;
get-block-state-id: func(pos: block-pos) -> u16;
get-block-state: func(pos: block-pos) -> block-state;
set-block-state: func(pos: block-pos, state: u16, update-flags: block-flags);
}
}

View File

@ -33,9 +33,9 @@ checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
[[package]]
name = "async-compression"
version = "0.4.40"
version = "0.4.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d67d43201f4d20c78bcda740c142ca52482d81da80681533d33bf3f0596c8e2"
checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1"
dependencies = [
"compression-codecs",
"compression-core",
@ -289,9 +289,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crossfire"
version = "3.1.3"
version = "3.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4150133c3f558d3190b52a4d36b571e29c18a24e98d24d12201bed0c038986f8"
checksum = "e0042e53977a94d5d10de04ce7eb6016aa212c08e83ea202f7a9f102ff104303"
dependencies = [
"crossbeam-utils",
"futures-core",
@ -1039,9 +1039,19 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "pumpkin-codecs"
version = "0.1.0-dev+26.1"
dependencies = [
"dashmap",
"either",
"serde_json",
"tracing",
]
[[package]]
name = "pumpkin-config"
version = "0.1.0-dev+1.21.11"
version = "0.1.0-dev+26.1"
dependencies = [
"pumpkin-util",
"serde",
@ -1052,18 +1062,19 @@ dependencies = [
[[package]]
name = "pumpkin-data"
version = "0.1.0-dev+1.21.11"
version = "0.1.0-dev+26.1"
dependencies = [
"crc-fast",
"phf",
"pumpkin-nbt",
"pumpkin-util",
"rand",
"serde",
]
[[package]]
name = "pumpkin-macros"
version = "0.1.0-dev+1.21.11"
version = "0.1.0-dev+26.1"
dependencies = [
"proc-macro-error2",
"pumpkin-data",
@ -1073,18 +1084,20 @@ dependencies = [
[[package]]
name = "pumpkin-nbt"
version = "0.1.0-dev+1.21.11"
version = "0.1.0-dev+26.1"
dependencies = [
"bytes",
"cesu8",
"flate2",
"pumpkin-codecs",
"serde",
"thiserror",
"tracing",
]
[[package]]
name = "pumpkin-protocol"
version = "0.1.0-dev+1.21.11"
version = "0.1.0-dev+26.1"
dependencies = [
"aes",
"async-compression",
@ -1110,12 +1123,13 @@ version = "0.0.0"
dependencies = [
"libfuzzer-sys",
"pumpkin-protocol",
"pumpkin-util",
"tokio",
]
[[package]]
name = "pumpkin-util"
version = "0.1.0-dev+1.21.11"
version = "0.1.0-dev+26.1"
dependencies = [
"base64",
"bytes",
@ -1139,7 +1153,7 @@ dependencies = [
[[package]]
name = "pumpkin-world"
version = "0.1.0-dev+1.21.11"
version = "0.1.0-dev+26.1"
dependencies = [
"bitflags",
"bytes",
@ -1160,7 +1174,7 @@ dependencies = [
"rustc-hash",
"ruzstd",
"serde",
"sha2 0.11.0-rc.5",
"sha2 0.11.0",
"slotmap",
"thiserror",
"tokio",
@ -1231,9 +1245,9 @@ dependencies = [
[[package]]
name = "rustc-hash"
version = "2.1.1"
version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
[[package]]
name = "rustversion"
@ -1321,9 +1335,9 @@ dependencies = [
[[package]]
name = "serde_spanned"
version = "1.0.4"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776"
checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
dependencies = [
"serde_core",
]
@ -1341,12 +1355,12 @@ dependencies = [
[[package]]
name = "sha2"
version = "0.11.0-rc.5"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c5f3b1e2dc8aad28310d8410bd4d7e180eca65fca176c52ab00d364475d0024"
checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
dependencies = [
"cfg-if",
"cpufeatures 0.2.17",
"cpufeatures 0.3.0",
"digest 0.11.0",
]
@ -1486,9 +1500,9 @@ dependencies = [
[[package]]
name = "tokio"
version = "1.49.0"
version = "1.50.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d"
dependencies = [
"bytes",
"libc",
@ -1528,9 +1542,9 @@ dependencies = [
[[package]]
name = "toml"
version = "1.0.3+spec-1.1.0"
version = "1.1.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7614eaf19ad818347db24addfa201729cf2a9b6fdfd9eb0ab870fcacc606c0c"
checksum = "994b95d9e7bae62b34bab0e2a4510b801fa466066a6a8b2b57361fa1eba068ee"
dependencies = [
"indexmap",
"serde_core",
@ -1543,27 +1557,27 @@ dependencies = [
[[package]]
name = "toml_datetime"
version = "1.0.0+spec-1.1.0"
version = "1.1.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e"
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
dependencies = [
"serde_core",
]
[[package]]
name = "toml_parser"
version = "1.0.9+spec-1.1.0"
version = "1.1.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4"
checksum = "39ca317ebc49f06bd748bfba29533eac9485569dc9bf80b849024b025e814fb9"
dependencies = [
"winnow",
]
[[package]]
name = "toml_writer"
version = "1.0.6+spec-1.1.0"
version = "1.1.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607"
checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
[[package]]
name = "tracing"
@ -1632,9 +1646,9 @@ checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "uuid"
version = "1.21.0"
version = "1.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb"
checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9"
dependencies = [
"getrandom 0.4.1",
"js-sys",
@ -1843,9 +1857,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]]
name = "winnow"
version = "0.7.14"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5"
[[package]]
name = "wit-bindgen"

View File

@ -19,9 +19,19 @@ libfuzzer-sys = "0.4"
[dependencies.pumpkin-protocol]
path = ".."
[dependencies.pumpkin-util]
path = "../../pumpkin-util"
[[bin]]
name = "decoder_java"
path = "fuzz_targets/decoder_java.rs"
test = false
doc = false
bench = false
[[bin]]
name = "decoder_bedrock"
path = "fuzz_targets/decoder_bedrock.rs"
test = false
doc = false
bench = false

View File

@ -0,0 +1,97 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use pumpkin_protocol::bedrock::packet_decoder::UDPNetworkDecoder;
use pumpkin_protocol::bedrock::server::{
client_cache_status::SClientCacheStatus,
command_request::SCommandRequest,
container_close::SContainerClose,
interaction::SInteraction,
loading_screen::SLoadingScreen,
login::SLogin,
player_auth_input::SPlayerAuthInput,
raknet::{
connection::SConnectionRequest,
open_connection::{SOpenConnectionRequest1, SOpenConnectionRequest2},
unconnected_ping::SUnconnectedPing,
},
request_chunk_radius::SRequestChunkRadius,
request_network_settings::SRequestNetworkSettings,
text::SText,
};
use pumpkin_protocol::serial::PacketRead;
use std::io::Cursor;
// ---------------------------------------------------------------------------
// Helper: Run every Serverbound packet's read method.
// ---------------------------------------------------------------------------
fn fuzz_serverbound_packets(payload: &[u8]) {
let mut cursor = Cursor::new(payload);
macro_rules! run_read {
($($packet:ty),* $(,)?) => {
$(
cursor.set_position(0);
// Attempt to read as a standard Game Packet (with version)
let _ = <$packet>::read(&mut cursor);
)*
};
}
// Standard Bedrock Serverbound Packets
run_read!(
SClientCacheStatus,
SCommandRequest,
SContainerClose,
SInteraction,
SLoadingScreen,
SLogin,
SPlayerAuthInput,
SRequestChunkRadius,
SRequestNetworkSettings,
SText,
);
// RakNet Handshake Packets (Usually read without version)
cursor.set_position(0);
let _ = SConnectionRequest::read(&mut cursor);
cursor.set_position(0);
let _ = SOpenConnectionRequest1::read(&mut cursor);
cursor.set_position(0);
let _ = SOpenConnectionRequest2::read(&mut cursor);
cursor.set_position(0);
let _ = SUnconnectedPing::read(&mut cursor);
}
// ---------------------------------------------------------------------------
// Fuzz Target
// ---------------------------------------------------------------------------
fuzz_target!(|data: &[u8]| {
if data.len() < 20 {
return;
}
// Split data for decoder configuration vs raw payload
let threshold_raw = data[0];
let key: [u8; 16] = data[1..17].try_into().unwrap();
let stream_data = &data[17..];
let mut decoder = UDPNetworkDecoder::new();
// Setup Decoder
if threshold_raw > 0 {
// Assuming your CompressionThreshold is a wrapper around u32
decoder.set_compression((threshold_raw as u32).try_into().unwrap());
}
decoder.set_encryption(&key);
// 1. Fuzz the Decoder (Framing/VarInts/Bitmasks)
let decoder_cursor = Cursor::new(stream_data.to_vec());
if let Ok(raw_packet) = decoder.get_game_packet(decoder_cursor) {
// If framed correctly, fuzz the internal payload
fuzz_serverbound_packets(&raw_packet.payload);
}
// 2. Shotgun Fuzz
// Pass raw data directly to all readers to catch panics in the parsers
fuzz_serverbound_packets(stream_data);
});

View File

@ -1,96 +1,102 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use pumpkin_protocol::{
ServerPacket,
java::{
packet_decoder::TCPNetworkDecoder,
server::{
config::{
SClientInformationConfig, SConfigCookieResponse, SConfigResourcePack, SKnownPacks,
SPluginMessage,
},
handshake::SHandShake,
login::{SEncryptionResponse, SLoginCookieResponse, SLoginPluginResponse, SLoginStart},
play::{
SChangeGameMode, SChatCommand, SChatMessage, SChunkBatch, SClickSlot,
SClientCommand, SClientInformationPlay, SCloseContainer, SCommandSuggestion,
SConfirmTeleport, SCookieResponse, SCustomPayload, SInteract, SKeepAlive,
SMoveVehicle, SPaddleBoat, SPickItemFromBlock, SPlayPingRequest, SPlayerAbilities,
SPlayerAction, SPlayerCommand, SPlayerInput, SPlayerLoaded, SPlayerPosition,
SPlayerPositionRotation, SPlayerRotation, SPlayerSession, SSetCommandBlock,
SSetCreativeSlot, SSetHeldItem, SSetPlayerGround, SSwingArm, SUpdateSign, SUseItem,
SUseItemOn,
},
status::SStatusPingRequest,
use pumpkin_protocol::ServerPacket;
use pumpkin_protocol::java::{
packet_decoder::TCPNetworkDecoder,
server::{
config::{
SClientInformationConfig, SConfigCookieResponse, SConfigResourcePack, SKnownPacks,
SPluginMessage,
},
handshake::SHandShake,
login::{SEncryptionResponse, SLoginCookieResponse, SLoginPluginResponse, SLoginStart},
play::{
SChangeGameMode, SChatCommand, SChatMessage, SChunkBatch, SClickSlot, SClientCommand,
SClientInformationPlay, SCloseContainer, SCommandSuggestion, SConfirmTeleport,
SCookieResponse, SCustomPayload, SInteract, SKeepAlive, SMoveVehicle, SPaddleBoat,
SPickItemFromBlock, SPlayPingRequest, SPlayerAbilities, SPlayerAction, SPlayerCommand,
SPlayerInput, SPlayerLoaded, SPlayerPosition, SPlayerPositionRotation, SPlayerRotation,
SPlayerSession, SSetCommandBlock, SSetCreativeSlot, SSetHeldItem, SSetPlayerGround,
SSwingArm, SUpdateSign, SUseItem, SUseItemOn,
},
status::SStatusPingRequest,
},
};
use pumpkin_util::version::MinecraftVersion;
use std::io::Cursor;
use tokio::runtime::Runtime;
const TARGET_VERSION: MinecraftVersion = MinecraftVersion::V_26_1;
// ---------------------------------------------------------------------------
// Helper: run every known ServerPacket::read against the same payload.
// We don't care about the result — only that nothing panics.
// Uses a Cursor and the Version enum as required by the new signature.
// ---------------------------------------------------------------------------
fn fuzz_all_deserializers(payload: &[u8]) {
// Handshake
let _ = SHandShake::read(payload);
let mut cursor = Cursor::new(payload);
// Status
//let _ = SStatusRequest::read(payload);
let _ = SStatusPingRequest::read(payload);
macro_rules! run_read {
($($packet:ty),* $(,)?) => {
$(
cursor.set_position(0);
let _ = <$packet>::read(&mut cursor, &TARGET_VERSION);
)*
};
}
// Login
let _ = SLoginStart::read(payload);
let _ = SEncryptionResponse::read(payload);
let _ = SLoginPluginResponse::read(payload);
//let _ = SLoginAcknowledged::read(payload);
let _ = SLoginCookieResponse::read(payload);
// Config
let _ = SClientInformationConfig::read(payload);
let _ = SPluginMessage::read(payload);
//let _ = SAcknowledgeFinishConfig::read(payload);
let _ = SKnownPacks::read(payload);
let _ = SConfigCookieResponse::read(payload);
let _ = SConfigResourcePack::read(payload);
// Play
let _ = SConfirmTeleport::read(payload);
let _ = SChangeGameMode::read(payload);
let _ = SChatCommand::read(payload);
let _ = SChatMessage::read(payload);
let _ = SClientInformationPlay::read(payload);
let _ = SClientCommand::read(payload);
let _ = SPlayerInput::read(payload);
let _ = SMoveVehicle::read(payload);
let _ = SPaddleBoat::read(payload);
let _ = SInteract::read(payload);
let _ = SKeepAlive::read(payload);
let _ = SPlayerPosition::read(payload);
let _ = SPlayerPositionRotation::read(payload);
let _ = SPlayerRotation::read(payload);
let _ = SSetPlayerGround::read(payload);
let _ = SPickItemFromBlock::read(payload);
let _ = SPlayerAbilities::read(payload);
let _ = SPlayerAction::read(payload);
let _ = SSetCommandBlock::read(payload);
let _ = SPlayerCommand::read(payload);
let _ = SPlayerLoaded::read(payload);
let _ = SPlayPingRequest::read(payload);
let _ = SClickSlot::read(payload);
let _ = SSetHeldItem::read(payload);
let _ = SSetCreativeSlot::read(payload);
let _ = SSwingArm::read(payload);
let _ = SUpdateSign::read(payload);
let _ = SUseItemOn::read(payload);
let _ = SUseItem::read(payload);
let _ = SCommandSuggestion::read(payload);
let _ = SCookieResponse::read(payload);
let _ = SCloseContainer::read(payload);
let _ = SChunkBatch::read(payload);
let _ = SPlayerSession::read(payload);
let _ = SCustomPayload::read(payload);
run_read!(
// Handshake
SHandShake,
// Status
SStatusPingRequest,
// Login
SLoginStart,
SEncryptionResponse,
SLoginPluginResponse,
SLoginCookieResponse,
// Config
SClientInformationConfig,
SPluginMessage,
SKnownPacks,
SConfigCookieResponse,
SConfigResourcePack,
// Play
SConfirmTeleport,
SChangeGameMode,
SChatCommand,
SChatMessage,
SClientInformationPlay,
SClientCommand,
SPlayerInput,
SMoveVehicle,
SPaddleBoat,
SInteract,
SKeepAlive,
SPlayerPosition,
SPlayerPositionRotation,
SPlayerRotation,
SSetPlayerGround,
SPickItemFromBlock,
SPlayerAbilities,
SPlayerAction,
SSetCommandBlock,
SPlayerCommand,
SPlayerLoaded,
SPlayPingRequest,
SClickSlot,
SSetHeldItem,
SSetCreativeSlot,
SSwingArm,
SUpdateSign,
SUseItemOn,
SUseItem,
SCommandSuggestion,
SCookieResponse,
SCloseContainer,
SChunkBatch,
SPlayerSession,
SCustomPayload,
);
}
// ---------------------------------------------------------------------------
@ -101,13 +107,6 @@ fuzz_target!(|data: &[u8]| {
return;
}
// Byte layout:
// [0] = decoder mode (0=plain, 1=compressed, 2=encrypted, 3=both)
// [1] = split point % rest.len() — how much goes to the decoder vs
// the deserializers, letting the fuzzer explore both paths
// independently with different byte patterns
// [2..18] = 16-byte AES key candidate
// [18..] = raw bytes fed to both fuzzing paths
let mode = data[0] % 4;
let key = &data[2..18];
let rest = &data[18..];
@ -143,5 +142,6 @@ fuzz_target!(|data: &[u8]| {
let _ = decoder.get_raw_packet().await;
});
// --- Path 2: Individual Packet Deserializers ---------------------------
fuzz_all_deserializers(deser_bytes);
});

View File

@ -9,7 +9,8 @@ use bytes::Bytes;
use tokio::io::{AsyncRead, BufReader, ReadBuf};
use crate::{
Aes128Cfb8Dec, CompressionThreshold, PacketDecodeError, RawPacket, StreamDecryptor,
Aes128Cfb8Dec, CompressionThreshold, MAX_PACKET_SIZE, PacketDecodeError, RawPacket,
StreamDecryptor,
codec::var_uint::VarUInt,
ser::{NetworkReadExt, ReadingError},
};
@ -138,6 +139,9 @@ impl UDPNetworkDecoder {
})?;
let packet_len = u64::from(packet_len.0);
if !(0..=MAX_PACKET_SIZE).contains(&packet_len) {
Err(PacketDecodeError::OutOfBounds)?;
}
let var_header = VarUInt::decode(&mut reader)?;
@ -159,8 +163,17 @@ impl UDPNetworkDecoder {
// Gamepacket ID: Remaining 10 bits (bits 4 to 13)
let gamepacket_id = (header & 0x3FF) as u16; // 0x3FF is 10 bits set to 1
let header_size = var_header.written_size();
if (packet_len as usize) < header_size {
return Err(PacketDecodeError::MalformedLength(
"Packet length is smaller than the header size".to_string(),
));
}
let payload_len = packet_len as usize - header_size;
let payload = reader
.read_boxed_slice(packet_len as usize - var_header.written_size())
.read_boxed_slice(payload_len)
.map_err(|err| PacketDecodeError::FailedDecompression(err.to_string()))?;
Ok(RawPacket {

View File

@ -1,5 +1,5 @@
use pumpkin_macros::packet;
use std::io::{Error, Read};
use std::io::{Error, ErrorKind, Read};
use crate::{codec::var_uint::VarUInt, serial::PacketRead};
@ -15,15 +15,30 @@ pub struct SLogin {
impl PacketRead for SLogin {
fn read<R: Read>(reader: &mut R) -> Result<Self, Error> {
const MAX_TOKEN_SIZE: usize = 256 * 1024; // 256KB limit
let protocol_version = i32::read_be(reader)?;
let _len = VarUInt::read(reader)?;
let jwt_len = u32::read(reader)?;
let mut jwt = vec![0; jwt_len as _];
let jwt_len = u32::read_be(reader)? as usize;
if jwt_len > MAX_TOKEN_SIZE {
return Err(Error::new(
ErrorKind::InvalidData,
"JWT length exceeds limit",
));
}
let mut jwt = vec![0u8; jwt_len];
reader.read_exact(&mut jwt)?;
let raw_token_len = u32::read(reader)?;
let mut raw_token = vec![0; raw_token_len as _];
let raw_token_len = u32::read_be(reader)? as usize;
if raw_token_len > MAX_TOKEN_SIZE {
return Err(Error::new(
ErrorKind::InvalidData,
"Raw token length exceeds limit",
));
}
let mut raw_token = vec![0u8; raw_token_len];
reader.read_exact(&mut raw_token)?;
Ok(Self {

View File

@ -1,5 +1,5 @@
use std::{
io::{Error, Read},
io::{Error, ErrorKind, Read},
net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6},
};
@ -133,8 +133,22 @@ impl<T: PacketRead, const N: usize> PacketRead for [T; N] {
impl PacketRead for String {
fn read<R: Read>(reader: &mut R) -> Result<Self, Error> {
let vec = Vec::read(reader)?;
Ok(unsafe { Self::from_utf8_unchecked(vec) })
const MAX_STRING_LENGTH: usize = 32767;
let len = u32::read(reader)? as usize;
if len > MAX_STRING_LENGTH {
return Err(Error::new(
ErrorKind::InvalidData,
format!("String length {len} exceeds maximum of {MAX_STRING_LENGTH}"),
));
}
let mut buf = vec![0u8; len];
reader.read_exact(&mut buf)?;
Self::from_utf8(buf)
.map_err(|_| Error::new(ErrorKind::InvalidData, "Invalid UTF-8 sequence"))
}
}

View File

@ -401,6 +401,135 @@ impl pumpkin::plugin::player::HostPlayer for PluginHostState {
Ok(player.experience_points.load(Ordering::Relaxed))
}
async fn set_experience_level(
&mut self,
player: Resource<Player>,
level: i32,
) -> Result<(), String> {
let player = player_from_resource(self, &player)?;
player.set_experience_level(level, true).await;
Ok(())
}
async fn set_experience_progress(
&mut self,
player: Resource<Player>,
progress: f32,
) -> Result<(), String> {
let player = player_from_resource(self, &player)?;
player
.set_experience(
player.experience_level.load(Ordering::Relaxed),
progress,
player.experience_points.load(Ordering::Relaxed),
)
.await;
Ok(())
}
async fn set_experience_points(
&mut self,
player: Resource<Player>,
points: i32,
) -> Result<(), String> {
let player = player_from_resource(self, &player)?;
player
.set_experience(
player.experience_level.load(Ordering::Relaxed),
player.experience_progress.load(),
points,
)
.await;
Ok(())
}
async fn add_experience_levels(
&mut self,
player: Resource<Player>,
levels: i32,
) -> Result<(), String> {
let player = player_from_resource(self, &player)?;
player.add_experience_levels(levels).await;
Ok(())
}
async fn add_experience_points(
&mut self,
player: Resource<Player>,
points: i32,
) -> Result<(), String> {
let player = player_from_resource(self, &player)?;
player.add_experience_points(points).await;
Ok(())
}
async fn is_sneaking(&mut self, player: Resource<Player>) -> Result<bool, String> {
let player = player_from_resource(self, &player)?;
Ok(player.get_entity().sneaking.load(Ordering::Relaxed))
}
async fn is_sprinting(&mut self, player: Resource<Player>) -> Result<bool, String> {
let player = player_from_resource(self, &player)?;
Ok(player.get_entity().sprinting.load(Ordering::Relaxed))
}
async fn is_on_ground(&mut self, player: Resource<Player>) -> Result<bool, String> {
let player = player_from_resource(self, &player)?;
Ok(player.get_entity().on_ground.load(Ordering::Relaxed))
}
async fn is_flying(&mut self, player: Resource<Player>) -> Result<bool, String> {
let player = player_from_resource(self, &player)?;
Ok(player.is_flying().await)
}
async fn set_flying(&mut self, player: Resource<Player>, flying: bool) -> Result<(), String> {
let player = player_from_resource(self, &player)?;
{
let mut abilities = player.abilities.lock().await;
abilities.flying = flying;
};
player.send_abilities_update().await;
Ok(())
}
async fn get_abilities(
&mut self,
player: Resource<Player>,
) -> Result<pumpkin::plugin::player::PlayerAbilities, String> {
let player = player_from_resource(self, &player)?;
let abilities = player.abilities.lock().await;
Ok(pumpkin::plugin::player::PlayerAbilities {
invulnerable: abilities.invulnerable,
flying: abilities.flying,
allow_flying: abilities.allow_flying,
creative: abilities.creative,
allow_modify_world: abilities.allow_modify_world,
fly_speed: abilities.fly_speed,
walk_speed: abilities.walk_speed,
})
}
async fn set_abilities(
&mut self,
player: Resource<Player>,
abilities: pumpkin::plugin::player::PlayerAbilities,
) -> Result<(), String> {
let player = player_from_resource(self, &player)?;
{
let mut a = player.abilities.lock().await;
a.invulnerable = abilities.invulnerable;
a.flying = abilities.flying;
a.allow_flying = abilities.allow_flying;
a.creative = abilities.creative;
a.allow_modify_world = abilities.allow_modify_world;
a.fly_speed = abilities.fly_speed;
a.walk_speed = abilities.walk_speed;
};
player.send_abilities_update().await;
Ok(())
}
async fn drop(&mut self, rep: Resource<Player>) -> wasmtime::Result<()> {
let _ = self
.resource_table

View File

@ -33,4 +33,22 @@ impl pumpkin::plugin::server::HostServer for PluginHostState {
pumpkin_util::Difficulty::Hard => Difficulty::Hard,
}
}
async fn get_player_count(
&mut self,
_server: Resource<pumpkin::plugin::server::Server>,
) -> u32 {
let server = self.server.as_ref().expect("server not available");
server.get_player_count() as u32
}
async fn get_mspt(&mut self, _server: Resource<pumpkin::plugin::server::Server>) -> f64 {
let server = self.server.as_ref().expect("server not available");
server.get_mspt()
}
async fn get_tps(&mut self, _server: Resource<pumpkin::plugin::server::Server>) -> f64 {
let server = self.server.as_ref().expect("server not available");
server.get_tps()
}
}

View File

@ -1,5 +1,12 @@
use pumpkin_data::block_state::PistonBehavior;
use pumpkin_util::math::position::BlockPos;
use pumpkin_world::world::BlockFlags;
use wasmtime::component::Resource;
use crate::plugin::loader::wasm::wasm_host::wit::v0_1_0::pumpkin::plugin::world::{
BlockFlags as WitBlockFlags, BlockPos as WitBlockPos, BlockState as WitBlockState,
PistonBehavior as WitPistonBehavior,
};
use crate::plugin::loader::wasm::wasm_host::{
DowncastResourceExt,
state::{PluginHostState, WorldResource},
@ -44,6 +51,85 @@ impl pumpkin::plugin::world::HostWorld for PluginHostState {
.to_string()
}
async fn get_block_state_id(&mut self, world: Resource<World>, pos: WitBlockPos) -> u16 {
let world_ref = world.downcast_ref(self);
let internal_pos = BlockPos::new(pos.x, pos.y, pos.z);
world_ref.provider.get_block_state_id(&internal_pos).await
}
async fn get_block_state(&mut self, world: Resource<World>, pos: WitBlockPos) -> WitBlockState {
let world_ref = world.downcast_ref(self);
let internal_pos = BlockPos::new(pos.x, pos.y, pos.z);
// Fetch the actual BlockState struct from the world
// get_block_state typically returns &'static BlockState in Pumpkin
let state = world_ref.provider.get_block_state(&internal_pos).await;
WitBlockState {
id: state.id,
luminance: state.luminance,
opacity: state.opacity,
hardness: state.hardness,
is_air: state.is_air(),
is_liquid: state.is_liquid(),
is_solid: state.is_solid(),
is_full_cube: state.is_full_cube(),
has_random_ticks: state.has_random_ticks(),
piston_behavior: match state.piston_behavior {
PistonBehavior::Normal => WitPistonBehavior::Normal,
PistonBehavior::Destroy => WitPistonBehavior::Destroy,
PistonBehavior::Block => WitPistonBehavior::Block,
PistonBehavior::Ignore => WitPistonBehavior::Ignore,
PistonBehavior::PushOnly => WitPistonBehavior::PushOnly,
},
}
}
async fn set_block_state(
&mut self,
world: Resource<World>,
pos: WitBlockPos,
state: u16,
update_flags: WitBlockFlags,
) {
let world_ref = world.downcast_ref(self);
let internal_pos = BlockPos::new(pos.x, pos.y, pos.z);
// Map WIT flags to your internal bitflags
let mut internal_flags = BlockFlags::empty();
if update_flags.contains(WitBlockFlags::NOTIFY_NEIGHBORS) {
internal_flags |= BlockFlags::NOTIFY_NEIGHBORS;
}
if update_flags.contains(WitBlockFlags::NOTIFY_LISTENERS) {
internal_flags |= BlockFlags::NOTIFY_LISTENERS;
}
if update_flags.contains(WitBlockFlags::FORCE_STATE) {
internal_flags |= BlockFlags::FORCE_STATE;
}
if update_flags.contains(WitBlockFlags::SKIP_DROPS) {
internal_flags |= BlockFlags::SKIP_DROPS;
}
if update_flags.contains(WitBlockFlags::MOVED) {
internal_flags |= BlockFlags::MOVED;
}
if update_flags.contains(WitBlockFlags::SKIP_REDSTONE_WIRE_STATE_REPLACEMENT) {
internal_flags |= BlockFlags::SKIP_REDSTONE_WIRE_STATE_REPLACEMENT;
}
if update_flags.contains(WitBlockFlags::SKIP_BLOCK_ENTITY_REPLACED_CALLBACK) {
internal_flags |= BlockFlags::SKIP_BLOCK_ENTITY_REPLACED_CALLBACK;
}
if update_flags.contains(WitBlockFlags::SKIP_BLOCK_ADDED_CALLBACK) {
internal_flags |= BlockFlags::SKIP_BLOCK_ADDED_CALLBACK;
}
// Update the world
world_ref
.provider
.set_block_state(&internal_pos, state, internal_flags)
.await;
}
async fn drop(&mut self, rep: Resource<World>) -> wasmtime::Result<()> {
let _ = self
.resource_table