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:
0: every entry succeeded or matched; recording published the manifest.1: verification found changed contents, with no file-operation errors.2: file IO, budget, manifest, package or embedding failure. Argparse usage errors also exit 2 and print usage to stderr.
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.