Skip to content

HashSha256

HashSha256 computes the SHA-256 digest of some bytes: the same 32 bytes sha256sum, openssl dgst -sha256, PowerShell’s Get-FileHash, Go, Rust and Python compute for the same input. Use it when the digest is not yours to choose — a download to verify, a manifest to check, a content-addressed store to key.

// Verify a download against the digest its publisher printed.
const Sha256Digest digest = HashSha256(downloaded_bytes);
const String8 text = Sha256DigestToHex(scratch, digest);
if (!Str8Equal(text, published_hex))
{
return Error::CorruptDownload;
}

HashSha256 is FIPS 180-4’s SHA-256, proved on NIST’s own published test vectors, so any conformant tool reproduces its values and it reproduces theirs. Basalt is not a cryptography library: there is no HMAC, no key derivation, no signatures, no constant-time guarantee and no security audit. It is for interop and for catching accidental corruption.

What the promise covers, and what it does not

Section titled “What the promise covers, and what it does not”

A digest is 32 bytes returned by value — no allocation, no out-parameter:

struct Sha256Digest
{
static constexpr usize digest_size = 32;
u8 bytes[digest_size]; // bytes[0] is the first hex pair sha256sum prints
};

The bytes are in the standard’s order, so you can write them straight into a file, compare them to another tool’s output byte for byte, or format them:

const String8 text = Sha256DigestToHex(arena, digest); // 64 lowercase hex characters
// or, for any bytes at all:
const String8 dump = Str8Hex(arena, Str8(header, sizeof(header)));

Sha256DigestToHex is one line over Str8Hex, which lives in String.h because hex formatting is a string concern and serves file headers and debug dumps just as well. Both results are NUL-terminated past their end, so they can be passed to a %s too.

Digests compare with == and !=. There is deliberately no integer view and no ordering: which end would be “the top” is exactly the byte-order question a byte array avoids.

The streaming form is Init / Update* / Final, and the state is plain data — 104 bytes, no allocation, no destructor:

HashSha256State state = HashSha256Initialize();
u8 chunk[64 * 1024];
usize read = 0;
while (ReadChunk(file, chunk, sizeof(chunk), &read))
{
HashSha256Update(state, Str8(chunk, read));
}
const Sha256Digest digest = HashSha256Final(state);

Four rules make it predictable:

Chunking never matters Update(a); Update(b) is exactly Update(a ++ b), for any split. Zero-length updates are legal and change nothing.
Final does not consume It takes the state by const& and pads a copy, so you can take the digest of a prefix and keep going.
Copy is snapshot The state is a POD; copying it forks the stream. There is no Reset — re-Init instead.
Same values as the one-shot HashSha256(bytes) is the streaming path fed once, through the same compression function.

One thing the streaming form does not do: it is not prefix-free. Update("ab") + Update("c") and Update("a") + Update("bc") are the same message. If you are digesting several fields, put your own separators or lengths between them.

  • Keying an in-memory table. HashFast — forty times faster and free to get faster still.
  • A stable 64-bit content identity for a cache header or a file format of your own. HashRapidV3Nano is frozen too, and far cheaper.
  • Authenticating data against a hostile writer. Use a vetted cryptography library; see the danger note above.

Parameters, assertions and the full contract are documented in basalt/core/hash/HashSha256.h.

Signature Purpose
HashSha256(String8 bytes) The 32-byte FIPS 180-4 digest of the bytes.
HashSha256Initialize() A fresh streaming state.
HashSha256Update(HashSha256State&, String8 bytes) Append bytes to the message.
HashSha256Final(const HashSha256State&) The digest so far. Does not consume the state.
Sha256DigestToHex(Arena&, const Sha256Digest&) The 64 lowercase hex characters sha256sum prints.
Str8Hex(Arena&, String8 bytes) Lowercase hex for any bytes (in String.h).