NURLNURL registrynurl-lang.org →

← cas

cas 0.1.0 API

cas.nu

packages/cas/src/cas.nu — a content-addressed store over BLAKE3.

The keystone primitive of a verifiable build/distribution chain: bytes go in, their BLAKE3-256 hex comes back, and from then on the hash IS the name. Storing the same bytes twice is free (dedup by existence), and every read re-hashes what it found and refuses to return bytes that don't match their name — a store you can rsync, cache, or fetch from an untrusted mirror without losing integrity.

Layout (git/OCI-shaped, two-level fan-out): <root>/objects/<hex[0:2]>/<hex[2:64]> immutable objects <root>/tmp/ staging for atomic writes

Writes stage into tmp/ and fs_rename into place — readers never see a torn object, and concurrent writers of the same content converge on the same file. Objects are plain files: no index, no database, no lock; the filesystem is the whole data structure.

( cas_open /path/to/store ) → !Cas String ( cas_put c bytes ) → !String String BLAKE3 hex ( cas_put_file c path ) → !String String ( cas_get c hex ) → !( Vec u ) String (verified) ( cas_has c hex ) → b ( cas_object_path c hex ) → String ( cas_hash_hex bytes ) → String the naming function ( cas_free c )

API

: Cas

: Cas {
    String root
}

@ cas_free Cas c → v

@ cas_hash_hex ( Vec u ) data → String

The naming function: BLAKE3-256, lowercase hex.

@ cas_open s root → !Cas String

@ cas_object_path Cas c s hex → String

<root>/objects/<hex[0:2]>/<hex[2:]>

@ cas_has Cas c s hex → b

@ cas_put Cas c ( Vec u ) data → !String String

Store bytes; returns their hex name. Existing object = dedup hit (the name already proves the content — cas_get verifies on the way out). New object: stage under tmp/ with a unique name, then one atomic rename into place.

@ cas_put_file Cas c s path → !String String

@ cas_get Cas c s hex → !( Vec u ) String

Fetch by name — and PROVE the name. The stored bytes are re-hashed and compared to the requested hex; a corrupted or tampered object is an error, never silently returned. This is the verifiability loop: any store you can point cas_get at (local disk, a synced mirror, a cache you don't trust) yields either the exact original bytes or a failure.


main.nu

cas — a content-addressed store over BLAKE3, from the command line.

cas put file.bin store bytes → prints the hash cas hash file.bin name without storing cas get <hash> [-o out.bin] fetch + VERIFY (stdout by default) cas snapshot ./dir whole tree → one manifest hash cas ls <manifest-hash> list a snapshot cas checkout <manifest-hash> ./dest materialise + verify cas verify <hash> deep-prove a blob or a whole snapshot

The store is plain files under --store DIR (default $CAS_STORE, else ~/.cas): rsync it, mirror it, cache it — get/checkout/verify re-hash everything they touch, so a lying store is an error, never silent corruption.

API

@ main → i


manifest.nu

packages/cas/src/manifest.nu — the minimal manifest: a named set of files as (hash, size, path) triples, serialised CANONICALLY so that the same tree always produces the same bytes — and therefore the same BLAKE3 name when the manifest is stored in the CAS. That makes a manifest hash a Merkle root: one 64-hex string names an entire tree, dedups against every other tree sharing files, and verifies all the way down.

Wire format (text, one object kind, self-identifying): cas-manifest v1\n <64-hex> <size> <path>\n entries sorted bytewise by path

Paths are relative, '/'-separated, no leading './'. The format is the canonical encoding — decode(encode(m)) round-trips and encode(decode(b)) == b for any valid manifest, so hashes are stable across implementations.

( manifest_new ) → CasManifest ( manifest_add m hex size path ) → v ( manifest_encode m ) → ( Vec u ) canonical bytes ( manifest_decode bytes ) → !CasManifest String ( manifest_len m ) / ( manifest_hash m i ) / ( manifest_size m i ) / ( manifest_path m i ) → accessors (borrowed views) ( manifest_free m )

Store-level operations built on it (import src/cas.nu first): ( cas_snapshot c dir ) → !String String manifest hex ( cas_checkout c manifest_hex dest ) → !i String files written ( cas_verify c hex ) → !i String objects proven

API

: CasManifest

: CasManifest {
    ( Vec String ) hashes
    ( Vec i ) sizes
    ( Vec String ) paths
}

@ manifest_new → CasManifest

@ manifest_free CasManifest m → v

@ manifest_len CasManifest m → i

@ manifest_hash CasManifest m i k → String

Accessors return OWNED copies (vec_get on Vec String hands back an aliasing copy the compiler tracks — copying keeps call sites simple).

@ manifest_size CasManifest m i k → i

@ manifest_path CasManifest m i k → String

@ manifest_add CasManifest m s hex i size s path → v

@ manifest_encode CasManifest m → ( Vec u )

Canonical bytes: header + entries sorted bytewise by path. Sorting happens on an index permutation so the manifest itself is untouched.

@ string_cmp_bytes String a String b → i

Bytewise string compare for the canonical sort (string_cmp in the stdlib is locale-free already, but be explicit and self-contained).

@ manifest_is ( Vec u ) data → b

True iff the bytes carry the manifest header (cheap kind probe).

@ manifest_decode ( Vec u ) data → !CasManifest String

@ cas_snapshot Cas c s dir → !String String

Recursively walk dir, cas_put every regular file, then cas_put the canonical manifest of the tree. Returns the manifest's hex — one name for the whole snapshot.

@ cas_checkout Cas c s manifest_hex s dest → !i String

Materialise a snapshot into dest. Every object read is verified by cas_get; a checkout therefore cannot produce bytes that differ from what was snapshotted. Returns the number of files written.

@ cas_verify Cas c s hex → !i String

Deep-verify a name: a blob proves itself (cas_get re-hashes); a manifest additionally proves every entry it references. Returns the total number of objects proven (manifest itself included).