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}