tricu

A lazy functional language based on Tree Calculus
Log | Files | Refs | README | LICENSE

README.md (6831B)


Python file-integrity application

tricu-filecheck records and verifies SHA-256 manifests. Python provides the CLI, JSON schema and complete-file manifest publication; a packaged tricu worker performs file inspection, reads and incremental hashing through native IO. One Python FileChecker instance loads its worker once and reuses its context.

Build and run

nix build .#tricu-filecheck --no-link
APP=$(nix path-info .#tricu-filecheck)
WORK=$(mktemp -d)
printf 'hello\n' > "$WORK/hello.txt"

# Both commands interpret file paths relative to the manifest's directory.
"$APP/bin/tricu-filecheck" record "$WORK/checks.json" hello.txt
"$APP/bin/tricu-filecheck" verify "$WORK/checks.json"

printf 'changed\n' > "$WORK/hello.txt"
"$APP/bin/tricu-filecheck" verify "$WORK/checks.json"
# Exit 1, with status "changed" and expected/actual digests in JSON.

Each invocation prints one JSON report. Exit codes are:

File-operation errors include the IO category, operation, detail and progress. Verification continues through ordinary per-file failures and distinguishes them from changed contents. Recording publishes only when every requested file succeeds. Existing manifests remain intact: publication uses a fully written, synchronized staging file and a no-replace hard link, then removes the staging name. The manifest filesystem must support hard links. Directory durability across crashes is subject to the host filesystem; the CLI does not synchronize the directory. To record a replacement, choose a new manifest name or explicitly remove the old manifest first. The tool never edits input files.

--max-bytes N selects the per-file limit (default 64 MiB; supported range 0..2^63-1). Empty files work with a zero limit. The worker prechecks file type/size, opens a scoped reader and SHA-256 context, and consumes chunks of at most 4096 bytes. It checks the opened handle's size and the observed read position. A concurrently growing file can consume one excess chunk before rejection. The byte count comes from the handle, avoiding a retained source-level accumulator. The limit bounds file consumption, not heap use or execution time.

Manifest contract

{
  "schema": "tricu.filecheck.v1",
  "algorithm": "sha256",
  "files": [
    {
      "path": "hello.txt",
      "bytes": 6,
      "sha256": "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03"
    }
  ]
}

The UTF-8 JSON document is at most 1 MiB and contains 1..4096 entries. Field sets are exact, keys and path strings are unique, byte counts are integers in 0..2^63-1, and hashes are 64 lowercase hexadecimal characters. Paths are nonempty, relative Unicode strings without NULs. Spaces, newlines and Unicode names are preserved. Both record arguments and stored paths resolve relative to the manifest's parent, independently of the process working directory. Lexically different paths may name the same file and receive separate entries.

These are observations under OS process authority, not authenticated manifests, snapshots or filesystem confinement. Symlinks follow normal OS resolution; .. can reach outside the manifest directory. Moving a manifest changes its relative root. A file can change while read, producing a digest of the bytes observed; metadata checks do not pin later path operations. Use trusted manifests and stable inputs where consistency matters. Permission checks run as the embedding process. External supervision is required for hard time/memory limits and races that substitute blocking special files after preflight.

Embed the application in Python

Select the tricu-filecheck Python package in a Nix Python environment via python3.withPackages, then:

from tricu_filecheck import FileChecker, FileProblem

with FileChecker() as checker:
    for path in ["one.bin", "two.bin"]:
        try:
            digest = checker.hash_file(path, max_bytes=1024 * 1024)
            print(path, digest.bytes, digest.sha256)
        except FileProblem as error:
            print(path, error.problem.category, error.problem.operation)

API paths follow the caller's working directory; the manifest-relative convention belongs to the CLI. FileChecker is a sequential, synchronous session. Each call creates a Python handle scope; arguments, application, task results and traversal handles are released after decoding, while the worker remains rooted for the next call. Returned dataclasses contain only Python integers and strings. Ordinary IO failures raise FileProblem with a structured Problem. Unexpected task outcomes, cleanup diagnostics or result shapes raise ProtocolError; the native binding retains its own TricuError boundary failures.

Distribution and validation

Nix compiles worker.tri with the packaged stable compiler at build time. The Python wheel contains worker.arboricx and a generated SHA-256 pin. Startup checks the pin and the native bundle verifier before execution. Runtime needs the Python packages and Haskell shared-library closure. It reads neither tricu source nor compiler data and creates no compiler cache. Missing/corrupt worker data fails without source lookup or compilation fallback. Package integrity still depends on the distributor: a checksum stored in the same package is not a signature.

nix build .#tricu-filecheck-test --no-link
CHECK=$(nix path-info .#tricu-filecheck-test)
cat "$CHECK/report.json"

Acceptance runs the installed CLI from an unrelated read-only working directory, with an empty read-only home, no native-library override and no runtime tools on PATH. It checks the runtime closure excludes the compiler executable and build source, compares full digests/byte counts against independent Python hashing, and covers Unicode/newline paths, symlinks, same-length mutations, missing/unreadable files, non-files, budget boundaries, strict manifests, no-replace publication and missing/corrupt package data.

A persistent Python session performs eight warmups, 128 successful and 128 expected-error calls, then hashes a 1-MiB file. It verifies the same worker/context remain loaded and descriptor counts stay stable. Python file opens are prohibited during the repeated successful hash calls. Reports include peak RSS and elapsed time; a separate process guard enforces a 512-MiB sampled RSS limit and a 90-second wall limit. These are bounded acceptance observations, not universal resource guarantees. This application check is opt-in; Python ownership-scope controls run in the embedding library's basic gate.