diff --git a/Cargo.lock b/Cargo.lock index f1d9362..e959c99 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -151,7 +151,7 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "udigest" -version = "0.1.0" +version = "0.2.0-rc1" dependencies = [ "blake2", "digest", diff --git a/cspell.yml b/cspell.yml index 2e5c24c..139373c 100644 --- a/cspell.yml +++ b/cspell.yml @@ -8,3 +8,4 @@ words: - sublist - docsrs - concated +- inlines diff --git a/udigest/Cargo.toml b/udigest/Cargo.toml index fa7f736..0737865 100644 --- a/udigest/Cargo.toml +++ b/udigest/Cargo.toml @@ -28,22 +28,27 @@ sha3 = "0.10" blake2 = "0.10" [features] -default = ["digest", "std"] +default = ["digest", "std", "inline-struct"] std = ["alloc"] alloc = [] derive = ["dep:udigest-derive"] digest = ["dep:digest"] +inline-struct = [] [[test]] name = "derive" required-features = ["std", "derive", "digest"] -[[example]] -name = "derivation" -required-features = ["std", "derive", "digest"] - [[test]] name = "deterministic_hash" required-features = ["derive", "digest"] + +[[test]] +name = "inline_struct" +required-features = ["derive", "inline-struct"] + +[[example]] +name = "derivation" +required-features = ["std", "derive", "digest"] diff --git a/udigest/src/inline_struct.rs b/udigest/src/inline_struct.rs new file mode 100644 index 0000000..5ee2dd8 --- /dev/null +++ b/udigest/src/inline_struct.rs @@ -0,0 +1,195 @@ +//! Digestable inline structs +//! +//! If you find yourself in situation in which you have to define a struct just +//! to use it only once for `udigest` hashing, [`inline_struct!`] macro can be +//! used instead: +//! +//! ```rust +//! let hash = udigest::hash::(&udigest::inline_struct!({ +//! name: "Alice", +//! age: 24_u32, +//! })); +//! ``` +//! +//! Which will produce identical hash as below: +//! +//! ```rust +//! #[derive(udigest::Digestable)] +//! struct Person { +//! name: &'static str, +//! age: u32, +//! } +//! +//! let hash = udigest::hash::(&Person { +//! name: "Alice", +//! age: 24, +//! }); +//! ``` +//! +//! See [`inline_struct!`] macro for more examples. + +/// Inline structure +/// +/// Normally, you don't need to use it directly. Use [`inline_struct!`] macro instead. +pub struct InlineStruct<'a, F: FieldsList + 'a = Nil> { + fields_list: F, + tag: Option<&'a [u8]>, +} + +impl InlineStruct<'static> { + /// Creates inline struct with no fields + /// + /// Normally, you don't need to use it directly. Use [`inline_struct!`] macro instead. + pub fn new() -> Self { + Self { + fields_list: Nil, + tag: None, + } + } +} + +impl<'a, F: FieldsList + 'a> InlineStruct<'a, F> { + /// Adds field to the struct + /// + /// Normally, you don't need to use it directly. Use [`inline_struct!`] macro instead. + pub fn add_field( + self, + field_name: &'a str, + field_value: &'a V, + ) -> InlineStruct<'a, impl FieldsList + 'a> + where + F: 'a, + V: crate::Digestable, + { + InlineStruct { + fields_list: cons(field_name, field_value, self.fields_list), + tag: self.tag, + } + } + + /// Sets domain-separation tag + /// + /// Normally, you don't need to use it directly. Use [`inline_struct!`] macro instead. + pub fn set_tag>(mut self, tag: &'a T) -> Self { + self.tag = Some(tag.as_ref()); + self + } +} + +impl<'a, F: FieldsList + 'a> crate::Digestable for InlineStruct<'a, F> { + fn unambiguously_encode(&self, encoder: crate::encoding::EncodeValue) { + let mut struct_encode = encoder.encode_struct(); + if let Some(tag) = self.tag { + struct_encode.set_tag(tag); + } + self.fields_list.encode(&mut struct_encode); + } +} + +/// Creates digestable inline struct +/// +/// Macro creates "inlined" (anonymous) struct instance containing specified fields and their +/// values. The inlined struct implements [`Digestable` trait](crate::Digestable), and therefore +/// can be unambiguously hashed, for instance, using [`udigest::hash`](crate::hash). It helps +/// reducing amount of code when otherwise you'd have to define a separate struct which would +/// only be used one. +/// +/// ## Usage +/// The code snippet below inlines `struct Person { name: &str, age: u32 }`. +/// ```rust +/// let hash = udigest::hash::(&udigest::inline_struct!({ +/// name: "Alice", +/// age: 24_u32, +/// })); +/// ``` +/// +/// You may add a domain separation tag: +/// ```rust +/// let hash = udigest::hash::( +/// &udigest::inline_struct!("some tag" { +/// name: "Alice", +/// age: 24_u32, +/// }) +/// ); +/// ``` +/// +/// Several structs may be embedded in each other: +/// ```rust +/// let hash = udigest::hash::(&udigest::inline_struct!({ +/// name: "Alice", +/// age: 24_u32, +/// preferences: udigest::inline_struct!({ +/// display_email: false, +/// receive_newsletter: false, +/// }), +/// })); +/// ``` +#[macro_export] +macro_rules! inline_struct { + ({$($field_name:ident: $field_value:expr),*$(,)?}) => {{ + $crate::inline_struct::InlineStruct::new() + $(.add_field(stringify!($field_name), &$field_value))* + }}; + ($tag:tt {$($field_name:ident: $field_value:expr),*$(,)?}) => {{ + $crate::inline_struct::InlineStruct::new() + .set_tag($tag) + $(.add_field(stringify!($field_name), &$field_value))* + + }}; +} + +pub use crate::inline_struct; + +mod sealed { + pub trait Sealed {} +} + +/// List of fields in inline struct +/// +/// Normally, you don't need to use it directly. Use [`inline_struct!`] macro instead. +pub trait FieldsList: sealed::Sealed { + /// Encodes all fields in order from the first to last + fn encode(&self, encoder: &mut crate::encoding::EncodeStruct); +} + +/// Empty list of fields +/// +/// Normally, you don't need to use it directly. Use [`inline_struct!`] macro instead. +pub struct Nil; +impl sealed::Sealed for Nil {} +impl FieldsList for Nil { + fn encode(&self, _encoder: &mut crate::encoding::EncodeStruct) { + // Empty list - do nothing + } +} + +fn cons<'a, V, T>(field_name: &'a str, field_value: &'a V, tail: T) -> impl FieldsList + 'a +where + V: crate::Digestable, + T: FieldsList + 'a, +{ + struct Cons<'a, V, T: 'a> { + field_name: &'a str, + field_value: &'a V, + tail: T, + } + + impl<'a, V, T: 'a> sealed::Sealed for Cons<'a, V, T> {} + + impl<'a, V: crate::Digestable, T: FieldsList + 'a> FieldsList for Cons<'a, V, T> { + fn encode(&self, encoder: &mut crate::encoding::EncodeStruct) { + // Since we store fields from last to first, we need to encode the tail first + // to reverse order of fields + self.tail.encode(encoder); + + let value_encoder = encoder.add_field(self.field_name); + self.field_value.unambiguously_encode(value_encoder); + } + } + + Cons { + field_name, + field_value, + tail, + } +} diff --git a/udigest/src/lib.rs b/udigest/src/lib.rs index 120dc84..8201511 100644 --- a/udigest/src/lib.rs +++ b/udigest/src/lib.rs @@ -201,6 +201,8 @@ pub use encoding::Buffer; pub use udigest_derive::Digestable; pub mod encoding; +#[cfg(feature = "inline-struct")] +pub mod inline_struct; /// Digests a structured `value` using fixed-output hash function (like sha2-256) #[cfg(feature = "digest")] @@ -323,6 +325,12 @@ macro_rules! digestable_integers { digestable_integers!(i8, u8, i16, u16, i32, u32, i64, u64, i128, u128); +impl Digestable for bool { + fn unambiguously_encode(&self, encoder: encoding::EncodeValue) { + u8::from(*self).unambiguously_encode(encoder) + } +} + impl Digestable for char { fn unambiguously_encode(&self, encoder: encoding::EncodeValue) { // Any char can be represented using two bytes, but strangely Rust does not provide diff --git a/udigest/tests/inline_struct.rs b/udigest/tests/inline_struct.rs new file mode 100644 index 0000000..3e1a113 --- /dev/null +++ b/udigest/tests/inline_struct.rs @@ -0,0 +1,77 @@ +#[test] +fn no_tag() { + #[derive(udigest::Digestable)] + struct Person { + name: &'static str, + age: u32, + } + + let hash_expected = udigest::hash::(&Person { + name: "Alice", + age: 24, + }); + + let hash_actual = udigest::hash::(&udigest::inline_struct!({ + name: "Alice", + age: 24_u32, + })); + + assert_eq!(hex::encode(hash_expected), hex::encode(hash_actual)); +} + +#[test] +fn with_tag() { + #[derive(udigest::Digestable)] + #[udigest(tag = "some_tag")] + struct Person { + name: &'static str, + age: u32, + } + + let hash_expected = udigest::hash::(&Person { + name: "Alice", + age: 24, + }); + + let hash_actual = udigest::hash::(&udigest::inline_struct!("some_tag" { + name: "Alice", + age: 24_u32, + })); + + assert_eq!(hex::encode(hash_expected), hex::encode(hash_actual)); +} + +#[test] +fn embedded_structs() { + #[derive(udigest::Digestable)] + struct Person { + name: &'static str, + age: u32, + preferences: Preferences, + } + #[derive(udigest::Digestable)] + struct Preferences { + display_email: bool, + receive_newsletter: bool, + } + + let hash_expected = udigest::hash::(&Person { + name: "Alice", + age: 24, + preferences: Preferences { + display_email: false, + receive_newsletter: false, + }, + }); + + let hash_actual = udigest::hash::(&udigest::inline_struct!({ + name: "Alice", + age: 24_u32, + preferences: udigest::inline_struct!({ + display_email: false, + receive_newsletter: false, + }) + })); + + assert_eq!(hex::encode(hash_expected), hex::encode(hash_actual)); +}