Files
alktype/CHANGELOG.md
T
glm-5.3-flash 9803d3b768 Pre-publish review #008: gate int_keys on canonical keys, restore no-prealloc array rule
Review #008 (docs/reviews/008-pre-publish-review.md) audits the two
post-#007 unreviewed commits (dea96f0 bench port, d4635d2 perf) before
the first crates.io publish of 0.3.0.

- F1: the int_keys integer dispatch accepted non-canonical mapping
  keys ("01", "+1" parse as u64 1) — the reader dispatched disc 1
  while the materializer, validation plan, and tunion rejected the
  same buffer. compile_int_keys now builds the table only when every
  key is canonical (v.to_string() == key); otherwise the string
  fallback applies (agreement restored, perf kept for canonical
  mappings). Tests: r8_non_canonical_mapping_key_disables_int_dispatch,
  r8_canonical_mapping_keys_keep_int_dispatch.
- F2: d4635d2 reintroduced Vec::with_capacity(count) on both array
  materializers. Bounded per array by MAX_ARRAY_ELEMENTS but nesting
  compounds: probe (counting allocator) measured ~477 MB simultaneous
  allocation from a ~1 KB schema + empty buffer (100-level stride-0
  chain, all legal under the caps). H1's layer-1 rule restored:
  Vec::new() + push. Bench unchanged (packet read 220µs vs 246µs
  baseline). Test: r8_deeply_nested_stride0_array_rejects_before_bulk_prealloc.
