Skip to main content

sails_reflect_hash_derive/
lib.rs

1// This file is part of Gear.
2
3// Copyright (C) 2025 Gear Technologies Inc.
4// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
5
6// This program is free software: you can redistribute it and/or modify
7// it under the terms of the GNU General Public License as published by
8// the Free Software Foundation, either version 3 of the License, or
9// (at your option) any later version.
10
11// This program is distributed in the hope that it will be useful,
12// but WITHOUT ANY WARRANTY; without even the implied warranty of
13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14// GNU General Public License for more details.
15
16// You should have received a copy of the GNU General Public License
17// along with this program. If not, see <https://www.gnu.org/licenses/>.
18
19//! Procedural macros for deriving `ReflectHash`.
20//!
21//! This crate provides the `#[derive(ReflectHash)]` macro which generates
22//! compile-time structural hashing for Rust types using Keccak256.
23
24use proc_macro::TokenStream;
25use proc_macro2::{Span, TokenStream as TokenStream2};
26use quote::quote;
27use syn::{
28    Attribute, Data, DeriveInput, Error, Field, Fields, Ident, Path, Result as SynResult, Variant,
29    parse::ParseStream, parse_macro_input, punctuated::Punctuated, token,
30};
31
32/// Derives the `ReflectHash` trait for structs and enums.
33///
34/// # Hashing Rules
35///
36/// The hash is computed at compile time using Keccak256 with the following rules:
37///
38/// ## Enums
39///
40/// The final hash is `keccak256(variant_hash_0 || variant_hash_1 || ... || variant_hash_N)`
41/// where variants are processed in declaration order.
42///
43/// - **Unit variant** `Transferred` → `keccak256(b"Transferred")`
44/// - **Tuple variant** `Approved(ActorId, u128)` → `keccak256(b"Approved" || ActorId::HASH || u128::HASH)`
45/// - **Named variant** `Paused { by: ActorId }` → `keccak256(b"Paused" || ActorId::HASH)`
46///
47/// ## Structs
48///
49/// - **Unit struct** `struct Empty;` → `keccak256(b"Empty")`
50/// - **Tuple struct** `struct Point(u32, u32);` → `keccak256(b"Point" || u32::HASH || u32::HASH)`
51/// - **Named struct** `struct User { id: u64, name: String }` → `keccak256(b"User" || u64::HASH || String::HASH)`
52///
53/// # Examples
54///
55/// ```ignore
56/// use sails_reflect_hash::ReflectHash;
57///
58/// #[derive(ReflectHash)]
59/// struct Transfer {
60///     from: ActorId,
61///     to: ActorId,
62///     amount: u128,
63/// }
64///
65/// #[derive(ReflectHash)]
66/// enum Event {
67///     Transferred { from: ActorId, to: ActorId },
68///     Approved(ActorId, u128),
69///     Paused,
70/// }
71/// ```
72///
73/// ## Custom crate path
74///
75/// If `sails-reflect-hash` is re-exported under a different name, you can specify
76/// the crate path using the `#[reflect_hash(crate = path)]` attribute:
77///
78/// ```ignore
79/// // Local re-export
80/// use sails_reflect_hash as my_hash;
81///
82/// #[derive(ReflectHash)]
83/// #[reflect_hash(crate = my_hash)]
84/// struct MyType {
85///     field: u32,
86/// }
87///
88/// // Absolute path
89/// #[derive(ReflectHash)]
90/// #[reflect_hash(crate = ::some::other::path::to::hash)]
91/// struct OtherType {
92///     field: u64,
93/// }
94/// ```
95#[proc_macro_derive(ReflectHash, attributes(reflect_hash))]
96pub fn derive_reflect_hash(input: TokenStream) -> TokenStream {
97    let input = parse_macro_input!(input as DeriveInput);
98
99    match derive_reflect_hash_impl(input) {
100        Ok(tokens) => tokens.into(),
101        Err(err) => err.to_compile_error().into(),
102    }
103}
104
105fn derive_reflect_hash_impl(input: DeriveInput) -> SynResult<TokenStream2> {
106    let ty_name = &input.ident;
107    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
108
109    // Find the actual crate name - first check for #[reflect_hash(crate = ...)] attribute,
110    // then fall back to proc-macro-crate detection
111    let crate_name = reflect_hash_crate_path(&input.attrs)?;
112
113    let hash_computation = match &input.data {
114        Data::Struct(data_struct) => {
115            generate_struct_hash(ty_name, &data_struct.fields, &crate_name)?
116        }
117        Data::Enum(data_enum) => generate_enum_hash(&data_enum.variants, &crate_name)?,
118        Data::Union(_) => {
119            return Err(Error::new_spanned(
120                ty_name,
121                "ReflectHash cannot be derived for unions",
122            ));
123        }
124    };
125
126    Ok(quote! {
127        impl #impl_generics ReflectHash for #ty_name #ty_generics #where_clause {
128            const HASH: [u8; 32] = #hash_computation;
129        }
130    })
131}
132
133// TODO: #1125
134/// Look for `#[reflect_hash(crate = ...)]` attribute and return the path.
135/// If not found, use proc-macro-crate to detect the crate name.
136fn reflect_hash_crate_path(attrs: &[Attribute]) -> SynResult<TokenStream2> {
137    // First check for explicit crate path attribute
138    for attr in attrs {
139        if attr.path().is_ident("reflect_hash") {
140            // Parser closure for: crate = some::path
141            let parser = |input: ParseStream| -> SynResult<Path> {
142                // parse the `crate` keyword
143                input.parse::<token::Crate>()?;
144                // parse the `=` token
145                input.parse::<token::Eq>()?;
146                // parse the path after `=`
147                input.parse::<Path>()
148            };
149            let path = attr.parse_args_with(parser)?;
150
151            return Ok(quote!(#path));
152        }
153    }
154
155    // Fall back to proc-macro-crate detection
156    match proc_macro_crate::crate_name("sails-reflect-hash") {
157        Ok(proc_macro_crate::FoundCrate::Itself) => Ok(quote!(crate)),
158        Ok(proc_macro_crate::FoundCrate::Name(name)) => {
159            let ident = Ident::new(&name, proc_macro2::Span::call_site());
160            Ok(quote!(::#ident))
161        }
162        Err(e) => Err(Error::new(
163            Span::call_site(),
164            format!(
165                "Could not detect sails-reflect-hash crate: {e}. Consider using #[reflect_hash(crate = path::to::crate)]"
166            ),
167        )),
168    }
169}
170
171/// Generates hash computation for a struct.
172fn generate_struct_hash(
173    ty_name: &Ident,
174    fields: &Fields,
175    crate_name: &TokenStream2,
176) -> SynResult<TokenStream2> {
177    let name_str = ty_name.to_string();
178
179    fn fields_hash<'a>(
180        fields: impl Iterator<Item = &'a Field>,
181        crate_name: &TokenStream2,
182        name_str: String,
183    ) -> TokenStream2 {
184        let field_hashes = fields.map(|field| {
185            let ty = &field.ty;
186            quote! {
187                .update(&<#ty as ReflectHash>::HASH)
188            }
189        });
190
191        quote! {
192            #crate_name::keccak_const::Keccak256::new()
193                .update(#name_str.as_bytes())
194                #(#field_hashes)*
195                .finalize()
196        }
197    }
198
199    match fields {
200        Fields::Unit => {
201            // Unit struct: hash(b"StructName")
202            Ok(quote! {
203                #crate_name::keccak_const::Keccak256::new()
204                    .update(#name_str.as_bytes())
205                    .finalize()
206            })
207        }
208        Fields::Unnamed(fields_unnamed) => {
209            // Tuple struct: hash(b"StructName" || T1::HASH || T2::HASH || ...)
210            Ok(fields_hash(
211                fields_unnamed.unnamed.iter(),
212                crate_name,
213                name_str,
214            ))
215        }
216        Fields::Named(fields_named) => {
217            // Named struct: hash(b"StructName" || T1::HASH || T2::HASH || ...)
218            // Field names are NOT included in the hash (structural hashing only)
219            Ok(fields_hash(fields_named.named.iter(), crate_name, name_str))
220        }
221    }
222}
223
224/// Generates hash computation for an enum.
225fn generate_enum_hash(
226    variants: &Punctuated<Variant, token::Comma>,
227    crate_name: &TokenStream2,
228) -> SynResult<TokenStream2> {
229    let mut variant_hash_computations = Vec::new();
230
231    for variant in variants {
232        let variant_hash = generate_struct_hash(&variant.ident, &variant.fields, crate_name)?;
233        variant_hash_computations.push(variant_hash);
234    }
235
236    Ok(quote! {
237        {
238            let mut final_hasher = #crate_name::keccak_const::Keccak256::new();
239            #(
240                {
241                    let variant_hash = #variant_hash_computations;
242                    final_hasher = final_hasher.update(&variant_hash);
243                }
244            )*
245            final_hasher.finalize()
246        }
247    })
248}