From 330f501cfbd8286aed6631d0bab5617c03eae07f Mon Sep 17 00:00:00 2001 From: Colin Snover Date: Thu, 8 Sep 2022 01:01:59 -0500 Subject: [PATCH] Add helper attribute macros for writing free function parsers/writers --- binrw/doc/attribute.md | 17 +- binrw/src/file_ptr.rs | 8 +- binrw/src/helpers.rs | 9 +- binrw/src/lib.rs | 174 +++++++++++++ binrw/src/punctuated.rs | 23 +- binrw/tests/derive/struct.rs | 11 +- binrw/tests/derive/write/custom_writer.rs | 8 +- binrw/tests/ui/bad_parse_with_fn.rs | 3 +- binrw/tests/ui/bad_parse_with_fn.stderr | 46 ++-- binrw/tests/ui/fn_helper_errors.rs | 71 ++++++ binrw/tests/ui/fn_helper_errors.stderr | 79 ++++++ binrw_derive/src/binrw/parser/mod.rs | 4 +- binrw_derive/src/binrw/parser/result.rs | 67 ----- binrw_derive/src/fn_helper/mod.rs | 298 ++++++++++++++++++++++ binrw_derive/src/lib.rs | 14 + binrw_derive/src/result.rs | 146 +++++++++++ 16 files changed, 834 insertions(+), 144 deletions(-) create mode 100644 binrw/tests/ui/fn_helper_errors.rs create mode 100644 binrw/tests/ui/fn_helper_errors.stderr delete mode 100644 binrw_derive/src/binrw/parser/result.rs create mode 100644 binrw_derive/src/fn_helper/mod.rs create mode 100644 binrw_derive/src/result.rs diff --git a/binrw/doc/attribute.md b/binrw/doc/attribute.md index 5e6bc4e3..1a709bb0 100644 --- a/binrw/doc/attribute.md +++ b/binrw/doc/attribute.md @@ -1067,6 +1067,9 @@ a type, or to parse types which have no `BinRead` implementation at all: #[br(parse_with = $parse_fn:expr)] or #[br(parse_with($parse_fn:expr))] ``` +Use the [`#[parser]`](crate::parser) attribute macro to create compatible +functions. + Any earlier field or [import](#arguments) can be referenced by the expression in the directive (for example, to construct a parser function at runtime by calling a function generator). @@ -1082,6 +1085,9 @@ implementation at all: #[bw(write_with = $write_fn:expr)] or #[bw(write_with($write_fn:expr))] ``` +Use the [`#[writer]`](crate::writer) attribute macro to create compatible +functions. + Any field or [import](#arguments) can be referenced by the expression in the directive (for example, to construct a serialisation function at runtime by calling a function generator). @@ -1096,9 +1102,8 @@ calling a function generator). ``` # use binrw::{prelude::*, io::{prelude::*, Cursor}, Endian}; # use std::collections::HashMap; -fn custom_parser(reader: &mut R, endian: Endian, _: ()) - -> BinResult> -{ +#[binrw::parser(reader, endian)] +fn custom_parser() -> BinResult> { let mut map = HashMap::new(); map.insert( <_>::read_options(reader, endian, ())?, @@ -1124,11 +1129,9 @@ struct MyType { ``` # use binrw::{prelude::*, io::{prelude::*, Cursor}, Endian}; # use std::collections::BTreeMap; -fn custom_writer( +#[binrw::writer(writer, endian)] +fn custom_writer( map: &BTreeMap, - writer: &mut R, - endian: Endian, - _: () ) -> BinResult<()> { for (key, val) in map.iter() { key.write_options(writer, endian, ())?; diff --git a/binrw/src/file_ptr.rs b/binrw/src/file_ptr.rs index b8fcbb15..40e2af17 100644 --- a/binrw/src/file_ptr.rs +++ b/binrw/src/file_ptr.rs @@ -173,13 +173,9 @@ impl + IntoSeekFrom, Value> FilePtr { /// # Errors /// /// If reading fails, an [`Error`](crate::Error) variant will be returned. - pub fn parse( - reader: &mut R, - endian: Endian, - args: FilePtrArgs, - ) -> BinResult + #[binrw::parser(reader, endian)] + pub fn parse(args: FilePtrArgs, ...) -> BinResult where - R: Read + Seek, Args: Clone, Value: BinRead, { diff --git a/binrw/src/helpers.rs b/binrw/src/helpers.rs index d8fd8207..e6a5b4fd 100644 --- a/binrw/src/helpers.rs +++ b/binrw/src/helpers.rs @@ -475,13 +475,8 @@ where } } -// Lint: Non-consumed argument is required to match the API. -#[allow(clippy::trivially_copy_pass_by_ref)] -fn default_reader>( - reader: &mut R, - endian: Endian, - args: T::Args, -) -> BinResult { +#[binrw::parser(reader, endian)] +fn default_reader>(args: T::Args, ...) -> BinResult { let mut value = T::read_options(reader, endian, args.clone())?; value.after_parse(reader, endian, args)?; Ok(value) diff --git a/binrw/src/lib.rs b/binrw/src/lib.rs index f245b22a..5b76b736 100644 --- a/binrw/src/lib.rs +++ b/binrw/src/lib.rs @@ -141,6 +141,180 @@ pub use binrw_derive::binrw; /// ``` pub use binrw_derive::NamedArgs; +/// Attribute macro used to generate +/// [`parse_with`](docs::attribute#custom-parserswriters) functions. +/// +/// Rust functions are transformed by this macro to match the binrw API. +/// +/// # Attribute options +/// +/// * `#[parser(reader)]` or `#[parser(reader: $ident)]`: Exposes the write +/// stream to the function. If no variable name is given, `reader` is used. +/// * `#[parser(endian)]` or `#[parser(endian: $ident)]`: Exposes the endianness +/// to the function. If no variable name is given, `endian` is used. +/// +/// Options are comma-separated. +/// +/// # Function parameters +/// +/// Parameters are transformed into either +/// [tuple-style arguments](docs::attribute#tuple-style-arguments) or +/// [raw arguments](docs::attribute#raw-arguments) depending upon the function +/// signature. +/// +/// ## Tuple-style arguments +/// +/// Use a normal function signature. The parameters in the signature will be +/// converted to a tuple. For example: +/// +/// ``` +/// #[binrw::parser(reader: r, endian)] +/// fn custom_parser(v0: u8, v1: i16) -> binrw::BinResult<()> { +/// Ok(()) +/// } +/// # custom_parser(&mut binrw::io::Cursor::new(b""), binrw::Endian::Little, (0, 0)).unwrap(); +/// ``` +/// +/// The transformed output for this function is: +/// +/// ``` +/// use binrw::{BinResult, Endian, io::{Read, Seek}}; +/// fn custom_parser( +/// r: &mut R, +/// endian: Endian, +/// (v0, v1): (u8, i16) +/// ) -> BinResult<()> { +/// Ok(()) +/// } +/// # custom_parser(&mut binrw::io::Cursor::new(b""), binrw::Endian::Little, (0, 0)).unwrap(); +/// ``` +/// +/// ## Raw arguments +/// +/// Use a *variadic* function signature with a single parameter. The name and +/// type of the parameter will be used as the raw argument. For example: +/// +/// ``` +/// # struct ArgsType; +/// #[binrw::parser] +/// fn custom_parser(args: ArgsType, ...) -> binrw::BinResult<()> { +/// Ok(()) +/// } +/// # custom_parser(&mut binrw::io::Cursor::new(b""), binrw::Endian::Little, ArgsType).unwrap(); +/// ``` +/// +/// The transformed output for this function is: +/// +/// ``` +/// # struct ArgsType; +/// use binrw::{BinResult, Endian, io::{Read, Seek}}; +/// fn custom_parser( +/// _: &mut R, +/// _: Endian, +/// args: ArgsType +/// ) -> BinResult<()> { +/// Ok(()) +/// } +/// # custom_parser(&mut binrw::io::Cursor::new(b""), binrw::Endian::Little, ArgsType).unwrap(); +/// ``` +/// +/// # Return value +/// +/// The return value of a parser function must be [`BinResult`](BinResult), +/// where `T` is the type of the object being parsed. +pub use binrw_derive::parser; + +/// Attribute macro used to generate +/// [`write_with`](docs::attribute#custom-parserswriters) functions. +/// +/// Rust functions are transformed by this macro to match the binrw API. +/// +/// # Attribute options +/// +/// * `#[writer(writer)]` or `#[writer(writer: $ident)]`: Exposes the write +/// stream to the function. If no variable name is given, `writer` is used. +/// * `#[writer(endian)]` or `#[writer(endian: $ident)]`: Exposes the endianness +/// to the function. If no variable name is given, `endian` is used. +/// +/// Options are comma-separated. +/// +/// # Function parameters +/// +/// The first parameter is required and receives a reference to the object being +/// written. +/// +/// Subsequent parameters are transformed into either +/// [tuple-style arguments](docs::attribute#tuple-style-arguments) or +/// [raw arguments](docs::attribute#raw-arguments) depending upon the function +/// signature. +/// +/// ## Tuple-style arguments +/// +/// Use a normal function signature. The remaining parameters in the signature +/// will be converted to a tuple. For example: +/// +/// ``` +/// # struct Object; +/// #[binrw::writer(writer: w, endian)] +/// fn custom_writer(obj: &Object, v0: u8, v1: i16) -> binrw::BinResult<()> { +/// Ok(()) +/// } +/// # custom_writer(&Object, &mut binrw::io::Cursor::new(vec![]), binrw::Endian::Little, (0, 0)).unwrap(); +/// ``` +/// +/// The transformed output for this function is: +/// +/// ``` +/// # struct Object; +/// use binrw::{BinResult, Endian, io::{Seek, Write}}; +/// fn custom_writer( +/// obj: &Object, +/// w: &mut W, +/// endian: Endian, +/// (v0, v1): (u8, i16) +/// ) -> BinResult<()> { +/// Ok(()) +/// } +/// # custom_writer(&Object, &mut binrw::io::Cursor::new(vec![]), binrw::Endian::Little, (0, 0)).unwrap(); +/// ``` +/// +/// ## Raw arguments +/// +/// Use a *variadic* function signature with a second parameter. The name and +/// type of the second parameter will be used as the raw argument. For example: +/// +/// ``` +/// # struct Object; +/// # struct ArgsType; +/// #[binrw::writer] +/// fn custom_writer(obj: &Object, args: ArgsType, ...) -> binrw::BinResult<()> { +/// Ok(()) +/// } +/// # custom_writer(&Object, &mut binrw::io::Cursor::new(vec![]), binrw::Endian::Little, ArgsType).unwrap(); +/// ``` +/// +/// The transformed output for this function is: +/// +/// ``` +/// # struct Object; +/// # struct ArgsType; +/// use binrw::{BinResult, Endian, io::{Seek, Write}}; +/// fn custom_writer( +/// obj: &Object, +/// _: &mut W, +/// _: Endian, +/// args: ArgsType +/// ) -> BinResult<()> { +/// Ok(()) +/// } +/// # custom_writer(&Object, &mut binrw::io::Cursor::new(vec![]), binrw::Endian::Little, ArgsType).unwrap(); +/// ``` +/// +/// # Return value +/// +/// The return value of a writer function must be [`BinResult<()>`](BinResult). +pub use binrw_derive::writer; + /// A specialized [`Result`] type for binrw operations. pub type BinResult = core::result::Result; diff --git a/binrw/src/punctuated.rs b/binrw/src/punctuated.rs index cf939729..447d3fcb 100644 --- a/binrw/src/punctuated.rs +++ b/binrw/src/punctuated.rs @@ -1,9 +1,6 @@ //! Type definitions for wrappers which parse interleaved data. -use crate::{ - io::{Read, Seek}, - BinRead, BinResult, Endian, VecArgs, -}; +use crate::{BinRead, BinResult, VecArgs}; use alloc::vec::Vec; use core::fmt; @@ -74,13 +71,8 @@ impl> Punctuated { /// # assert_eq!(*y.x, vec![3, 2, 1]); /// # assert_eq!(y.x.separators, vec![0, 1]); /// ``` - // Lint: Non-consumed argument is required to match the API. - #[allow(clippy::needless_pass_by_value)] - pub fn separated( - reader: &mut R, - endian: Endian, - args: VecArgs, - ) -> BinResult { + #[crate::parser(reader, endian)] + pub fn separated(args: VecArgs, ...) -> BinResult { let mut data = Vec::with_capacity(args.count); let mut separators = Vec::with_capacity(args.count.max(1) - 1); @@ -102,13 +94,8 @@ impl> Punctuated { /// # Errors /// /// If reading fails, an [`Error`](crate::Error) variant will be returned. - // Lint: Non-consumed argument is required to match the API. - #[allow(clippy::needless_pass_by_value)] - pub fn separated_trailing( - reader: &mut R, - endian: Endian, - args: VecArgs, - ) -> BinResult { + #[crate::parser(reader, endian)] + pub fn separated_trailing(args: VecArgs, ...) -> BinResult { let mut data = Vec::with_capacity(args.count); let mut separators = Vec::with_capacity(args.count); diff --git a/binrw/tests/derive/struct.rs b/binrw/tests/derive/struct.rs index df2cd258..abd7a483 100644 --- a/binrw/tests/derive/struct.rs +++ b/binrw/tests/derive/struct.rs @@ -1,7 +1,7 @@ use binrw::{ args, binread, - io::{Cursor, Read, Seek, SeekFrom}, - BinRead, BinResult, Endian, FilePtr, NullString, + io::{Cursor, Seek, SeekFrom}, + BinRead, BinResult, FilePtr, NullString, }; #[test] @@ -26,11 +26,8 @@ fn all_the_things() { calc_test: u32, } - fn read_offsets( - reader: &mut R, - endian: Endian, - _: (), - ) -> BinResult<(u16, u16)> { + #[binrw::parser(reader, endian)] + fn read_offsets() -> BinResult<(u16, u16)> { Ok(( u16::read_options(reader, endian, ())?, u16::read_options(reader, endian, ())?, diff --git a/binrw/tests/derive/write/custom_writer.rs b/binrw/tests/derive/write/custom_writer.rs index 092eae93..336af978 100644 --- a/binrw/tests/derive/write/custom_writer.rs +++ b/binrw/tests/derive/write/custom_writer.rs @@ -10,12 +10,8 @@ fn custom_writer() { y: u16, } - fn custom_writer( - _this: &u16, - writer: &mut W, - _: Endian, - _: (), - ) -> binrw::BinResult<()> { + #[binrw::writer(writer)] + fn custom_writer(_this: &u16) -> binrw::BinResult<()> { writer.write_all(b"abcd")?; Ok(()) } diff --git a/binrw/tests/ui/bad_parse_with_fn.rs b/binrw/tests/ui/bad_parse_with_fn.rs index 67478474..03b466c2 100644 --- a/binrw/tests/ui/bad_parse_with_fn.rs +++ b/binrw/tests/ui/bad_parse_with_fn.rs @@ -1,6 +1,7 @@ use binrw::{BinRead, BinResult}; -fn wrong(_: R, _: binrw::Endian, _: ()) -> BinResult { +#[binrw::parser] +fn wrong() -> BinResult { Ok(true) } diff --git a/binrw/tests/ui/bad_parse_with_fn.stderr b/binrw/tests/ui/bad_parse_with_fn.stderr index e320480f..f9c700b5 100644 --- a/binrw/tests/ui/bad_parse_with_fn.stderr +++ b/binrw/tests/ui/bad_parse_with_fn.stderr @@ -1,49 +1,49 @@ error[E0425]: cannot find value `missing` in this scope - --> tests/ui/bad_parse_with_fn.rs:17:23 + --> tests/ui/bad_parse_with_fn.rs:18:23 | -17 | #[br(parse_with = missing)] +18 | #[br(parse_with = missing)] | ^^^^^^^ not found in this scope error[E0277]: expected a `FnOnce<(&mut _, binrw::Endian, _)>` closure, found `{integer}` - --> tests/ui/bad_parse_with_fn.rs:9:23 - | -9 | #[br(parse_with = 56)] - | ^^ expected an `FnOnce<(&mut _, binrw::Endian, _)>` closure, found `{integer}` - | - = help: the trait `for<'a> FnOnce<(&'a mut _, binrw::Endian, _)>` is not implemented for `{integer}` + --> tests/ui/bad_parse_with_fn.rs:10:23 + | +10 | #[br(parse_with = 56)] + | ^^ expected an `FnOnce<(&mut _, binrw::Endian, _)>` closure, found `{integer}` + | + = help: the trait `for<'a> FnOnce<(&'a mut _, binrw::Endian, _)>` is not implemented for `{integer}` note: required by a bound in `parse_fn_type_hint` - --> src/private.rs - | - | ParseFn: FnOnce(&mut R, Endian, Args) -> BinResult, - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `parse_fn_type_hint` + --> src/private.rs + | + | ParseFn: FnOnce(&mut R, Endian, Args) -> BinResult, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `parse_fn_type_hint` error[E0618]: expected function, found `{integer}` - --> tests/ui/bad_parse_with_fn.rs:9:23 - | -9 | #[br(parse_with = 56)] - | ^^ call expression requires function + --> tests/ui/bad_parse_with_fn.rs:10:23 + | +10 | #[br(parse_with = 56)] + | ^^ call expression requires function error[E0308]: mismatched types - --> tests/ui/bad_parse_with_fn.rs:11:35 + --> tests/ui/bad_parse_with_fn.rs:12:35 | -11 | #[br(parse_with = |_, _, _| { true })] +12 | #[br(parse_with = |_, _, _| { true })] | ^^^^ expected enum `Result`, found `bool` | = note: expected enum `Result<_, binrw::Error>` found type `bool` help: try wrapping the expression in `Ok` | -11 | #[br(parse_with = |_, _, _| { Ok(true) })] +12 | #[br(parse_with = |_, _, _| { Ok(true) })] | +++ + error[E0308]: mismatched types - --> tests/ui/bad_parse_with_fn.rs:13:23 + --> tests/ui/bad_parse_with_fn.rs:14:23 | -13 | #[br(parse_with = |_, _, _| { Ok(true) })] +14 | #[br(parse_with = |_, _, _| { Ok(true) })] | ^^^^^^^^^^^^^^^^^^^^^^ expected `i32`, found `bool` error[E0308]: mismatched types - --> tests/ui/bad_parse_with_fn.rs:15:23 + --> tests/ui/bad_parse_with_fn.rs:16:23 | -15 | #[br(parse_with = wrong)] +16 | #[br(parse_with = wrong)] | ^^^^^ expected `i32`, found `bool` diff --git a/binrw/tests/ui/fn_helper_errors.rs b/binrw/tests/ui/fn_helper_errors.rs new file mode 100644 index 00000000..ded6a86d --- /dev/null +++ b/binrw/tests/ui/fn_helper_errors.rs @@ -0,0 +1,71 @@ +use binrw::{parser, writer, BinResult}; + +#[parser(reader:)] +fn fn_helper_invalid_option_value() -> BinResult<()> { + Ok(()) +} + +#[parser(reader = invalid)] +fn fn_helper_invalid_option_token() -> BinResult<()> { + Ok(()) +} + +#[parser(invalid)] +fn fn_helper_invalid_reader() -> BinResult<()> { + Ok(()) +} + +#[writer(invalid)] +fn fn_helper_invalid_writer() -> BinResult<()> { + Ok(()) +} + +#[parser(reader, reader)] +fn fn_helper_conflicting_reader() -> BinResult<()> { + Ok(()) +} + +#[writer(writer, writer)] +fn fn_helper_conflicting_writer() -> BinResult<()> { + Ok(()) +} + +#[parser(endian, endian)] +fn fn_helper_conflicting_endian() -> BinResult<()> { + Ok(()) +} + +struct InvalidSelf; +impl InvalidSelf { + #[parser] + fn fn_helper_invalid_self(&self) -> BinResult<()> { + Ok(()) + } +} + +#[writer] +fn fn_helper_missing_object() -> BinResult<()> { + Ok(()) +} + +#[parser] +fn fn_helper_missing_args_reader(...) -> BinResult<()> { + Ok(()) +} + +#[parser] +fn fn_helper_extra_args_reader(arg0: (), arg1: (), ...) -> BinResult<()> { + Ok(()) +} + +#[writer] +fn fn_helper_extra_args_writer(arg0: &(), arg1: (), arg2: (), ...) -> BinResult<()> { + Ok(()) +} + +#[writer] +fn fn_helper_missing_args_writer(obj: &(), ...) -> BinResult<()> { + Ok(()) +} + +fn main() {} diff --git a/binrw/tests/ui/fn_helper_errors.stderr b/binrw/tests/ui/fn_helper_errors.stderr new file mode 100644 index 00000000..6a7118ac --- /dev/null +++ b/binrw/tests/ui/fn_helper_errors.stderr @@ -0,0 +1,79 @@ +error: unexpected end of input, expected identifier + --> tests/ui/fn_helper_errors.rs:3:1 + | +3 | #[parser(reader:)] + | ^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the attribute macro `parser` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: expected `:` + --> tests/ui/fn_helper_errors.rs:8:17 + | +8 | #[parser(reader = invalid)] + | ^ + +error: expected `reader` or `endian` + --> tests/ui/fn_helper_errors.rs:13:10 + | +13 | #[parser(invalid)] + | ^^^^^^^ + +error: expected `writer` or `endian` + --> tests/ui/fn_helper_errors.rs:18:10 + | +18 | #[writer(invalid)] + | ^^^^^^^ + +error: conflicting `reader` keyword + --> tests/ui/fn_helper_errors.rs:23:18 + | +23 | #[parser(reader, reader)] + | ^^^^^^ + +error: conflicting `writer` keyword + --> tests/ui/fn_helper_errors.rs:28:18 + | +28 | #[writer(writer, writer)] + | ^^^^^^ + +error: conflicting `endian` keyword + --> tests/ui/fn_helper_errors.rs:33:18 + | +33 | #[parser(endian, endian)] + | ^^^^^^ + +error: invalid `self` in free function + --> tests/ui/fn_helper_errors.rs:41:31 + | +41 | fn fn_helper_invalid_self(&self) -> BinResult<()> { + | ^^^^^ + +error: missing required value parameter + --> tests/ui/fn_helper_errors.rs:47:4 + | +47 | fn fn_helper_missing_object() -> BinResult<()> { + | ^^^^^^^^^^^^^^^^^^^^^^^^ + +error: missing raw arguments parameter + --> tests/ui/fn_helper_errors.rs:52:34 + | +52 | fn fn_helper_missing_args_reader(...) -> BinResult<()> { + | ^^^ + +error: unexpected extra parameter after raw arguments + --> tests/ui/fn_helper_errors.rs:57:42 + | +57 | fn fn_helper_extra_args_reader(arg0: (), arg1: (), ...) -> BinResult<()> { + | ^^^^^^^^ + +error: unexpected extra parameter after raw arguments + --> tests/ui/fn_helper_errors.rs:62:53 + | +62 | fn fn_helper_extra_args_writer(arg0: &(), arg1: (), arg2: (), ...) -> BinResult<()> { + | ^^^^^^^^ + +error: missing raw arguments parameter + --> tests/ui/fn_helper_errors.rs:67:44 + | +67 | fn fn_helper_missing_args_writer(obj: &(), ...) -> BinResult<()> { + | ^^^ diff --git a/binrw_derive/src/binrw/parser/mod.rs b/binrw_derive/src/binrw/parser/mod.rs index 8efc79ec..248702b4 100644 --- a/binrw_derive/src/binrw/parser/mod.rs +++ b/binrw_derive/src/binrw/parser/mod.rs @@ -2,7 +2,6 @@ mod attrs; mod field_level_attrs; mod keywords; mod macros; -mod result; mod top_level_attrs; mod try_set; mod types; @@ -14,11 +13,12 @@ use crate::{ }; pub(crate) use field_level_attrs::{EnumVariant, StructField, UnitEnumField}; use macros::attr_struct; -pub(crate) use result::ParseResult; pub(crate) use top_level_attrs::{Enum, Input, Struct, UnitOnlyEnum}; use try_set::TrySet; pub(crate) use types::*; +pub(crate) type ParseResult = crate::result::PartialResult; + trait FromAttrs { fn try_from_attrs(attrs: &[syn::Attribute], options: Options) -> ParseResult where diff --git a/binrw_derive/src/binrw/parser/result.rs b/binrw_derive/src/binrw/parser/result.rs deleted file mode 100644 index 28037609..00000000 --- a/binrw_derive/src/binrw/parser/result.rs +++ /dev/null @@ -1,67 +0,0 @@ -#[derive(Debug)] -pub(crate) enum PartialResult { - Ok(T), - Partial(T, E), - Err(E), -} - -impl PartialResult { - #[cfg(test)] - #[cfg_attr(coverage_nightly, no_coverage)] - pub(crate) fn err(self) -> Option { - match self { - PartialResult::Ok(_) => None, - PartialResult::Partial(_, error) | PartialResult::Err(error) => Some(error), - } - } - - pub(crate) fn map(self, f: F) -> PartialResult - where - F: FnOnce(T) -> U, - { - match self { - PartialResult::Ok(value) => PartialResult::Ok(f(value)), - PartialResult::Partial(value, error) => PartialResult::Partial(f(value), error), - PartialResult::Err(error) => PartialResult::Err(error), - } - } - - pub(crate) fn ok(self) -> Option { - match self { - PartialResult::Ok(value) | PartialResult::Partial(value, _) => Some(value), - PartialResult::Err(_) => None, - } - } -} - -impl PartialResult { - #[cfg(test)] - #[cfg_attr(coverage_nightly, no_coverage)] - #[track_caller] - pub(crate) fn unwrap(self) -> T { - match self { - PartialResult::Ok(value) => value, - PartialResult::Partial(_, error) => panic!( - "called `PartialResult::unwrap() on a `Partial` value: {:?}", - &error - ), - PartialResult::Err(error) => panic!( - "called `PartialResult::unwrap() on an `Err` value: {:?}", - &error - ), - } - } - - pub(crate) fn unwrap_tuple(self) -> (T, Option) { - match self { - PartialResult::Ok(value) => (value, None), - PartialResult::Partial(value, error) => (value, Some(error)), - PartialResult::Err(error) => panic!( - "called `PartialResult::unwrap_tuple() on an `Err` value: {:?}", - &error - ), - } - } -} - -pub(crate) type ParseResult = PartialResult; diff --git a/binrw_derive/src/fn_helper/mod.rs b/binrw_derive/src/fn_helper/mod.rs new file mode 100644 index 00000000..dc17dbf7 --- /dev/null +++ b/binrw_derive/src/fn_helper/mod.rs @@ -0,0 +1,298 @@ +use crate::{ + combine_error, + result::PartialResult, + util::{from_crate, ident_str}, +}; +use proc_macro::TokenStream; +use quote::{quote, ToTokens}; +use syn::{ + parse::{Parse, ParseStream}, + parse_macro_input, parse_quote, + punctuated::Punctuated, + spanned::Spanned, + Error, FnArg, Ident, ItemFn, Pat, Token, +}; + +#[cfg_attr(coverage_nightly, no_coverage)] +pub(crate) fn derive_from_attribute( + attr: TokenStream, + input: TokenStream, +) -> TokenStream { + match generate::( + parse_macro_input!(attr as Options), + parse_macro_input!(input as ItemFn), + ) { + PartialResult::Ok(func) => func.into_token_stream(), + PartialResult::Partial(func, err) => { + let err = err.into_compile_error(); + quote! { + #func + #err + } + } + PartialResult::Err(err) => err.into_compile_error(), + } + .into() +} + +fn generate( + Options { stream, endian }: Options, + mut func: ItemFn, +) -> PartialResult { + // Since these functions are written to match the binrw API, args must be + // passed by value even when they are not consumed, so suppress this lint + func.attrs + .push(parse_quote!(#[allow(clippy::needless_pass_by_value)])); + + let raw_args_span = func.sig.variadic.take().map(|variadic| variadic.span()); + + func.sig.generics.params.push({ + let stream_trait = if WRITE { WRITE_TRAIT } else { READ_TRAIT }; + + parse_quote!(#STREAM_T: #stream_trait + #SEEK_TRAIT) + }); + + let mut args = core::mem::take(&mut func.sig.inputs).into_iter(); + let mut args_pat = Punctuated::<_, Token![,]>::new(); + let mut args_ty = Punctuated::<_, Token![,]>::new(); + + if WRITE { + if let Some(arg) = args.next() { + func.sig.inputs.push(arg); + } else { + let span = func.sig.ident.span(); + return PartialResult::Partial( + func, + Error::new(span, "missing required value parameter"), + ); + } + } + + func.sig.inputs.push(parse_quote!(#stream: &mut #STREAM_T)); + func.sig.inputs.push(parse_quote!(#endian: #ENDIAN_ENUM)); + + if let Some(raw_args_span) = raw_args_span { + if let Some(arg) = args.next() { + func.sig.inputs.push(arg); + } else { + return PartialResult::Partial( + func, + Error::new(raw_args_span, "missing raw arguments parameter"), + ); + } + + if let Some(arg) = args.next() { + return PartialResult::Partial( + func, + Error::new(arg.span(), "unexpected extra parameter after raw arguments"), + ); + } + } else { + for arg in args { + match arg { + FnArg::Receiver(r) => { + return PartialResult::Partial( + func, + Error::new(r.span(), "invalid `self` in free function"), + ); + } + FnArg::Typed(ty) => { + args_pat.push(ty.pat); + args_ty.push(ty.ty); + } + } + } + + func.sig.inputs.push(parse_quote!((#args_pat): (#args_ty))); + } + + PartialResult::Ok(func) +} + +ident_str! { + STREAM_T = "__BinrwGeneratedStreamT"; + ENDIAN_ENUM = from_crate!(Endian); + READ_TRAIT = from_crate!(io::Read); + WRITE_TRAIT = from_crate!(io::Write); + SEEK_TRAIT = from_crate!(io::Seek); +} + +struct Options { + stream: Pat, + endian: Pat, +} + +impl Parse for Options { + fn parse(input: ParseStream<'_>) -> syn::Result { + fn try_set( + kw: &str, + value: Ident, + out: &mut Option, + all_errors: &mut Option, + ) { + if out.is_none() { + *out = Some(value); + } else { + combine_error( + all_errors, + Error::new(value.span(), format!("conflicting `{}` keyword", kw)), + ); + } + } + + let mut stream = None; + let mut endian = None; + + let mut all_errors = None; + + for arg in Punctuated::, Token![,]>::parse_terminated(input)? { + match arg { + Arg::Stream(ident) => try_set( + if WRITE { "writer" } else { "reader" }, + ident, + &mut stream, + &mut all_errors, + ), + Arg::Endian(ident) => try_set("endian", ident, &mut endian, &mut all_errors), + } + } + + if let Some(error) = all_errors { + Err(error) + } else { + Ok(Self { + stream: stream.map_or_else(|| parse_quote!(_), |ident| parse_quote!(#ident)), + endian: endian.map_or_else(|| parse_quote!(_), |ident| parse_quote!(#ident)), + }) + } + } +} + +enum Arg { + Stream(Ident), + Endian(Ident), +} + +impl Parse for Arg { + fn parse(input: ParseStream<'_>) -> syn::Result { + fn maybe_ident(default: Ident, input: ParseStream<'_>) -> syn::Result { + if input.is_empty() || input.peek(Token![,]) { + Ok(default) + } else { + let next = input.lookahead1(); + if next.peek(Token![:]) { + input.parse::()?; + input.parse() + } else { + Err(next.error()) + } + } + } + + let kw = input.lookahead1(); + if (WRITE && kw.peek(kw::writer)) || (!WRITE && kw.peek(kw::reader)) { + let kw = input.parse::()?; + Ok(Arg::Stream(maybe_ident(kw, input)?)) + } else if kw.peek(kw::endian) { + let kw = input.parse::()?; + Ok(Arg::Endian(maybe_ident(kw, input)?)) + } else { + Err(kw.error()) + } + } +} + +mod kw { + syn::custom_keyword!(endian); + syn::custom_keyword!(reader); + syn::custom_keyword!(value); + syn::custom_keyword!(writer); +} + +#[cfg(test)] +mod tests { + use super::*; + use proc_macro2::TokenStream; + + #[cfg_attr(coverage_nightly, no_coverage)] + fn try_input(attr: TokenStream, params: &TokenStream) { + let options = syn::parse2::>(attr).unwrap(); + let func = syn::parse2::(quote::quote! { + fn test(#params) -> binrw::BinResult<()> { Ok(()) } + }) + .unwrap(); + generate::(options, func).unwrap(); + } + + macro_rules! try_error ( + (read $name:ident: $message:literal $opts:tt $params:tt) => { + #[test] + #[cfg_attr(coverage_nightly, no_coverage)] + #[should_panic(expected = $message)] + fn $name() { + try_input::(quote::quote! $opts, "e::quote! $params); + } + }; + + (write $name:ident: $message:literal $opts:tt $params:tt) => { + #[test] + #[cfg_attr(coverage_nightly, no_coverage)] + #[should_panic(expected = $message)] + fn $name() { + try_input::(quote::quote! $opts, "e::quote! $params); + } + }; + ); + + try_error!(read fn_helper_invalid_option_value: "expected identifier" + [reader:] () + ); + + try_error!(read fn_helper_invalid_option_token: "expected `:`" + [reader = invalid] () + ); + + try_error!(read fn_helper_invalid_reader: "expected `reader` or `endian`" + [invalid] () + ); + + try_error!(write fn_helper_invalid_writer: "expected `writer` or `endian`" + [invalid] () + ); + + try_error!(read fn_helper_conflicting_reader: "conflicting `reader`" + [reader, reader] () + ); + + try_error!(write fn_helper_conflicting_writer: "conflicting `writer`" + [writer, writer] () + ); + + try_error!(read fn_helper_conflicting_endian: "conflicting `endian`" + [endian, endian] () + ); + + try_error!(read fn_helper_invalid_self: "invalid `self`" + [] (&self) + ); + + try_error!(write fn_helper_missing_object: "missing required value" + [] () + ); + + try_error!(read fn_helper_missing_args_reader: "missing raw arguments" + [] (...) + ); + + try_error!(read fn_helper_extra_args_reader: "unexpected extra parameter" + [] (arg0: (), arg1: (), ...) + ); + + try_error!(write fn_helper_extra_args_writer: "unexpected extra parameter" + [] (arg0: &(), arg1: (), arg2: (), ...) + ); + + try_error!(write fn_helper_missing_args_writer: "missing raw arguments" + [] (obj: &(), ...) + ); +} diff --git a/binrw_derive/src/lib.rs b/binrw_derive/src/lib.rs index 08cec916..54235fb5 100644 --- a/binrw_derive/src/lib.rs +++ b/binrw_derive/src/lib.rs @@ -6,8 +6,10 @@ extern crate alloc; mod binrw; +mod fn_helper; mod meta_types; mod named_args; +mod result; pub(crate) mod util; use proc_macro::TokenStream; @@ -67,6 +69,18 @@ pub fn named_args_derive(input: TokenStream) -> TokenStream { named_args::derive_from_input(parse_macro_input!(input as DeriveInput)).into() } +#[proc_macro_attribute] +#[cfg_attr(coverage_nightly, no_coverage)] +pub fn parser(attr: TokenStream, input: TokenStream) -> TokenStream { + fn_helper::derive_from_attribute::(attr, input) +} + +#[proc_macro_attribute] +#[cfg_attr(coverage_nightly, no_coverage)] +pub fn writer(attr: TokenStream, input: TokenStream) -> TokenStream { + fn_helper::derive_from_attribute::(attr, input) +} + fn combine_error(all_errors: &mut Option, new_error: syn::Error) { if let Some(all_errors) = all_errors { all_errors.combine(new_error); diff --git a/binrw_derive/src/result.rs b/binrw_derive/src/result.rs new file mode 100644 index 00000000..8d1453d8 --- /dev/null +++ b/binrw_derive/src/result.rs @@ -0,0 +1,146 @@ +#[derive(Debug, Eq, PartialEq)] +pub(crate) enum PartialResult { + Ok(T), + Partial(T, E), + Err(E), +} + +impl PartialResult { + #[cfg(test)] + pub(crate) fn err(self) -> Option { + match self { + PartialResult::Ok(_) => None, + PartialResult::Partial(_, error) | PartialResult::Err(error) => Some(error), + } + } + + pub(crate) fn map(self, f: F) -> PartialResult + where + F: FnOnce(T) -> U, + { + match self { + PartialResult::Ok(value) => PartialResult::Ok(f(value)), + PartialResult::Partial(value, error) => PartialResult::Partial(f(value), error), + PartialResult::Err(error) => PartialResult::Err(error), + } + } + + pub(crate) fn ok(self) -> Option { + match self { + PartialResult::Ok(value) | PartialResult::Partial(value, _) => Some(value), + PartialResult::Err(_) => None, + } + } +} + +impl PartialResult { + #[cfg(test)] + #[track_caller] + pub(crate) fn unwrap(self) -> T { + match self { + PartialResult::Ok(value) => value, + PartialResult::Partial(_, error) => panic!( + "called `PartialResult::unwrap()` on a `Partial` value: {:?}", + &error + ), + PartialResult::Err(error) => panic!( + "called `PartialResult::unwrap()` on an `Err` value: {:?}", + &error + ), + } + } + + pub(crate) fn unwrap_tuple(self) -> (T, Option) { + match self { + PartialResult::Ok(value) => (value, None), + PartialResult::Partial(value, error) => (value, Some(error)), + PartialResult::Err(error) => panic!( + "called `PartialResult::unwrap_tuple()` on an `Err` value: {:?}", + &error + ), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Debug, Eq, PartialEq)] + struct Pass; + + #[derive(Debug, Eq, PartialEq)] + struct Error; + + #[test] + #[cfg_attr(coverage_nightly, no_coverage)] + fn err() { + assert_eq!(PartialResult::<_, Error>::Ok(Pass).err(), None); + assert_eq!(PartialResult::Partial(Pass, Error).err(), Some(Error)); + assert_eq!(PartialResult::::Err(Error).err(), Some(Error)); + } + + #[test] + #[cfg_attr(coverage_nightly, no_coverage)] + fn map() { + assert_eq!( + PartialResult::<_, Error>::Ok(()).map(|_| Pass), + PartialResult::Ok(Pass) + ); + assert_eq!( + PartialResult::Partial((), Error).map(|_| Pass), + PartialResult::Partial(Pass, Error) + ); + assert_eq!( + PartialResult::<(), _>::Err(Error).map(|_| Pass), + PartialResult::Err(Error) + ); + } + + #[test] + #[cfg_attr(coverage_nightly, no_coverage)] + fn ok() { + assert_eq!(PartialResult::<_, Error>::Ok(Pass).ok(), Some(Pass)); + assert_eq!(PartialResult::Partial(Pass, Error).ok(), Some(Pass)); + assert_eq!(PartialResult::::Err(Error).ok(), None); + } + + #[test] + fn unwrap() { + assert_eq!(PartialResult::<_, Error>::Ok(Pass).unwrap(), Pass); + } + + #[test] + #[cfg_attr(coverage_nightly, no_coverage)] + #[should_panic(expected = "called `PartialResult::unwrap()` on an `Err` value")] + fn unwrap_err() { + PartialResult::::Err(Error).unwrap(); + } + + #[test] + #[cfg_attr(coverage_nightly, no_coverage)] + #[should_panic(expected = "called `PartialResult::unwrap()` on a `Partial` value")] + fn unwrap_partial() { + PartialResult::Partial(Pass, Error).unwrap(); + } + + #[test] + #[cfg_attr(coverage_nightly, no_coverage)] + fn unwrap_tuple() { + assert_eq!( + PartialResult::<_, Error>::Ok(Pass).unwrap_tuple(), + (Pass, None) + ); + assert_eq!( + PartialResult::Partial(Pass, Error).unwrap_tuple(), + (Pass, Some(Error)) + ); + } + + #[test] + #[cfg_attr(coverage_nightly, no_coverage)] + #[should_panic(expected = "called `PartialResult::unwrap_tuple()` on an `Err` value")] + fn unwrap_tuple_err() { + PartialResult::::Err(Error).unwrap_tuple(); + } +}