tsaur — Rust reference implementation of the T-saur (.tsr) format
Workspace with two crates:
| crate | what it is |
|---|---|
tsaur-core |
library: on-disk framing (format), FastCDC chunking + BLAKE3 (chunk), codecs and branch filters (codec), ZIP/OPC, PDF and JPEG container recipes (container), envelope encryption + hybrid recipients + signatures (crypto), manifest model (manifest), streaming packer (pack), memory-mapped reader / verifier / extractor and credential-free inspect (read), streaming pieces + Reed-Solomon parity sidecars (pieces), offline volume sets (volumes), direct piece exchange (transfer), pinned-fingerprint TLS identities (tls), canonical views (canonical), path sanitisation (paths) |
tsaur (tsaur-cli) |
the command-line tool; ops = operations shared by the CLI and the MCP server, mcp = the Model Context Protocol server (tsaur mcp) |
Build and test
Tests cover: granular round trip with dictionary and dedup, solid + encrypted + signed round trip (wrong password
rejected, foreign key rejected), byte-identical determinism, corruption detection (150 random flips + 15 truncations
never panic), range reads, container (ZIP, PDF, JPEG) explode/rebuild, reference archives, hybrid post-quantum
recipients, multi-stripe pieces recovery (damage, truncation, appended garbage, beyond-parity), branch-filter round
trips on adversarial inputs, unsafe path rejection, CLI exit codes, info on locked archives, diff, and an
end-to-end MCP session (both protocol eras).
CLI
tsaur pack <out.tsr> <inputs...> [--block <MiB> | --block-kib <KiB> | --solid <MiB> | --granular]
[--chunk fine|p2p|archive] [--level N] [--codec best|zstd|xz|ppmd] [--effort 1..5] [--jobs N] [--no-dict] [--no-container] [--no-delta]
[--ref base.tsr ...] [--password PW | TSAUR_PASSWORD env] [--kdf-memory-mib N] [--to recipient.pub ...]
[--sign-key FILE] [--keep-mtime] [--timestamp] [--pieces] [--piece-size-kib N] [--parity-pct N]
[--canonical] [--json]
Modes (all bit-exact, all verified per chunk):
default 64 KiB chunks for dedup/verification, 1 MiB compression blocks -> random access per block, ratio close to solid
--solid N large blocks (e.g. 8 MiB) -> best ratio
--granular one chunk per blob + trained dictionary -> best random access / P2P fetch, weakest ratio
Codecs per block: --codec best (default: zstd -19, xz/LZMA2 9e and — on text-like blocks — PPMd H are tried, the
smallest is kept; blocks zstd cannot shrink by 3 % are stored; x86 and ARM64 branch filters are tried on machine
code), zstd (fast: --level 9 ≈ 200 MB/s), xz, ppmd. --effort 1..5 (best only; default 5 = every codec on every
block): at 2..4 the filter and the codec are decided on a sample of each block above 128 KiB (8+ evenly spaced 8 KiB
slices) and only the winner runs on the whole block; 1 = zstd only, 2 = zstd/xz, 3 = zstd/xz/PPMd, 4 = 3 plus an xz
check when PPMd wins the sample. Delta coding (--no-delta to disable): an entry that is a new
version of content the archive or a --ref archive already holds is also tried with zstd against that content as
dictionary (codec "zstd+delta", blob field `d` = dictionary chunks); the smaller encoding wins.
Containers: ZIP/OPC members and PDF FlateDecode streams are inverted with preflate; JPEG files and DCTDecode streams
are recoded losslessly with Lepton (~22 % smaller). Everything is rebuilt bit-exact or stored raw.
Reference archives: `pack --ref base.tsr` stores only the chunks that base.tsr does not already hold ("compression by
reference"; the manifest records the reference root and counts); readers resolve them with the same `--ref`.
Hybrid fidelity: `pack --canonical` also stores the Markdown/text view of every DOCX/PDF as `.tsaur/views/<path>.md|.txt`
(manifest `derived {from, view, generator, tokens_est}`); `read --view canonical` serves it, `unpack` skips views
unless they are selected, a view that cannot be generated leaves a note on the source entry.
tsaur unpack <archive> <dir> [--entry PATH|GLOB ...] [--json]
tsaur list <archive> [--md] [--json] # --md = TSAUR.md agent view with token estimates; --json adds citation URIs
tsaur info <archive> [--json] # sections, codecs, filters, chunks, blobs, refs; works on locked archives
tsaur stat <archive> <entry> [--json] # hash, mode, chunks, container details, token estimates, citation URI
tsaur grep <archive> <regex> [--entries GLOB] [-i] [--max N] [--json] # search without extracting
tsaur read <archive> <entry> [--bytes A-B | --lines A-B] [--view raw|canonical] # canonical = DOCX->Markdown, PDF->text
tsaur diff <a.tsr> <b.tsr> [--json] # entries added/removed/changed; chunks of B already in A (= what --ref saves)
tsaur verify <archive> [--pubkey HEX] [--pieces] [--json]
tsaur pieces <archive> [--piece-size-kib N] [--parity-pct N]
tsaur recover <archive>
tsaur volumes split <archive> --data N --parity M [--piece-size-kib K] --out DIR ... # N + M .tsrv volumes, round-robin over the DIRs; piece size adaptive by default
tsaur volumes inspect <files|dirs ...> [--verify] # sets found, missing volumes, reconstructible?; exit 5 when not
tsaur volumes join <out.tsr> <files|dirs ...> # any N of N + M volumes rebuild the archive (hash-verified before rename)
tsaur volumes repair <files|dirs ...> [--out DIR] # recreate missing/damaged volumes byte-identical
tsaur volumes serve <files|dirs ...> [--listen 127.0.0.1:7407] [--expose-lan (--tls-identity FILE (--allow FP ... | --allow-set SET=FP ... | --allow-file FILE) [--revoke FILE] | --allow-anyone)] [--max-connections N] [--max-connections-per-peer N] [--max-peers N] [--max-requests-per-second N] [--min-rate-kib K] [--max-bandwidth-kib K] # serve pieces (read-only)
tsaur volumes fetch --set HEX [--descriptor HEX] --from HOST:PORT ... [--peer-id FP ...] [--tls-identity FILE] [--revoke FILE] --out DIR [--local PATH ...] [--volumes needed|all|1,2] [--join OUT.tsr] [--stop-after N]
tsaur pack ... --no-lepton # full build: store JPEGs as they are, so the lite build can read the archive
tsaur unpack ... --overwrite # replace existing files (refused otherwise, before anything is written)
tsaur volumes keygen --out FILE # peer identity for TLS transfers: FILE (private key) + FILE.crt; prints the fingerprint
tsaur volumes fingerprint FILE # fingerprint of an existing identity
tsaur keygen --out FILE # Ed25519 signing key: FILE (hex seed, secret) + FILE.pub (public key for verify --pubkey)
tsaur keygen --recipient --out FILE # hybrid X25519 + ML-KEM-768 identity: FILE (secret) + FILE.pub (recipient key)
tsaur pack ... --to alice.key.pub --to bob.key.pub [--password PW] # one stanza per recipient/passphrase
tsaur unpack ... --identity alice.key # any one recipient (or the passphrase) unlocks the archive key
tsaur mcp [--root DIR ...] [--archive FILE ...] [--ref ...] [--identity ...] # MCP server on stdin/stdout
Exit codes: 0 ok · 1 I/O · 2 corrupt or hash mismatch · 3 refused by policy · 4 resource limit · 5 missing data · 6 cryptography · 7 invalid argument.
MCP server
tsaur mcp speaks JSON-RPC over stdio, one message per line. It implements the current Model Context Protocol
revision (2026-07-28: per-request _meta protocol version, server/discover, UnsupportedProtocolVersionError) and
the legacy initialize handshake (2025-11-25 and earlier), so any client works. Tools: tsaur_info, tsaur_list,
tsaur_stat, tsaur_read (byte/line ranges, canonical views, base64 for binary content, citation URI), tsaur_grep,
tsaur_diff, tsaur_verify, tsaur_unpack. Archives registered with --archive are listed as resources
tsaur://<file name>/<entry>. Only paths below --root (default: the current directory) can be opened or extracted.
Tool failures come back as results with isError: true and the CLI exit code in structuredContent.exit_code.
Format v1 (implemented subset of the spec)
header 32 B "TSR\x1A" | version u16 | flags u16 | header_len u32 | reserved
sections recipients (cleartext CBOR stanzas: argon2id / x25519mlkem768) | dictionary | blobs | chunk index | manifest | signatures
table CBOR array {t, off, len, blake3}
trailer 24 B table_off u64 | table_len u64 | crc32 | "RST\x1A"
- chunk identity: BLAKE3-256; Merkle root over the chunk table = content identity (
root), also the base of citation URIstsaur://<root>/<path>#L10-20 - blobs:
{off, clen, ulen, codec, n, p, f, d}— one chunk (granular, with a trained zstd dictionary) or a block of consecutive unique chunks; codecs store / zstd / zstd+dict / xz / PPMd / zstd+delta chosen per block;p= codec parameters,f= pre-filter (low nibble: 0 none, 1 x86 BCJ, 2 ARM64 BCJ; high nibble: filter parameter),d= dictionary chunks of a delta blob (never delta blobs themselves; external chunks resolve through--ref) - containers: ZIP-based files parsed with a strict framing parser that keeps every header byte; deflate members are inverted with preflate (plaintext + a few bytes of corrections) and recreated bit-exact whatever encoder produced them; stored members are chunked as-is; anything else (encrypted members, unknown methods, ZIP64, gaps) falls back to storing the file raw — the rebuild is verified at pack time
- embedded streams: PDF
/FlateDecodeobjects are inverted like ZIP members (zlib header and Adler-32 kept);/DCTDecodeobjects and standalone JPEG files are recoded with Lepton (single-threaded, deterministic) - references: chunks flagged external (
x = 1) live in an archive listed in the manifestrefs; readers resolve them with--refand report what stays unresolved - encryption: XChaCha20-Poly1305 per blob and per structured section, keys derived from a random archive key
(BLAKE3
derive_key); the archive key is wrapped once per stanza: Argon2id(passphrase) — reader-enforced floor 64 MiB / t=3 — and/or hybrid X25519 + ML-KEM-768 recipients (FIPS 203; KEK = HKDF-SHA-256 over both shared secrets, so confidentiality survives the break of either component — "harvest now, decrypt later" protection) - signature: Ed25519 over header + section table (hash, offset, length of every section)
- limits: declared totals, per-entry size and expansion ratio are checked before allocation; paths are labels
(NFC, relative, no
.., no drive letters, no:/ADS, no reserved device names) - pieces:
<archive>.pieces(BLAKE3 per piece + Merkle root, stripe table) and<archive>.par(systematic Reed-Solomon, GF(2^8), stripes ≤ 200 data pieces and ≤ 64 MiB); creation, verification andrecoverstream one stripe at a time - volume sets:
.tsrvfiles = 64 B header (TSV\x1A, index, N, M, piece size, archive size, set id, payload length)- pieces + CBOR descriptor (archive name/size/BLAKE3, geometry, every piece and parity hash, Merkle root) + 56 B
trailer; one piece per volume per stripe, any N of N + M volumes rebuild the archive; memory
(N + M) × piece size
- pieces + CBOR descriptor (archive name/size/BLAKE3, geometry, every piece and parity hash, Merkle root) + 56 B
trailer; one piece per volume per stripe, any N of N + M volumes rebuild the archive; memory
Not yet: section-aware executable filtering, float-split codec, ML-DSA composite signatures, stored embeddings and summaries, exact token counts, P2P transport (iroh), RaptorQ.