Digests and checksums#

Message digests produce fixed-size opaque bytes. Checksums produce numeric error-detection values. General-purpose hashes such as FNV are nupp.util.fnv1a64. Base64 and hexadecimal are reversible codecs.

Incremental digests#

local algorithm = nupp.digest.algorithm("sha256")
assert(algorithm.digestSize == 32)
local rolling = algorithm:newDigest()
rolling:write("first chunk")
rolling:write("second chunk")
local raw: string = rolling:digest()

nupp.digest.newDigest(name) is shorthand for algorithm(name):newDigest(). lookup(name) returns nil when an algorithm is unavailable; algorithm and newDigest raise instead. algorithms() returns sorted canonical names. Descriptors expose name and digestSize before construction; a created digest exposes algorithm() and digestSize().

The built-ins are md5 (16 bytes), sha1 (20), sha256 (32), and sha512 (64). MD5 and SHA-1 are legacy interoperability algorithms and must not be used where collision resistance is required.

A digest is an io.Writer: write(string) and writeSpan(ByteSpan) accept chunks without retaining input views, and the state stays bounded as the message grows. Anything that fills a writer fills a digest, so reader:transferTo(rolling) hashes a file or a response body a window at a time.

Finalization and caller storage#

digest() returns raw bytes, digest(destination) writes a writable byte span and returns the byte count, and hexDigest() returns lowercase hexadecimal. Every finalization consumes the digest; it neither snapshots nor resets it. Scope exit closes an unfinished provider context.

local rolling = nupp.digest.newDigest("sha256")
rolling:write("payload")
local bytes = nupp.mem.array.bytes(rolling:digestSize())
local output = bytes:write()
local written: integer = rolling:digest(output)
assert(written == 32)
nupp.drop(output)
local readable = bytes:read()
assert(#readable == 32)

A destination must contain at least digestSize() bytes. Only that prefix is written; pass a slice to choose an offset. A short destination raises before any output is written. The digest is consumed and the provider context is closed even when finalization fails. The destination remains owned by the caller.

One-shot conveniences are nupp.digest.digest(name, bytes) and nupp.digest.hexDigest(name, bytes). Encoding is also available separately: nupp.codec.hex.encode(raw) or nupp.codec.base64.encode(raw).

Checksums and MACs#

local sum = nupp.checksum.newChecksum("crc32c")
assert(sum:width() == 32)
sum:write("1234")
local partial = sum:value()
sum:write("56789")
assert(sum:value() == 0xe3069283ULL)

A checksum is an io.Writer too. value() is a non-consuming numeric snapshot, returned as uint64. width(), and the descriptor's width field, specify meaningful bits. Built-ins are adler32, crc32, crc32c and crc64-ecma. crc32 is the reflected IEEE variant. The digest names and crc32/crc32c match Smithy's httpChecksum algorithm names in lowercase. CRC64 uses ECMA-182 with no reflection, zero initialization and zero final xor. It differs from Smithy's CRC64NVME algorithm. The checksum API chooses no byte order: a protocol writes the value using its own scalar serialization rules. Checksums provide no authentication.

nupp.mac.newMac("hmac-sha256", key) accepts a raw-byte key and returns the same writer and consuming finalization as a digest. Its descriptor has digestSize = 32. The one-shot forms are mac.digest(name, key, bytes) and mac.hexDigest(name, key, bytes).

hmac-sha256 names the HMAC construction over SHA-256. Smithy's httpChecksum trait defines no MAC algorithms. AWS KMS uses HMAC_SHA_256 for this construction; SigV4's AWS4-HMAC-SHA256 names the complete signing scheme.

Providers#

Each family declares its shared types beside the public module:

Facade Interface module Provider contents
nupp.digest nupp.digest.spi algorithms: {[string]: Algorithm}
nupp.checksum nupp.checksum.spi algorithms: {[string]: Algorithm}
nupp.mac nupp.mac.spi algorithms: {[string]: Algorithm}

A package advertises its module in nupp/spi.json:

{"nupp.digest.spi.Provider":["acme.digests"]}

The implementation exports a Provider directly, with an optional integer priority. The facade chooses the unique highest priority, treating absence as zero, and reports a tie. It overlays that catalog's entries on the built-ins; unreplaced built-ins remain available. Empty discovery uses the built-in catalog. Dependency order does not decide which catalog wins.

The compiler checks the export against Provider, including its owned state signatures. Lua providers need a .d.nupp declaration or typed adapter. Descriptors are retained during module initialization. Lookup, listing, context creation, writes, and finalization make no SPI calls.

Descriptors must agree with their map keys and report a positive fixed output size, or a checksum width from 1 through 64. Built-in names retain their standard sizes and widths. Invalid descriptors fail the facade's require. Provider failures propagate without retrying another implementation.

Each digest descriptor creates a fresh canonical State. Its update borrows input bytes, finish borrows the caller's writable destination exclusively, and close consumes ownership without suspension. The facade allocates final output, encodes hexadecimal, and closes state on both successful and failed finalization. Providers must not retain input or output views and must write exactly their advertised byte count. Checksums use their canonical State with a non-consuming value(): uint64; MAC factories additionally accept the raw-byte key and return the shared digest state. Reuse these interfaces rather than defining public nominal identities in a provider.

See SPI for selection, metadata, and worker behavior.