antares-format — reader and writer
antares-format is the canonical implementation of the .ant container: a streaming writer (records in, .ant bytes out) and a streaming reader that verifies integrity as it goes. It is the writer that produced the conformance golden files, and it is the implementation the format’s reference bindings are checked against.
cargo add ant-types antares-formatAPI reference: docs.rs/antares-format. Record payloads are the serde JSON of the ant-types record types.
The Antares engine that fills and reasons over these files is a separate, private system — this crate depends on none of it, and neither do you: reading, writing, and verifying .ant files needs only what is on this page.
The container in one paragraph
Section titled “The container in one paragraph”One zstd-compressed stream of NDJSON records: a manifest first, data records in the middle, and a trailer last carrying per-kind counts and a SHA-256 over every preceding uncompressed line — so truncation and tampering are detectable in a single pass, without a side channel. Compatibility is same-major: any minor at the same major is readable (minor bumps are additive-only; unknown record kinds are skipped, and AntReader::minor_ahead reports when a file is newer than the reader). A different major is refused explicitly. The full rules live in the specification.
Write a file, read it back, validate it
Section titled “Write a file, read it back, validate it”A complete program: write a small .ant with a vertex (including an exact decimal property) and an evidence record, read it back, and confirm the reader verified the trailer hash and counts.
// [dependencies] ant-types = "0.1", antares-format = "0.1"use std::collections::BTreeMap;
use ant_types::{ Decimal, Evidence, ProjectId, PropertyValue, TenantId, TypeName, Vertex, VertexId,};use antares_format::{AntError, AntReader, AntRecord, AntWriter, Manifest, FORMAT_VERSION};
fn main() -> Result<(), AntError> { // --- write a small .ant file --- let manifest = Manifest { format: "antares".into(), version: FORMAT_VERSION.into(), tenant_id: 1, project_id: 1, selection: None, created_at: None, producer: Some("quickstart/0.1".into()), };
// Any `std::io::Write` works as the sink; level 0 = zstd default. let mut writer = AntWriter::new(Vec::new(), manifest, 0)?;
let mut properties = BTreeMap::new(); properties.insert("stage".to_string(), PropertyValue::Text("signed".into())); // Decimal is exact (i128 unscaled digits + scale) — this value does // not fit an IEEE double, and it round-trips digit for digit. properties.insert( "exact_amount".to_string(), PropertyValue::Decimal(Decimal::parse("12345678901234567.89").unwrap()), );
writer.write(AntRecord::Vertex { data: Vertex { id: VertexId("deal_1".into()), name: "Example Deal".into(), label: TypeName("Demo.Deal".into()), properties, }, })?; writer.write(AntRecord::Evidence { data: Evidence::quick( "ev1", TenantId(1), ProjectId(1), "note", "n1", "The deal was signed on the call.", ), })?;
let bytes = writer.finish()?; // appends the trailer: counts + sha256 std::fs::write("hello.ant", &bytes).expect("write hello.ant");
// --- read it back, verifying as we go --- let file = std::fs::File::open("hello.ant").expect("open hello.ant"); let mut reader = AntReader::new(file)?; while let Some(record) = reader.next_record()? { match record { AntRecord::Vertex { data } => { println!("vertex {} ({})", data.id.0, data.label.0); if let Some(PropertyValue::Decimal(d)) = data.properties.get("exact_amount") { println!(" exact_amount = {d} (exact decimal)"); } } AntRecord::Evidence { data } => println!("evidence {}: {}", data.id.0, data.content), _ => {} } }
// `verified` flips true only after the trailer's sha256 and per-kind // counts matched what the reader saw. assert!(reader.verified); assert!(!reader.minor_ahead); println!( "verified .ant, format version {} (this build supports {}.x)", reader.manifest.version, antares_format::SUPPORTED_FORMAT_VERSION ); Ok(())}Output:
vertex deal_1 (Demo.Deal) exact_amount = 12345678901234567.89 (exact decimal)evidence ev1: The deal was signed on the call.verified .ant, format version 0.3 (this build supports 0.3.x)The file it wrote is a real, spec-complete .ant — the CLI validates it:
$ openantares validate hello.anthello.ant: OK version=0.3 records=2The API surface
Section titled “The API surface”Writer — AntWriter:
AntWriter::new(out, manifest, level)writes the manifest line into anystd::io::Writesink (levelis the zstd compression level,0= default).write(AntRecord)appends one record; the writer keeps the running per-kindCountsand the running hash.finish()appends the trailer (counts + SHA-256) and flushes the zstd frame, returning the sink.
Reader — AntReader:
AntReader::new(input)reads and checks the manifest from anystd::io::Readsource: zstd framing,format == "antares", and the version policy — a different major is refused at read time, with an error naming both versions.next_record()streams records one at a time; unknown kinds are skipped (but still hashed).Ok(None)is returned only after the trailer verified.manifest(public field) is available immediately after construction;versionis the parsed file version.verified— true once the trailer’s SHA-256 and per-kind counts matched what the reader saw. Truncation, tampering, data after the trailer, and count mismatches all surface as errors instead.minor_ahead— true when the file’s minor version is ahead of this build: the file is readable, but it may contain record kinds or value encodings this reader does not know. A caller that needs completeness can refuse; one that does not can proceed.
Version constants — FORMAT_VERSION (the MAJOR.MINOR written into new manifests) and SUPPORTED_FORMAT_VERSION (the format this build reads and writes — see Versioning for why this is not the crate version), plus FormatVersion with the parse and compatibility rules.
Errors — everything is AntError, one enum for framing, version, JSON, and integrity failures.

