diff --git a/Cargo.lock b/Cargo.lock index 6e43b8d..2cb23cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,6 +37,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + [[package]] name = "auth-service" version = "0.1.0" @@ -117,6 +126,15 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -147,6 +165,12 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "crossbeam-queue" version = "0.3.13" @@ -221,6 +245,18 @@ dependencies = [ "serde", ] +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + [[package]] name = "equivalent" version = "1.0.2" @@ -386,7 +422,6 @@ dependencies = [ "async-trait", "logic", "serde", - "serde_json", "service-rpc", "thiserror", "titan", @@ -416,6 +451,15 @@ dependencies = [ "wasi", ] +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.15.5" @@ -442,6 +486,20 @@ dependencies = [ "hashbrown 0.15.5", ] +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "serde", + "spin", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.5.0" @@ -872,6 +930,19 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "heapless", + "serde", +] + [[package]] name = "potential_utf" version = "0.1.6" @@ -1007,6 +1078,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustls" version = "0.23.43" @@ -1076,6 +1156,12 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + [[package]] name = "serde" version = "1.0.229" @@ -1136,8 +1222,8 @@ name = "service-rpc" version = "0.1.0" dependencies = [ "async-trait", + "postcard", "serde", - "serde_json", "thiserror", "tokio", "tracing", diff --git a/crates/gateway/Cargo.toml b/crates/gateway/Cargo.toml index 2762f22..7142069 100644 --- a/crates/gateway/Cargo.toml +++ b/crates/gateway/Cargo.toml @@ -14,7 +14,6 @@ thiserror = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } serde = { workspace = true } -serde_json = { workspace = true } async-trait = { workspace = true } [[bin]] diff --git a/crates/service-rpc/Cargo.toml b/crates/service-rpc/Cargo.toml index 0fb9690..6d02af4 100644 --- a/crates/service-rpc/Cargo.toml +++ b/crates/service-rpc/Cargo.toml @@ -9,5 +9,5 @@ tokio = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } serde = { workspace = true } -serde_json = { workspace = true } async-trait = { workspace = true } +postcard = { version = "1.1.3", features = ["alloc"] } diff --git a/crates/service-rpc/src/auth.rs b/crates/service-rpc/src/auth.rs index 9bb9ad4..e95e25c 100644 --- a/crates/service-rpc/src/auth.rs +++ b/crates/service-rpc/src/auth.rs @@ -35,7 +35,6 @@ pub struct Session { pub server_time: String, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(tag = "outcome", rename_all = "snake_case")] pub enum LoginOutcome { Accepted(Session), Rejected { @@ -44,7 +43,6 @@ pub enum LoginOutcome { }, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(tag = "op", rename_all = "snake_case")] pub enum AuthRequest { Login { account: AccountRef, @@ -56,7 +54,6 @@ pub enum AuthRequest { }, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(tag = "result", rename_all = "snake_case")] pub enum AuthResponse { Login(LoginOutcome), Resolved { known: bool }, diff --git a/crates/service-rpc/src/base64.rs b/crates/service-rpc/src/base64.rs deleted file mode 100644 index 7c7bd23..0000000 --- a/crates/service-rpc/src/base64.rs +++ /dev/null @@ -1,49 +0,0 @@ -const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; -pub fn encode(input: &[u8]) -> String { - let mut out = String::with_capacity(input.len().div_ceil(3) * 4); - for chunk in input.chunks(3) { - let b0 = chunk[0] as u32; - let b1 = chunk.get(1).copied().unwrap_or(0) as u32; - let b2 = chunk.get(2).copied().unwrap_or(0) as u32; - let triple = (b0 << 16) | (b1 << 8) | b2; - out.push(ALPHABET[(triple >> 18) as usize & 0x3F] as char); - out.push(ALPHABET[(triple >> 12) as usize & 0x3F] as char); - out.push(if chunk.len() > 1 { - ALPHABET[(triple >> 6) as usize & 0x3F] as char - } else { - '=' - }); - out.push(if chunk.len() > 2 { - ALPHABET[triple as usize & 0x3F] as char - } else { - '=' - }); - } - out -} -pub fn decode(input: &str) -> Option> { - let bytes: Vec = input.bytes().filter(|byte| *byte != b'=').collect(); - let mut out = Vec::with_capacity(bytes.len() * 3 / 4); - let mut accumulator: u32 = 0; - let mut bits: u32 = 0; - for byte in bytes { - let value = ALPHABET.iter().position(|candidate| *candidate == byte)? as u32; - accumulator = (accumulator << 6) | value; - bits += 6; - if bits >= 8 { - bits -= 8; - out.push((accumulator >> bits) as u8); - } - } - Some(out) -} -pub mod serde_bytes { - use serde::{Deserialize, Deserializer, Serializer}; - pub fn serialize(value: &[u8], serializer: S) -> Result { - serializer.serialize_str(&super::encode(value)) - } - pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result, D::Error> { - let text = String::deserialize(deserializer)?; - super::decode(&text).ok_or_else(|| serde::de::Error::custom("invalid base64 payload")) - } -} diff --git a/crates/service-rpc/src/client.rs b/crates/service-rpc/src/client.rs index c7a6d6d..58563b6 100644 --- a/crates/service-rpc/src/client.rs +++ b/crates/service-rpc/src/client.rs @@ -33,7 +33,7 @@ where &self.endpoint } pub async fn call(&self, request: &Req) -> RpcResult { - let encoded = serde_json::to_vec(request)?; + let encoded = postcard::to_allocvec(request)?; let slot = self.cursor.fetch_add(1, Ordering::Relaxed) % self.pool.len(); let mut guard = self.pool[slot].lock().await; let mut last_error: Option = None; @@ -56,7 +56,7 @@ where read_frame(stream).await }; match exchange.await { - Ok(payload) => return Ok(serde_json::from_slice(&payload)?), + Ok(payload) => return Ok(postcard::from_bytes(&payload)?), Err(error) => { *guard = None; last_error = Some(error); diff --git a/crates/service-rpc/src/error.rs b/crates/service-rpc/src/error.rs index 890cb8b..2de5692 100644 --- a/crates/service-rpc/src/error.rs +++ b/crates/service-rpc/src/error.rs @@ -4,7 +4,7 @@ pub enum RpcError { #[error("transport: {0}")] Transport(#[from] std::io::Error), #[error("encoding: {0}")] - Encoding(#[from] serde_json::Error), + Encoding(#[from] postcard::Error), #[error("frame of {actual} byte(s) exceeds the {limit} byte limit")] FrameTooLarge { actual: usize, limit: usize }, #[error("peer closed the connection")] diff --git a/crates/service-rpc/src/game.rs b/crates/service-rpc/src/game.rs index eaa4e16..603c076 100644 --- a/crates/service-rpc/src/game.rs +++ b/crates/service-rpc/src/game.rs @@ -28,7 +28,6 @@ pub enum HomeRequestKind { GoHome, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(tag = "op", rename_all = "snake_case")] pub enum GameRequest { LoadHome { account: AccountRef, @@ -40,17 +39,14 @@ pub enum GameRequest { }, EndClientTurn { account: AccountRef, - #[serde(with = "crate::base64::serde_bytes")] payload: Vec, }, StartMission { account: AccountRef, - #[serde(with = "crate::base64::serde_bytes")] payload: Vec, }, HomeLogicStopped { account: AccountRef, - #[serde(with = "crate::base64::serde_bytes")] payload: Vec, }, BattleTick { @@ -61,7 +57,6 @@ pub enum GameRequest { }, SectorCommand { account: AccountRef, - #[serde(with = "crate::base64::serde_bytes")] payload: Vec, }, CancelMatchmake { @@ -69,7 +64,6 @@ pub enum GameRequest { }, BattleEvent { account: AccountRef, - #[serde(with = "crate::base64::serde_bytes")] payload: Vec, }, Disconnect { @@ -77,7 +71,6 @@ pub enum GameRequest { }, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(tag = "result", rename_all = "snake_case")] pub enum GameResponse { Messages { messages: Vec }, Empty, diff --git a/crates/service-rpc/src/lib.rs b/crates/service-rpc/src/lib.rs index ecc9ce9..66d7450 100644 --- a/crates/service-rpc/src/lib.rs +++ b/crates/service-rpc/src/lib.rs @@ -1,5 +1,4 @@ pub mod auth; -pub mod base64; pub mod client; pub mod error; pub mod game; diff --git a/crates/service-rpc/src/server.rs b/crates/service-rpc/src/server.rs index 4eaee80..cf25a54 100644 --- a/crates/service-rpc/src/server.rs +++ b/crates/service-rpc/src/server.rs @@ -31,9 +31,9 @@ async fn handle_connection(mut stream: TcpStream, service: Arc Err(RpcError::Closed) => return Ok(()), Err(error) => return Err(error), }; - let decoded: S::Request = serde_json::from_slice(&request)?; + let decoded: S::Request = postcard::from_bytes(&request)?; let response = service.call(decoded).await?; - let encoded = serde_json::to_vec(&response)?; + let encoded = postcard::to_allocvec(&response)?; write_frame(&mut stream, &encoded).await?; } } diff --git a/crates/service-rpc/src/wire.rs b/crates/service-rpc/src/wire.rs index 9ff6f64..faa773f 100644 --- a/crates/service-rpc/src/wire.rs +++ b/crates/service-rpc/src/wire.rs @@ -4,7 +4,6 @@ pub struct WireMessage { pub message_type: u16, #[serde(default)] pub message_version: u16, - #[serde(with = "crate::base64::serde_bytes")] pub payload: Vec, } impl WireMessage { diff --git a/crates/service-rpc/tests/postcard_roundtrip.rs b/crates/service-rpc/tests/postcard_roundtrip.rs new file mode 100644 index 0000000..563050c --- /dev/null +++ b/crates/service-rpc/tests/postcard_roundtrip.rs @@ -0,0 +1,64 @@ +use service_rpc::{ + AccountRef, AuthRequest, AuthResponse, DeviceInfo, GameRequest, GameResponse, HomeRequestKind, + LoginOutcome, Session, WireMessage, +}; + +fn roundtrip(value: T) +where + T: serde::Serialize + serde::de::DeserializeOwned + PartialEq + std::fmt::Debug, +{ + let bytes = postcard::to_allocvec(&value).expect("encode"); + let back: T = postcard::from_bytes(&bytes).expect("decode"); + assert_eq!(value, back); +} + +fn session() -> Session { + Session { + account: AccountRef::new(0, 5), + pass_token: "tok".to_owned(), + session_count: 3, + play_time_seconds: 42, + days_since_started_playing: 1, + account_created_date: "2026-01-01".to_owned(), + server_time: "2026-08-28".to_owned(), + } +} + +#[test] +fn game_request_survives_the_wire() { + roundtrip(GameRequest::BattleTick { + account: AccountRef::new(0, 5), + }); + roundtrip(GameRequest::SectorCommand { + account: AccountRef::new(0, 5), + payload: vec![0, 1, 2, 253, 254, 255], + }); + roundtrip(GameRequest::LoadHome { + account: AccountRef::new(1, 2), + kind: HomeRequestKind::Login, + }); +} + +#[test] +fn game_response_survives_the_wire() { + roundtrip(GameResponse::Empty); + roundtrip(GameResponse::messages(vec![ + WireMessage::new(20104, 0, vec![9, 8, 7, 0, 255]), + WireMessage::new(24101, 1, Vec::new()), + ])); +} + +#[test] +fn auth_exchange_survives_the_wire() { + roundtrip(AuthRequest::Login { + account: AccountRef::new(0, 5), + pass_token: Some("tok".to_owned()), + device: DeviceInfo::default(), + }); + roundtrip(AuthResponse::Login(LoginOutcome::Accepted(session()))); + roundtrip(AuthResponse::Login(LoginOutcome::Rejected { + error_code: 11, + message: Some("banned".to_owned()), + })); + roundtrip(AuthResponse::Resolved { known: true }); +}