Skip to content

Commit

Permalink
Add udigest::inline_struct!
Browse files Browse the repository at this point in the history
  • Loading branch information
survived committed Jun 3, 2024
1 parent d214f7b commit dea1bc8
Show file tree
Hide file tree
Showing 6 changed files with 292 additions and 6 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions cspell.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ words:
- sublist
- docsrs
- concated
- inlines
15 changes: 10 additions & 5 deletions udigest/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
195 changes: 195 additions & 0 deletions udigest/src/inline_struct.rs
Original file line number Diff line number Diff line change
@@ -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::<sha2::Sha256, _>(&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::<sha2::Sha256, _>(&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<V>(
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<T: ?Sized + AsRef<[u8]>>(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<B: crate::Buffer>(&self, encoder: crate::encoding::EncodeValue<B>) {
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::<sha2::Sha256, _>(&udigest::inline_struct!({
/// name: "Alice",
/// age: 24_u32,
/// }));
/// ```
///
/// You may add a domain separation tag:
/// ```rust
/// let hash = udigest::hash::<sha2::Sha256, _>(
/// &udigest::inline_struct!("some tag" {
/// name: "Alice",
/// age: 24_u32,
/// })
/// );
/// ```
///
/// Several structs may be embedded in each other:
/// ```rust
/// let hash = udigest::hash::<sha2::Sha256, _>(&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<B: crate::Buffer>(&self, encoder: &mut crate::encoding::EncodeStruct<B>);
}

/// 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<B: crate::Buffer>(&self, _encoder: &mut crate::encoding::EncodeStruct<B>) {
// 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<B: crate::Buffer>(&self, encoder: &mut crate::encoding::EncodeStruct<B>) {
// 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,
}
}
8 changes: 8 additions & 0 deletions udigest/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -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<B: Buffer>(&self, encoder: encoding::EncodeValue<B>) {
u8::from(*self).unambiguously_encode(encoder)
}
}

impl Digestable for char {
fn unambiguously_encode<B: Buffer>(&self, encoder: encoding::EncodeValue<B>) {
// Any char can be represented using two bytes, but strangely Rust does not provide
Expand Down
77 changes: 77 additions & 0 deletions udigest/tests/inline_struct.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#[test]
fn no_tag() {
#[derive(udigest::Digestable)]
struct Person {
name: &'static str,
age: u32,
}

let hash_expected = udigest::hash::<sha2::Sha256, _>(&Person {
name: "Alice",
age: 24,
});

let hash_actual = udigest::hash::<sha2::Sha256, _>(&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::<sha2::Sha256, _>(&Person {
name: "Alice",
age: 24,
});

let hash_actual = udigest::hash::<sha2::Sha256, _>(&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::<sha2::Sha256, _>(&Person {
name: "Alice",
age: 24,
preferences: Preferences {
display_email: false,
receive_newsletter: false,
},
});

let hash_actual = udigest::hash::<sha2::Sha256, _>(&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));
}

0 comments on commit dea1bc8

Please sign in to comment.