diff --git a/Cargo.toml b/Cargo.toml index 3ca20de..3f29f09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/macros/src/str.rs b/macros/src/str.rs index 83313c6..6f71161 100644 --- a/macros/src/str.rs +++ b/macros/src/str.rs @@ -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(); diff --git a/src/collections.rs b/src/collections.rs index f140c5f..3c3f9e7 100644 --- a/src/collections.rs +++ b/src/collections.rs @@ -2,5 +2,5 @@ pub use hashbrown::{Equivalent, HashTable, hash_map, hash_table}; use std::hash::BuildHasherDefault; -pub type HashMap = hashbrown::HashMap>; -pub type HashSet = hashbrown::HashSet>; +pub type HashMap = hashbrown::HashMap>; +pub type HashSet = hashbrown::HashSet>; diff --git a/src/hash.rs b/src/hash.rs index 4708cac..15dcd43 100644 --- a/src/hash.rs +++ b/src/hash.rs @@ -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 { + buf: Seq>, + _marker: PhantomDataStr, +} + +impl Hex { + pub const fn parse(self) -> Hash { + 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 { + buf: [u8; SIZE], + _marker: PhantomData<[IH; 0]>, +} + +impl fmt::Display for Hash { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.hex().as_str()) + } +} + +impl JsonSchema for Hash { + 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": >::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 +where + [u8; SIZE]: Deserialize<'de>, +{ + fn deserialize(deserializer: D) -> Result + 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 Serialize for Hash { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + if serializer.is_human_readable() { + self.hex().serialize(serializer) + } else { + self.buf.serialize(serializer) + } + } +} + +impl FromStr for Hash { + type Err = ParseError; + + fn from_str(s: &str) -> Result { + Hex::from_str(s).map(|h| h.parse()) + } +} + +impl Hash { + pub const fn new(buf: [u8; SIZE]) -> Self { + Self { + buf, + _marker: PhantomData, + } + } + + pub const fn hex(&self) -> Hex { + 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; diff --git a/src/hash/blake3.rs b/src/hash/blake3.rs new file mode 100644 index 0000000..4d4f6e1 --- /dev/null +++ b/src/hash/blake3.rs @@ -0,0 +1,32 @@ +use crate::hash::{self, IsHash, State}; + +pub const FULL_SIZE: usize = 32; +pub type Hash = hash::Hash; + +#[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()) +} diff --git a/src/hash/tests.rs b/src/hash/tests.rs new file mode 100644 index 0000000..caf98fe --- /dev/null +++ b/src/hash/tests.rs @@ -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); +} diff --git a/src/str.rs b/src/str.rs index a313c80..842ecd6 100644 --- a/src/str.rs +++ b/src/str.rs @@ -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(pub PhantomData); + +impl HasPattern for PhantomDataStr { + #[inline] + fn pat_into(buf: &mut String) { + _ = buf; + } +} + +unsafe impl FixedUtf8 for PhantomDataStr {} +impl FromStr for PhantomDataStr { + type Err = ParseError; + + fn from_str(s: &str) -> Result { + if s.is_empty() { + Ok(Self(PhantomData)) + } else { + Err(ParseError::Length) + } + } +} + /// Simple parse error. #[data(copy, error, display(doc), crate = crate)] pub enum ParseError {