flowscripter_io_cli_hash_native/lib.rs
1//! # flowscripter-io-cli-hash-native
2//!
3//! Rust-backed sha256 hasher exposed via Bun FFI, for demonstrating a
4//! zero-copy `ChunkRef` handoff outside of `pluggable-io-framework` itself.
5//! `init`/`update`/`final` mirrors OpenSSL's `EVP_Digest*` shape: `update`
6//! is called once per chunk with a raw pointer into JS- or Rust-owned
7//! memory, `final` copies out the (small, 32-byte) digest and frees the
8//! context.
9
10use sha2::{Digest, Sha256};
11
12pub const DIGEST_LENGTH: usize = 32;
13
14/// Opaque hashing context. Only ever accessed through the `hasher_*`
15/// functions below - never dereferenced directly by callers.
16pub struct HasherContext {
17 hasher: Sha256,
18}
19
20/// Allocates a new hashing context. Must be freed by a matching call to
21/// [`hasher_final`].
22#[no_mangle]
23pub extern "C" fn hasher_init() -> *mut HasherContext {
24 Box::into_raw(Box::new(HasherContext {
25 hasher: Sha256::new(),
26 }))
27}
28
29/// Feeds `len` bytes starting at `data` into the hash. `data` may point
30/// into memory owned by either the JS heap (via `bun:ffi`'s `ptr()`) or
31/// Rust-owned memory (e.g. a future Rust-backed source's `NativeChunk`) -
32/// this function reads it in place either way, without copying.
33///
34/// # Safety
35/// `ctx` must be a valid pointer returned by [`hasher_init`] and not yet
36/// passed to [`hasher_final`]. `data` must be valid for reads of `len`
37/// bytes for the duration of this call.
38#[no_mangle]
39pub unsafe extern "C" fn hasher_update(ctx: *mut HasherContext, data: *const u8, len: usize) {
40 if ctx.is_null() || data.is_null() {
41 return;
42 }
43 let context = unsafe { &mut *ctx };
44 let slice = unsafe { std::slice::from_raw_parts(data, len) };
45 context.hasher.update(slice);
46}
47
48/// Finalizes the hash, copies the `DIGEST_LENGTH`-byte digest into the
49/// caller-supplied `out` buffer, and frees `ctx` - `ctx` must not be used
50/// again after this call.
51///
52/// # Safety
53/// `ctx` must be a valid pointer returned by [`hasher_init`], not
54/// previously passed to `hasher_final`. `out` must be valid for writes of
55/// `DIGEST_LENGTH` bytes.
56#[no_mangle]
57pub unsafe extern "C" fn hasher_final(ctx: *mut HasherContext, out: *mut u8) {
58 if ctx.is_null() || out.is_null() {
59 return;
60 }
61 let context = unsafe { Box::from_raw(ctx) };
62 let digest = context.hasher.finalize();
63 unsafe {
64 std::ptr::copy_nonoverlapping(digest.as_ptr(), out, digest.len());
65 }
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71
72 #[test]
73 fn hashes_known_vector() {
74 let ctx = hasher_init();
75 let data = b"hello";
76 let mut out = [0u8; DIGEST_LENGTH];
77 unsafe {
78 hasher_update(ctx, data.as_ptr(), data.len());
79 hasher_final(ctx, out.as_mut_ptr());
80 }
81
82 let expected =
83 hex::decode("2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824")
84 .unwrap();
85 assert_eq!(out.to_vec(), expected);
86 }
87
88 #[test]
89 fn hashes_across_multiple_updates() {
90 let ctx = hasher_init();
91 let mut out = [0u8; DIGEST_LENGTH];
92 unsafe {
93 hasher_update(ctx, b"hel".as_ptr(), 3);
94 hasher_update(ctx, b"lo".as_ptr(), 2);
95 hasher_final(ctx, out.as_mut_ptr());
96 }
97
98 let expected =
99 hex::decode("2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824")
100 .unwrap();
101 assert_eq!(out.to_vec(), expected);
102 }
103}