bathy-interpret 0.1.0-alpha.2

The pure interpretation layer for bathy: turns recorded probe bytes into structured, evidence-backed claims.
Documentation

bathy-interpret: the pure interpretation layer.

This crate turns the raw bytes a [bathy_types::ProbeCapture] recorded into zero or more [Interpretation]s -- structured claims about what service, product, and version a peer's response is evidence for. It is the reason findings in this project are explainable and replayable:

  • Explainable. Every [Interpretation] names the exact rule_id that produced it and the exact byte range of the response that justified it (matched_span). [explain] resolves any rule id this crate can produce back to human-readable documentation and its provenance. M5's fingerprint.explain tool is built directly on this.
  • Replayable. [interpret] is a pure function: no I/O, no clock, no randomness, no async runtime. Feed it the same bytes years later and it makes the same claim, because nothing except those bytes and this crate's own source code ever fed the decision. M4 Task 4's replay corpus depends on this holding exactly, with the network interface down; M7's fuzz target depends on it never panicking regardless.

Purity is enforced structurally, not just by convention

bathy-interpret depends on exactly two crates: bathy-types (for [bathy_types::ProbeCapture], [bathy_types::event::Observation], and [bathy_types::confidence::Confidence] -- the shapes this crate consumes and produces) and regex (for matching text-shaped protocol banners). This crate's own code never touches tokio, the filesystem, a clock, or a random-number generator -- cargo tree -p bathy-interpret --edges normal is asserted in CI to show only bathy-types, regex, and their own transitive dependencies (AC-4.10), and no matcher in rules.rs calls anything from either crate except regex::Regex itself and plain byte/string operations.

(Narrowed claim, M4 Task 3 review round 1: an earlier version of this paragraph said "no randomness anywhere in ... its dependency graph," which overstates what AC-4.10 actually checks. bathy-types itself depends on ulid, which depends on rand/getrandom -- getrandom is present in this crate's dependency tree; bathy_types::clock is the sanctioned, sole call site for it in this workspace, and this crate's own code never calls it. That inheritance is unavoidable and judged acceptable: it is linkable, not callable, from here, and removing it would not actually shrink the built artifact -- Cargo's feature unification means every other crate in the workspace that depends on bathy-types already pulls the same dependency in, so it is compiled into any binary that links this crate either way.)

This crate also sits below bathy-probe in this workspace's layer order (xtask's LAYERS), specifically so ProbeCapture fixtures can be built by hand in a test or fuzz target with no socket anywhere in the dependency graph at all.

No panics on the byte path (Global Constraint), and how it is scoped

The #![cfg_attr(not(test), deny(...))] above is the executable form of the overview's "No panics in parsing paths" constraint. That constraint said unwrap()/expect()/indexing-slice panics were "denied by lint" in this crate and in bathy-probe from M1, and no such lint existed anywhere in the tree until the M7 verification round. It was an aspiration written in the indicative mood for six milestones, in the one crate whose entire input is bytes a scanned peer chose.

It found real hits here, and they are not stylistic: utf8_lines sliced bytes[start..i], u16_at indexed a two-byte window it had just get-checked, mysql_handshake_v10 and dns_bind_version sliced with offsets built by unchecked +, tls_server_hello indexed a header, and every text-shaped rule computed its matched_span as a bare `line_start

  • m.start(). That last expression is the one the from_utf8_lossydefect corrupted and the one seven span mutants attacked across three review rounds; it now lives in exactly one place,rules::absolute_span, and it is checked. See rules.rs`'s own "Byte safety" note.

How test code is exempt. cfg_attr(not(test), ...), not a bare deny. Under cargo clippy --all-targets the library is compiled twice: once as the lib, where cfg(test) is off and the deny is live over every line of production code, and once as the unit-test harness, where cfg(test) is on and the deny is absent -- so #[cfg(test)] mod tests keeps unwrap(). tests/replay.rs, tests/span_edge_corpus.rs and benches/interpret.rs are separate crates that never see the attribute.

A crate-level #![allow] of any of these would reproduce the exact defect being closed, so the two exceptions in this crate (Specificity::confidence and rules::static_regex, both over compile-time-constant inputs) are site-level, carry a reason, and are each backed by a test that fails if the reasoning stops being true. cargo run -p xtask -- check-panics enforces that shape, and additionally holds the overview's constraint text to the set of crates that actually carry the attribute.

Never guess (AC-4.13)

[interpret] returns an empty vector when nothing in its rule set recognizes the input. A scanner that invents a service from bytes that do not structurally support the claim is worse than one that reports nothing -- see interpret::tests::unrecognized_bytes_yield_no_observation_rather_than_a_guess.