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}