commit 85743b6e16a951f845577ea01c123102552cec0d Author: mars Date: Tue May 24 01:52:50 2022 -0600 Basic protocol + var uints + encode derive diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..580148f --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,67 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "proc-macro2" +version = "1.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "protocol" +version = "0.1.0" +dependencies = [ + "byteorder", +] + +[[package]] +name = "protocol-derive" +version = "0.1.0" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbaf6116ab8924f39d52792136fb74fd60a80194cf1b1c6ffa6453eef1c3f942" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "udp-mud" +version = "0.1.0" +dependencies = [ + "protocol", + "protocol-derive", +] + +[[package]] +name = "unicode-ident" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..51ed65c --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[workspace] +members = [ + "protocol", + "protocol-derive" +] + +[package] +name = "udp-mud" +version = "0.1.0" +edition = "2021" + +[dependencies] +protocol = { path = "./protocol" } +protocol-derive = { path = "./protocol-derive" } diff --git a/protocol-derive/Cargo.toml b/protocol-derive/Cargo.toml new file mode 100644 index 0000000..178052a --- /dev/null +++ b/protocol-derive/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "protocol-derive" +version = "0.1.0" +edition = "2021" + +[lib] +proc-macro = true + +[dependencies] +syn = "1.0" +quote = "1.0" diff --git a/protocol-derive/src/lib.rs b/protocol-derive/src/lib.rs new file mode 100644 index 0000000..cdf56c2 --- /dev/null +++ b/protocol-derive/src/lib.rs @@ -0,0 +1,39 @@ +use proc_macro::TokenStream; +use quote::{quote, quote_spanned}; +use syn::{parse_macro_input, spanned::Spanned, Data, DeriveInput, Fields}; + +// with help from: https://github.com/dtolnay/syn/blob/master/examples/heapsize/heapsize_derive/src/lib.rs + +#[proc_macro_derive(Encode)] +pub fn derive_encode(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let name = input.ident; + let data = &input.data; + + let encode_members = match data { + Data::Struct(ref data) => match data.fields { + Fields::Named(ref fields) => { + let recurse = fields.named.iter().map(|f| { + let name = &f.ident; + quote_spanned! { f.span() => + ::protocol::Encode::encode(&self.#name, writer)?; + } + }); + quote! { #(#recurse)* } + } + _ => unimplemented!(), + }, + _ => unimplemented!(), + }; + + let expanded = quote! { + impl ::protocol::Encode for #name { + fn encode(&self, writer: &mut impl ::std::io::Write) -> ::std::io::Result<()> { + #encode_members + Ok(()) + } + } + }; + + expanded.into() +} diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml new file mode 100644 index 0000000..04930f6 --- /dev/null +++ b/protocol/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "protocol" +version = "0.1.0" +edition = "2021" + +[dependencies] +byteorder = "1" diff --git a/protocol/src/lib.rs b/protocol/src/lib.rs new file mode 100644 index 0000000..b35341b --- /dev/null +++ b/protocol/src/lib.rs @@ -0,0 +1,182 @@ +use std::io::{Read, Result as IoResult, Write}; +use byteorder::ReadBytesExt; +use std::ops::{Deref, DerefMut}; + +pub trait Encode { + fn encode(&self, writer: &mut impl Write) -> IoResult<()>; +} + +pub trait Decode: Sized { + fn decode(reader: &mut impl Read) -> IoResult; +} + +#[repr(transparent)] +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct Var(pub T); + +impl Deref for Var { + type Target = T; + fn deref(&self) -> &T { + &self.0 + } +} + +impl DerefMut for Var { + fn deref_mut(&mut self) -> &mut T { + &mut self.0 + } +} + +macro_rules! impl_var_uint ( + ($type: ident) => ( + impl Encode for Var<$type> { + fn encode(&self, writer: &mut impl Write) -> IoResult<()> { + let mut value = self.0; + + loop { + if (value & !0x7f) == 0 { + writer.write(&[value as u8])?; + return Ok(()); + } + + let next = (value as u8 & 0x7f) | 0x80; + writer.write(&[next])?; + value >>= 7; + } + } + } + + impl Decode for Var<$type> { + fn decode(reader: &mut impl Read) -> IoResult { + let mut value = 0; + let mut position = 0; + + while position < $type::BITS { + let b = reader.read_u8()?; + value |= (b as $type & 0x7f) << position; + + if (b & 0x80) == 0 { + break; + } + + position += 7; + } + + Ok(Var(value)) + } + } + ) +); + +impl_var_uint!(u16); +impl_var_uint!(u32); +impl_var_uint!(u64); +impl_var_uint!(u128); + +impl Encode for String { + fn encode(&self, writer: &mut impl Write) -> IoResult<()> { + let len = Var::(self.len().try_into().unwrap()); + len.encode(writer)?; + writer.write_all(self.as_bytes())?; + Ok(()) + } +} + +impl Decode for String { + fn decode(reader: &mut impl Read) -> IoResult { + let len = Var::::decode(reader)?; + let mut buf = vec![0u8; len.0 as usize]; + reader.read_exact(&mut buf)?; + + if let Ok(string) = String::from_utf8(buf) { + Ok(string) + } else { + Err(std::io::ErrorKind::InvalidData.into()) + } + } +} + +impl Encode for Vec { + fn encode(&self, writer: &mut impl Write) -> IoResult<()> { + let len = Var::(self.len().try_into().unwrap()); + len.encode(writer)?; + + for item in self.iter() { + item.encode(writer)?; + } + + Ok(()) + } +} + +impl Decode for Vec { + fn decode(reader: &mut impl Read) -> IoResult { + let len = Var::::decode(reader)?; + let mut buf = Vec::with_capacity(len.0 as usize); + for _ in 0..len.0 { + buf.push(T::decode(reader)?); + } + + Ok(buf) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use core::fmt::Debug; + + fn test_roundtrip(original: T) { + let mut buf = Vec::new(); + original.encode(&mut buf).unwrap(); + let mut reader = buf.as_slice(); + let decoded = T::decode(&mut reader).unwrap(); + assert_eq!(original, decoded, "Round-trip encoded values do not match!"); + } + + mod var { + use super::*; + + macro_rules! test_var_int ( + ($type: ident) => ( + mod $type { + use super::*; + + #[test] + fn min() { + test_roundtrip(Var($type::MIN)); + } + + #[test] + fn max() { + test_roundtrip(Var($type::MAX)); + } + + #[test] + fn one() { + test_roundtrip(Var::<$type>(1)); + } + } + ) + ); + + test_var_int!(u16); + test_var_int!(u32); + test_var_int!(u64); + test_var_int!(u128); + } + + mod string { + use super::*; + + #[test] + fn hello_world() { + test_roundtrip("Hello world!".to_string()); + } + + #[test] + fn empty() { + test_roundtrip("".to_string()); + } + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..fc4cc3e --- /dev/null +++ b/src/main.rs @@ -0,0 +1,19 @@ +use protocol::*; +use protocol_derive::Encode; + +#[derive(Debug, Encode)] +struct TestData { + var_int: Var, + string: String, +} + +fn main() { + let data = TestData { + var_int: Var(42), + string: "Hello world!".to_string(), + }; + + let mut buf = Vec::new(); + data.encode(&mut buf).unwrap(); + println!("encoded: {:?}", buf); +}