diff --git a/Cargo.toml b/Cargo.toml index 15633c072..019b989de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ members = [ "parity-path", "plain_hasher", "rlp", + "rlp-derive", "runtime", "transaction-pool", "trace-time", diff --git a/rlp-derive/CHANGELOG.md b/rlp-derive/CHANGELOG.md new file mode 100644 index 000000000..592d3fbf9 --- /dev/null +++ b/rlp-derive/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog + +The format is based on [Keep a Changelog]. + +[Keep a Changelog]: http://keepachangelog.com/en/1.0.0/ + +## [Unreleased] + +## [0.1.0] - 2020-02-12 +- Extracted from parity-ethereum repo. [#343](https://github.com/paritytech/parity-common/pull/343) diff --git a/rlp-derive/Cargo.toml b/rlp-derive/Cargo.toml new file mode 100644 index 000000000..bf4d0eaf0 --- /dev/null +++ b/rlp-derive/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "rlp-derive" +version = "0.1.0" +authors = ["Parity Technologies "] +license = "MIT/Apache-2.0" +description = "Derive macro for #[derive(RlpEncodable, RlpDecodable)]" +homepage = "http://parity.io" +edition = "2018" + +[lib] +proc-macro = true + +[dependencies] +syn = "1.0.14" +quote = "1.0.2" +proc-macro2 = "1.0.8" + +[dev-dependencies] +rlp = "0.4.0" diff --git a/rlp-derive/src/de.rs b/rlp-derive/src/de.rs new file mode 100644 index 000000000..d1b4e4ca5 --- /dev/null +++ b/rlp-derive/src/de.rs @@ -0,0 +1,163 @@ +// Copyright 2015-2020 Parity Technologies (UK) Ltd. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use proc_macro2::TokenStream; +use quote::quote; + +struct ParseQuotes { + single: TokenStream, + list: TokenStream, + takes_index: bool, +} + +fn decodable_parse_quotes() -> ParseQuotes { + ParseQuotes { single: quote! { rlp.val_at }, list: quote! { rlp.list_at }, takes_index: true } +} + +fn decodable_wrapper_parse_quotes() -> ParseQuotes { + ParseQuotes { single: quote! { rlp.as_val }, list: quote! { rlp.as_list }, takes_index: false } +} + +pub fn impl_decodable(ast: &syn::DeriveInput) -> TokenStream { + let body = match ast.data { + syn::Data::Struct(ref s) => s, + _ => panic!("#[derive(RlpDecodable)] is only defined for structs."), + }; + + let mut default_attribute_encountered = false; + let stmts: Vec<_> = body + .fields + .iter() + .enumerate() + .map(|(i, field)| decodable_field(i, field, decodable_parse_quotes(), &mut default_attribute_encountered)) + .collect(); + let name = &ast.ident; + + let impl_block = quote! { + impl rlp::Decodable for #name { + fn decode(rlp: &rlp::Rlp) -> Result { + let result = #name { + #(#stmts)* + }; + + Ok(result) + } + } + }; + + quote! { + const _: () = { + extern crate rlp; + #impl_block + }; + } +} + +pub fn impl_decodable_wrapper(ast: &syn::DeriveInput) -> TokenStream { + let body = match ast.data { + syn::Data::Struct(ref s) => s, + _ => panic!("#[derive(RlpDecodableWrapper)] is only defined for structs."), + }; + + let stmt = { + let fields: Vec<_> = body.fields.iter().collect(); + if fields.len() == 1 { + let field = fields.first().expect("fields.len() == 1; qed"); + let mut default_attribute_encountered = false; + decodable_field(0, field, decodable_wrapper_parse_quotes(), &mut default_attribute_encountered) + } else { + panic!("#[derive(RlpEncodableWrapper)] is only defined for structs with one field.") + } + }; + + let name = &ast.ident; + + let impl_block = quote! { + impl rlp::Decodable for #name { + fn decode(rlp: &rlp::Rlp) -> Result { + let result = #name { + #stmt + }; + + Ok(result) + } + } + }; + + quote! { + const _: () = { + extern crate rlp; + #impl_block + }; + } +} + +fn decodable_field( + index: usize, + field: &syn::Field, + quotes: ParseQuotes, + default_attribute_encountered: &mut bool, +) -> TokenStream { + let id = match field.ident { + Some(ref ident) => quote! { #ident }, + None => { + let index: syn::Index = index.into(); + quote! { #index } + } + }; + + let index = index - *default_attribute_encountered as usize; + let index = quote! { #index }; + + let single = quotes.single; + let list = quotes.list; + + let attributes = &field.attrs; + let default = if let Some(attr) = attributes.iter().find(|attr| attr.path.is_ident("rlp")) { + if *default_attribute_encountered { + panic!("only 1 #[rlp(default)] attribute is allowed in a struct") + } + match attr.parse_args() { + Ok(proc_macro2::TokenTree::Ident(ident)) if ident.to_string() == "default" => {} + _ => panic!("only #[rlp(default)] attribute is supported"), + } + *default_attribute_encountered = true; + true + } else { + false + }; + + match field.ty { + syn::Type::Path(ref path) => { + let ident = &path.path.segments.first().expect("there must be at least 1 segment").ident; + let ident_type = ident.to_string(); + if &ident_type == "Vec" { + if quotes.takes_index { + if default { + quote! { #id: #list(#index).unwrap_or_default(), } + } else { + quote! { #id: #list(#index)?, } + } + } else { + quote! { #id: #list()?, } + } + } else { + if quotes.takes_index { + if default { + quote! { #id: #single(#index).unwrap_or_default(), } + } else { + quote! { #id: #single(#index)?, } + } + } else { + quote! { #id: #single()?, } + } + } + } + _ => panic!("rlp_derive not supported"), + } +} diff --git a/rlp-derive/src/en.rs b/rlp-derive/src/en.rs new file mode 100644 index 000000000..9eb0d6afb --- /dev/null +++ b/rlp-derive/src/en.rs @@ -0,0 +1,109 @@ +// Copyright 2015-2020 Parity Technologies (UK) Ltd. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use proc_macro2::TokenStream; +use quote::quote; + +pub fn impl_encodable(ast: &syn::DeriveInput) -> TokenStream { + let body = match ast.data { + syn::Data::Struct(ref s) => s, + _ => panic!("#[derive(RlpEncodable)] is only defined for structs."), + }; + + let stmts: Vec<_> = body.fields.iter().enumerate().map(|(i, field)| encodable_field(i, field)).collect(); + let name = &ast.ident; + + let stmts_len = stmts.len(); + let stmts_len = quote! { #stmts_len }; + let impl_block = quote! { + impl rlp::Encodable for #name { + fn rlp_append(&self, stream: &mut rlp::RlpStream) { + stream.begin_list(#stmts_len); + #(#stmts)* + } + } + }; + + quote! { + const _: () = { + extern crate rlp; + #impl_block + }; + } +} + +pub fn impl_encodable_wrapper(ast: &syn::DeriveInput) -> TokenStream { + let body = match ast.data { + syn::Data::Struct(ref s) => s, + _ => panic!("#[derive(RlpEncodableWrapper)] is only defined for structs."), + }; + + let stmt = { + let fields: Vec<_> = body.fields.iter().collect(); + if fields.len() == 1 { + let field = fields.first().expect("fields.len() == 1; qed"); + encodable_field(0, field) + } else { + panic!("#[derive(RlpEncodableWrapper)] is only defined for structs with one field.") + } + }; + + let name = &ast.ident; + + let impl_block = quote! { + impl rlp::Encodable for #name { + fn rlp_append(&self, stream: &mut rlp::RlpStream) { + #stmt + } + } + }; + + quote! { + const _: () = { + extern crate rlp; + #impl_block + }; + } +} + +fn encodable_field(index: usize, field: &syn::Field) -> TokenStream { + let ident = match field.ident { + Some(ref ident) => quote! { #ident }, + None => { + let index: syn::Index = index.into(); + quote! { #index } + } + }; + + let id = quote! { self.#ident }; + + match field.ty { + syn::Type::Path(ref path) => { + let top_segment = path.path.segments.first().expect("there must be at least 1 segment"); + let ident = &top_segment.ident; + if &ident.to_string() == "Vec" { + let inner_ident = match top_segment.arguments { + syn::PathArguments::AngleBracketed(ref angle) => { + let ty = angle.args.first().expect("Vec has only one angle bracketed type; qed"); + match *ty { + syn::GenericArgument::Type(syn::Type::Path(ref path)) => { + &path.path.segments.first().expect("there must be at least 1 segment").ident + } + _ => panic!("rlp_derive not supported"), + } + } + _ => unreachable!("Vec has only one angle bracketed type; qed"), + }; + quote! { stream.append_list::<#inner_ident, _>(&#id); } + } else { + quote! { stream.append(&#id); } + } + } + _ => panic!("rlp_derive not supported"), + } +} diff --git a/rlp-derive/src/lib.rs b/rlp-derive/src/lib.rs new file mode 100644 index 000000000..47efd2ffe --- /dev/null +++ b/rlp-derive/src/lib.rs @@ -0,0 +1,54 @@ +// Copyright 2015-2020 Parity Technologies (UK) Ltd. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +//! Derive macro for `#[derive(RlpEncodable, RlpDecodable)]`. +//! +//! For example of usage see `./tests/rlp.rs`. +//! +//! This library also supports up to 1 `#[rlp(default)]` in a struct, +//! which is similar to [`#[serde(default)]`](https://serde.rs/field-attrs.html#default) +//! with the caveat that we use the `Default` value if +//! the field deserialization fails, as we don't serialize field +//! names and there is no way to tell if it is present or not. + +extern crate proc_macro; + +mod de; +mod en; + +use de::{impl_decodable, impl_decodable_wrapper}; +use en::{impl_encodable, impl_encodable_wrapper}; +use proc_macro::TokenStream; + +#[proc_macro_derive(RlpEncodable, attributes(rlp))] +pub fn encodable(input: TokenStream) -> TokenStream { + let ast = syn::parse(input).unwrap(); + let gen = impl_encodable(&ast); + gen.into() +} + +#[proc_macro_derive(RlpEncodableWrapper)] +pub fn encodable_wrapper(input: TokenStream) -> TokenStream { + let ast = syn::parse(input).unwrap(); + let gen = impl_encodable_wrapper(&ast); + gen.into() +} + +#[proc_macro_derive(RlpDecodable, attributes(rlp))] +pub fn decodable(input: TokenStream) -> TokenStream { + let ast = syn::parse(input).unwrap(); + let gen = impl_decodable(&ast); + gen.into() +} + +#[proc_macro_derive(RlpDecodableWrapper)] +pub fn decodable_wrapper(input: TokenStream) -> TokenStream { + let ast = syn::parse(input).unwrap(); + let gen = impl_decodable_wrapper(&ast); + gen.into() +} diff --git a/rlp-derive/tests/rlp.rs b/rlp-derive/tests/rlp.rs new file mode 100644 index 000000000..e3cda4dbc --- /dev/null +++ b/rlp-derive/tests/rlp.rs @@ -0,0 +1,71 @@ +// Copyright 2015-2020 Parity Technologies (UK) Ltd. +// +// Licensed under the Apache License, Version 2.0 or the MIT license +// , at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +use rlp::{decode, encode}; +use rlp_derive::{RlpDecodable, RlpDecodableWrapper, RlpEncodable, RlpEncodableWrapper}; + +#[derive(Debug, PartialEq, RlpEncodable, RlpDecodable)] +struct Foo { + a: String, +} + +#[derive(Debug, PartialEq, RlpEncodableWrapper, RlpDecodableWrapper)] +struct FooWrapper { + a: String, +} + +#[test] +fn test_encode_foo() { + let foo = Foo { a: "cat".into() }; + + let expected = vec![0xc4, 0x83, b'c', b'a', b't']; + let out = encode(&foo); + assert_eq!(out, expected); + + let decoded = decode(&expected).expect("decode failure"); + assert_eq!(foo, decoded); +} + +#[test] +fn test_encode_foo_wrapper() { + let foo = FooWrapper { a: "cat".into() }; + + let expected = vec![0x83, b'c', b'a', b't']; + let out = encode(&foo); + assert_eq!(out, expected); + + let decoded = decode(&expected).expect("decode failure"); + assert_eq!(foo, decoded); +} + +#[test] +fn test_encode_foo_default() { + #[derive(Debug, PartialEq, RlpEncodable, RlpDecodable)] + struct FooDefault { + a: String, + /// It works with other attributes. + #[rlp(default)] + b: Option>, + } + + let attack_of = String::from("clones"); + let foo = Foo { a: attack_of.clone() }; + + let expected = vec![0xc7, 0x86, b'c', b'l', b'o', b'n', b'e', b's']; + let out = encode(&foo); + assert_eq!(out, expected); + + let foo_default = FooDefault { a: attack_of.clone(), b: None }; + + let decoded = decode(&expected).expect("default failure"); + assert_eq!(foo_default, decoded); + + let foo_some = FooDefault { a: attack_of.clone(), b: Some(vec![1, 2, 3]) }; + let out = encode(&foo_some); + assert_eq!(decode(&out), Ok(foo_some)); +}