Skip to main content

onenote_parser_macros/
lib.rs

1//! Internal proc-macro support crate for `onenote_parser`.
2//!
3//! This crate provides `#[derive(Parse)]`, which is used by the parser's
4//! low-level OneStore/OneNote decoding types to reduce handwritten binary
5//! parsing boilerplate.
6//!
7//! It is intended for internal use inside this workspace. Most users should use
8//! the high-level `onenote_parser` crate API instead of depending on this crate
9//! directly.
10//!
11//! The derive generates an implementation of
12//! `crate::onestore::desktop::parse::Parse` for a struct by parsing each field
13//! in declaration order from a shared reader.
14//!
15//! # Supported attributes
16//! - `#[validate(expr)]` (struct-level): Runs after field parsing and returns
17//!   `ParseValidationFailed` when `expr` evaluates to `false`.
18//! - `#[assert_offset(n)]` (field-level): Validates that a field starts at byte
19//!   offset `n` from the start of the struct.
20//! - `#[pad_to_alignment(n)]` (field-level): Advances the reader by up to
21//!   `n - 1` bytes after parsing a field to align subsequent parsing.
22//! - `#[parse_additional_args(args)]` (field-level): Passes extra arguments to
23//!   a field type's `::parse(...)` call.
24//!
25//! # Scope and limitations
26//! - Only named-field structs are currently implemented.
27//! - Tuple structs, unit structs, enums, and unions are not yet supported.
28
29// Development ref: See the relevant syn example: https://github.com/dtolnay/syn/blob/master/examples/heapsize/heapsize_derive/src/lib.rs
30
31use proc_macro2::TokenStream;
32
33use quote::{ToTokens, quote, quote_spanned};
34use syn::{DeriveInput, Expr, parse_macro_input, spanned::Spanned};
35
36/// Auto-implements the `Parse` trait. Can be applied using `#[derive(Parse)]`.
37/// Attributes:
38/// - #[validate(callback)]: Extra validation logic. This should go at the beginning of the struct declaration.
39/// - #[assert_offset(n)]: Ensures a struct field has offset `n` from the beginning of the struct.
40/// - #[pad_to_alignment(n)]: Attaches to a struct field to add up to n bytes of padding.
41/// - #[parse_additional_args(arg1, arg2)]: Attaches to a struct field. Includes additional arguments to be provided to that field's `::parse` method.
42#[proc_macro_derive(
43    Parse,
44    attributes(pad_to_alignment, parse_additional_args, validate, assert_offset)
45)]
46pub fn parseable_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
47    let ast = parse_macro_input!(input as DeriveInput);
48    let name = &ast.ident;
49    let (impl_generics, ty_generics, where_clause) = &ast.generics.split_for_impl();
50
51    let parse_impl = process_fields(&ast.data, &ast.attrs);
52    let generated = quote! {
53        impl #impl_generics crate::onestore::desktop::parse::Parse for #name #ty_generics #where_clause {
54            fn parse(reader: crate::Reader) -> crate::errors::Result<Self> {
55                #parse_impl
56            }
57        }
58    };
59
60    proc_macro::TokenStream::from(generated)
61}
62
63fn process_fields(data: &syn::Data, attrs: &Vec<syn::Attribute>) -> TokenStream {
64    let validation = attrs.iter().filter_map(|a| {
65        if a.path().is_ident("validate") {
66            let validation: Expr = a
67                .parse_args()
68                .expect("validate must have a single validation argument");
69            let validation_str = format!(
70                "Failed to validate: {:}",
71                validation.clone().into_token_stream()
72            );
73            Some(quote_spanned! {validation.span() =>
74                #[allow(clippy::nonminimal_bool)]
75                if ! (#validation) {
76                    return Err(crate::errors::ErrorKind::ParseValidationFailed(
77                        ( #validation_str ).into()
78                    ).into());
79                }
80            })
81        } else {
82            None
83        }
84    });
85
86    for attr in attrs {
87        if attr.path().is_ident("pad_to_alignment") {
88            panic!("#[pad_to_alignment(...)] is only permitted on fields");
89        }
90        if attr.path().is_ident("assert_offset") {
91            panic!("#[assert_offset(...)] is only permitted on fields");
92        }
93    }
94
95    match *data {
96        syn::Data::Struct(ref data) => {
97            match data.fields {
98                syn::Fields::Named(ref fields) => {
99                    let parse_logic = fields.named.iter().map(|f| {
100                        let name = &f.ident;
101                        let type_name = &f.ty;
102                        let attrs = &f.attrs;
103
104                        // Validate attrs
105                        for attr in attrs {
106                            if attr.path().is_ident("validate") {
107                                panic!("The #[validate(...)] attr is only permitted on toplevel blocks");
108                            }
109                        }
110
111                        let pad_to_alignment = attrs.iter().find_map(|a| {
112                            if a.path().is_ident("pad_to_alignment") {
113                                let padding: Expr = a.parse_args().expect("pad_to_alignment must have a single argument. For example, to align to 8 bytes, use #[pad_to_alignment(8)].");
114                                Some(quote_spanned! {padding.span() =>
115                                    {
116                                        let remaining_1 = reader.remaining();
117                                        let field_size = _parse_remaining_0 - remaining_1;
118                                        let padding = #padding;
119                                        let k = field_size / padding + 1;
120                                        // Want (field_size + advance = k * padding) for some k.
121                                        //    => advance = k * padding - field_size
122                                        let advance_by = (k * (#padding) - field_size) % padding;
123                                        reader.advance(advance_by)?;
124                                    }
125                                })
126                            } else {
127                                None
128                            }
129                        });
130
131                        let assert_offset = attrs.iter().find_map(|a| {
132                            if a.path().is_ident("assert_offset") {
133                                let offset: Expr = a.parse_args().expect("assert_offset must have a single numeric argument.");
134                                Some(quote_spanned! {offset.span() =>
135                                    {
136                                        let remaining_1 = reader.remaining();
137                                        let actual_offset = _parse_remaining_0 - remaining_1;
138                                        let expected_offset = #offset;
139                                        if actual_offset != expected_offset {
140                                            return Err(crate::errors::ErrorKind::ParseValidationFailed(
141                                                format!(
142                                                    "Field offset mismatch: expected {}, got {}",
143                                                    expected_offset, actual_offset
144                                                ).into()
145                                            ).into());
146                                        }
147                                    }
148                                })
149                            } else {
150                                None
151                            }
152                        });
153
154                        let parse_args = attrs.iter().find_map(|a| {
155                            if a.path().is_ident("parse_additional_args") {
156                                let args: Expr = a.parse_args().expect("parse_additional_args must have a single argument");
157                                Some(quote_spanned! {args.span() =>
158                                    reader, #args
159                                })
160                            } else {
161                                None
162                            }
163                        }).unwrap_or(quote! { reader });
164
165                        quote_spanned! {f.span() =>
166                            #assert_offset
167                            let #name = < #type_name >::parse( #parse_args )?;
168                            #pad_to_alignment
169                        }
170                    });
171                    let names = fields.named.iter().map(|f| {
172                        let name = &f.ident;
173                        quote_spanned! {f.span() =>
174                            #name
175                        }
176                    });
177
178                    quote! {
179                        let _parse_remaining_0 = reader.remaining();
180
181                        #(#parse_logic)*
182                        #(#validation)*
183
184                        Ok(Self {
185                            #(#names),*
186                        })
187                    }
188                }
189                syn::Fields::Unnamed(ref _fields) => unimplemented!(),
190                syn::Fields::Unit => unimplemented!(),
191            }
192        }
193        syn::Data::Enum(ref _data) => unimplemented!(),
194        syn::Data::Union(ref _data) => unimplemented!(),
195    }
196}