add blake3 hashing

This commit is contained in:
Aleksandr 2025-06-28 14:25:19 +03:00
parent 75a589f235
commit 0ceb55546b
7 changed files with 266 additions and 6 deletions

View file

@ -40,3 +40,4 @@ seq-macro = "0.3.6"
bytesize = { version = "2.0.1", features = ["serde"] }
bytes = { version = "1.10.1", features = ["serde"] }
url = { version = "2.5.4", features = ["serde"] }
blake3 = "1.8.2"

View file

@ -52,13 +52,22 @@ pub fn transform(args: TokenStream, body: TokenStream) -> TokenStream {
bounds: syn::parse_quote!(#eva::str::FixedUtf8),
};
let predicate = syn::WherePredicate::Type(predicate);
if let Some(ref mut clause) = generics.where_clause {
let clause = if let Some(ref mut clause) = generics.where_clause {
clause.predicates.push(predicate);
clause
} else {
generics.where_clause = Some(syn::parse_quote! {
where #predicate
});
}
generics.where_clause.as_mut().unwrap()
};
clause.predicates.push(syn::WherePredicate::Type(syn::PredicateType {
lifetimes: None,
bounded_ty: error_ty.clone(),
colon_token: Token![:](proc_macro2::Span::mixed_site()),
bounds: syn::parse_quote!(::std::convert::From<<#ty as ::std::str::FromStr>::Err>),
}));
}
let (ig, tyg, where_clause) = generics.split_for_impl();

View file

@ -2,5 +2,5 @@ pub use hashbrown::{Equivalent, HashTable, hash_map, hash_table};
use std::hash::BuildHasherDefault;
pub type HashMap<K, V> = hashbrown::HashMap<K, V, BuildHasherDefault<crate::hash::Hasher>>;
pub type HashSet<V> = hashbrown::HashSet<V, BuildHasherDefault<crate::hash::Hasher>>;
pub type HashMap<K, V> = hashbrown::HashMap<K, V, BuildHasherDefault<crate::hash::U64Hasher>>;
pub type HashSet<V> = hashbrown::HashSet<V, BuildHasherDefault<crate::hash::U64Hasher>>;

View file

@ -1 +1,183 @@
pub type Hasher = ahash::AHasher;
use std::{borrow::Cow, fmt, marker::PhantomData, mem, str::FromStr};
use perfect_derive::perfect_derive;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::{
error::shit_happens,
int, single_ascii_char, str,
str::{HasPattern, ParseError, PhantomDataStr, Seq},
};
pub mod blake3;
pub type U64Hasher = ahash::AHasher;
#[int(u8, b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z', crate = crate)]
pub enum HexChar {}
single_ascii_char!(HexChar);
unsafe impl str::FixedUtf8 for HexChar {}
/// Hexadecimal representation of the hash.
#[str(fixed(error = ParseError), crate = crate)]
pub struct Hex<IH: IsHash, const SIZE: usize> {
buf: Seq<SIZE, Seq<2, HexChar>>,
_marker: PhantomDataStr<IH>,
}
impl<IH: IsHash, const SIZE: usize> Hex<IH, SIZE> {
pub const fn parse(self) -> Hash<IH, SIZE> {
const fn hexc(c: u8) -> u8 {
match c.to_ascii_uppercase() {
b'0'..=b'9' => c - b'0',
b'A'..=b'F' => 10 + c - b'A',
_ => shit_happens(),
}
}
let mut out = [0_u8; SIZE];
let mut offset = 0_usize;
while offset < SIZE {
let [upper, lower] = self.buf.0[offset].0;
let upper = hexc(upper.into_inner());
let lower = hexc(lower.into_inner());
out[offset] = (upper << 4) | lower;
offset += 1;
}
Hash::new(out)
}
}
pub trait State {
fn update(&mut self, buf: &[u8]);
}
#[perfect_derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Hash<IH: IsHash, const SIZE: usize> {
buf: [u8; SIZE],
_marker: PhantomData<[IH; 0]>,
}
impl<IH: IsHash, const SIZE: usize> fmt::Display for Hash<IH, SIZE> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.hex().as_str())
}
}
impl<IH: IsHash, const SIZE: usize> JsonSchema for Hash<IH, SIZE> {
fn schema_id() -> Cow<'static, str> {
format!("{}::{}", module_path!(), Self::schema_name()).into()
}
fn schema_name() -> Cow<'static, str> {
format!("Hash_{}__{}", IH::schema_name(), SIZE).into()
}
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
_ = generator;
schemars::json_schema!({"anyOf": [
{
"type": "string",
"description": format!("Hexadecimal representation of {} hash, only in human-readable formats", IH::schema_name()),
"minLength": SIZE * 2,
"maxLength": SIZE * 2,
"pattern": <Hex<IH, SIZE>>::regex_pat_fullmatch(),
},
{"type": "array", "minItems": SIZE, "maxItems": SIZE, "items": {
"type": "integer",
"minimum": 0,
"maximum": 20
}}
]})
}
}
impl<'de, IH: IsHash, const SIZE: usize> Deserialize<'de> for Hash<IH, SIZE>
where
[u8; SIZE]: Deserialize<'de>,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
if deserializer.is_human_readable() {
let hex = Hex::deserialize(deserializer)?;
Ok(hex.parse())
} else {
let buf = <[u8; SIZE] as Deserialize<'de>>::deserialize(deserializer)?;
Ok(Self {
buf,
_marker: PhantomData,
})
}
}
}
impl<IH: IsHash, const SIZE: usize> Serialize for Hash<IH, SIZE> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
if serializer.is_human_readable() {
self.hex().serialize(serializer)
} else {
self.buf.serialize(serializer)
}
}
}
impl<IH: IsHash, const SIZE: usize> FromStr for Hash<IH, SIZE> {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Hex::from_str(s).map(|h| h.parse())
}
}
impl<IH: IsHash, const SIZE: usize> Hash<IH, SIZE> {
pub const fn new(buf: [u8; SIZE]) -> Self {
Self {
buf,
_marker: PhantomData,
}
}
pub const fn hex(&self) -> Hex<IH, SIZE> {
const fn hexc(c: u8) -> u8 {
match c {
0..=9 => b'0' + c,
10..=15 => b'a' + (c - 10),
_ => shit_happens(),
}
}
let mut out: [[u8; 2]; SIZE] = [[0, 0]; SIZE];
let mut offset = 0_usize;
while offset < SIZE {
let num = self.buf[offset];
let [upper, lower] = &mut out[offset];
*upper = hexc(num >> 4);
*lower = hexc(num & 0b1111);
offset += 1;
}
Hex {
buf: unsafe { mem::transmute_copy(&out) },
_marker: PhantomDataStr(PhantomData),
}
}
}
pub trait IsHash: JsonSchema + Copy + 'static + Send + Sync + Ord {
const FULL_SIZE: usize;
}
#[cfg(test)]
mod tests;

