//! Artifact Format — draft-fossil-artifact-format-00.
//!
//! An artifact is an immutable byte sequence named by the lowercase-hex hash
//! of its content (Section 3). Structural artifacts are line-oriented "cards".
//! This module owns the canonical vocabulary for the whole suite: naming, the
//! card grammar, the structural types, and the two checksums (Z, R).
//!
//! Scope note: zit targets the version-control substrate — the check-in
//! (manifest), cluster, and tag/control artifact types. The collaboration
//! types (wiki, ticket, forum, attachment, technote) are carried opaquely.
const std = @import("std");
/// Card-type letters. Only the version-control cards are interpreted; others
/// are recognised syntactically and otherwise carried opaquely.
pub const CardType = enum(u8) {
B = 'B', // baseline (delta manifest)
C = 'C', // comment
D = 'D', // date (mtime)
F = 'F', // file
M = 'M', // cluster member
N = 'N', // mimetype
P = 'P', // parent(s)
Q = 'Q', // cherrypick
R = 'R', // aggregate content checksum (MD5)
T = 'T', // tag
U = 'U', // user
Z = 'Z', // artifact self-checksum (MD5), always last
};
/// Structural artifact types in scope for the version-control substrate.
pub const ArtifactType = enum {
manifest, // a check-in
cluster, // a set of artifact hashes (sync housekeeping)
control, // a tag/branch mutation
other, // recognised but carried opaquely (wiki/ticket/forum/...)
};
/// Hash algorithm that names an artifact (Section 3, Section 7 hash policy).
/// The canonical (writer) form is always lowercase hex.
pub const HashAlgo = enum { sha1, sha3_256 };
/// Compute the artifact name: the lowercase-hex hash of `content`.
/// Writes into `out` and returns the populated slice (40 hex for SHA-1,
/// 64 hex for SHA3-256).
pub fn artifactName(algo: HashAlgo, content: []const u8, out: *[64]u8) []u8 {
switch (algo) {
.sha1 => {
var digest: [20]u8 = undefined;
std.crypto.hash.Sha1.hash(content, &digest, .{});
const hex = std.fmt.bytesToHex(digest, .lower);
@memcpy(out[0..hex.len], &hex);
return out[0..hex.len];
},
.sha3_256 => {
var digest: [32]u8 = undefined;
std.crypto.hash.sha3.Sha3_256.hash(content, &digest, .{});
const hex = std.fmt.bytesToHex(digest, .lower);
@memcpy(out[0..hex.len], &hex);
return out[0..hex.len];
},
}
}
/// Length-to-algorithm dispatch (§3.2-m1). The two name lengths are disjoint,
/// so length alone fully determines the algorithm: 40 hex digits is SHA-1,
/// 64 is SHA3-256, any other length names nothing.
pub fn nameAlgo(len: usize) ?HashAlgo {
return switch (len) {
40 => .sha1,
64 => .sha3_256,
else => null,
};
}
/// ABNF `lhexdig` (§3.5): a lowercase hex digit, 0-9 or a-f. Governs the
/// canonical (writer/parse) form only.
fn isHexLower(c: u8) bool {
return (c >= '0' and c <= '9') or (c >= 'a' and c <= 'f');
}
/// ABNF `hexdig-ci` (§3.5): 0-9, A-F, or a-f. Used only on the prefix-lookup
/// path (§3.6), which canonicalizes case; the parse path never uses this.
fn isHexCi(c: u8) bool {
return (c >= '0' and c <= '9') or (c >= 'a' and c <= 'f') or (c >= 'A' and c <= 'F');
}
/// Canonical-form validation (§3.2, §3.3, §3.5 `artifact-name`). A token is
/// valid iff it is exactly 40 or 64 lowercase hex digits. Length is checked
/// before character class (§3.2-r1: length gate precedes char-class check).
/// Uppercase digits are rejected outright, never case-normalized (§3.3-m2).
pub fn validateName(token: []const u8) bool {
if (nameAlgo(token.len) == null) return false;
std.debug.assert(token.len == 40 or token.len == 64);
var i: usize = 0;
while (i < token.len) : (i += 1) {
if (!isHexLower(token[i])) return false;
}
return true;
}
/// Verify `name` against `content` (§3.4). Dispatch on length picks the
/// digest to recompute; the compare is exact byte-for-byte lowercase hex.
/// Wrong length, non-hex characters, and a digest mismatch collapse into the
/// same `false` (§3.4-m1) — nothing here distinguishes them for the caller.
/// An uppercase name never verifies either, since the compare never
/// case-folds (§3.4-r1).
pub fn verify(name: []const u8, content: []const u8) bool {
const algo = nameAlgo(name.len) orelse return false;
var buf: [64]u8 = undefined;
const computed = artifactName(algo, content, &buf);
std.debug.assert(computed.len == name.len);
return std.mem.eql(u8, name, computed);
}
/// Prefix-resolution outcome (§3.6, informative — not part of the wire
/// format). `one` carries the matching index into the caller's `names`.
pub const Resolution = union(enum) { none, one: usize, ambiguous };
/// Resolve a hex name or prefix against a candidate universe (§3.6).
/// `prefix` must be 4-64 hex characters of any case; it is lowercased before
/// matching (the lookup path canonicalizes case, unlike the parse path —
/// see `validateName`). One query walk covers both name lengths, so a
/// cross-algorithm collision (one 40-char match, one 64-char match) reports
/// `.ambiguous` exactly like a same-algorithm collision (§3.6-r1).
pub fn resolvePrefix(prefix: []const u8, names: []const []const u8) error{InvalidPrefix}!Resolution {
if (prefix.len < 4 or prefix.len > 64) return error.InvalidPrefix;
std.debug.assert(prefix.len >= 4 and prefix.len <= 64);
var lowered: [64]u8 = undefined;
var i: usize = 0;
while (i < prefix.len) : (i += 1) {
const c = prefix[i];
if (!isHexCi(c)) return error.InvalidPrefix;
lowered[i] = if (c >= 'A' and c <= 'F') c + ('a' - 'A') else c;
}
const query = lowered[0..prefix.len];
var first: ?usize = null;
var matches: usize = 0;
var n: usize = 0;
while (n < names.len) : (n += 1) {
std.debug.assert(matches <= names.len);
if (std.mem.startsWith(u8, names[n], query)) {
matches += 1;
if (first == null) first = n;
if (matches > 1) return .ambiguous;
}
}
if (first) |idx| {
std.debug.assert(idx < names.len);
return .{ .one = idx };
}
return .none;
}
/// The Z-card value: lowercase-hex MD5 of all stripped-content bytes that
/// precede the `Z ` line (Section 6, Z card).
pub fn selfChecksum(stripped_prefix: []const u8, out: *[32]u8) []u8 {
var digest: [16]u8 = undefined;
std.crypto.hash.Md5.hash(stripped_prefix, &digest, .{});
const hex = std.fmt.bytesToHex(digest, .lower);
@memcpy(out, &hex);
return out;
}
// ---------------------------------------------------------------------------
// §4.1.1 Signature-Wrapper Removal
// ---------------------------------------------------------------------------
/// PGP/SSH clearsign open-armor literals (§4.1.1-r1): stripping fires only
/// when one of these matches the first 34 bytes of the input, at offset 0.
const pgp_open_armor = "-----BEGIN PGP SIGNED MESSAGE-----";
const ssh_open_armor = "-----BEGIN SSH SIGNED MESSAGE-----";
comptime {
std.debug.assert(pgp_open_armor.len == 34);
std.debug.assert(ssh_open_armor.len == 34);
}
/// Trailing-armor compare literals (§4.1.1-p3). The real armor line is 30
/// bytes (LF + a 29-byte dashed literal), but only 29 are ever compared:
/// LF plus the first 28 armor bytes, one dash short of the closing run —
/// the 30th byte, whatever it is, is never read.
const pgp_close_armor = "\n-----BEGIN PGP SIGNATURE----";
const ssh_close_armor = "\n-----BEGIN SSH SIGNATURE----";
comptime {
std.debug.assert(pgp_close_armor.len == 29);
std.debug.assert(ssh_close_armor.len == 29);
}
/// Forward scan for the first blank-line separator (§4.1.1-p2): LF LF or
/// LF CR LF — the sole CR tolerance anywhere in the parse pipeline. Returns
/// the offset just past the blank line (start of body), or null if none is
/// found before end of input.
fn firstBlankLineEnd(bytes: []const u8) ?usize {
var i: usize = 0;
while (i < bytes.len) : (i += 1) {
if (bytes[i] != '\n') continue;
if (i + 1 < bytes.len and bytes[i + 1] == '\n') return i + 2;
if (i + 2 < bytes.len and bytes[i + 1] == '\r' and bytes[i + 2] == '\n') return i + 3;
}
return null;
}
/// Backward scan for the trailing signature armor (§4.1.1-p3). "First
/// occurrence" read right-to-left makes the rightmost match win. Returns
/// the truncation length (keeping the matched LF as the final byte), or
/// `body.len` when no armor is found.
fn trailingArmorEnd(body: []const u8) usize {
if (body.len < pgp_close_armor.len) return body.len;
var p: usize = body.len - pgp_close_armor.len;
while (true) : (p -= 1) {
std.debug.assert(p + pgp_close_armor.len <= body.len);
const slice = body[p .. p + pgp_close_armor.len];
if (std.mem.eql(u8, slice, pgp_close_armor) or std.mem.eql(u8, slice, ssh_close_armor)) {
return p + 1; // truncate here, keeping the matched LF
}
if (p == 0) break;
}
return body.len;
}
/// §4.1.1 signature-wrapper removal. Total: never fails. Returns `bytes`
/// unchanged unless the first 34 bytes are a recognized open armor AND a
/// blank line follows somewhere in the input — either condition failing
/// leaves the input wholly unstripped (§4.1.1-r1, -m1).
pub fn stripSignatureWrapper(bytes: []const u8) []const u8 {
const is_pgp = bytes.len >= pgp_open_armor.len and
std.mem.eql(u8, bytes[0..pgp_open_armor.len], pgp_open_armor);
const is_ssh = bytes.len >= ssh_open_armor.len and
std.mem.eql(u8, bytes[0..ssh_open_armor.len], ssh_open_armor);
if (!is_pgp and !is_ssh) return bytes;
const body_start = firstBlankLineEnd(bytes) orelse return bytes;
std.debug.assert(body_start <= bytes.len);
const body = bytes[body_start..];
const body_end = trailingArmorEnd(body);
std.debug.assert(body_end <= body.len);
const result = body[0..body_end];
std.debug.assert(result.len <= bytes.len);
return result;
}
// ---------------------------------------------------------------------------
// §4.1 Artifact Framing
// ---------------------------------------------------------------------------
/// §6.2 Z-card verification outcome (Table 6).
pub const ZVerdict = enum { absent, match, mismatch };
/// §6.2 Z self-checksum, evaluated positionally on the final 35 bytes of
/// `content` (already wrapper-stripped): the marker sits at n-35, the
/// digest at n-33..n-1. Never reads `content[n-1]`, the artifact's
/// terminating byte — that byte belongs to the §4.1 raw-LF framing rule.
pub fn verifyZCard(content: []const u8) ZVerdict {
const n = content.len;
if (n < 35) return .absent;
if (content[n - 35] != 'Z' or content[n - 34] != ' ') return .absent;
const stored = content[n - 33 .. n - 1];
std.debug.assert(stored.len == 32);
var digest_buf: [32]u8 = undefined;
const computed = selfChecksum(content[0 .. n - 35], &digest_buf);
std.debug.assert(computed.len == 32);
return if (std.mem.eql(u8, stored, computed)) .match else .mismatch;
}
/// §4.1 framing gates, evaluated in RFC order before any card is
/// interpreted. Returns the wrapper-stripped, gate-passed content, or null
/// if the input fails any gate — every rejection reason collapses to null,
/// as the RFC only ever says "reject" (the §4.1.2 fast well-formedness
/// probe reads this way too).
///
/// The final §4.1 rule — unconsumed bytes after the card loop MUST cause
/// rejection — is not enforced here: it is defined relative to the card
/// loop, which this module does not yet implement. It lands with the §4.2
/// card-loop parser, which calls `frame` as its entry seam.
pub fn frame(bytes: []const u8) ?[]const u8 {
// Gate 1: raw bytes, non-empty, final byte LF — checked before strip.
if (bytes.len == 0 or bytes[bytes.len - 1] != '\n') return null;
// Gate 2: strip signature wrapper, if present.
const stripped = stripSignatureWrapper(bytes);
if (stripped.len > 0) std.debug.assert(stripped[stripped.len - 1] == '\n');
// Gate 3: strict line-1 (length, byte0 A-Z, byte1 SP).
if (stripped.len < 10) return null;
if (stripped[0] < 'A' or stripped[0] > 'Z') return null;
if (stripped[1] != ' ') return null;
// Gate 4: Z self-checksum — a mismatch rejects; absent or matching pass.
if (verifyZCard(stripped) == .mismatch) return null;
return stripped;
}
// ---------------------------------------------------------------------------
// §4.2 Line and Token Grammar / §4.2.2 Closed Card Alphabet
// ---------------------------------------------------------------------------
/// §4.2.2 closed alphabet: true for exactly the 21 defined card letters.
/// A-Z minus the five in-range gaps (O S V X Y) — closed, no
/// forward-compatibility path for an unknown letter (RFC lines 623-627).
pub fn isCardLetter(b: u8) bool {
return switch (b) {
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'T', 'U', 'W', 'Z' => true,
else => false,
};
}
comptime {
var count: usize = 0;
var b: usize = 0;
while (b < 256) : (b += 1) {
if (isCardLetter(@intCast(b))) count += 1;
}
std.debug.assert(count == 21);
std.debug.assert(!isCardLetter('O'));
std.debug.assert(!isCardLetter('S'));
std.debug.assert(!isCardLetter('V'));
std.debug.assert(!isCardLetter('X'));
std.debug.assert(!isCardLetter('Y'));
std.debug.assert(!isTokenByte(' '));
std.debug.assert(!isTokenByte('\n'));
std.debug.assert(isTokenByte(0x00));
std.debug.assert(isTokenByte(0xFF));
}
/// §4.2 tchar: any octet except SP (0x20) and LF (0x0A).
pub fn isTokenByte(b: u8) bool {
return b != ' ' and b != '\n';
}
/// Splits a card's raw argument bytes on SP only (§4.2-m2 — tab is never a
/// delimiter). `rest == null` yields zero tokens (argument-less card);
/// `rest.?.len == 0` yields exactly one empty token (§4.7.17's "empty
/// token" case) before going null.
pub const TokenIterator = struct {
rest: ?[]const u8,
pub fn next(self: *TokenIterator) ?[]const u8 {
const s = self.rest orelse return null;
var i: usize = 0;
while (i < s.len) : (i += 1) {
if (s[i] == ' ') {
self.rest = s[i + 1 ..];
return s[0..i];
}
}
self.rest = null;
return s;
}
};
/// One tokenized card (§4.2 `card`). Borrows from the input; no allocation.
pub const CardLine = struct {
letter: u8,
/// null => argument-less card (`X\n`), 0 tokens.
/// slice => SP form (`X ...\n`); raw arg bytes with the LF and the one
/// leading SP removed. May be "" (one empty token).
args: ?[]const u8,
pub fn tokens(self: CardLine) TokenIterator {
return .{ .rest = self.args };
}
};
/// Tokenize one card, given its full byte extent including the terminating
/// LF (the card loop supplies the extent; W's byte-counted body is that
/// loop's concern, not this function's — §4.7.18). Returns null iff the
/// letter is outside the closed 21-letter alphabet (§4.2.2-r1,-r2) or the
/// byte after the letter is neither SP nor LF (§4.2-m1).
pub fn tokenizeCardLine(line: []const u8) ?CardLine {
std.debug.assert(line.len >= 1);
std.debug.assert(line[line.len - 1] == '\n');
// Non-W invariant: no embedded LF before the terminator. The W card's
// byte-counted body is the sole exception (§4.7.18), realized by the
// card loop, not here.
var i: usize = 0;
while (i < line.len - 1) : (i += 1) {
std.debug.assert(line[i] != '\n');
}
if (line.len < 2) return null;
if (!isCardLetter(line[0])) return null;
const result: CardLine = if (line[1] == '\n')
.{ .letter = line[0], .args = null }
else if (line[1] == ' ')
.{ .letter = line[0], .args = line[2 .. line.len - 1] }
else
return null;
std.debug.assert(isCardLetter(result.letter));
return result;
}
// ---------------------------------------------------------------------------
// §4.4 Card Ordering
// ---------------------------------------------------------------------------
comptime {
// Z is the last defined letter (§4.4 "Nothing follows Z", lines 739-741):
// every card letter sorts at or below it.
var b: usize = 0;
while (b < 256) : (b += 1) {
if (isCardLetter(@intCast(b))) std.debug.assert(b <= 'Z');
}
// Premises the N-after-P exception rests on (lines 714-717, §A.1 2499-2500).
std.debug.assert('N' < 'P');
std.debug.assert('E' < 'P');
}
/// §4.4 card-type ordering, fed one accepted card letter at a time in
/// document order. Holds the previous letter and whether a valid E card has
/// been presented (the N-after-P technote exception, lines 714-717).
pub const OrderState = struct {
prev: ?u8 = null,
e_seen: bool = false,
/// Admit `letter` as the next card. Returns false iff it is out of
/// order and not the N-after-P exception; the caller collapses false
/// to reject, matching the module's "every rejection reason collapses
/// to null/false" posture (§4.1). Pure predicate: no error set.
pub fn admit(self: *OrderState, letter: u8) bool {
std.debug.assert(isCardLetter(letter));
if (self.prev) |p| std.debug.assert(isCardLetter(p));
const ok = if (self.prev == null)
true
else if (letter >= self.prev.?)
true
else
letter == 'N' and self.prev.? == 'P' and self.e_seen;
if (!ok) return false;
if (letter == 'E') self.e_seen = true;
self.prev = letter;
std.debug.assert(self.prev.? == letter);
std.debug.assert(!(letter == 'E') or self.e_seen);
return true;
}
};
// ---------------------------------------------------------------------------
// §4.3 Character-Escape Encoding
// ---------------------------------------------------------------------------
/// §4.3 Table 1: the decoded octet for a recognized escape successor, or
/// null for an unrecognized one. Exactly eight pairs are defined; every
/// other successor byte falls through to `else`.
fn escapeOctet(c: u8) ?u8 {
return switch (c) {
'n' => 0x0A, // LF
's' => 0x20, // SP
't' => 0x09, // TAB
'r' => 0x0D, // CR
'v' => 0x0B, // VT
'f' => 0x0C, // FF
'0' => 0x00, // NUL
'\\' => 0x5C, // backslash
else => null,
};
}
comptime {
var count: usize = 0;
var b: usize = 0;
while (b < 256) : (b += 1) {
if (escapeOctet(@intCast(b)) != null) count += 1;
}
std.debug.assert(count == 8);
std.debug.assert(escapeOctet('n').? == 0x0A);
std.debug.assert(escapeOctet('s').? == 0x20);
std.debug.assert(escapeOctet('0').? == 0x00);
std.debug.assert(escapeOctet('\\').? == 0x5C);
}
/// §4.3 in-place escape decode of one free-text token. Writes the decoded
/// bytes into `out` and returns the used prefix. Never errors (§4.3: "No
/// error is raised"). Precondition: `out.len >= token.len` — decoded length
/// never exceeds input length (a recognized or unrecognized escape turns 2
/// input bytes into 1; an ordinary byte or a lone trailing backslash is 1:1,
/// §4.3-amb1).
pub fn decodeEscapes(token: []const u8, out: []u8) []u8 {
std.debug.assert(out.len >= token.len);
var i: usize = 0;
var w: usize = 0;
while (i < token.len) {
std.debug.assert(w <= i);
if (token[i] == '\\' and i + 1 < token.len) {
out[w] = escapeOctet(token[i + 1]) orelse token[i + 1];
i += 2;
} else {
// Ordinary byte, or a backslash with no successor (§4.3-amb1):
// pass through literally.
out[w] = token[i];
i += 1;
}
w += 1;
}
std.debug.assert(w <= token.len);
return out[0..w];
}
// ---------------------------------------------------------------------------
// Manifest parsing (Sections 4, 5) — TODO
// ---------------------------------------------------------------------------
pub const Manifest = struct {
// Populated by parse(): cards, type, name. Skeleton for now.
kind: ArtifactType = .other,
};
pub const ParseError = error{ SyntaxError, NotImplemented };
/// Parse a structural artifact from its bytes. See Section 4 (card syntax),
/// Section 5 (types + per-type validity). Not yet implemented.
pub fn parse(allocator: std.mem.Allocator, bytes: []const u8) ParseError!Manifest {
_ = allocator;
_ = bytes;
return ParseError.NotImplemented;
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
test "artifact §3.1: name is SHA3-256 of exact raw bytes" {
var out: [64]u8 = undefined;
// §A.3 / Table 8 (RFC line 2584): alpha.txt's content is "hello world\n",
// named by the SHA3-256 given as its F-card hash.
const name = artifactName(.sha3_256, "hello world\n", &out);
try std.testing.expectEqualStrings(
"a8009a7a528d87778c356da3a55d964719e818666a04e4f960c9e2439e35f138",
name,
);
}
// The §A.1.1 SHA-1 worked-example manifest: 127 raw bytes (27+22+2+35+6+35
// per the §A.2 Z-card offset table), literal backslash-s escapes retained
// exactly as printed in the RFC. Its stated name is
// a28c83647dfa805f05f3204a7e146eb1f0d90505 (RFC line 2426).
const a1_1_manifest = @embedFile("rfc_vectors/artifact_a1_1_sha1_manifest.txt");
const a1_1_name = "a28c83647dfa805f05f3204a7e146eb1f0d90505";
test "artifact §3.2: 40-hex name is SHA-1 over raw bytes" {
try std.testing.expectEqual(@as(usize, 127), a1_1_manifest.len);
var out: [64]u8 = undefined;
const name = artifactName(.sha1, a1_1_manifest, &out);
try std.testing.expectEqualStrings(a1_1_name, name);
}
test "artifact §3.2: name length selects algorithm" {
try std.testing.expectEqual(HashAlgo.sha1, nameAlgo(40).?);
try std.testing.expectEqual(HashAlgo.sha3_256, nameAlgo(64).?);
}
test "artifact §3.2: name of any other length is rejected" {
const bad_lens = [_]usize{ 0, 4, 39, 41, 63, 65, 128 };
for (bad_lens) |len| {
try std.testing.expect(nameAlgo(len) == null);
// Fabricate an all-'a' token of that length: rejection here must be
// on length alone, before any character-class check (§3.2-r1).
var buf: [128]u8 = undefined;
std.debug.assert(len <= buf.len);
@memset(buf[0..len], 'a');
try std.testing.expect(!validateName(buf[0..len]));
}
}
test "artifact §3.3: computed name is lowercase hex" {
var out: [64]u8 = undefined;
const sha1_name = artifactName(.sha1, a1_1_manifest, &out);
for (sha1_name) |c| try std.testing.expect(isHexLower(c));
var out2: [64]u8 = undefined;
const sha3_name = artifactName(.sha3_256, "hello world\n", &out2);
for (sha3_name) |c| try std.testing.expect(isHexLower(c));
}
test "artifact §3.3: validation rejects uppercase, does not normalize" {
// Same fixture as the rejection test above, restated to make the point
// explicit: a normalize-then-accept parser would flip this to true.
var upper: [40]u8 = undefined;
for (a1_1_name, 0..) |c, i| upper[i] = std.ascii.toUpper(c);
try std.testing.expect(!validateName(&upper));
// And the lowercase original still validates, proving the reject is
// case-specific rather than a blanket failure.
try std.testing.expect(validateName(a1_1_name));
}
test "artifact §3.4: verify dispatches on length" {
try std.testing.expect(verify(a1_1_name, a1_1_manifest));
try std.testing.expect(verify(
"a8009a7a528d87778c356da3a55d964719e818666a04e4f960c9e2439e35f138",
"hello world\n",
));
}
test "artifact §3.4: uppercase name never verifies" {
var upper: [40]u8 = undefined;
for (a1_1_name, 0..) |c, i| upper[i] = std.ascii.toUpper(c);
try std.testing.expect(!verify(&upper, a1_1_manifest));
}
test "artifact §3.4: one undifferentiated failure signal" {
// Wrong length.
try std.testing.expect(!verify("deadbeef10", a1_1_manifest));
// Non-hex character at a valid (40) length.
var non_hex: [40]u8 = undefined;
@memcpy(&non_hex, a1_1_name);
non_hex[0] = 'g';
try std.testing.expect(!verify(&non_hex, a1_1_manifest));
// Valid-form 40-hex name, wrong digest (mismatched content).
try std.testing.expect(!verify(a1_1_name, "hello world\n"));
}
test "artifact §3.5: ABNF canonical grammar accepts lhexdig only" {
var boundary: [40]u8 = undefined;
@memset(&boundary, '0');
try std.testing.expect(validateName(&boundary));
@memset(&boundary, '9');
try std.testing.expect(validateName(&boundary));
@memset(&boundary, 'a');
try std.testing.expect(validateName(&boundary));
@memset(&boundary, 'f');
try std.testing.expect(validateName(&boundary));
var has_g: [40]u8 = undefined;
@memset(&has_g, 'a');
has_g[39] = 'g';
try std.testing.expect(!validateName(&has_g));
var has_upper: [40]u8 = undefined;
@memset(&has_upper, 'a');
has_upper[0] = 'A';
try std.testing.expect(!validateName(&has_upper));
var sha3_all_f: [64]u8 = undefined;
@memset(&sha3_all_f, 'f');
try std.testing.expect(validateName(&sha3_all_f));
}
test "artifact §3.6: prefix resolves and lowercases uppercase input" {
const names = [_][]const u8{a1_1_name};
switch (try resolvePrefix("a28c", &names)) {
.one => |idx| try std.testing.expectEqual(@as(usize, 0), idx),
else => return error.TestUnexpectedResult,
}
switch (try resolvePrefix("A28C", &names)) {
.one => |idx| try std.testing.expectEqual(@as(usize, 0), idx),
else => return error.TestUnexpectedResult,
}
}
test "artifact §3.6: prefix length outside 4..64 is invalid" {
const names = [_][]const u8{a1_1_name};
try std.testing.expectError(error.InvalidPrefix, resolvePrefix("a28", &names));
var too_long: [65]u8 = undefined;
@memset(&too_long, 'a');
try std.testing.expectError(error.InvalidPrefix, resolvePrefix(&too_long, &names));
try std.testing.expectError(error.InvalidPrefix, resolvePrefix("zzzz", &names));
}
test "artifact §3.6: ambiguous prefix, incl. cross-algorithm collision" {
// Cross-algorithm: one SHA-1-named, one SHA3-named, sharing "a28c".
const cross = [_][]const u8{
a1_1_name,
"a28c000000000000000000000000000000000000000000000000000000000000",
};
try std.testing.expectEqual(Resolution.ambiguous, try resolvePrefix("a28c", &cross));
// Same-algorithm: two 40-char names sharing a prefix, reported the same.
const same = [_][]const u8{
a1_1_name,
"a28c000000000000000000000000000000000000",
};
try std.testing.expectEqual(Resolution.ambiguous, try resolvePrefix("a28c", &same));
}
test "self-checksum = lowercase MD5 (empty input vector)" {
var out: [32]u8 = undefined;
const z = selfChecksum("", &out);
try std.testing.expectEqualStrings("d41d8cd98f00b204e9800998ecf8427e", z);
}
test "artifact §4.1.1: stripping only when first 34 bytes match armor" {
// Under 34 bytes: too short to match, returned unchanged.
const short = "-----BEGIN PGP SIGNED MESSAGE---"; // 32 bytes, two dashes short
try std.testing.expectEqualStrings(short, stripSignatureWrapper(short));
// Armor text present, but not at offset 0: unchanged.
const offset5 = "12345-----BEGIN PGP SIGNED MESSAGE-----\n\nbody\n";
try std.testing.expectEqualStrings(offset5, stripSignatureWrapper(offset5));
// 34 bytes, none of them the armor literal: unchanged.
const not_armor = "x" ** 34 ++ "\n\nbody\n";
try std.testing.expectEqualStrings(not_armor, stripSignatureWrapper(not_armor));
// Exact open armor at offset 0: stripped.
const wrapped = "-----BEGIN PGP SIGNED MESSAGE-----\n\nbody\n";
try std.testing.expectEqualStrings("body\n", stripSignatureWrapper(wrapped));
}
test "artifact §4.1.1: body begins after first blank line (LF LF or LF CR LF)" {
const wrapped_lf = "-----BEGIN PGP SIGNED MESSAGE-----\nHash: SHA256\n\nbody line\n";
const wrapped_crlf = "-----BEGIN PGP SIGNED MESSAGE-----\nHash: SHA256\n\r\nbody line\n";
try std.testing.expectEqualStrings("body line\n", stripSignatureWrapper(wrapped_lf));
try std.testing.expectEqualStrings("body line\n", stripSignatureWrapper(wrapped_crlf));
}
test "artifact §4.1.1: no blank line leaves content wholly unstripped" {
const wrapped = "-----BEGIN PGP SIGNED MESSAGE-----\nhello\n";
try std.testing.expectEqualStrings(wrapped, stripSignatureWrapper(wrapped));
}
test "artifact §4.1.1: trailing signature armor truncates, retaining the LF" {
const pgp_wrapped = "-----BEGIN PGP SIGNED MESSAGE-----\n\nbody\n" ++
"-----BEGIN PGP SIGNATURE-----\nabc\n-----END PGP SIGNATURE-----\n";
try std.testing.expectEqualStrings("body\n", stripSignatureWrapper(pgp_wrapped));
const ssh_wrapped = "-----BEGIN SSH SIGNED MESSAGE-----\n\nbody\n" ++
"-----BEGIN SSH SIGNATURE-----\nabc\n-----END SSH SIGNATURE-----\n";
try std.testing.expectEqualStrings("body\n", stripSignatureWrapper(ssh_wrapped));
// No trailing armor at all: runs to end of input.
const no_close = "-----BEGIN PGP SIGNED MESSAGE-----\n\nbody\n";
try std.testing.expectEqualStrings("body\n", stripSignatureWrapper(no_close));
}
test "artifact §4.1.1: trailing armor one dash short still matches (29 of 30)" {
// 4 trailing dashes (one short of the closing run) + arbitrary byte: matches.
const one_short = "-----BEGIN PGP SIGNED MESSAGE-----\n\nbody\n" ++
"-----BEGIN PGP SIGNATURE----X\ntrailer\n";
try std.testing.expectEqualStrings("body\n", stripSignatureWrapper(one_short));
// 3 trailing dashes (two short): the 29-byte compare fails, runs to EOF.
const two_short = "-----BEGIN PGP SIGNED MESSAGE-----\n\nbody\n" ++
"-----BEGIN PGP SIGNATURE---\ntrailer\n";
const expected_two_short = "body\n-----BEGIN PGP SIGNATURE---\ntrailer\n";
try std.testing.expectEqualStrings(expected_two_short, stripSignatureWrapper(two_short));
}
test "artifact §4.1.1: PGP and SSH wrappers are removed before parsing" {
const pgp_wrapped = "-----BEGIN PGP SIGNED MESSAGE-----\nHash: SHA256\n\n" ++
a1_1_manifest ++ "-----BEGIN PGP SIGNATURE-----\nabc123\n-----END PGP SIGNATURE-----\n";
try std.testing.expectEqualStrings(a1_1_manifest, stripSignatureWrapper(pgp_wrapped));
const ssh_wrapped = "-----BEGIN SSH SIGNED MESSAGE-----\n\n" ++
a1_1_manifest ++ "-----BEGIN SSH SIGNATURE-----\nabc123\n-----END SSH SIGNATURE-----\n";
try std.testing.expectEqualStrings(a1_1_manifest, stripSignatureWrapper(ssh_wrapped));
}
test "artifact §4.1: raw input must be non-empty and end in LF" {
try std.testing.expect(frame("") == null);
try std.testing.expect(frame("C comment") == null); // no trailing LF
try std.testing.expect(frame("C comment\n") != null);
}
test "artifact §4.1: stripped content under 10 bytes is rejected" {
try std.testing.expect(frame("C 123456\n") == null); // 9 bytes
try std.testing.expect(frame("C 1234567\n") != null); // 10 bytes
}
test "artifact §4.1: first stripped byte must be uppercase A-Z" {
const table = [_]struct { byte0: u8, ok: bool }{
.{ .byte0 = '@', .ok = false }, // 0x40, just below 'A'
.{ .byte0 = 'A', .ok = true },
.{ .byte0 = 'Z', .ok = true },
.{ .byte0 = '[', .ok = false }, // 0x5B, just above 'Z'
.{ .byte0 = 'c', .ok = false }, // lowercase
};
for (table) |row| {
var buf = [_]u8{ row.byte0, ' ', '1', '2', '3', '4', '5', '6', '7', '\n' };
std.debug.assert(buf.len == 10);
try std.testing.expectEqual(row.ok, frame(&buf) != null);
}
}
test "artifact §4.1: second stripped byte must be a single space" {
const table = [_]struct { byte1: u8, ok: bool }{
.{ .byte1 = ' ', .ok = true },
.{ .byte1 = '\t', .ok = false },
.{ .byte1 = 'X', .ok = false },
.{ .byte1 = '\n', .ok = false }, // argument-less first card
};
for (table) |row| {
var buf = [_]u8{ 'C', row.byte1, '1', '2', '3', '4', '5', '6', '7', '\n' };
std.debug.assert(buf.len == 10);
try std.testing.expectEqual(row.ok, frame(&buf) != null);
}
}
test "artifact §4.1/§6.2: present but mismatched Z card is rejected" {
// Z card occupies the final 35 bytes of the 127-byte manifest: Z@92,
// SP@93, digest 94..125, LF@126 (§A.2 offset table).
var flipped: [127]u8 = undefined;
@memcpy(&flipped, a1_1_manifest);
flipped[94] = 'd'; // digest[0] was 'c'; still hex, still a mismatch
try std.testing.expectEqual(ZVerdict.mismatch, verifyZCard(&flipped));
try std.testing.expect(frame(&flipped) == null);
var uppercased: [127]u8 = undefined;
@memcpy(&uppercased, a1_1_manifest);
uppercased[94] = 'C'; // numerically same digit, wrong case
try std.testing.expectEqual(ZVerdict.mismatch, verifyZCard(&uppercased));
try std.testing.expect(frame(&uppercased) == null);
}
test "artifact §4.1/§6.2: absent Z card passes the framing gate" {
try std.testing.expectEqual(ZVerdict.absent, verifyZCard("C comment\n"));
try std.testing.expect(frame("C comment\n") != null);
try std.testing.expectEqual(ZVerdict.match, verifyZCard(a1_1_manifest));
try std.testing.expect(frame(a1_1_manifest) != null);
}
test "artifact §4.1: gates apply in order (raw-LF before strip)" {
// Outer wrapper omits its final LF: gate 1 must fire on the raw bytes,
// before stripping ever runs — the stripped inner body (a1_1_manifest)
// always ends in LF regardless, so this only fails if gates run in order.
const wrapped_no_lf = "-----BEGIN PGP SIGNED MESSAGE-----\nHash: SHA256\n\n" ++
a1_1_manifest ++ "-----BEGIN PGP SIGNATURE-----\nabc\n-----END PGP SIGNATURE-----";
try std.testing.expect(frame(wrapped_no_lf) == null);
const wrapped_with_lf = wrapped_no_lf ++ "\n";
try std.testing.expect(frame(wrapped_with_lf) != null);
}
// ---------------------------------------------------------------------------
// §4.2 Line and Token Grammar / §4.2.2 Closed Card Alphabet — tests
// ---------------------------------------------------------------------------
test "artifact §4.2.2: exactly the 21 defined letters are accepted" {
// Lines 622-624: "Exactly 21 card letters are defined: A, B, C, D, E, F,
// G, H, I, J, K, L, M, N, P, Q, R, T, U, W, Z." Closed set, no
// forward-compat (lines 626-627).
const undefined_in_range = "OSVXY";
var accepted: usize = 0;
var c: u8 = 'A';
while (true) : (c += 1) {
const is_undefined = std.mem.indexOfScalar(u8, undefined_in_range, c) != null;
try std.testing.expectEqual(!is_undefined, isCardLetter(c));
if (isCardLetter(c)) accepted += 1;
if (c == 'Z') break;
}
try std.testing.expectEqual(@as(usize, 21), accepted);
}
test "artifact §4.2.2: card letter outside A-Z is rejected" {
// Line 624: "A card letter outside A-Z MUST be rejected."
const table = [_]u8{ 0x40, '[', 'c', '0' }; // '@' just below 'A', '[' just above 'Z'
for (table) |b| {
try std.testing.expect(!isCardLetter(b));
var buf = [_]u8{ b, '\n' };
try std.testing.expect(tokenizeCardLine(&buf) == null);
}
}
test "artifact §4.2.2: in-range undefined letters O S V X Y are rejected" {
// Line 625: "an in-range but undefined letter (O, S, V, X, Y) MUST be
// rejected."
const table = [_]u8{ 'O', 'S', 'V', 'X', 'Y' };
for (table) |b| {
try std.testing.expect(!isCardLetter(b));
var buf = [_]u8{ b, '\n' };
try std.testing.expect(tokenizeCardLine(&buf) == null);
}
}
test "artifact §4.2: tchar admits every octet except SP and LF" {
// Lines 590-591: "tchar = %x00-09 / %x0B-1F / %x21-FF ; any octet except
// SP (0x20) and LF (0x0A)".
const admitted = [_]u8{ 0x00, 0x09, 0x0B, 0x1F, 0x21, 0xFF };
for (admitted) |b| try std.testing.expect(isTokenByte(b));
const excluded = [_]u8{ 0x0A, 0x20 };
for (excluded) |b| try std.testing.expect(!isTokenByte(b));
}
test "artifact §4.2: card tokenizes to letter and SP-split tokens" {
// Grammar lines 586-589: card-args = token *( SP token ).
const line = "F path abc123 w\n";
const card = tokenizeCardLine(line).?;
try std.testing.expectEqual(@as(u8, 'F'), card.letter);
try std.testing.expectEqualStrings("path abc123 w", card.args.?);
var it = card.tokens();
try std.testing.expectEqualStrings("path", it.next().?);
try std.testing.expectEqualStrings("abc123", it.next().?);
try std.testing.expectEqualStrings("w", it.next().?);
try std.testing.expect(it.next() == null);
}
test "artifact §4.2-m1: byte after card letter must be SP or LF, tab rejected" {
// Lines 593-595: "MUST be exactly one space (0x20 ...) or LF ... Any
// other second byte — including tab (0x09) — makes the card invalid."
{
var buf = "C x\n".*;
const card = tokenizeCardLine(&buf).?;
try std.testing.expectEqualStrings("x", card.args.?);
}
{
var buf = "C\n".*;
const card = tokenizeCardLine(&buf).?;
try std.testing.expectEqual(@as(?[]const u8, null), card.args);
var it = card.tokens();
try std.testing.expect(it.next() == null);
}
{
var buf = "C\tx\n".*;
try std.testing.expect(tokenizeCardLine(&buf) == null);
}
{
var buf = "CX\n".*;
try std.testing.expect(tokenizeCardLine(&buf) == null);
}
}
test "artifact §4.2-m2: tokens split on space only; tab retained in token" {
// Lines 596-598: "split on space and LF only; tab is not a delimiter and
// is retained ... a token can never contain an embedded space." The
// byte after the letter must still be SP (§4.2-m1); the tab lives
// inside the token that follows, not in the letter/SP boundary.
var buf = "T \tvalue\n".*;
const card = tokenizeCardLine(&buf).?;
var it = card.tokens();
const only = it.next().?;
try std.testing.expectEqualStrings("\tvalue", only);
try std.testing.expect(std.mem.indexOfScalar(u8, only, ' ') == null);
try std.testing.expect(it.next() == null);
var buf2 = "T a b c\n".*;
const card2 = tokenizeCardLine(&buf2).?;
var it2 = card2.tokens();
var n: usize = 0;
while (it2.next()) |tok| : (n += 1) {
try std.testing.expect(std.mem.indexOfScalar(u8, tok, ' ') == null);
}
try std.testing.expectEqual(@as(usize, 3), n);
}
test "artifact §4.2-m3: CR is an ordinary token byte, not a delimiter" {
// Lines 598-600: "There is no CR (0x0D) handling in the card and token
// path — a CR is an ordinary token byte."
var buf = "C a\rb\n".*;
const card = tokenizeCardLine(&buf).?;
var it = card.tokens();
try std.testing.expectEqualStrings("a\rb", it.next().?);
try std.testing.expect(it.next() == null);
}
test "artifact §4.2: bare U vs empty-token U distinguished (RFC line 1418)" {
// §4.7.17: a bare U line (0 tokens) differs from a U followed by an
// empty token (1 empty token) — both collapse to "anonymous" at the
// per-card layer, but the tokenizer must still tell them apart.
var bare = "U\n".*;
const bare_card = tokenizeCardLine(&bare).?;
try std.testing.expectEqual(@as(?[]const u8, null), bare_card.args);
var bare_it = bare_card.tokens();
try std.testing.expect(bare_it.next() == null);
var empty = "U \n".*;
const empty_card = tokenizeCardLine(&empty).?;
try std.testing.expectEqualStrings("", empty_card.args.?);
var empty_it = empty_card.tokens();
try std.testing.expectEqualStrings("", empty_it.next().?);
try std.testing.expect(empty_it.next() == null);
}
// ---------------------------------------------------------------------------
// §4.3 Character-Escape Encoding — tests
// ---------------------------------------------------------------------------
test "artifact §4.3-m1: the eight escape pairs decode to their octets" {
// Table 1 (lines 635-653): each row's "Decoded octet" column.
const table = [_]struct { input: []const u8, expected: u8 }{
.{ .input = "\\n", .expected = 0x0A },
.{ .input = "\\s", .expected = 0x20 },
.{ .input = "\\t", .expected = 0x09 },
.{ .input = "\\r", .expected = 0x0D },
.{ .input = "\\v", .expected = 0x0B },
.{ .input = "\\f", .expected = 0x0C },
.{ .input = "\\0", .expected = 0x00 },
.{ .input = "\\\\", .expected = 0x5C },
};
for (table) |row| {
var out: [2]u8 = undefined;
const decoded = decodeEscapes(row.input, &out);
try std.testing.expectEqual(@as(usize, 1), decoded.len);
try std.testing.expectEqual(row.expected, decoded[0]);
}
}
test "artifact §4.3-m2: unrecognized escape drops the backslash, keeps the successor" {
// Lines 657-660: "the decoder discards the backslash and emits the
// following character unchanged ... decodes to the single byte C."
const table = [_]struct { input: []const u8, expected: u8 }{
.{ .input = "\\x", .expected = 'x' },
.{ .input = "\\1", .expected = '1' }, // only \0 is in Table 1
.{ .input = "\\!", .expected = '!' },
};
for (table) |row| {
var out: [2]u8 = undefined;
const decoded = decodeEscapes(row.input, &out);
try std.testing.expectEqual(@as(usize, 1), decoded.len);
try std.testing.expectEqual(row.expected, decoded[0]);
}
}
test "artifact §4.3-amb1: a lone trailing backslash is emitted literally" {
// ambiguities.md RESOLVED reading #1 (pass-through, operator 2026-07-09):
// a backslash with no successor forms no pair, emitted as its own byte.
var out1: [1]u8 = undefined;
const decoded1 = decodeEscapes("\\", &out1);
try std.testing.expectEqualStrings("\\", decoded1);
var out2: [2]u8 = undefined;
const decoded2 = decodeEscapes("a\\", &out2);
try std.testing.expectEqualStrings("a\\", decoded2);
}
test "artifact §4.3-w1: a pure-hexadecimal wire token decodes to itself" {
// Line 698: "A pure-hexadecimal wire token decodes to itself" — no
// backslash present, every byte is ordinary.
const short = "deadbeef";
var out_short: [short.len]u8 = undefined;
try std.testing.expectEqualStrings(short, decodeEscapes(short, &out_short));
const long = "a28c83647dfa805f05f3204a7e146eb1f0d9050500000000000000000000000";
var out_long: [long.len]u8 = undefined;
try std.testing.expectEqualStrings(long, decodeEscapes(long, &out_long));
}
test "artifact §4.3: escapes decode left-to-right without reprocessing output" {
// §4.3 escapes are "decoded in place" over the wire token (lines
// 632-633): the decoded backslash from \\ -> 0x5C is emitted, not
// re-read, so a trailing 'n' after it stays literal (not LF).
const input = "\\\\n"; // backslash, backslash, n
var out: [3]u8 = undefined;
const decoded = decodeEscapes(input, &out);
try std.testing.expectEqual(@as(usize, 2), decoded.len);
try std.testing.expectEqual(@as(u8, 0x5C), decoded[0]);
try std.testing.expectEqual(@as(u8, 'n'), decoded[1]);
var out_empty: [0]u8 = undefined;
try std.testing.expectEqualStrings("", decodeEscapes("", &out_empty));
}
// ---------------------------------------------------------------------------
// §4.4 Card Ordering — tests
// ---------------------------------------------------------------------------
test "artifact §4.4-p1: cards admit in non-decreasing type order" {
// Lines 705-707: "MUST appear in non-decreasing lexicographic card-type
// order: each card's type letter MUST be greater than or equal to the
// previous card's type letter."
var s = OrderState{};
const in_order = "CCDFTUZ";
for (in_order) |letter| try std.testing.expect(s.admit(letter));
var below = OrderState{};
try std.testing.expect(below.admit('F'));
try std.testing.expect(!below.admit('D')); // D (0x44) < F (0x46)
}
test "artifact §4.4-p2: consecutive equal-letter cards are permitted" {
// Lines 708-711: consecutive same-letter cards permitted; the ordering
// is non-decreasing, not strictly increasing.
var s1 = OrderState{};
try std.testing.expect(s1.admit('F'));
try std.testing.expect(s1.admit('F'));
var s2 = OrderState{};
try std.testing.expect(s2.admit('P'));
try std.testing.expect(s2.admit('P'));
}
test "artifact §4.4-r1: a letter sorting below the previous is rejected" {
// Lines 708-709: "The rejection condition fires only when a card's
// letter sorts below the previous card's letter" — exactly that
// condition, nothing else.
const table = [_]struct { prev: u8, cur: u8, ok: bool }{
.{ .prev = 'D', .cur = 'C', .ok = false },
.{ .prev = 'P', .cur = 'C', .ok = false },
.{ .prev = 'Z', .cur = 'U', .ok = false },
.{ .prev = 'C', .cur = 'C', .ok = true },
.{ .prev = 'C', .cur = 'D', .ok = true },
};
for (table) |row| {
var s = OrderState{};
try std.testing.expect(s.admit(row.prev));
try std.testing.expectEqual(row.ok, s.admit(row.cur));
}
}
test "artifact §4.4-m1: N after P admitted once a valid E precedes" {
// Lines 714-717: "an N card MAY appear immediately after a P card when
// the artifact has already presented a valid E card. A parser MUST
// accept that sequence, and any other out-of-order card MUST be
// rejected."
var s = OrderState{};
for ("CDE") |letter| try std.testing.expect(s.admit(letter));
try std.testing.expect(s.admit('P'));
try std.testing.expect(s.admit('N')); // N (0x4E) < P (0x50): exception fires
// Same driver still rejects a non-exception out-of-order step.
try std.testing.expect(!s.admit('C')); // C (0x43) < N (0x4E), not the exception
}
test "artifact §4.4-r2: the N-after-P exception requires a prior E card" {
// Lines 714-716 precondition: "has already presented a valid E card."
var no_e = OrderState{};
try std.testing.expect(no_e.admit('P'));
try std.testing.expect(!no_e.admit('N')); // N < P, but e_seen is false: inert
var with_e = OrderState{};
try std.testing.expect(with_e.admit('E'));
try std.testing.expect(with_e.admit('P'));
try std.testing.expect(with_e.admit('N')); // exception fires
}
test "artifact §4.4-r3: out-of-order cards other than N-after-P are rejected" {
// Lines 716-717: "any other out-of-order card MUST be rejected."
// Appendix C (lines 2952-2955): "other out-of-order cards remain
// rejected." Only N-immediately-after-P is tolerated.
{
var s = OrderState{};
try std.testing.expect(s.admit('E'));
try std.testing.expect(s.admit('P'));
try std.testing.expect(!s.admit('M')); // M (0x4D) < P, not N
}
{
var s = OrderState{};
try std.testing.expect(s.admit('E'));
try std.testing.expect(s.admit('Q'));
try std.testing.expect(!s.admit('N')); // prev is Q, not P: inert
}
{
var s = OrderState{};
for ("EPN") |letter| try std.testing.expect(s.admit(letter));
try std.testing.expect(!s.admit('M')); // M < N, below-N step still rejects
}
}
test "artifact §4.4-r4: no letter follows Z; the exception does not involve Z" {
// Lines 739-741: "Z is the lexicographically last defined letter, the
// ordering rule admits no letter after it (the N-after-P exception
// does not involve Z)."
var s1 = OrderState{};
try std.testing.expect(s1.admit('Z'));
try std.testing.expect(!s1.admit('U'));
var s2 = OrderState{};
try std.testing.expect(s2.admit('Z'));
try std.testing.expect(!s2.admit('C'));
var s3 = OrderState{};
try std.testing.expect(s3.admit('E'));
try std.testing.expect(s3.admit('Z'));
try std.testing.expect(!s3.admit('N')); // prev is Z, not P: exception inert
}
test "artifact §4.4-w1: the technote sequence C D E P N T U W Z is admitted" {
// §A.1 worked example (lines 2471-2489) card letters in order; lines
// 2498-2501: "the N card (MIME type) appears after the P card even
// though N sorts before P in ASCII order. This is accepted because the
// E card has already been parsed when N is encountered."
var s = OrderState{};
const sequence = "CDEPNTUWZ";
for (sequence) |letter| try std.testing.expect(s.admit(letter));
}