From e5bc91f8bba7e72d325c3113ea0ee97ba069483c Mon Sep 17 00:00:00 2001 From: eater <=@eater.me> Date: Mon, 20 Apr 2020 18:57:30 +0200 Subject: [PATCH] Initial proof of concept --- .gitignore | 9 +++ Cargo.toml | 6 ++ packd-proc-macro/Cargo.toml | 16 ++++ packd-proc-macro/src/lib.rs | 145 ++++++++++++++++++++++++++++++++++++ packd-tests/Cargo.toml | 10 +++ packd-tests/src/main.rs | 14 ++++ packd/Cargo.toml | 12 +++ packd/src/lib.rs | 6 ++ packd/src/parser.rs | 38 ++++++++++ 9 files changed, 256 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 packd-proc-macro/Cargo.toml create mode 100644 packd-proc-macro/src/lib.rs create mode 100644 packd-tests/Cargo.toml create mode 100644 packd-tests/src/main.rs create mode 100644 packd/Cargo.toml create mode 100644 packd/src/lib.rs create mode 100644 packd/src/parser.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1e4c808 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +/target + + +#Added by cargo +# +#already existing elements were commented out + +#/target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..2e5ff4e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,6 @@ +[workspace] +members = [ +"packd", +"packd-proc-macro", +"packd-tests" +] \ No newline at end of file diff --git a/packd-proc-macro/Cargo.toml b/packd-proc-macro/Cargo.toml new file mode 100644 index 0000000..2aabf71 --- /dev/null +++ b/packd-proc-macro/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "packd-proc-macro" +version = "0.1.0" +authors = ["eater <=@eater.me>"] +edition = "2018" + +[lib] +proc-macro = true + +[dependencies] +proc-macro-hack = "0.5" +proc_macro_roids = "0.7.0" +proc-macro2 = "1.0.9" +syn = "1.0.16" +quote = "1.0.3" +byteorder = "1.3.4" diff --git a/packd-proc-macro/src/lib.rs b/packd-proc-macro/src/lib.rs new file mode 100644 index 0000000..eaa5a40 --- /dev/null +++ b/packd-proc-macro/src/lib.rs @@ -0,0 +1,145 @@ +extern crate proc_macro; + +use proc_macro::{Literal, TokenStream, TokenTree}; +use proc_macro_hack::proc_macro_hack; +use proc_macro_roids::{DeriveInputExt, DeriveInputStructExt, FieldExt}; +use quote::{format_ident, quote}; +use std::str::FromStr; + +use byteorder::{BigEndian, LittleEndian, NativeEndian}; +use proc_macro2::TokenStream as TokenStream2; +use std::iter::FromIterator; +use std::mem::align_of; +use syn::parse::{Parse, ParseBuffer, Parser}; +use syn::{parse_macro_input, parse_quote, DeriveInput, Error, Expr, LitStr, Token}; + +#[derive(Debug)] +struct UnpackInput { + pattern: String, + input: Box, +} + +impl Parse for UnpackInput { + fn parse<'a>(input: &'a ParseBuffer<'a>) -> Result { + let pattern = input.parse::()?.value().into(); + input.parse::()?; + + Ok(UnpackInput { + pattern, + input: input.parse()?, + }) + } +} + +enum Endian { + NativeEndian, + LittleEndian, + BigEndian, +} + +#[proc_macro_hack] +pub fn unpack(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as UnpackInput); + if input.pattern.is_empty() { + return quote! { () }.into(); + } + + let (order, alignment, indic) = match input + .pattern + .chars() + .nth(0) + .expect("Can't read first letter of pattern") + { + '!' | '>' => (Endian::BigEndian, false, true), + + '<' => (Endian::LittleEndian, false, true), + + '=' => (Endian::NativeEndian, false, true), + + '@' => (Endian::NativeEndian, true, true), + + _ => (Endian::NativeEndian, true, false), + }; + + let rest = if indic { + input.pattern.chars().skip(1).collect() + } else { + input.pattern + }; + + let mut i = 0; + + let x: &[u8] = &[]; + + let endian = match order { + Endian::BigEndian => quote! { ::packd::parser::Endian::BigEndian }, + Endian::LittleEndian => quote! { ::packd::parser::Endian::LittleEndian }, + Endian::NativeEndian => quote! { ::packd::parser::Endian::NativeEndian }, + }; + + let mut parsers: Vec<_> = vec![]; + let mut marked_values = vec![]; + let mut offset = 0; + + for char in rest.chars() { + let value = format_ident!("value_{}", i.to_string()); + match char { + 'x' => offset += 1, + 'h' => { + marked_values.push(i); + parsers.push(quote! { + let #value = ::packd::parser::unpack_h(#endian, &input[offset..offset + 2]); + offset += 2; + }); + } + + 'H' => { + marked_values.push(i); + parsers.push(quote! { + let #value = ::packd::parser::unpack_H(#endian, &input[offset..offset + 2]); + offset += 2; + }); + + if alignment { + parsers.push(quote! { + offset += 2 % ::std::mem::align_of::(); + }); + } + } + + 'l' => { + marked_values.push(i); + parsers.push(quote! { + let #value = ::packd::parser::unpack_l(#endian, &input[offset..offset + 4]); + offset += 4; + }); + + if alignment { + parsers.push(quote! { + offset += 2 % ::std::mem::align_of::(); + }); + } + } + + _ => panic!("Found unrecognized format char '{}'", char), + } + i += 1; + } + + let items = marked_values + .iter() + .map(|x| format_ident!("value_{}", x.to_string())) + .collect::>(); + + let input_val = input.input; + + return quote! { + { + let input = #input_val; + let mut offset = 0; + #(#parsers)* + (#(#items,)*) + } + } + .into(); +} diff --git a/packd-tests/Cargo.toml b/packd-tests/Cargo.toml new file mode 100644 index 0000000..fa58af7 --- /dev/null +++ b/packd-tests/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "packd-tests" +version = "0.1.0" +authors = ["eater <=@eater.me>"] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +packd = { path = "../packd" } \ No newline at end of file diff --git a/packd-tests/src/main.rs b/packd-tests/src/main.rs new file mode 100644 index 0000000..ae40dcd --- /dev/null +++ b/packd-tests/src/main.rs @@ -0,0 +1,14 @@ +fn main() { + println!("Hello, world!"); +} + +#[cfg(test)] +mod tests { + use packd::unpack; + + #[test] + fn proc_macro_works() { + let output = unpack!(">hhl", b"\x00\x01\x00\x02\x00\x00\x00\x03"); + assert_eq!((1, 2, 3), output); + } +} diff --git a/packd/Cargo.toml b/packd/Cargo.toml new file mode 100644 index 0000000..06ecaa6 --- /dev/null +++ b/packd/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "packd" +version = "0.1.0" +authors = ["eater <=@eater.me>"] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +packd-proc-macro = { path = "../packd-proc-macro" } +proc-macro-hack = "0.5" +byteorder = "1.3.4" diff --git a/packd/src/lib.rs b/packd/src/lib.rs new file mode 100644 index 0000000..393f943 --- /dev/null +++ b/packd/src/lib.rs @@ -0,0 +1,6 @@ +use proc_macro_hack::proc_macro_hack; + +pub mod parser; + +#[proc_macro_hack] +pub use packd_proc_macro::unpack; diff --git a/packd/src/parser.rs b/packd/src/parser.rs new file mode 100644 index 0000000..c26bd62 --- /dev/null +++ b/packd/src/parser.rs @@ -0,0 +1,38 @@ +#![allow(non_snake_case)] + +use byteorder::{ByteOrder, LittleEndian, NativeEndian, ReadBytesExt}; +use std::convert::TryInto; +use std::io; + +#[derive(Clone, Copy, Debug)] +pub enum Endian { + NativeEndian, + LittleEndian, + BigEndian, +} + +macro_rules! unpack_num { + ($type:ty, $endian:expr, $input:expr) => { + match $endian { + Endian::LittleEndian => <$type>::from_le_bytes($input.try_into().unwrap()), + Endian::BigEndian => <$type>::from_be_bytes($input.try_into().unwrap()), + Endian::NativeEndian => <$type>::from_ne_bytes($input.try_into().unwrap()), + } + }; +} + +pub fn unpack_h(endian: Endian, input: &[u8]) -> i16 { + unpack_num!(i16, endian, input) +} + +pub fn unpack_H(endian: Endian, input: &[u8]) -> u16 { + unpack_num!(u16, endian, input) +} + +pub fn unpack_l(endian: Endian, input: &[u8]) -> i32 { + unpack_num!(i32, endian, input) +} + +pub fn unpack_L(endian: Endian, input: &[u8]) -> u32 { + unpack_num!(u32, endian, input) +}