Skip to content

Hash Generator — Zig source

Compute SHA-1, SHA-256, SHA-384 and SHA-512 hashes of any text at once, using the browser's native SubtleCrypto. Copy each digest. 100% client-side.

This is the Zig implementation — the same logic the interactive tool runs, in a shareable, citable form.

//! hash — SHA-1 / SHA-256 / SHA-384 / SHA-512 digest of a UTF-8 string.
//!
//! Language: Zig 0.13 (standard library only)
//! Source:   CosmoDev polyglot showcase port of the `hash` tool, ported from
//!           src/tools/HashGenerator.tsx (the canonical TypeScript island).
//! License:  display source — part of CosmoDev's polyglot tool pages.
//!
//! Unkeyed SHA digests via Zig's std.crypto — the standard library ships
//! audited implementations of all four algorithms, so no third-party
//! packages are needed (the position the Rust port needs the RustCrypto
//! `sha1`/`sha2` crates for). Behavior mirrors the TypeScript reference:
//! UTF-8 input, lowercase hex output, and rejection of unknown algorithm
//! names. SHA-1 is offered for legacy compatibility only; it is not
//! collision-resistant.

const std = @import("std");

/// The standard-library hash types, re-exported so callers never touch
/// std.crypto directly (mirrors the Rust port's public use statements).
pub const Sha1 = std.crypto.hash.Sha1;
pub const Sha256 = std.crypto.hash.sha2.Sha256;
pub const Sha384 = std.crypto.hash.sha2.Sha384;
pub const Sha512 = std.crypto.hash.sha2.Sha512;

/// Canonical algorithm names. The spellings match the TypeScript union so
/// the same string works across every port.
pub const sha1_name = "SHA-1";
pub const sha256_name = "SHA-256"; // default algorithm
pub const sha384_name = "SHA-384";
pub const sha512_name = "SHA-512";

/// Every supported algorithm, in the order the React island renders them.
pub const algorithm_names = [_][]const u8{ sha1_name, sha256_name, sha384_name, sha512_name };

/// Errors returned by `digest`. Kept as a concrete error set (not a string)
/// so callers can switch on the kind — the Zig standing of the Rust port's
/// HashError enum.
pub const HashError = error{UnknownAlgorithm};

/// Longest hex digest, in bytes — size output buffers with this.
pub const max_hex_len = 128;

/// Every supported digest of one text — the Zig shape of the Record the
/// React island renders. Fixed-size fields, so no allocation is needed.
pub const HashAllResult = struct {
    sha1: [40]u8,
    sha256: [64]u8,
    sha384: [96]u8,
    sha512: [128]u8,
};

/// One-shot digest of `text` as lowercase hex, written into `out`.
///
/// `text` is the string's UTF-8 byte sequence (a Zig slice is untyped bytes,
/// so a UTF-8 string needs no encoding step). An empty `algorithm` selects
/// SHA-256 — the optional-parameter default the TypeScript reference
/// declares. `out` must be at least `max_hex_len` bytes; the written slice is
/// returned so callers get the exact length without computing it.
pub fn digest(out: []u8, algorithm: []const u8, text: []const u8) HashError![]const u8 {
    if (algorithm.len == 0)
        return hexOf(Sha256, out, text);
    if (std.mem.eql(u8, algorithm, sha1_name))
        return hexOf(Sha1, out, text);
    if (std.mem.eql(u8, algorithm, sha256_name))
        return hexOf(Sha256, out, text);
    if (std.mem.eql(u8, algorithm, sha384_name))
        return hexOf(Sha384, out, text);
    if (std.mem.eql(u8, algorithm, sha512_name))
        return hexOf(Sha512, out, text);
    return HashError.UnknownAlgorithm;
}

/// Compute all four digests of `text` into `out`. The four names are the
/// canonical constants, so the unknown-algorithm error is statically
/// unreachable — `catch unreachable` documents that rather than hiding it.
pub fn hashAll(text: []const u8) HashAllResult {
    var out: HashAllResult = undefined;
    var buf: [max_hex_len]u8 = undefined;

    // Each hash is independent. digest() returns a length-typed slice, so a
    // scratch buffer is reused between calls and copied into the fixed-size
    // fields.
    const hex1 = digest(&buf, sha1_name, text) catch unreachable;
    @memcpy(out.sha1[0..hex1.len], hex1);
    const hex2 = digest(&buf, sha256_name, text) catch unreachable;
    @memcpy(out.sha256[0..hex2.len], hex2);
    const hex3 = digest(&buf, sha384_name, text) catch unreachable;
    @memcpy(out.sha384[0..hex3.len], hex3);
    const hex4 = digest(&buf, sha512_name, text) catch unreachable;
    @memcpy(out.sha512[0..hex4.len], hex4);
    return out;
}

/// Generic one-shot: hash `text` with the std.crypto type `H` and write its
/// lowercase hex into `out`. bytesToHex returns a fixed [2*N]u8 array, which
/// is why the copy goes through a comptime-sized temporary.
fn hexOf(comptime H: type, out: []u8, text: []const u8) []const u8 {
    var raw: [H.digest_length]u8 = undefined;
    H.hash(text, &raw, .{});
    const hex = std.fmt.bytesToHex(raw, .lower);
    @memcpy(out[0..hex.len], &hex);
    return out[0..hex.len];
}

test "digest: known vectors for all four algorithms" {
    var buf: [max_hex_len]u8 = undefined;

    // SHA-256("hello") — the default algorithm.
    try std.testing.expectEqualStrings(
        "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824",
        try digest(&buf, "SHA-256", "hello"),
    );
    // SHA-1("hello")
    try std.testing.expectEqualStrings(
        "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d",
        try digest(&buf, "SHA-1", "hello"),
    );
    // SHA-384("hello")
    try std.testing.expectEqualStrings(
        "59e1748777448c69de6b800d7a33bbfb9ff1b463e44354c3553bcdb9c666fa90125a3c79f90397bdf5f6a13de828684f",
        try digest(&buf, "SHA-384", "hello"),
    );
    // SHA-512("hello")
    try std.testing.expectEqualStrings(
        "9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043",
        try digest(&buf, "SHA-512", "hello"),
    );
}

test "digest: empty text and empty algorithm" {
    var buf: [max_hex_len]u8 = undefined;

    // Empty input uses the empty-message IV digests (a padding edge case:
    // the 0x80 lands at offset 0).
    try std.testing.expectEqualStrings(
        "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
        try digest(&buf, "SHA-256", ""),
    );
    // An empty algorithm name selects the SHA-256 default.
    try std.testing.expectEqualStrings(
        try digest(&buf, "SHA-256", "default"),
        try digest(&buf, "", "default"),
    );
}

test "digest: unknown algorithm is an error" {
    var buf: [max_hex_len]u8 = undefined;
    try std.testing.expectError(HashError.UnknownAlgorithm, digest(&buf, "MD5", "hello"));
}

test "hashAll: fills all four fields and agrees with digest" {
    const all = hashAll("hello");
    var buf: [max_hex_len]u8 = undefined;
    try std.testing.expectEqualStrings(try digest(&buf, sha1_name, "hello"), &all.sha1);
    try std.testing.expectEqualStrings(try digest(&buf, sha256_name, "hello"), &all.sha256);
    try std.testing.expectEqualStrings(try digest(&buf, sha384_name, "hello"), &all.sha384);
    try std.testing.expectEqualStrings(try digest(&buf, sha512_name, "hello"), &all.sha512);
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →