Repository: sharksforarms/deku
Pull request #112 (2020-10-20 20:35:09)

Support magic values prefixing types
This PR as-is is an initial implementation of #109, hacked-in with very little experience in this library or with developing proc-macros in general.

It currently supports reads only, and is very specific to my use-case, so will want to be expanded to support writes and to be more useful in general, and will also want tests implementing and documentation adding for the new attribute.

codecov[bot]: # [Codecov](https://codecov.io/gh/sharksforarms/deku/pull/112?src=pr&el=h1) Report
> Merging [#112](https://codecov.io/gh/sharksforarms/deku/pull/112?src=pr&el=desc) into [master](https://codecov.io/gh/sharksforarms/deku/commit/cea1a43156e611f61254a67afb5c97d6dbdcbfb1?el=desc) will **increase** coverage by `0.04%`.
> The diff coverage is `75.00%`.

[![Impacted file tree graph](https://codecov.io/gh/sharksforarms/deku/pull/112/graphs/tree.svg?width=650&height=150&src=pr&token=MFMETW02MZ)](https://codecov.io/gh/sharksforarms/deku/pull/112?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #112      +/-   ##
==========================================
+ Coverage   83.66%   83.71%   +0.04%     
==========================================
  Files          11       11              
  Lines        1004     1007       +3     
==========================================
+ Hits          840      843       +3     
  Misses        164      164              
```


| [Impacted Files](https://codecov.io/gh/sharksforarms/deku/pull/112?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [deku-derive/src/macros/deku\_read.rs](https://codecov.io/gh/sharksforarms/deku/pull/112/diff?src=pr&el=tree#diff-ZGVrdS1kZXJpdmUvc3JjL21hY3Jvcy9kZWt1X3JlYWQucnM=) | `84.61% <66.66%> (-0.48%)` | :arrow_down: |
| [deku-derive/src/lib.rs](https://codecov.io/gh/sharksforarms/deku/pull/112/diff?src=pr&el=tree#diff-ZGVrdS1kZXJpdmUvc3JjL2xpYi5ycw==) | `61.62% <100.00%> (+0.20%)` | :arrow_up: |
| [src/lib.rs](https://codecov.io/gh/sharksforarms/deku/pull/112/diff?src=pr&el=tree#diff-c3JjL2xpYi5ycw==) | `96.89% <0.00%> (+0.37%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/sharksforarms/deku/pull/112?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/sharksforarms/deku/pull/112?src=pr&el=footer). Last update [cea1a43...22e271d](https://codecov.io/gh/sharksforarms/deku/pull/112?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

samuelsleight: Thanks for the pointers! I'll hopefully get a chance to look into these tomorrow
samuelsleight: Cool, this should now be somewhat less hacked-in!

Patch:
diff --git a/deku-derive/src/lib.rs b/deku-derive/src/lib.rs
index eb323ff..c1eb4bf 100644
--- a/deku-derive/src/lib.rs
+++ b/deku-derive/src/lib.rs
@@ -29,6 +29,9 @@ struct DekuData {
     /// default context passed to the field
     ctx_default: Option<Punctuated<syn::Expr, syn::token::Comma>>,
 
+    /// A magic value that must appear at the start of this struct/enum's data
+    magic: Option<syn::LitByteStr>,
+
     /// enum only: `id` value
     id: Option<TokenStream>,
 
@@ -87,6 +90,7 @@ impl DekuData {
             endian: receiver.endian,
             ctx,
             ctx_default,
+            magic: receiver.magic,
             id: receiver.id,
             id_type: receiver.id_type,
             bits,
@@ -386,6 +390,10 @@ struct DekuReceiver {
     #[darling(default)]
     ctx_default: Option<syn::LitStr>,
 
+    /// A magic value that must appear at the start of this struct/enum's data
+    #[darling(default)]
+    magic: Option<syn::LitByteStr>,
+
     /// enum only: `id` value
     #[darling(default, map = "option_as_tokenstream")]
     id: Option<TokenStream>,
diff --git a/deku-derive/src/macros/deku_read.rs b/deku-derive/src/macros/deku_read.rs
index 1d524a2..591d979 100644
--- a/deku-derive/src/macros/deku_read.rs
+++ b/deku-derive/src/macros/deku_read.rs
@@ -32,6 +32,8 @@ fn emit_struct(input: &DekuData) -> Result<TokenStream, syn::Error> {
         .and_then(|v| v.ident.as_ref())
         .is_some();
 
+    let magic_read = emit_magic_read(input)?;
+
     let (field_idents, field_reads) = emit_field_reads(input, &fields)?;
 
     let internal_fields = gen_internal_field_idents(is_named_struct, field_idents);
@@ -48,6 +50,8 @@ fn emit_struct(input: &DekuData) -> Result<TokenStream, syn::Error> {
                 let mut rest = input.0.view_bits::<Msb0>();
                 rest = &rest[input.1..];
 
+                #magic_read
+
                 #(#field_reads)*
                 let value = #initialize_struct;
 
@@ -71,6 +75,8 @@ fn emit_struct(input: &DekuData) -> Result<TokenStream, syn::Error> {
         use core::convert::TryFrom;
         let mut rest = input;
 
+        #magic_read
+
         #(#field_reads)*
         let value = #initialize_struct;
 
@@ -118,6 +124,8 @@ fn emit_enum(input: &DekuData) -> Result<TokenStream, syn::Error> {
 
     let id_args = gen_id_args(input.endian.as_ref(), input.bits)?;
 
+    let magic_read = emit_magic_read(input)?;
+
     let mut variant_matches = vec![];
     let mut has_default_match = false;
 
@@ -220,6 +228,8 @@ fn emit_enum(input: &DekuData) -> Result<TokenStream, syn::Error> {
                 let mut rest = input.0.bits::<Msb0>();
                 rest = &rest[input.1..];
 
+                #magic_read
+
                 #variant_read
 
                 let pad = 8 * ((rest.len() + 7) / 8) - rest.len();
@@ -242,6 +252,8 @@ fn emit_enum(input: &DekuData) -> Result<TokenStream, syn::Error> {
         use core::convert::TryFrom;
         let mut rest = input;
 
+        #magic_read
+
         #variant_read
 
         Ok((rest, value))
@@ -271,6 +283,27 @@ fn emit_enum(input: &DekuData) -> Result<TokenStream, syn::Error> {
     Ok(tokens)
 }
 
+fn emit_magic_read(input: &DekuData) -> Result<TokenStream, syn::Error> {
+    let tokens = if let Some(magic) = &input.magic {
+        quote! {
+            let magic = #magic;
+
+            for byte in magic {
+                let (new_rest, read_byte) = u8::read(rest, ())?;
+                if *byte != read_byte {
+                    return Err(DekuError::Parse(format!("Missing magic value {:?}", #magic)));
+                }
+
+                rest = new_rest;
+            }
+        }
+    } else {
+        quote! {}
+    };
+
+    Ok(tokens)
+}
+
 fn emit_field_reads(
     input: &DekuData,
     fields: &Fields<&FieldData>,
diff --git a/deku-derive/src/macros/deku_write.rs b/deku-derive/src/macros/deku_write.rs
index 9f061d1..033ad3d 100644
--- a/deku-derive/src/macros/deku_write.rs
+++ b/deku-derive/src/macros/deku_write.rs
@@ -21,6 +21,8 @@ fn emit_struct(input: &DekuData) -> Result<TokenStream, syn::Error> {
     let ident = &input.ident;
     let ident = quote! { #ident #ty };
 
+    let magic_write = emit_magic_write(input)?;
+
     // Checked in `emit_deku_write`.
     let fields = input.data.as_ref().take_struct().unwrap();
 
@@ -45,6 +47,7 @@ fn emit_struct(input: &DekuData) -> Result<TokenStream, syn::Error> {
                     #destructured => {
                         let mut acc: BitVec<Msb0, u8> = BitVec::new();
                         let output = &mut acc;
+                        #magic_write
                         #(#field_writes)*
 
                         Ok(acc)
@@ -91,6 +94,7 @@ fn emit_struct(input: &DekuData) -> Result<TokenStream, syn::Error> {
     let write_body = quote! {
         match *self {
             #destructured => {
+                #magic_write
                 #(#field_writes)*
 
                 Ok(())
@@ -141,6 +145,8 @@ fn emit_enum(input: &DekuData) -> Result<TokenStream, syn::Error> {
 
     let (imp, ty, wher) = input.generics.split_for_impl();
 
+    let magic_write = emit_magic_write(input)?;
+
     // checked in emit_deku_write
     let variants = input.data.as_ref().take_enum().unwrap();
 
@@ -237,6 +243,8 @@ fn emit_enum(input: &DekuData) -> Result<TokenStream, syn::Error> {
                 let mut acc: BitVec<Msb0, u8> = BitVec::new();
                 let output = &mut acc;
 
+                #magic_write
+
                 match self {
                     #(#variant_writes),*
                 }
@@ -281,6 +289,8 @@ fn emit_enum(input: &DekuData) -> Result<TokenStream, syn::Error> {
     let (ctx_types, ctx_arg) = gen_ctx_types_and_arg(input.ctx.as_ref())?;
 
     let write_body = quote! {
+        #magic_write
+
         match self {
             #(#variant_writes),*
         }
@@ -329,6 +339,18 @@ fn emit_enum(input: &DekuData) -> Result<TokenStream, syn::Error> {
     Ok(tokens)
 }
 
+fn emit_magic_write(input: &DekuData) -> Result<TokenStream, syn::Error> {
+    let tokens = if let Some(magic) = &input.magic {
+        quote! {
+            #magic.write(output, ())?;
+        }
+    } else {
+        quote! {}
+    };
+
+    Ok(tokens)
+}
+
 fn emit_field_writes(
     input: &DekuData,
     fields: &Fields<&FieldData>,
diff --git a/src/attributes.rs b/src/attributes.rs
index b199516..1e7cb5f 100644
--- a/src/attributes.rs
+++ b/src/attributes.rs
@@ -6,6 +6,7 @@ A documentation-only module for #\[deku\] attributes
 | Attribute | Scope | Description
 |-----------|------------------|------------
 | [endian](#endian) | top-level, field | Set the endianness
+| [magic](#magic) | top-level | A magic value that must be present at the start of this struct/enum
 | [bits](#bits) | field | Set the bit-size of the field
 | [bytes](#bytes) | field | Set the byte-size of the field
 | [count](#count) | field | Set the field representing the element count of a container
@@ -101,6 +102,35 @@ let value: Vec<u8> = value.try_into().unwrap();
 assert_eq!(data, value);
 ```
 
+# magic
+
+Sets a "magic" value that must be present in the data at the start of
+a struct/enum when reading, and that is written out of the start of
+that type's data when writing.
+
+Example:
+```rust
+# use deku::prelude::*;
+# use std::convert::{TryInto, TryFrom};
+# #[derive(Debug, PartialEq, DekuRead, DekuWrite)]
+#[deku(magic = b"deku")]
+pub struct DekuTest {
+    data: u8
+}
+
+let data: Vec<u8> = vec![0x64, 0x65, 0x6B, 0x75, 50];
+
+let value = DekuTest::try_from(data.as_ref()).unwrap();
+
+assert_eq!(
+    DekuTest { data: 50 },
+    value
+);
+
+let value: Vec<u8> = value.try_into().unwrap();
+assert_eq!(data, value);
+```
+
 # bits
 
 Set the bit-size of the field
diff --git a/src/lib.rs b/src/lib.rs
index 11eb0af..d0adf8b 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -604,11 +604,7 @@ impl<T: DekuWrite<Ctx>, Ctx: Copy> DekuWrite<Ctx> for Option<T> {
     /// assert_eq!(output, bitvec![0, 0, 0, 0, 0, 0, 0, 1])
     /// ```
     fn write(&self, output: &mut BitVec<Msb0, u8>, inner_ctx: Ctx) -> Result<(), DekuError> {
-        if let Some(v) = self {
-            v.write(output, inner_ctx)
-        } else {
-            Ok(())
-        }
+        self.as_ref().map_or(Ok(()), |v| v.write(output, inner_ctx))
     }
 }
 


Test patch:
diff --git a/tests/test_magic.rs b/tests/test_magic.rs
new file mode 100644
index 0000000..f48052f
--- /dev/null
+++ b/tests/test_magic.rs
@@ -0,0 +1,72 @@
+use deku::prelude::*;
+use hexlit::hex;
+use rstest::rstest;
+use std::convert::{TryFrom, TryInto};
+
+#[rstest(input,
+    case(&hex!("64656b75")),
+
+    #[should_panic(expected = "Parse(\"Missing magic value [100, 101, 107, 117]\")")]
+    case(&hex!("64656bde")),
+
+    #[should_panic(expected = "Parse(\"Missing magic value [100, 101, 107, 117]\")")]
+    case(&hex!("6465ad75")),
+
+    #[should_panic(expected = "Parse(\"Missing magic value [100, 101, 107, 117]\")")]
+    case(&hex!("64be6b75")),
+
+    #[should_panic(expected = "Parse(\"Missing magic value [100, 101, 107, 117]\")")]
+    case(&hex!("ef656b75")),
+
+    #[should_panic(expected = "Parse(\"not enough data: expected 8 bits got 0 bits\")")]
+    case(&hex!("64656b")),
+)]
+fn test_magic_struct(input: &[u8]) {
+    #[derive(PartialEq, Debug, DekuRead, DekuWrite)]
+    #[deku(magic = b"deku")]
+    pub struct TestStruct {}
+
+    let ret_read = TestStruct::try_from(input).unwrap();
+
+    assert_eq!(TestStruct {}, ret_read);
+
+    let ret_write: Vec<u8> = ret_read.try_into().unwrap();
+    assert_eq!(ret_write, input)
+}
+
+#[rstest(input,
+    case(&hex!("64656b7500")),
+
+    #[should_panic(expected = "Parse(\"Missing magic value [100, 101, 107, 117]\")")]
+    case(&hex!("64656bde00")),
+
+    #[should_panic(expected = "Parse(\"Missing magic value [100, 101, 107, 117]\")")]
+    case(&hex!("6465ad7500")),
+
+    #[should_panic(expected = "Parse(\"Missing magic value [100, 101, 107, 117]\")")]
+    case(&hex!("64be6b7500")),
+
+    #[should_panic(expected = "Parse(\"Missing magic value [100, 101, 107, 117]\")")]
+    case(&hex!("ef656b7500")),
+
+    #[should_panic(expected = "Parse(\"Missing magic value [100, 101, 107, 117]\")")]
+    case(&hex!("64656b00")),
+
+    #[should_panic(expected = "Parse(\"not enough data: expected 8 bits got 0 bits\")")]
+    case(&hex!("64656b")),
+)]
+fn test_magic_enum(input: &[u8]) {
+    #[derive(PartialEq, Debug, DekuRead, DekuWrite)]
+    #[deku(magic = b"deku", type = "u8")]
+    pub enum TestEnum {
+        #[deku(id = "0")]
+        Variant,
+    }
+
+    let ret_read = TestEnum::try_from(input).unwrap();
+
+    assert_eq!(TestEnum::Variant, ret_read);
+
+    let ret_write: Vec<u8> = ret_read.try_into().unwrap();
+    assert_eq!(ret_write, input)
+}