F1.2: std.hash zero-heap [64]u8 hex API + chunked file + pinned vectors
Make the SHA-256 digest path allocation-free (foundation heap-discipline):
- final() and sha256_hex() now return the 64-char lowercase hex digest as
a [64]u8 by value on the stack; the cstring(64) heap allocation is gone.
- sha256_file() streams the file in fixed 64KB stack chunks via open_file/
File.read/File.close (defer-closed on every path) instead of slurping it
with read_file; peak memory is O(chunk), not O(filesize).
Tests (compare via a zero-copy string view over the [64]u8):
- 0710 updated to the by-value API (output unchanged).
- 0711 known-answer vectors: "", "abc", NIST-56/112, padding boundaries
{0,55,56,57,63,64,65,119,120}, and 1000 / 1,000,000 'a' repeats, each
pinned to its published digest (cross-checked with shasum -a 256).
- 0712 streaming equivalence (one-shot == byte-at-a-time == split-mid-block
== split-on-boundary) plus sha256_file(temp) == in-memory digest.
src/ untouched. zig build && zig build test && tests/run_examples.sh green.
This commit is contained in:
@@ -7,17 +7,27 @@
|
||||
// `& MASK32`, so the result is identical regardless of the host's
|
||||
// native integer width or overflow behaviour.
|
||||
//
|
||||
// Zero-heap: the digest path never touches `context.allocator`. The
|
||||
// hash is a fixed `[64]u8` of lowercase hex returned by value on the
|
||||
// stack, and file hashing streams the input in fixed-size chunks, so
|
||||
// peak memory is O(chunk) regardless of file size.
|
||||
//
|
||||
// Streaming API (the by-value `init` / `*self` pattern):
|
||||
//
|
||||
// h := hash.init(); // Sha256, stack-local
|
||||
// h.update("hello, "); // absorb across calls
|
||||
// h.update("world");
|
||||
// digest := h.final(); // 64-char lowercase hex
|
||||
// digest := h.final(); // [64]u8, 64-char lowercase hex, by value
|
||||
//
|
||||
// One-shot convenience:
|
||||
//
|
||||
// digest := hash.sha256_hex("abc");
|
||||
// digest := hash.sha256_file("path\0"); // ?string, null on I/O error
|
||||
// digest := hash.sha256_hex("abc"); // [64]u8 by value
|
||||
// digest := hash.sha256_file("path\0"); // ?[64]u8, null on I/O error
|
||||
//
|
||||
// To print or compare a digest, build a `string` VIEW over it (no copy):
|
||||
//
|
||||
// d := hash.sha256_hex("abc");
|
||||
// view := string.{ ptr = @d[0], len = 64 };
|
||||
// =====================================================================
|
||||
|
||||
#import "modules/std.sx";
|
||||
@@ -127,8 +137,9 @@ Sha256 :: struct {
|
||||
}
|
||||
|
||||
// Finish: apply FIPS padding and emit the 32-byte digest as 64
|
||||
// lowercase hex characters. The state is consumed by this call.
|
||||
final :: (self: *Sha256) -> string {
|
||||
// lowercase hex characters in a stack `[64]u8`, returned by value.
|
||||
// The state is consumed by this call. No heap allocation.
|
||||
final :: (self: *Sha256) -> [64]u8 {
|
||||
bit_len := self.total_len * 8;
|
||||
|
||||
// 0x80 terminator, then zero-pad until 56 bytes mod 64.
|
||||
@@ -156,7 +167,7 @@ Sha256 :: struct {
|
||||
self.process_block();
|
||||
self.buf_len = 0;
|
||||
|
||||
digest := cstring(64);
|
||||
digest : [64]u8 = ---;
|
||||
i := 0;
|
||||
while i < 8 {
|
||||
word := self.h[i] & MASK32;
|
||||
@@ -196,16 +207,32 @@ init :: () -> Sha256 {
|
||||
s
|
||||
}
|
||||
|
||||
// One-shot: digest of a single buffer as 64-char lowercase hex.
|
||||
sha256_hex :: (data: string) -> string {
|
||||
// One-shot: digest of a single buffer as 64-char lowercase hex,
|
||||
// returned by value. No heap allocation.
|
||||
sha256_hex :: (data: string) -> [64]u8 {
|
||||
h := init();
|
||||
h.update(data);
|
||||
h.final()
|
||||
}
|
||||
|
||||
// Digest of a file's contents. Returns null if the file can't be read.
|
||||
sha256_file :: (path: [:0]u8) -> ?string {
|
||||
content := read_file(path);
|
||||
if content == null { return null; }
|
||||
sha256_hex(content!)
|
||||
// Digest of a file's contents, returned by value. Streams the file in
|
||||
// fixed 64KB chunks, so peak memory is O(chunk) even for multi-hundred-
|
||||
// MB artifacts. Returns null if the file can't be opened. No heap
|
||||
// allocation: the chunk buffer is a stack array.
|
||||
sha256_file :: (path: [:0]u8) -> ?[64]u8 {
|
||||
handle := open_file(path, .read);
|
||||
if handle == null { return null; }
|
||||
file := handle!;
|
||||
defer file.close();
|
||||
|
||||
h := init();
|
||||
chunk : [65536]u8 = ---;
|
||||
reading := true;
|
||||
while reading {
|
||||
n := file.read(string.{ ptr = @chunk[0], len = 65536 });
|
||||
if n < 0 { return null; }
|
||||
if n == 0 { reading = false; }
|
||||
if n > 0 { h.update(string.{ ptr = @chunk[0], len = n }); }
|
||||
}
|
||||
h.final()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user