32
src/hash/blake3.rs Normal file
View file

@ -0,0 +1,32 @@
use crate::hash::{self, IsHash, State};
pub const FULL_SIZE: usize = 32;
pub type Hash = hash::Hash<Blake3, FULL_SIZE>;
#[derive(Debug, Clone, Default)]
pub struct Hasher(blake3::Hasher);
impl Hasher {
pub fn finish(&self) -> Hash {
let h = self.0.finalize();
Hash::new(*h.as_bytes())
}
}
impl State for Hasher {
fn update(&mut self, buf: &[u8]) {
self.0.update(buf);
}
}
#[crate::data(copy, ord, crate = crate)]
pub enum Blake3 {}
impl IsHash for Blake3 {
const FULL_SIZE: usize = FULL_SIZE;
}
pub fn hash(bytes: &[u8]) -> Hash {
let h = blake3::hash(bytes);
Hash::new(*h.as_bytes())
}

11
src/hash/tests.rs Normal file
View file

@ -0,0 +1,11 @@
use crate::hash::blake3;
#[test]
fn blake3_from_str_and_back_identical() {
const STR: &str = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff";
let h: blake3::Hash = STR.parse().unwrap();
let hex = h.hex();
assert_eq!(hex.as_str(), STR);
}

View file

@ -1,8 +1,9 @@
use std::{fmt, mem::MaybeUninit, slice, str::FromStr};
use std::{fmt, marker::PhantomData, mem::MaybeUninit, slice, str::FromStr};
pub use compact_str::{
CompactString, CompactStringExt, ToCompactString, ToCompactStringError, format_compact,
};
use perfect_derive::perfect_derive;
use crate::data;
@ -35,6 +36,30 @@ macro_rules! single_ascii_char {
};
}
#[perfect_derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct PhantomDataStr<C>(pub PhantomData<C>);
impl<C> HasPattern for PhantomDataStr<C> {
#[inline]
fn pat_into(buf: &mut String) {
_ = buf;
}
}
unsafe impl<C> FixedUtf8 for PhantomDataStr<C> {}
impl<C> FromStr for PhantomDataStr<C> {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.is_empty() {
Ok(Self(PhantomData))
} else {
Err(ParseError::Length)
}
}
}
/// Simple parse error.
#[data(copy, error, display(doc), crate = crate)]
pub enum ParseError {