- N3a disposition (review #007's deferred item): BastField::synthetic
  (pub(crate), zero callers, #[allow(dead_code)]) deleted; the seven
  source() accessors are public API and stay (semver decision —
  removal needs an explicit ask); resolve_typeref_as_def's inline
  struct/union/enum arms probe-verified reachable (inline struct
  union variants are legal) — kept.
- CHANGELOG: 0.3.0 entry (compiled forms, breaking surface, hardening
  fixes, coverage). README: ReadPlan/ValidationPlan roles, union
  conventions, untrusted-schema bounds.

Verification: 569 tests green (491 lib + 17 + 34 + 15 + 12, + 2
ignored doctests), clippy -D warnings clean, cargo doc 0 warnings,
wasm32 build green, cargo publish --dry-run clean.
2026-09-07 10:58:49 +00:00

12 KiB
Raw Blame History

Changelog

All notable changes to this crate are documented here. The format is based on Keep a Changelog, and this crate adheres to Semantic Versioning.

0.3.0 - 2026-09-07

The compiled-forms release. The packed read path — the hot path for stream parsing — is driven by a compile-once ReadPlan instead of a per-call walk of the BAST typed tree; byte validation runs a compiled ValidationPlan; plans and offset maps fingerprint to a stable hash (ADR-011/ADR-012). Reads of SFTP-shaped packet streams went from ~189× hand-rolled Rust to ~74× (~2.4× faster), fixed-stride chunk reads from ~18× to ~11×, and SequentialReader::read_next_borrowed makes the per-field hot loop allocation-free.

Breaking changes

  • Bast* types are owned. BastDoc/BastStruct/BastField/… no longer borrow from the source serde_json::Value; all v0.2.0 lifetimes are gone. BastDoc::new parses the root eagerly; $refs resolve lazily.
  • OffsetMap::get / PackedLayout::get return &OffsetEntry (was Option<OffsetEntry> by value), backed by an O(log n) BTreeMap path→index (first-occurrence-wins for duplicate names).
  • SequentialReader::new takes the compiled plan; construct via AlkTypeEngine::sequential_reader() (packed mode only).
  • materialize_packed / materialize_aligned take the compiled plan / (&BastDoc, &OffsetMap) pair respectively.
  • Field-name-discriminator union wire convention (ADR-011 addendum): the builder lays out the union's declared fields (shared) first, then the variant's own fields. Variants must not re-declare the discriminator or any shared field, and the discriminator field must be the first entry in fields — all enforced at parse with clean Schema errors. Schemas relying on 0.2.0's variant-only layout are rejected (they produced reader↔builder-disagreeing bytes).
  • maxLength is string/bytes-only — rejected at parse on every other kind (it was silently unenforced there).
  • Aligned-mode Record fields reject offset-indirect (the materializer always walks the inline count-prefixed form — the annotated shape was never readable).
  • Schema input bounds (untrusted-schema hardening, AGENTS.md §3): array count ≤ 2^16 and count × stride ≤ 2^26 bytes; align ≤ 4096; maxLength ≤ 2^26; cyclic $ref graphs and >128-deep nesting are rejected by every public walker (OffsetMap::compute, LayoutBuilder::new, materialize_aligned included), not just the engine.

Additions

  • ReadPlan (ADR-011) — the compiled packed-read plan, re-exported with CompositePlan/FieldPlan/ReadKind/DiscriminatorPlan. ReadPlan::compile is untrusted-input-safe standalone (depth cap + cycle set). fixed_size() exposes the compile-time-known byte size for fixed structs.
  • ValidationPlan (ADR-012 §3) — the compiled validate_bytes walker, with ValidNode/ValidVariant sub-types.
  • fingerprint() on ReadPlan/OffsetMap/ValidationPlan + Hash/Eq derives on the plan types (ADR-012 §1/§4) — plan identity for cache-keying across processes.
  • OffsetMap LeafMeta — each entry records whether it is fixed/length-prefixed/offset-indirect so read_field/write_field dispatch without re-walking the schema; OffsetEntry type re-exported.
  • SequentialReader::read_next_borrowed — zero-allocation variant of read_next (field name borrowed from the plan).
  • AlkTypeEngine::validate_bytes now runs the compiled ValidationPlan (was an interpretive BAST walk in 0.2.0).

Fixes (post-release-commit hardening — reviews #006, #007, #008)

All found and fixed before the first crates.io publish of 0.3.0, so no published version ever exhibited them.

  • Untrusted-input crashes removed. A huge declared array count OOM-aborted the process (Vec::with_capacity(count) before reading a byte) — now compile-capped and walked with push-only growth. Cyclic $ref graphs stack-overflowed the three standalone layout walkers — now guarded by a shared reference-graph check. Deeply nested stride-0 arrays briefly allowed ~477 MB of simultaneous allocation from a ~1 KB schema — restored to incremental growth.
  • Cross-consumer divergences closed. Builder, reader, materializer, tunion, and the validation plan now agree on field-disc union layout (shared-then-variant), on the discriminator field's position (must be first), and on union mapping-key matching (numeric fast-path dispatch only for canonical keys like "2"; "01"/"+1" fall back to the string comparison all consumers share). The legacy BAST walker's field-disc union arm walks shared fields before the variant (it previously materialized variant fields from shared fields' bytes).
  • Silently-corrupt layouts rejected. Aligned record fields with maxLength/offset-indirect; non-final inline length-prefixed fields (records included — the ADR-006 check now sees them); aligned-mode maxLength/offset-indirect on records; unions in aligned mode (pre-existing, now tested).
  • Coverage: 90.67% lines / 86.32% functions at review #007's audit, 91.66% after its fixes; every uncovered region outside test modules read and classified in-tree (docs/reviews/007).

Non-breaking improvements

  • Engine compile is one-shot and allocation-tidy; plans are Send + Sync (statically asserted) and fingerprintable.
  • Zero-progress array-element guard on all three array walkers (a zero-size element makes the declared count unbounded on the wire).
  • WASM-clean unchanged: two dependencies (jsonschema default-features off, serde_json with preserve_order), no async, no unsafe, no feature flags.
  • Benches (benches/wire_vs_bast.rs): read/write chunk streams, an SFTP-shaped union packet stream, and validate_bytes per buffer — the numbers quoted above and in ADR-007/ADR-011.

0.2.0 - 2026-08-17

A breaking release that replaces the v0.1.0 AlkType:* custom-keyword JSON Schema format with BAST (Binary Abstract Syntax Tree) — a JSON document that describes binary layouts using a kind-based vocabulary with $defs/$ref for composition. BAST is itself a valid JSON Schema instance (it has a meta-schema), making it self-validating, editor-friendly, and trivially consumable from any language with a JSON parser. The engine works the same way as before: compile a document once into an AlkTypeEngine, then read/write fields at computed offsets and validate bytes/JSON. The pivot was made now because v0.1.0 has no real consumers (≈15 crates.io downloads, mostly bots/scanners), so the custom-keyword wart could be removed cleanly.

Breaking changes

  • Schema format. The v0.1.0 AlkType:* custom-keyword JSON Schema format ({ "AlkType:Struct": true, "fields": [...] }) is removed. Schemas are now BAST documents: { "$defs": { "<TypeName>": { "kind": "struct", "fields": [...] } } }. The kind-based vocabulary covers 18 binary kinds (integers, floats, bytes, string, struct, union, enum, array, etc.).
  • AlkTypeEngine::compile signature. Now takes (bast_doc: &Value, root_name: &str, mode: LayoutMode, json_schema: Option<&Value>). The root type name is a required parameter — it selects which $defs entry is the top-level type (previously the root was implicit from the single top-level schema object).
  • Builder API output. Definitions/Schema/Discriminator now produce BAST JSON via Definitions::build_doc(name, schema). The builder method names are unchanged; only the emitted JSON shape changed. Schema::struct_() produces a BAST struct; Schema::object() produces a standard JSON Schema (for the validate_json path).
  • Validation split. v0.1.0 used a single jsonschema validator with 19 custom AlkType:* keywords for both bytes and JSON validation. 0.2.0 splits this into two independent paths:
    • validate_bytes uses a new BAST-native validator (bast_validation) — a recursive walker over the BAST type tree.
    • validate_json / is_valid_json use a standard jsonschema::Validator built from a consumer-provided JSON Schema (passed to compile as the json_schema parameter). No custom keywords; BAST is not involved — BAST describes bytes, not JSON shape. Both paths return AlkTypeError::Validation with a uniform jsonschema::ValidationError<'static> payload.
  • Removed. The v0.1.0 custom-keyword accessor layer (AlkTypeKind::FromStr, parse_*, resolve_ref*, DiscriminatorKind) is removed. The BAST parser (bast module) exposes a cleaner typed surface (BastDoc/BastDef/BastStruct/ BastField/BastType/etc.) that borrows from the source serde_json::Value without cloning field data.
  • Public module surface. New public modules: bast, bast_meta, bast_validation, builder, materialize. The schema module is retained but now holds only Endian/AlkTypeKind/VariableEncoding (the binary-kind vocabulary); the v0.1.0 custom-keyword machinery is gone.

Additions

  • BAST meta-schema. Embedded in the crate as BAST_META_SCHEMA (re-exported from the crate root) and published at https://alk.dev/bast/v1/schema. BAST documents are validated against it at compile time (AlkTypeEngine::compile calls validate_bast_doc before parsing).
  • materialize module. Materializes a serde_json::Value tree from a binary buffer by walking the BAST typed tree. Used by AlkTypeEngine::validate_bytes (ADR-010).
  • Builder for JSON Schemas. Schema::object() produces a standard JSON Schema object (for the validate_json path), complementing Schema::struct_() which produces a BAST struct (for the bytes path). One builder, two output shapes — the method name selects which.

Bug fixes vs v0.1.0

  • Enum index bounds are now checked. The v0.1.0 validator had a dead constraint: enum variant indices were never bounds-checked against values.len(). The BAST-native validator enforces it (validate_enum checks idx < values.len()).
  • Offset-indirect, field-level endian, and aligned materialization bugs found during review #003 are fixed.

Non-breaking improvements

  • $ref is restricted to #/$defs/<name> — one hash lookup, no normalize_refs pass (the v0.1.0 engine needed one).
  • Schemas remain untrusted input: every engine path that walks a BAST document returns Err on a malformed document, never panic!/ unreachable!. Overflow-safe arithmetic (checked_add, usize::try_from) on all offset/count casts.
  • Still two dependencies (jsonschema with default-features = false, serde_json with preserve_order), no async, no unsafe, no platform deps, no feature flags. Compiles to wasm32-unknown-unknown.

Upgrade notes

There is no migration path from v0.1.0 AlkType:* schemas — the format is incompatible. Rewrite schemas as BAST documents (the builder API produces them; see the README usage example) and update compile calls to pass the root type name and the optional JSON Schema. The read/write/validate API surface (read_field, write_field, sequential_reader, validate_bytes, validate_json, is_valid_json) is unchanged.

0.1.0 - 2025-11-10

Initial crates.io release. Custom-keyword JSON Schema format (AlkType:*), single jsonschema validator for both bytes and JSON, AlkTypeEngine with packed/aligned layout modes, builder API producing serde_json::Value.