From bd202c4965fb9ec330f7f096c4091b69ccfdf16e Mon Sep 17 00:00:00 2001 From: Yuri Astrakhan Date: Thu, 21 Dec 2023 00:38:31 -0500 Subject: [PATCH] Add FNV and XXHASH hashing algorithms (#8) fixes #6 --- Cargo.lock | 32 ++++++++++++++++++++++++++++---- Cargo.toml | 9 ++++++--- README.md | 4 +++- justfile | 26 ++++++++++++++------------ src/fnv.rs | 24 ++++++++++++++++++++++++ src/lib.rs | 34 +++++++++++++++++++++++++++++++++- src/scalar.rs | 17 ++++++++++++++++- src/xxhash.rs | 38 ++++++++++++++++++++++++++++++++++++++ tests/_utils.rs | 19 +++++++++++++++++++ tests/test-ext.sh | 5 +++++ 10 files changed, 186 insertions(+), 22 deletions(-) create mode 100644 src/fnv.rs create mode 100644 src/xxhash.rs diff --git a/Cargo.lock b/Cargo.lock index acf090c..cf58d34 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -157,6 +157,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "generic-array" version = "0.14.7" @@ -288,6 +294,17 @@ version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +[[package]] +name = "noncrypto-digests" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab36a8c9c2f5750c015e9d567e4d64a66383b464f8b1a52b705c5025b4c52fdb" +dependencies = [ + "digest", + "fnv", + "xxhash-rust", +] + [[package]] name = "once_cell" version = "1.19.0" @@ -296,9 +313,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "pkg-config" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +checksum = "69d3587f8a9e599cc7ec2c00e331f71c4e69a5f9a4b8a6efd5b07466b9736f9a" [[package]] name = "prettyplease" @@ -430,6 +447,7 @@ dependencies = [ "insta", "log", "md-5", + "noncrypto-digests", "rusqlite", "sha1", "sha2", @@ -437,9 +455,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.41" +version = "2.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c8b28c477cc3bf0e7966561e3460130e1255f7a1cf71931075f1c5e7a7e269" +checksum = "5b7d0a2c048d661a1a59fcd7355baa232f7ed34e0ee4df2eef3c1c1c0d3852d8" dependencies = [ "proc-macro2", "quote", @@ -708,6 +726,12 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +[[package]] +name = "xxhash-rust" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53be06678ed9e83edb1745eb72efc0bbcd7b5c3c35711a860906aed827a13d61" + [[package]] name = "yaml-rust" version = "0.4.5" diff --git a/Cargo.toml b/Cargo.toml index 656980e..3325213 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sqlite-hashes" version = "0.6.0" # This value is also used in the README.md -description = "Hashing functions for SQLite with aggregation support: MD5, SHA1, SHA256, SHA512" +description = "Hashing functions for SQLite with aggregation support: MD5, SHA1, SHA256, SHA512, fnv1a, xxhash" authors = ["Yuri Astrakhan "] repository = "https://github.com/nyurik/sqlite-hashes" edition = "2021" @@ -21,11 +21,11 @@ crate-type = ["cdylib"] required-features = ["loadable_extension"] [features] -default = ["trace", "aggregate", "window", "hex", "md5", "sha1", "sha224", "sha256", "sha384", "sha512"] +default = ["trace", "aggregate", "window", "hex", "md5", "sha1", "sha224", "sha256", "sha384", "sha512", "fnv", "xxhash"] # Use this feature to build loadable extension. # Assumes --no-default-features. # Does not support windowing functionality yet. -default_loadable_extension = ["loadable_extension", "aggregate", "hex", "md5", "sha1", "sha224", "sha256", "sha384", "sha512"] +default_loadable_extension = ["loadable_extension", "aggregate", "hex", "md5", "sha1", "sha224", "sha256", "sha384", "sha512", "fnv", "xxhash"] # # Enable Trace Logging trace = ["dep:hex", "dep:log"] @@ -52,6 +52,8 @@ sha224 = ["dep:sha2"] sha256 = ["dep:sha2"] sha384 = ["dep:sha2"] sha512 = ["dep:sha2"] +fnv = ["dep:noncrypto-digests", "noncrypto-digests?/fnv"] +xxhash = ["dep:noncrypto-digests", "noncrypto-digests?/xxh3", "noncrypto-digests?/xxh32", "noncrypto-digests?/xxh64"] [dependencies] hex = { version = "0.4", optional = true } @@ -63,6 +65,7 @@ digest = "0.10.7" md-5 = { version = "0.10.6", optional = true } sha1 = { version = "0.10.6", optional = true } sha2 = { version = "0.10.8", optional = true } +noncrypto-digests = { version = "0.3.0", optional = true } [dev-dependencies] cargo-husky = { version = "1", features = ["user-hooks"], default-features = false } diff --git a/README.md b/README.md index 103cb12..a3bcd89 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![CI build](https://github.com/nyurik/sqlite-hashes/actions/workflows/ci.yml/badge.svg)](https://github.com/nyurik/sqlite-hashes/actions) -Use this crate to add various hash functions to SQLite, including MD5, SHA1, SHA224, SHA256, SHA384, and SHA512. +Use this crate to add various hash functions to SQLite, including MD5, SHA1, SHA224, SHA256, SHA384, SHA512, FNV1a, XXHASH. This crate uses [rusqlite](https://crates.io/crates/rusqlite) to add user-defined functions using static linking. Eventually it would be good to build dynamically loadable extension binaries usable from other languages (PRs welcome). @@ -131,6 +131,8 @@ sqlite-hashes = { version = "0.6", default-features = false, features = ["hex", * **sha256** - enable SHA256 hash support * **sha384** - enable SHA384 hash support * **sha512** - enable SHA512 hash support +* **fnv** - enable fnv1a hash support +* **xxhash** - enable xxh32, xxh64, xxh3_64, xxh3_128 hash support ## Development * This project is easier to develop with [just](https://github.com/casey/just#readme), a modern alternative to `make`. Install it with `cargo install just`. diff --git a/justfile b/justfile index 0edbb66..2c0923e 100644 --- a/justfile +++ b/justfile @@ -73,22 +73,24 @@ test-lib *ARGS: \ ( test-one-lib "--no-default-features" "--features" "trace,hex,window,sha256" ) \ ( test-one-lib "--no-default-features" "--features" "trace,hex,window,sha384" ) \ ( test-one-lib "--no-default-features" "--features" "trace,hex,window,sha512" ) \ + ( test-one-lib "--no-default-features" "--features" "trace,hex,window,fnv" ) \ + ( test-one-lib "--no-default-features" "--features" "trace,hex,window,xxhash" ) \ \ - ( test-one-lib "--no-default-features" "--features" "md5,sha1,sha224,sha256,sha384,sha512" ) \ - ( test-one-lib "--no-default-features" "--features" "md5,sha1,sha224,sha256,sha384,sha512,aggregate" ) \ - ( test-one-lib "--no-default-features" "--features" "md5,sha1,sha224,sha256,sha384,sha512,window" ) \ + ( test-one-lib "--no-default-features" "--features" "md5,sha1,sha224,sha256,sha384,sha512,fnv,xxhash" ) \ + ( test-one-lib "--no-default-features" "--features" "md5,sha1,sha224,sha256,sha384,sha512,fnv,xxhash,aggregate" ) \ + ( test-one-lib "--no-default-features" "--features" "md5,sha1,sha224,sha256,sha384,sha512,fnv,xxhash,window" ) \ \ - ( test-one-lib "--no-default-features" "--features" "md5,sha1,sha224,sha256,sha384,sha512,hex" ) \ - ( test-one-lib "--no-default-features" "--features" "md5,sha1,sha224,sha256,sha384,sha512,hex,aggregate" ) \ - ( test-one-lib "--no-default-features" "--features" "md5,sha1,sha224,sha256,sha384,sha512,hex,window" ) \ + ( test-one-lib "--no-default-features" "--features" "md5,sha1,sha224,sha256,sha384,sha512,fnv,xxhash,hex" ) \ + ( test-one-lib "--no-default-features" "--features" "md5,sha1,sha224,sha256,sha384,sha512,fnv,xxhash,hex,aggregate" ) \ + ( test-one-lib "--no-default-features" "--features" "md5,sha1,sha224,sha256,sha384,sha512,fnv,xxhash,hex,window" ) \ \ - ( test-one-lib "--no-default-features" "--features" "md5,sha1,sha224,sha256,sha384,sha512,trace" ) \ - ( test-one-lib "--no-default-features" "--features" "md5,sha1,sha224,sha256,sha384,sha512,trace,aggregate" ) \ - ( test-one-lib "--no-default-features" "--features" "md5,sha1,sha224,sha256,sha384,sha512,trace,window" ) \ + ( test-one-lib "--no-default-features" "--features" "md5,sha1,sha224,sha256,sha384,sha512,fnv,xxhash,trace" ) \ + ( test-one-lib "--no-default-features" "--features" "md5,sha1,sha224,sha256,sha384,sha512,fnv,xxhash,trace,aggregate" ) \ + ( test-one-lib "--no-default-features" "--features" "md5,sha1,sha224,sha256,sha384,sha512,fnv,xxhash,trace,window" ) \ \ - ( test-one-lib "--no-default-features" "--features" "md5,sha1,sha224,sha256,sha384,sha512,hex,trace" ) \ - ( test-one-lib "--no-default-features" "--features" "md5,sha1,sha224,sha256,sha384,sha512,hex,trace,aggregate" ) \ - ( test-one-lib "--no-default-features" "--features" "md5,sha1,sha224,sha256,sha384,sha512,hex,trace,window" ) \ + ( test-one-lib "--no-default-features" "--features" "md5,sha1,sha224,sha256,sha384,sha512,fnv,xxhash,hex,trace" ) \ + ( test-one-lib "--no-default-features" "--features" "md5,sha1,sha224,sha256,sha384,sha512,fnv,xxhash,hex,trace,aggregate" ) \ + ( test-one-lib "--no-default-features" "--features" "md5,sha1,sha224,sha256,sha384,sha512,fnv,xxhash,hex,trace,window" ) \ test-ext: ./tests/test-ext.sh diff --git a/src/fnv.rs b/src/fnv.rs new file mode 100644 index 0000000..da259cd --- /dev/null +++ b/src/fnv.rs @@ -0,0 +1,24 @@ +use noncrypto_digests::Fnv; + +use crate::rusqlite::{Connection, Result}; + +/// Register the `fnv1a` SQL function with the given `SQLite` connection. +/// The `fnv1a` function uses [Fowler–Noll–Vo hash function](https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function#FNV-1a_hash) to compute the hash of the argument(s). +/// +/// # Example +/// +/// ``` +/// # use sqlite_hashes::rusqlite::{Connection, Result}; +/// # use sqlite_hashes::register_fnv_functions; +/// # fn main() -> Result<()> { +/// let db = Connection::open_in_memory()?; +/// register_fnv_functions(&db)?; +/// let hash: Vec = db.query_row("SELECT fnv1a('hello')", [], |r| r.get(0))?; +/// let expected = b"\xA4\x30\xD8\x46\x80\xAA\xBD\x0B"; +/// assert_eq!(hash, expected); +/// # Ok(()) +/// # } +/// ``` +pub fn register_fnv_functions(conn: &Connection) -> Result<()> { + crate::scalar::create_hash_fn::(conn, "fnv1a") +} diff --git a/src/lib.rs b/src/lib.rs index 3247402..b56f258 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,9 +10,11 @@ feature = "sha256", feature = "sha384", feature = "sha512", + feature = "fnv", + feature = "xxhash", )))] compile_error!( - "At least one of these features must be enabled: md5,sha1,sha224,sha256,sha384,sha512" + "At least one of these features must be enabled: md5,sha1,sha224,sha256,sha384,sha512,fnv,xxhash" ); /// Re-export of the [`rusqlite`](https://crates.io/crates/rusqlite) crate to avoid version conflicts. @@ -60,6 +62,18 @@ mod sha512; #[cfg(feature = "sha512")] pub use crate::sha512::register_sha512_function; +#[cfg(feature = "fnv")] +mod fnv; + +#[cfg(feature = "fnv")] +pub use crate::fnv::register_fnv_functions; + +#[cfg(feature = "xxhash")] +mod xxhash; + +#[cfg(feature = "xxhash")] +pub use crate::xxhash::register_xxhash_functions; + /// Register all hashing functions for the given `SQLite` connection. /// This is a convenience function that calls all of the `register_*_function` functions. /// Features must be enabled for the corresponding functions to be registered. @@ -96,6 +110,20 @@ pub use crate::sha512::register_sha512_function; /// let hash: String = db.query_row("SELECT sha512_hex('hello')", [], |r| r.get(0))?; /// assert_eq!(hash, "9B71D224BD62F3785D96D46AD3EA3D73319BFBC2890CAADAE2DFF72519673CA72323C3D99BA5C11D7C7ACC6E14B8C5DA0C4663475C2E5C3ADEF46F73BCDEC043"); /// # } +/// # if cfg!(all(feature = "hex", feature = "fnv")) { +/// let hash: String = db.query_row("SELECT fnv1a_hex('hello')", [], |r| r.get(0))?; +/// assert_eq!(hash, "A430D84680AABD0B"); +/// # } +/// # if cfg!(all(feature = "hex", feature = "xxhash")) { +/// let hash: String = db.query_row("SELECT xxh32_hex('hello')", [], |r| r.get(0))?; +/// assert_eq!(hash, "FB0077F9"); +/// let hash: String = db.query_row("SELECT xxh64_hex('hello')", [], |r| r.get(0))?; +/// assert_eq!(hash, "26C7827D889F6DA3"); +/// let hash: String = db.query_row("SELECT xxh3_64_hex('hello')", [], |r| r.get(0))?; +/// assert_eq!(hash, "9555E8555C62DCFD"); +/// let hash: String = db.query_row("SELECT xxh3_128_hex('hello')", [], |r| r.get(0))?; +/// assert_eq!(hash, "B5E9C1AD071B3E7FC779CFAA5E523818"); +/// # } /// # Ok(()) /// # } /// ``` @@ -112,6 +140,10 @@ pub fn register_hash_functions(conn: &Connection) -> Result<()> { register_sha384_function(conn)?; #[cfg(feature = "sha512")] register_sha512_function(conn)?; + #[cfg(feature = "fnv")] + register_fnv_functions(conn)?; + #[cfg(feature = "xxhash")] + register_xxhash_functions(conn)?; Ok(()) } diff --git a/src/scalar.rs b/src/scalar.rs index 75cbd23..30c31a8 100644 --- a/src/scalar.rs +++ b/src/scalar.rs @@ -25,8 +25,15 @@ pub trait NamedDigest: Digest { macro_rules! digest_names { ($($typ:ty => $name:literal),* $(,)?) => { + digest_names!( + $( + $typ => $name @ $name, + )* + ); + }; + ($($typ:ty => $name:literal @ $feature:literal),* $(,)?) => { $( - #[cfg(feature = $name)] + #[cfg(feature = $feature)] impl NamedDigest for $typ { fn name() -> &'static str { $name @@ -45,6 +52,14 @@ digest_names! { sha2::Sha512 => "sha512", } +digest_names! { + noncrypto_digests::Fnv => "fnv1a" @ "fnv", + noncrypto_digests::Xxh32 => "xxh32" @ "xxhash", + noncrypto_digests::Xxh64 => "xxh64" @ "xxhash", + noncrypto_digests::Xxh3_64 => "xxh3_64" @ "xxhash", + noncrypto_digests::Xxh3_128 => "xxh3_128" @ "xxhash", +} + pub(crate) fn create_hash_fn( conn: &Connection, fn_name: &str, diff --git a/src/xxhash.rs b/src/xxhash.rs new file mode 100644 index 0000000..a8640d1 --- /dev/null +++ b/src/xxhash.rs @@ -0,0 +1,38 @@ +use noncrypto_digests::{Xxh32, Xxh3_128, Xxh3_64, Xxh64}; + +use crate::rusqlite::{Connection, Result}; + +/// Register `xxh32`, `xxh64`, `xxh3_64`, `xxh3_128`, `xxh3_64` SQL functions with the given `SQLite` connection. +/// The functions use [Rust xxhash implementation](https://github.com/DoumanAsh/xxhash-rust) to compute the hash of the argument(s) using zero as the seed value. +/// +/// # Example +/// +/// ``` +/// # // Use Python to convert: +/// # // print('"\\x' + '\\x'.join([f"{v:02X}" for v in [251, 0, 119, 249]])+'"') +/// # use sqlite_hashes::rusqlite::{Connection, Result}; +/// # use sqlite_hashes::register_xxhash_functions; +/// # fn main() -> Result<()> { +/// let db = Connection::open_in_memory()?; +/// register_xxhash_functions(&db)?; +/// let hash: Vec = db.query_row("SELECT xxh32('hello')", [], |r| r.get(0))?; +/// let expected = b"\xFB\x00\x77\xF9"; +/// assert_eq!(hash, expected); +/// let hash: Vec = db.query_row("SELECT xxh64('hello')", [], |r| r.get(0))?; +/// let expected = b"\x26\xC7\x82\x7D\x88\x9F\x6D\xA3"; +/// assert_eq!(hash, expected); +/// let hash: Vec = db.query_row("SELECT xxh3_64('hello')", [], |r| r.get(0))?; +/// let expected = b"\x95\x55\xE8\x55\x5C\x62\xDC\xFD"; +/// assert_eq!(hash, expected); +/// let hash: Vec = db.query_row("SELECT xxh3_128('hello')", [], |r| r.get(0))?; +/// let expected = b"\xb5\xe9\xc1\xad\x07\x1b\x3e\x7f\xc7\x79\xcf\xaa\x5e\x52\x38\x18"; +/// assert_eq!(hash, expected); +/// # Ok(()) +/// # } +/// ``` +pub fn register_xxhash_functions(conn: &Connection) -> Result<()> { + crate::scalar::create_hash_fn::(conn, "xxh32")?; + crate::scalar::create_hash_fn::(conn, "xxh64")?; + crate::scalar::create_hash_fn::(conn, "xxh3_64")?; + crate::scalar::create_hash_fn::(conn, "xxh3_128") +} diff --git a/tests/_utils.rs b/tests/_utils.rs index e421201..b23a55f 100644 --- a/tests/_utils.rs +++ b/tests/_utils.rs @@ -48,6 +48,15 @@ fn hasher() { assert_snapshot!(hash_hex::("test".as_bytes()), @"768412320F7B0AA5812FCE428DC4706B3CAE50E02A64CAA16A782249BFE8EFC4B7EF1CCB126255D196047DFEDF17A0A9"); #[cfg(feature = "sha512")] assert_snapshot!(hash_hex::("test".as_bytes()), @"EE26B0DD4AF7E749AA1A8EE3C10AE9923F618980772E473F8819A5D4940E0DB27AC185F8A0E1D5F84F88BC887FD67B143732C304CC5FA9AD8E6F57F50028A8FF"); + #[cfg(feature = "fnv")] + assert_snapshot!(hash_hex::("test".as_bytes()), @"EE26B0DD4AF7E749AA1A8EE3C10AE9923F618980772E473F8819A5D4940E0DB27AC185F8A0E1D5F84F88BC887FD67B143732C304CC5FA9AD8E6F57F50028A8FF"); + #[cfg(feature = "xxhash")] + { + assert_snapshot!(hash_hex::("test".as_bytes()), @"EE26B0DD4AF7E749AA1A8EE3C10AE9923F618980772E473F8819A5D4940E0DB27AC185F8A0E1D5F84F88BC887FD67B143732C304CC5FA9AD8E6F57F50028A8FF"); + assert_snapshot!(hash_hex::("test".as_bytes()), @"EE26B0DD4AF7E749AA1A8EE3C10AE9923F618980772E473F8819A5D4940E0DB27AC185F8A0E1D5F84F88BC887FD67B143732C304CC5FA9AD8E6F57F50028A8FF"); + assert_snapshot!(hash_hex::("test".as_bytes()), @"EE26B0DD4AF7E749AA1A8EE3C10AE9923F618980772E473F8819A5D4940E0DB27AC185F8A0E1D5F84F88BC887FD67B143732C304CC5FA9AD8E6F57F50028A8FF"); + assert_snapshot!(hash_hex::("test".as_bytes()), @"EE26B0DD4AF7E749AA1A8EE3C10AE9923F618980772E473F8819A5D4940E0DB27AC185F8A0E1D5F84F88BC887FD67B143732C304CC5FA9AD8E6F57F50028A8FF"); + } } /// Create macros like `md5!` asserting that first expression equals to the hash of the second one. @@ -130,6 +139,11 @@ hash_macros!( "sha256" sha256 sha2::Sha256, "sha384" sha384 sha2::Sha384, "sha512" sha512 sha2::Sha512, + "fnv" fnv1a noncrypto_digests::Fnv, + "xxhash" xxh32 noncrypto_digests::Xxh32, + "xxhash" xxh64 noncrypto_digests::Xxh64, + "xxhash" xxh3_64 noncrypto_digests::Xxh3_64, + "xxhash" xxh3_128 noncrypto_digests::Xxh3_128, ); macro_rules! test_all { @@ -145,6 +159,11 @@ macro_rules! test_all { sha256!( $conn.$func(&format!("sha256{suffix}")), $($any)* ); sha384!( $conn.$func(&format!("sha384{suffix}")), $($any)* ); sha512!( $conn.$func(&format!("sha512{suffix}")), $($any)* ); + fnv1a!( $conn.$func(&format!("fnv1a{suffix}")), $($any)* ); + xxh32!( $conn.$func(&format!("xxh32{suffix}")), $($any)* ); + xxh64!( $conn.$func(&format!("xxh64{suffix}")), $($any)* ); + xxh3_64!( $conn.$func(&format!("xxh3_64{suffix}")), $($any)* ); + xxh3_128!( $conn.$func(&format!("xxh3_128{suffix}")), $($any)* ); }}; } diff --git a/tests/test-ext.sh b/tests/test-ext.sh index df1724e..95472fc 100755 --- a/tests/test-ext.sh +++ b/tests/test-ext.sh @@ -51,3 +51,8 @@ test_hash "sha224" "A7470858E79C282BC2F6ADFD831B132672DFD1224C1E78CBF5BCD057" test_hash "sha256" "5994471ABB01112AFCC18159F6CC74B4F511B99806DA59B3CAF5A9C173CACFC5" test_hash "sha384" "0FA76955ABFA9DAFD83FACCA8343A92AA09497F98101086611B0BFA95DBC0DCC661D62E9568A5A032BA81960F3E55D4A" test_hash "sha512" "3627909A29C31381A071EC27F7C9CA97726182AED29A7DDD2E54353322CFB30ABB9E3A6DF2AC2C20FE23436311D678564D0C8D305930575F60E2D3D048184D79" +test_hash "fnv1a" "E575E8883C0F89F8" +test_hash "xxh32" "B30D56B4" +test_hash "xxh64" "C6F2D2DD0AD64FB6" +test_hash "xxh3_64" "F34099EDE96B5581" +test_hash "xxh3_128" "4AF3DA69F61E14CF26F4C14B6B6BFDB4"