Skip to main content

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}