const std = @import("std");
const af = @import("artifact.zig");
// Public-surface aliases so the moved tests read exactly as they did inline.
const ArtifactType = af.ArtifactType;
const BCard = af.BCard;
const BaselineResolver = af.BaselineResolver;
const Census = af.Census;
const ClassifyError = af.ClassifyError;
const FCard = af.FCard;
const FCardAdmission = af.FCardAdmission;
const FCardSortError = af.FCardSortError;
const FileMode = af.FileMode;
const HashAlgo = af.HashAlgo;
const HashPolicy = af.HashPolicy;
const IntraRunRule = af.IntraRunRule;
const LookupResult = af.LookupResult;
const Manifest = af.Manifest;
const ManifestFiles = af.ManifestFiles;
const MergeWalk = af.MergeWalk;
const OccurrenceClass = af.OccurrenceClass;
const OrderState = af.OrderState;
const ParseError = af.ParseError;
const QSign = af.QSign;
const Resolution = af.Resolution;
const SingleOccurrenceSet = af.SingleOccurrenceSet;
const TSign = af.TSign;
const TrackedFile = af.TrackedFile;
const WCard = af.WCard;
const ZVerdict = af.ZVerdict;
const aggregateChecksum = af.aggregateChecksum;
const alternativeHash = af.alternativeHash;
const artifactName = af.artifactName;
const classify = af.classify;
const classifyTimestamp = af.classifyTimestamp;
const coerceAssignment = af.coerceAssignment;
const compareBytes = af.compareBytes;
const compareOptional = af.compareOptional;
const compareTag = af.compareTag;
const create = af.create;
const decodeEscapes = af.decodeEscapes;
const dedupNameAlgo = af.dedupNameAlgo;
const defaultPolicy = af.defaultPolicy;
const determineType = af.determineType;
const encodeEscapes = af.encodeEscapes;
const enumerate = af.enumerate;
const frame = af.frame;
const intraRunRule = af.intraRunRule;
const isCardLetter = af.isCardLetter;
const isDelta = af.isDelta;
const isReservedFilename = af.isReservedFilename;
const isSimplePathname = af.isSimplePathname;
const isTokenByte = af.isTokenByte;
const isWikiName = af.isWikiName;
const isWriterTimestamp = af.isWriterTimestamp;
const lookup = af.lookup;
const nameAlgo = af.nameAlgo;
const occurrenceClass = af.occurrenceClass;
const parse = af.parse;
const parseACard = af.parseACard;
const parseBCard = af.parseBCard;
const parseCCard = af.parseCCard;
const parseDCard = af.parseDCard;
const parseECard = af.parseECard;
const parseFCard = af.parseFCard;
const parseGCard = af.parseGCard;
const parseHCard = af.parseHCard;
const parseICard = af.parseICard;
const parseJCard = af.parseJCard;
const parseKCard = af.parseKCard;
const parseLCard = af.parseLCard;
const parseMCard = af.parseMCard;
const parseNCard = af.parseNCard;
const parsePCard = af.parsePCard;
const parseQCard = af.parseQCard;
const parseRCard = af.parseRCard;
const parseTCard = af.parseTCard;
const parseUCard = af.parseUCard;
const parseWCard = af.parseWCard;
const parseZCard = af.parseZCard;
const policyFromString = af.policyFromString;
const policyName = af.policyName;
const primaryHash = af.primaryHash;
const ratchet = af.ratchet;
const resolvePrefix = af.resolvePrefix;
const selfChecksum = af.selfChecksum;
const stripSignatureWrapper = af.stripSignatureWrapper;
const tokenizeCardLine = af.tokenizeCardLine;
const validateHashToken = af.validateHashToken;
const validateName = af.validateName;
const verify = af.verify;
const verifyZCard = af.verifyZCard;
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,
);
}
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: 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));
}
const sha3_name_64 = "2cee2f88646e224275d9445d523e4a8021f8e6cffa2b3978f8ea324cb0210261";
test "artifact §4.7.1: artifact-hash accepts 40 and 64 hex" {
try std.testing.expect(validateHashToken(.artifact_hash, a1_1_name));
try std.testing.expect(validateHashToken(.artifact_hash, sha3_name_64));
}
test "artifact §4.7.1: artifact-hash is case-insensitive per its explicit MUST" {
// §4.7.1: "MUST be exactly 40 or 64 case-insensitive hexadecimal
// characters" — a harder requirement than artifact-name's MAY (§3.3),
// so upper- and mixed-case tokens are accepted, unlike validateName.
var upper: [40]u8 = undefined;
for (a1_1_name, 0..) |c, i| upper[i] = std.ascii.toUpper(c);
try std.testing.expect(validateHashToken(.artifact_hash, &upper));
var mixed: [64]u8 = undefined;
for (sha3_name_64, 0..) |c, i| {
mixed[i] = if (i % 2 == 0) std.ascii.toUpper(c) else c;
}
try std.testing.expect(validateHashToken(.artifact_hash, &mixed));
}
test "artifact §4.7.1: artifact-hash rejects wrong length and non-hex" {
// §4.7.1-p1 length gate; §3.2-r1 length precedes char-class check.
const bad_lens = [_]usize{ 39, 41, 32, 63, 65 };
var buf: [65]u8 = undefined;
@memset(&buf, 'a');
for (bad_lens) |len| {
std.debug.assert(len <= buf.len);
try std.testing.expect(!validateHashToken(.artifact_hash, buf[0..len]));
}
var non_hex: [40]u8 = undefined;
@memcpy(&non_hex, a1_1_name);
non_hex[0] = 'g';
try std.testing.expect(!validateHashToken(.artifact_hash, &non_hex));
}
test "artifact §4.7.1: K ticket id is 40, never 64" {
// §4.7.1-r1: pinned to SHA-1 length regardless of hash policy.
try std.testing.expect(validateHashToken(.ticket_id, a1_1_name));
try std.testing.expect(!validateHashToken(.ticket_id, sha3_name_64));
// Negative space beyond the two named lengths, and a non-hex char.
try std.testing.expect(!validateHashToken(.ticket_id, a1_1_name[0..39]));
var non_hex: [40]u8 = undefined;
@memcpy(&non_hex, a1_1_name);
non_hex[0] = 'g';
try std.testing.expect(!validateHashToken(.ticket_id, &non_hex));
}
test "artifact §4.7.1: R/Z checksum tokens are exactly 32 hex" {
// §4.7.1-r2, §6.1 md5-token = 32hexdig-ci.
const md5 = "d41d8cd98f00b204e9800998ecf8427e";
try std.testing.expect(validateHashToken(.checksum, md5));
try std.testing.expect(!validateHashToken(.checksum, a1_1_name));
try std.testing.expect(!validateHashToken(.checksum, md5[0..31]));
var too_long: [33]u8 = undefined;
@memcpy(too_long[0..32], md5);
too_long[32] = 'a';
try std.testing.expect(!validateHashToken(.checksum, &too_long));
var non_hex: [32]u8 = undefined;
@memcpy(&non_hex, md5);
non_hex[0] = 'g';
try std.testing.expect(!validateHashToken(.checksum, &non_hex));
}
test "artifact §4.7.1: checksum is case-insensitive; ticket id is lowercase-only" {
// §6.1 "parse-accept grammar is case-insensitive" for R/Z; §4.7.1's K
// exception takes the artifact-name-style MAY and stays lowercase-only,
// unlike the artifact-hash arm's case-insensitive MUST (covered above).
try std.testing.expect(validateHashToken(.checksum, "D41D8CD98F00B204E9800998ECF8427E"));
var upper: [40]u8 = undefined;
for (a1_1_name, 0..) |c, i| upper[i] = std.ascii.toUpper(c);
try std.testing.expect(!validateHashToken(.ticket_id, &upper));
}
test "artifact §4.7.1: the map never escape-decodes a raw token" {
// §4.7.1-m2; §4.3.1 defines the A-target decode-then-validate contract
// that lives entirely in the future A-card parser, not here. A decoding
// validator would strip the leading "\a" -> "a" and see 40 valid hex
// bytes; the raw validator must see 41 bytes including a non-hex '\'.
var wire: [41]u8 = undefined;
wire[0] = '\\';
@memcpy(wire[1..41], a1_1_name);
try std.testing.expect(!validateHashToken(.artifact_hash, &wire));
// Interior backslash inside an otherwise 40-byte token.
var interior: [40]u8 = undefined;
@memcpy(&interior, a1_1_name);
interior[10] = '\\';
try std.testing.expect(!validateHashToken(.artifact_hash, &interior));
}
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);
}
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);
}
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));
}
test "artifact §4.3: encodeEscapes emits the Table 1 escape for each named octet" {
const Row = struct { octet: u8, succ: u8 };
const rows = [_]Row{
.{ .octet = 0x0A, .succ = 'n' },
.{ .octet = 0x20, .succ = 's' },
.{ .octet = 0x09, .succ = 't' },
.{ .octet = 0x0D, .succ = 'r' },
.{ .octet = 0x0B, .succ = 'v' },
.{ .octet = 0x0C, .succ = 'f' },
.{ .octet = 0x00, .succ = '0' },
.{ .octet = 0x5C, .succ = '\\' },
};
for (rows) |row| {
var out: [2]u8 = undefined;
const encoded = encodeEscapes(&[_]u8{row.octet}, &out);
try std.testing.expectEqual(@as(usize, 2), encoded.len);
try std.testing.expectEqual(@as(u8, '\\'), encoded[0]);
try std.testing.expectEqual(row.succ, encoded[1]);
}
}
test "artifact §4.3/§A.1.1: the worked-example comment round-trips" {
var buf: [64]u8 = undefined;
const encoded = encodeEscapes("initial empty baseline", &buf);
try std.testing.expectEqualStrings("initial\\sempty\\sbaseline", encoded);
}
test "artifact §4.3: encodeEscapes then decodeEscapes round-trips" {
var prng = std.Random.DefaultPrng.init(0xA47F_0100);
const rand = prng.random();
var round: usize = 0;
while (round < 400) : (round += 1) {
var src: [64]u8 = undefined;
const len = rand.uintLessThan(usize, src.len + 1);
for (src[0..len]) |*b| b.* = rand.int(u8);
const token = src[0..len];
var enc_buf: [128]u8 = undefined;
const encoded = encodeEscapes(token, &enc_buf);
try std.testing.expect(encoded.len <= 2 * token.len);
try std.testing.expect(encoded.len >= token.len);
var dec_buf: [128]u8 = undefined;
const decoded = decodeEscapes(encoded, dec_buf[0..encoded.len]);
try std.testing.expectEqualSlices(u8, token, decoded);
}
}
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));
}
test "artifact §4.5.1: card letters partition into single / repeatable / neither" {
// Lines 753-766: 14 single-occurrence, 6 repeatable, 1 (Z) neither —
// 21 total, matching the closed alphabet.
var single: usize = 0;
var repeatable: usize = 0;
var neither: usize = 0;
var c: u8 = 'A';
while (true) : (c += 1) {
if (isCardLetter(c)) {
switch (occurrenceClass(c)) {
.single => single += 1,
.repeatable => repeatable += 1,
.neither => neither += 1,
}
}
if (c == 'Z') break;
}
try std.testing.expectEqual(@as(usize, 14), single);
try std.testing.expectEqual(@as(usize, 6), repeatable);
try std.testing.expectEqual(@as(usize, 1), neither);
}
test "artifact §4.5.1-r1: a single-occurrence duplicate is rejected" {
// Line 755-756: "Single-occurrence (a duplicate MUST be rejected): A, B,
// C, D, E, G, H, I, K, L, N, R, U, W." Rejection: 2nd occurrence.
const single_letters = "ABCDEGHIKLNRUW";
try std.testing.expectEqual(@as(usize, 14), single_letters.len);
for (single_letters) |letter| {
var set = SingleOccurrenceSet{};
try std.testing.expect(set.admit(letter));
try std.testing.expect(!set.admit(letter));
}
}
test "artifact §4.5.1-m1: repeatable card types are not single-occurrence" {
// Lines 758-762: "Repeatable (each occurrence appends ...): F, J, M, T,
// P, Q. It is not the case that any duplicate card type is a syntax
// error." A duplicate must be admitted, not rejected.
const repeatable_letters = "FJMTPQ";
try std.testing.expectEqual(@as(usize, 6), repeatable_letters.len);
for (repeatable_letters) |letter| {
try std.testing.expectEqual(OccurrenceClass.repeatable, occurrenceClass(letter));
var set = SingleOccurrenceSet{};
try std.testing.expect(set.admit(letter));
try std.testing.expect(set.admit(letter));
}
}
test "artifact §4.5.1-m2: the Z card belongs to neither class and has no duplicate guard" {
// Lines 764-766: "The Z card ... belongs to neither class. It has no
// duplicate guard, and the ordering rule permits equal consecutive
// letters."
try std.testing.expectEqual(OccurrenceClass.neither, occurrenceClass('Z'));
var set = SingleOccurrenceSet{};
try std.testing.expect(set.admit('Z'));
try std.testing.expect(set.admit('Z'));
try std.testing.expectEqual(@as(?IntraRunRule, null), intraRunRule('Z'));
}
test "artifact §4.5.2-r1: F cards must strictly ascend by decoded filename" {
// Table 2 line 792: "Strictly ascending by decoded filename (equal or
// descending rejected)."
try std.testing.expectEqual(IntraRunRule.ascending, intraRunRule('F').?);
try std.testing.expectEqual(std.math.Order.lt, compareBytes("a", "b"));
// Rejection: equal.
try std.testing.expectEqual(std.math.Order.eq, compareBytes("a", "a"));
// Rejection: descending.
try std.testing.expectEqual(std.math.Order.gt, compareBytes("b", "a"));
}
test "artifact §4.5.2-m1: the F sort check runs on the post-decode filename" {
// Lines 813-817: "runs on the post-decode filename, not on the raw
// wire-encoded token (decoding happens before the sort check)." Wire
// "a\sb" decodes (§4.3 Table 1, \s -> SP 0x20) to "a b"; wire "a!b" has
// no escape and stays "a!b". Decoded 2nd byte 0x20 < 0x21 ascends.
var buf1: [8]u8 = undefined;
const decoded1 = decodeEscapes("a\\sb", &buf1);
try std.testing.expectEqualStrings("a b", decoded1);
const raw2 = "a!b";
try std.testing.expectEqual(std.math.Order.lt, compareBytes(decoded1, raw2));
// On the RAW wire token (undecoded), "a\sb" is 0x61 0x5C 0x73 0x62: 2nd
// byte 0x5C > 0x21, so a raw-token compare would report .gt (reject) —
// the opposite of the post-decode result, proving decode precedes sort.
const raw1 = "a\\sb";
try std.testing.expectEqual(std.math.Order.gt, compareBytes(raw1, raw2));
}
test "artifact §4.5.2-r2: J cards must strictly ascend by field name" {
// Table 2 line 795: "Strictly ascending by field name (equal or
// descending rejected)."
try std.testing.expectEqual(IntraRunRule.ascending, intraRunRule('J').?);
try std.testing.expectEqual(std.math.Order.lt, compareBytes("a", "b"));
try std.testing.expectEqual(std.math.Order.eq, compareBytes("a", "a"));
try std.testing.expectEqual(std.math.Order.gt, compareBytes("b", "a"));
}
test "artifact §4.5.2-r3: M cards must strictly ascend by hash" {
// Table 2 line 798: "Strictly ascending by hash (equal or descending
// rejected)."
try std.testing.expectEqual(IntraRunRule.ascending, intraRunRule('M').?);
try std.testing.expectEqual(std.math.Order.lt, compareBytes("aa", "ab"));
try std.testing.expectEqual(std.math.Order.eq, compareBytes("aa", "aa"));
try std.testing.expectEqual(std.math.Order.gt, compareBytes("ab", "aa"));
}
test "artifact §4.5.2-r4: T cards ascend two-level by tag name then target hash" {
// Table 2 lines 801-802: "Two-level ascending by (tag name, then target
// hash); equal names require strictly increasing hash."
try std.testing.expectEqual(IntraRunRule.tag_two_level, intraRunRule('T').?);
// Names ascend: hash irrelevant.
try std.testing.expectEqual(std.math.Order.lt, compareTag("a", "zz", "b", "aa"));
// Equal name, ascending hash.
try std.testing.expectEqual(std.math.Order.lt, compareTag("a", "aa", "a", "ab"));
// Rejection: equal name, equal hash.
try std.testing.expectEqual(std.math.Order.eq, compareTag("a", "aa", "a", "aa"));
// Rejection: equal name, descending hash.
try std.testing.expectEqual(std.math.Order.gt, compareTag("a", "ab", "a", "aa"));
// Rejection: descending name (hash ascending doesn't rescue it).
try std.testing.expectEqual(std.math.Order.gt, compareTag("b", "aa", "a", "zz"));
}
test "artifact §4.5.2-m2: P cards are neither sorted nor deduplicated" {
// Table 2 lines 804-805: "Not sorted or deduplicated; order is semantic
// (primary parent first)."
try std.testing.expectEqual(IntraRunRule.unordered, intraRunRule('P').?);
}
test "artifact §4.5.2-m3: Q cards are unordered-repeatable with no duplicate guard" {
// Table 2 lines 807-808: "No duplicate guard or ordering check;
// unordered-repeatable."
try std.testing.expectEqual(IntraRunRule.unordered, intraRunRule('Q').?);
var set = SingleOccurrenceSet{};
try std.testing.expect(set.admit('Q'));
try std.testing.expect(set.admit('Q'));
}
test "artifact §4.5.3-p1: the comparator is unsigned, case-sensitive, and normalization-free" {
// Lines 827-829: "octets are compared as unsigned values,
// locale-independent, with no Unicode normalization and no case
// folding."
try std.testing.expectEqual(std.math.Order.lt, compareBytes("A", "a")); // 0x41 < 0x61
// 0x80 vs 0x7F: unsigned compare puts 0x80 above 0x7F (a signed i8
// compare would invert this, since 0x80 as i8 is negative).
try std.testing.expectEqual(
std.math.Order.gt,
compareBytes(&[_]u8{0x80}, &[_]u8{0x7F}),
);
// A precomposed vs. decomposed representation of the same visual
// character (e.g. NFC "é" = 0xC3 0xA9 vs. NFD "e" + combining acute =
// 0x65 0xCC 0x81) compares by raw octets, not by Unicode equivalence:
// first byte 0xC3 > 0x65, so no normalization occurred.
const nfc = [_]u8{ 0xC3, 0xA9 };
const nfd = [_]u8{ 0x65, 0xCC, 0x81 };
try std.testing.expectEqual(std.math.Order.gt, compareBytes(&nfc, &nfd));
}
test "artifact §4.5.3-m1: shorter sorts before an extension, absent before present" {
// Lines 829-830: "A shorter string sorts before any extension of
// itself, and an absent value sorts before any present value."
try std.testing.expectEqual(std.math.Order.lt, compareBytes("abc", "abcd"));
try std.testing.expectEqual(std.math.Order.lt, compareOptional(null, "x"));
// Absent precedes even the empty string.
try std.testing.expectEqual(std.math.Order.lt, compareOptional(null, ""));
try std.testing.expectEqual(std.math.Order.eq, compareOptional(null, null));
try std.testing.expectEqual(std.math.Order.eq, compareOptional("", ""));
}
test "artifact §4.5.3-m2: the directory separator receives no special treatment" {
// Line 831: "The directory separator / (0x2F) receives no special
// treatment."
try std.testing.expectEqual(std.math.Order.lt, compareBytes("a.b", "a/b")); // 0x2E < 0x2F
try std.testing.expectEqual(std.math.Order.lt, compareBytes("a/", "a0")); // 0x2F < 0x30
// Shorter-first still wins even though '/' would otherwise look like a
// path-component boundary worth privileging.
try std.testing.expectEqual(std.math.Order.lt, compareBytes("a", "a/b"));
}
test "artifact §4.6.1-r1: an empty decoded name is rejected" {
// Line 862: "it is empty".
try std.testing.expect(!isSimplePathname(""));
}
test "artifact §4.6.1-r2: a leading slash is rejected" {
// Line 864: "its first byte is /".
try std.testing.expect(!isSimplePathname("/foo"));
try std.testing.expect(!isSimplePathname("/"));
}
test "artifact §4.6.1-r3: the whole name being . or .. is rejected" {
// Line 866: "the whole string is . or ..".
try std.testing.expect(!isSimplePathname("."));
try std.testing.expect(!isSimplePathname(".."));
}
test "artifact §4.6.1-r4: a name beginning ./ or ../ is rejected" {
// Line 868: "it begins with ./ or ../".
try std.testing.expect(!isSimplePathname("./a"));
try std.testing.expect(!isSimplePathname("../a"));
}
test "artifact §4.6.1-r5: an embedded // is rejected" {
// Line 870: "it contains //".
try std.testing.expect(!isSimplePathname("a//b"));
}
test "artifact §4.6.1-r6: a trailing slash is rejected" {
// Line 872: "it ends with /".
try std.testing.expect(!isSimplePathname("a/"));
}
test "artifact §4.6.1-r7: a . or .. path element is rejected" {
// Lines 874-875: "any path element equals . or .. -- that is, it
// contains /./ or /../, or ends with /. or /..". Table-driven over all
// four element positions the RFC text distinguishes.
const cases = [_][]const u8{
"a/./b", // interior /./
"a/../b", // interior /../
"a/.", // ends /.
"a/..", // ends /..
};
for (cases) |name| try std.testing.expect(!isSimplePathname(name));
}
test "artifact §4.6.1-p1: the predicate accepts well-formed simple pathnames" {
// Positive-space / "if and only if" side of the procedure clause
// (line 858). Accept vectors from the §A.3 (lines 2561-2565) and §A.4
// (lines 2618-2620) worked-example F-card filenames, plus a nested
// path and two names that look dot-like but are not exact `.`/`..`
// elements (r3/r7 are exact-match, so these must pass).
const cases = [_][]const u8{
"alpha.txt", "beta.txt", "gamma.txt", "file-a.txt",
"a/b/c", "...", ".hidden",
};
for (cases) |name| try std.testing.expect(isSimplePathname(name));
}
test "artifact §4.6.1-m1: byte content does not affect acceptance" {
// Lines 877-880: "backslashes, spaces ... control bytes, and
// arbitrary or invalid UTF-8 sequences all pass. Accept or reject is
// decidable from the separator structure alone."
const accept_cases = [_][]const u8{
"a\\b", // backslash 0x5C
"a b", // space 0x20, reachable via \s decoding
"a\x00b", // control byte
"a\xffb", // arbitrary high byte
"\xc3\x28", // invalid UTF-8 (lone continuation-less lead byte)
};
for (accept_cases) |name| try std.testing.expect(isSimplePathname(name));
// A hostile name with bad separator structure still rejects, so
// acceptance above is not vacuous permissiveness.
try std.testing.expect(!isSimplePathname("a\xff/../b"));
}
test "artifact §4.6.1-m2: the writer profile does not constrain acceptance" {
// Lines 888-890: "A validator of existing artifacts MUST apply only
// the parser rules; the writer profile does not constrain acceptance."
// This name violates both writer-only bans (lines 882-887): it has a
// backslash and an invalid UTF-8 byte. The validator still accepts it
// because its separator structure is clean.
try std.testing.expect(isSimplePathname("dir/a\\\xffb.txt"));
}
test "artifact §4.6.2-r1: a leading space, control byte, or empty name is rejected" {
// Lines 907-908: "its first byte is less than or equal to 0x20 (this
// rejects the empty string, a leading space, and any leading control
// byte)".
try std.testing.expect(!isWikiName(""));
try std.testing.expect(!isWikiName(" abc")); // leading 0x20
try std.testing.expect(!isWikiName("\x00abc")); // leading NUL
try std.testing.expect(!isWikiName("\x1fabc")); // leading control
}
test "artifact §4.6.2-r2: a control byte after the first is rejected" {
// Lines 910-911: "any subsequent byte is less than 0x20 (all control
// bytes, including tab and LF)".
const cases = [_][]const u8{
"a\tb", // 0x09 tab
"a\nb", // 0x0A LF
"a\x1fb", // interior control
"ab\x01", // trailing control
};
for (cases) |name| try std.testing.expect(!isWikiName(name));
}
test "artifact §4.6.2-r3: two consecutive spaces are rejected" {
// Line 913: "two consecutive space bytes (0x20 0x20) appear anywhere".
try std.testing.expect(!isWikiName("a b")); // interior double space
try std.testing.expect(!isWikiName("ab cd")); // triple contains a pair
}
test "artifact §4.6.2-r4: a trailing space is rejected" {
// Line 915: "its final byte is a space (0x20)".
try std.testing.expect(!isWikiName("abc "));
try std.testing.expect(!isWikiName("a b "));
}
test "artifact §4.6.2-r5: a length outside 1 through 100 is rejected" {
// Line 917: "its total length in bytes is outside 1 through 100
// inclusive." Boundary accepts (1-byte and 100-byte) live in the p1
// test to keep the iff both-sided.
try std.testing.expect(!isWikiName("")); // 0 bytes
try std.testing.expect(!isWikiName("x" ** 101)); // 101 bytes
}
test "artifact §4.6.2-p1: the predicate accepts well-formed wiki names" {
// Positive/"if and only if" side of the procedure clause (line
// 903-905).
const cases = [_][]const u8{
"A", // r5 lower bound, 1 byte
"Wiki Page",
"a b c", // single interior spaces are fine
"x" ** 100, // r5 upper bound, 100 bytes
};
for (cases) |name| try std.testing.expect(isWikiName(name));
}
test "artifact §4.6.2-m1: bytes >= 0x21 are unconstrained; length is bytes not characters" {
// Lines 919-921: "All bytes greater than or equal to 0x21 are
// otherwise unconstrained: 0x7F and all bytes at or above 0x80 are
// accepted, no UTF-8 validity check is performed, and length is
// counted in bytes, not characters."
const accept_cases = [_][]const u8{
"a\x7fb", // 0x7F
"a\x80b", // >=0x80
"\xff\xfe", // arbitrary high bytes
"\xc3\x28", // invalid UTF-8 (lone continuation-less lead byte)
};
for (accept_cases) |name| try std.testing.expect(isWikiName(name));
// Length-in-bytes proof: 50 two-byte sequences = 100 bytes accepts;
// 51 = 102 bytes rejects even though it is only 51 "characters".
try std.testing.expect(isWikiName("\xc3\xa9" ** 50));
try std.testing.expect(!isWikiName("\xc3\xa9" ** 51));
// A control byte still rejects even amid high bytes, so acceptance
// above is not vacuous permissiveness.
try std.testing.expect(!isWikiName("a\x80\x01"));
}
test "artifact §4.6.3-p1: one sidecar suffix stripped, case-insensitively" {
// Line 929: "ends, case-insensitively, with exactly one of -wal, -shm,
// or -journal, strip that one suffix". "_FOSSIL_-WAL" strips to
// "_FOSSIL_" (mixed case), which is then the whole-string tail (p2/p3a)
// => reserved.
try std.testing.expect(isReservedFilename("_FOSSIL_-WAL"));
// "Exactly one": only the LAST-matching suffix strips. "-shm" strips
// first (it's the trailing one), leaving "_FOSSIL_-wal", which ends
// with neither reserved tail (line 934) => p2 fails => not reserved.
try std.testing.expect(!isReservedFilename("_FOSSIL_-wal-shm"));
}
test "artifact §4.6.3-p2: reserved tail matched case-insensitively" {
// Lines 933-934: "_FOSSIL_ (8 bytes) or .fslckout (9 bytes)",
// case-insensitive.
try std.testing.expect(isReservedFilename("_fossil_")); // lowercase tail
try std.testing.expect(isReservedFilename(".FSLCKOUT")); // uppercase tail
// No reserved tail present at all => p2 fails.
try std.testing.expect(!isReservedFilename("readme.txt"));
}
test "artifact §4.6.3-p3: tail qualifies only via whole-string, slash, or sidecar" {
// Line 936: "the reserved tail is the entire remaining string".
try std.testing.expect(isReservedFilename("_FOSSIL_"));
try std.testing.expect(isReservedFilename(".fslckout"));
// Line 937: "the byte immediately before the tail is /".
try std.testing.expect(isReservedFilename("dir/_FOSSIL_"));
try std.testing.expect(isReservedFilename("dir/.fslckout"));
}
test "artifact §4.6.3-r1: a tail match satisfying no step-3 condition is not reserved" {
// Line 940, verbatim worked counter-example: "A tail match that
// satisfies none of step 3, such as x_FOSSIL_, is not reserved." Not
// the whole string, the byte before the tail is 'x' not '/', and no
// sidecar suffix was stripped.
try std.testing.expect(!isReservedFilename("x_FOSSIL_"));
}
test "artifact §4.6.3-w1: a sidecar-suffix match is reserved via the step-1 strip" {
// Lines 941-942, verbatim worked examples: "A sidecar-suffix match such
// as x_FOSSIL_-wal or dir/.fslckout-journal is reserved because step 1
// stripped a suffix." Neither would qualify under 3a/3b alone (tail
// isn't the whole string and isn't preceded by '/' after the strip
// leaves "x_FOSSIL_" / "dir/.fslckout"), so only 3c (sidecar stripped)
// explains the reserved verdict.
try std.testing.expect(isReservedFilename("x_FOSSIL_-wal"));
try std.testing.expect(isReservedFilename("dir/.fslckout-journal"));
}
test "artifact §4.6.3-m1: excludes platform checkout filters and Windows device names" {
// Lines 943-945: "The predicate is limited to Fossil repository
// metadata filenames; it does not include platform checkout filters or
// Windows device names such as CON." CON has no reserved tail, so p2
// fails outright -- this locks the scope, not merely a coincidence of
// step 3.
try std.testing.expect(!isReservedFilename("CON"));
}
test "artifact §4.6.3-r2: reserved-ness is a bool drop signal, never a parse error" {
// Line 961: "MUST be silently dropped, not rejected." Locked
// structurally: the return type is bool, not an error union, so there
// is no rejection variant for a reserved name to be mistaken for.
try std.testing.expectEqual(bool, @TypeOf(isReservedFilename("_FOSSIL_")));
try std.testing.expect(isReservedFilename("_FOSSIL_"));
}
test "artifact §4.6.3: sidecar strip with no remaining reserved tail is not reserved" {
// "foo-wal" strips to "foo" (step 1), which ends with neither reserved
// tail (step 2 fails) => not reserved. Covers the sidecar-strip path
// that does NOT lead to a match.
try std.testing.expect(!isReservedFilename("foo-wal"));
}
test "artifact §4.6.3: the empty string is not reserved" {
// No reserved tail can match an empty remaining string; also exercises
// the underflow-guard boundary (remaining.len < tail_len).
try std.testing.expect(!isReservedFilename(""));
}
test "artifact §4.6.3-m2: sort check runs before the reserved-name drop" {
// Lines 967-969: "the F card sort check (Section 4.5.2) runs before
// the reserved-name drop, so a reserved-named F card must itself be in
// sort position relative to the previously retained entry." Anchor
// starts at "z"; "_FOSSIL_" is reserved but sorts BEFORE "z"
// ('_' 0x5F < 'z' 0x7A), so the order check rejects it first -- it is
// never reached far enough to be dropped.
var adm = FCardAdmission{};
try std.testing.expect(try adm.admit("z"));
try std.testing.expectError(FCardSortError.OutOfOrder, adm.admit("_FOSSIL_"));
// Reserved-ness alone never rescues an out-of-order name: "a" is not
// reserved and is also out of order against anchor "z".
try std.testing.expectError(FCardSortError.OutOfOrder, adm.admit("a"));
}
test "artifact §4.6.3-m2: the dropped name never becomes the comparison anchor" {
// Lines 969-972: "the next F card is compared against that same
// retained entry. The dropped name never becomes the comparison
// anchor." "dir/_FOSSIL_" is in sort order after "a" (passes §4.5.2-r1)
// and is reserved (3b, preceded by '/'), so it is dropped -- anchor
// stays "a". "b" then proves the anchor is still "a": "b" sorts after
// "a" but BEFORE "dir/_FOSSIL_" ('b' 0x62 < 'd' 0x64), so if the dropped
// name had wrongly become the anchor, "b" would fail the order check.
var adm = FCardAdmission{};
try std.testing.expect(try adm.admit("a"));
try std.testing.expect(!(try adm.admit("dir/_FOSSIL_"))); // dropped
try std.testing.expect(try adm.admit("b")); // compared against "a", not "dir/_FOSSIL_"
}
test "artifact §4.6.3-r2: a dropped reserved F card is removed from the effective file set" {
// Lines 961-963: "An F card whose decoded current filename is reserved
// MUST be silently dropped, not rejected. Dropping removes the card
// from the effective file set." Feed three cards in wire order and
// collect the retained ones: the reserved middle card must be absent
// from the resulting set.
const wire_order = [_][]const u8{ "a", "dir/_FOSSIL_", "b" };
var effective_set: [wire_order.len][]const u8 = undefined;
var effective_len: usize = 0;
var adm = FCardAdmission{};
for (wire_order) |name| { // bound: wire_order.len, fixed at 3
if (try adm.admit(name)) {
std.debug.assert(effective_len < wire_order.len);
effective_set[effective_len] = name;
effective_len += 1;
}
}
try std.testing.expectEqual(@as(usize, 2), effective_len);
try std.testing.expectEqualStrings("a", effective_set[0]);
try std.testing.expectEqualStrings("b", effective_set[1]);
}
test "artifact §4.6.3: the rule tests only the current filename, never old-name" {
// Lines 957-959: "This rule applies only to the current filename field
// of an F card, after escape decoding. The old-name field is not
// tested by this reserved-name rule." Reinforced at lines 1202-1203.
// Simulate a rename FROM a reserved name TO a plain one: old-name
// "_FOSSIL_" is reserved (sanity check below), but `admit` has exactly
// one filename parameter -- the decoded current name -- so old-name
// never reaches the check. The card is retained, not dropped.
const old_name = "_FOSSIL_";
try std.testing.expect(isReservedFilename(old_name)); // would drop, if tested
var adm = FCardAdmission{};
try std.testing.expect(try adm.admit("plain.txt")); // current name only: not reserved
}
test "artifact §4.6.4-p1: the token is classified raw, never escape-decoded" {
// p1: "The D and E card timestamps are each a single raw (never
// decoded) token." Feed a wire token containing a literal backslash
// (0x5C) followed by 't' (0x74) where a decoded tab would sit: if this
// were escape-decoded first, \t -> 0x09 (whitespace) would satisfy the
// m1 separator and the token would classify as date_time. Undecoded, the
// separator scan sees 0x5C, which is neither 'T' nor whitespace, so the
// date-time branch has zero separator bytes and rejects.
const token = "2017-12-06\\t15:36:24";
try std.testing.expect(classifyTimestamp(token) == null);
}
test "artifact §4.6.4-m1: date-time accepts T and whitespace separators, optional :SS/.frac" {
// m1: "a date-time: an optional leading -, then YYYY-MM-DD, then one or
// more separator bytes each of which is either a whitespace-class byte
// or the uppercase letter T, then HH:MM, HH:MM:SS, or HH:MM:SS. followed
// by one or more digits"
const cases = [_][]const u8{
"2017-12-06T15:36:24",
"2017-12-06T15:36",
"2017-12-06T15:36:24.287", // w1
"2017-12-06\x0915:36:24", // tab separator (whitespace class)
"2017-12-06TT15:36", // 1+ separator bytes
"-2017-12-06T15:36:24", // optional leading '-'
};
for (cases) |c| { // bound: cases.len, fixed at 6
try std.testing.expect(classifyTimestamp(c).? == .date_time);
}
}
test "artifact §4.6.4-m2: optional timezone, +HH:MM / -HH:MM / Z / z, optional leading ws" {
// m2: "then an optional timezone of optional whitespace plus +HH:MM,
// -HH:MM, Z, or z"
const cases = [_][]const u8{
"2017-12-06T15:36:24+05:30",
"2017-12-06T15:36:24-08:00",
"2017-12-06T15:36:24Z",
"2017-12-06T15:36:24z",
"2017-12-06T15:36:24\x09Z", // optional whitespace before tz
};
for (cases) |c| { // bound: cases.len, fixed at 5
try std.testing.expect(classifyTimestamp(c).? == .date_time);
}
}
test "artifact §4.6.4-r1: lowercase t as the separator is rejected" {
// r1 verbatim: "Lowercase t as the separator is rejected."
try std.testing.expect(classifyTimestamp("2017-12-06t15:36:24") == null);
}
test "artifact §4.6.4-m3: date only, with and without leading -" {
// m3: "A date only: optional leading -, then YYYY-MM-DD."
try std.testing.expect(classifyTimestamp("2017-12-06").? == .date);
try std.testing.expect(classifyTimestamp("-2017-12-06").? == .date);
}
test "artifact §4.6.4-m4: time only is interpreted on 2000-01-01" {
// m4: "A time only: HH:MM, optionally :SS and a fraction, interpreted
// as a time on 2000-01-01." Year 2000 is always well past the r3
// boundary, so the gate never has anything to reject for this form.
const cases = [_][]const u8{ "15:36", "15:36:24", "15:36:24.287" };
for (cases) |c| { // bound: cases.len, fixed at 3
try std.testing.expect(classifyTimestamp(c).? == .time);
}
}
test "artifact §4.6.4-m5: now/subsec/subsecond, case-insensitive" {
// m5 verbatim: "The literals now, subsec, or subsecond, case-insensitive."
const cases = [_][]const u8{ "now", "NOW", "Now", "subsec", "SubSec", "subsecond", "SUBSECOND" };
for (cases) |c| { // bound: cases.len, fixed at 7
try std.testing.expect(classifyTimestamp(c).? == .relative);
}
}
test "artifact §4.6.4-m6: bare decimal accepted as Julian day iff 0 < r < 5373484.5" {
// m6 (frozen RFC L1014): "value r satisfies 0 < r < 5373484.5."
try std.testing.expect(classifyTimestamp("2451545").? == .julian);
try std.testing.expect(classifyTimestamp("2451545.0").? == .julian);
try std.testing.expect(classifyTimestamp("0.5").? == .julian);
try std.testing.expect(classifyTimestamp("5373484.4").? == .julian);
try std.testing.expect(classifyTimestamp("5373484.5") == null); // upper bound exclusive
try std.testing.expect(classifyTimestamp("5373485") == null);
}
test "artifact §4.6.4-m7: exact digit counts and field ranges" {
// m7 verbatim: "the year is exactly 4 digits, the month exactly 2
// digits in 01-12, the day exactly 2 digits in 01-31 with no
// calendar-aware check (2024-02-31 is accepted), the hour 2 digits in
// 00-24, the minute and second 2 digits each in 00-59, and the fraction
// one or more digits."
const rejects = [_][]const u8{
"2017-1-06", // month 1 digit
"2017-13-06", // month 13
"2017-12-00", // day 00
"2017-12-32", // day 32
"2017-12-06T25:36", // hour 25
"2017-12-06T15:60", // minute 60
"2017-12-06T15:36:60", // second 60
"201-12-06", // year 3 digits
"20171-12-06", // year 5 digits
"2017-12-06T15:36:24.", // fraction 0 digits
};
for (rejects) |c| { // bound: rejects.len, fixed at 10
try std.testing.expect(classifyTimestamp(c) == null);
}
try std.testing.expect(classifyTimestamp("2017-12-31").? == .date);
try std.testing.expect(classifyTimestamp("2024-02-31").? == .date); // no calendar check
try std.testing.expect(classifyTimestamp("2017-12-06T24:00").? == .date_time); // hour 24 in 00-24
}
test "artifact §4.6.4-r3: JD>0 gate at the -4713-11-24 boundary and numeric 0" {
// r3: "the token's value expressed as a Julian day number MUST be
// greater than zero; this additionally rejects dates at or before
// -4713-11-24 and the numeric token 0." RESOLVED (ambiguities.md,
// 2026-07-09): date-granularity reading -- reject iff
// (year,month,day) <= (-4713,11,24), independent of time-of-day.
const rejects = [_][]const u8{
"-4713-11-24",
"-4713-11-24T18:00:00", // any time on the boundary date still rejects
"-4713-11-23",
"-4713-10-31",
"-4714-01-01",
"0",
"0.0",
};
for (rejects) |c| { // bound: rejects.len, fixed at 7
try std.testing.expect(classifyTimestamp(c) == null);
}
try std.testing.expect(classifyTimestamp("-4713-11-25").? == .date);
try std.testing.expect(classifyTimestamp("-4713-12-01").? == .date);
try std.testing.expect(classifyTimestamp("-4712-01-01").? == .date);
}
test "artifact §4.6.4-m8: the writer form is the interoperable absolute date-time" {
// m8: "For stored structural artifacts whose D or E card timestamp can
// feed repository-state ordering, a producer MUST emit the absolute
// date-time form above." isWriterTimestamp is the oracle for that exact
// canonical subset: YYYY-MM-DDTHH:MM:SS[.frac], no sign, no tz.
try std.testing.expect(isWriterTimestamp("2017-12-06T15:36:24.287")); // w1
try std.testing.expect(isWriterTimestamp("2017-12-06T15:36:24"));
const rejects = [_][]const u8{
"2017-12-06T15:36", // no :SS
"2017-12-06", // date only
"2017-12-06T15:36:24Z", // tz present
"-2017-12-06T15:36:24", // leading sign
"2017-12-06\x0915:36:24", // non-'T' separator
};
for (rejects) |c| { // bound: rejects.len, fixed at 5
try std.testing.expect(!isWriterTimestamp(c));
}
}
test "artifact §4.6.4-s1: a reader accepts the broader forms 2 through 5" {
// s1: "A reader MAY accept the broader forms in items 2 through 5 for
// compatibility." Realized as classifyTimestamp acceptance of one
// representative token per non-date-time form.
try std.testing.expect(classifyTimestamp("2017-12-06").? == .date);
try std.testing.expect(classifyTimestamp("15:36:24").? == .time);
try std.testing.expect(classifyTimestamp("now").? == .relative);
try std.testing.expect(classifyTimestamp("2451545").? == .julian);
}
test "artifact §4.6.4-m9: relative literals are classified, never resolved against a clock" {
// m9: "now, subsec, and subsecond are resolved against parse-time clock
// state rather than denoting a stable instant in the artifact bytes.
// Two ingestions of identical bytes can therefore yield different
// stored mtime values." classifyTimestamp does no clock resolution at
// all (that's [FossilRepoState]'s job); it is pure, so two calls on the
// identical bytes always agree on the FORM even though a later resolver
// may pick different instants.
try std.testing.expect(classifyTimestamp("now").? == .relative);
try std.testing.expect(classifyTimestamp("subsec").? == .relative);
try std.testing.expect(classifyTimestamp("subsecond").? == .relative);
try std.testing.expect(classifyTimestamp("now").? == classifyTimestamp("now").?);
}
test "artifact §4.6.4-w1: the technote worked example's D card timestamp" {
// w1 verbatim: "D 2017-12-06T15:36:24.287" -- the token after the card
// letter and space is the date-time form with a fractional second, and
// it is also the exact writer-canonical form (no sign, exactly 'T', SS
// present, no tz).
const token = "2017-12-06T15:36:24.287";
try std.testing.expect(classifyTimestamp(token).? == .date_time);
try std.testing.expect(isWriterTimestamp(token));
}
const A_CARD_HASH40: []const u8 = "0123456789" ** 4; // 40 hex chars
const A_CARD_HASH64: []const u8 = "0123456789abcdef" ** 4; // 64 hex chars
comptime {
std.debug.assert(A_CARD_HASH40.len == 40);
std.debug.assert(A_CARD_HASH64.len == 64);
}
test "artifact §4.7.2-p1: A card accepts filename+target and filename+target+source" {
// Grammar: `a-card = "A" SP filename SP target [ SP source ] LF`
// (line 1074) -- both the 2-token and 3-token forms are valid.
var buf: [256]u8 = undefined;
const two = tokenizeCardLine("A alpha.txt " ++ A_CARD_HASH40 ++ "\n").?;
const parsed_two = parseACard(two, &buf).?;
try std.testing.expectEqualStrings("alpha.txt", parsed_two.name);
try std.testing.expect(parsed_two.source == null);
const three = tokenizeCardLine("A alpha.txt " ++ A_CARD_HASH40 ++ " " ++ A_CARD_HASH64 ++ "\n").?;
const parsed_three = parseACard(three, &buf).?;
try std.testing.expectEqualStrings("alpha.txt", parsed_three.name);
try std.testing.expectEqualStrings(A_CARD_HASH64, parsed_three.source.?);
}
test "artifact §4.7.2-p1/§4.2.1: A card arity is exact" {
// p1: filename and target are both required. §4.2.1 line 610: "a
// fourth on A" -- the grammar has no room for a fourth token.
var buf: [256]u8 = undefined;
const zero = tokenizeCardLine("A\n").?;
try std.testing.expect(parseACard(zero, &buf) == null);
const one = tokenizeCardLine("A alpha.txt\n").?;
try std.testing.expect(parseACard(one, &buf) == null);
const four = tokenizeCardLine("A alpha.txt " ++ A_CARD_HASH40 ++ " " ++ A_CARD_HASH64 ++ " extra\n").?;
try std.testing.expect(parseACard(four, &buf) == null);
}
test "artifact §4.7.2-p1/§4.5.1: a second A card is a syntax error" {
// "a second A card is a syntax error" (line 1077); A is single-
// occurrence (occurrenceClass('A') == .single, line 575).
var set = SingleOccurrenceSet{};
try std.testing.expect(set.admit('A'));
try std.testing.expect(!set.admit('A'));
}
test "artifact §4.7.2-r1: the filename must pass the pathname predicate" {
// "MUST pass the pathname predicate (Section 4.6.1)" (line 1083).
var buf: [256]u8 = undefined;
const cases = [_][]const u8{
"A /etc " ++ A_CARD_HASH40 ++ "\n", // leading '/' (§4.6.1-r2)
"A " ++ A_CARD_HASH40 ++ "\n", // empty filename token (§4.6.1-r1)
"A a/../b " ++ A_CARD_HASH40 ++ "\n", // path element .. (§4.6.1-r7)
};
for (cases) |line| {
const card = tokenizeCardLine(line).?;
try std.testing.expect(parseACard(card, &buf) == null);
}
}
test "artifact §4.7.2-m1: only the final path component is retained" {
// "Only its final path component is retained ... any directory
// prefix is discarded" (lines 1084-1086).
var buf: [256]u8 = undefined;
const cases = [_]struct { line: []const u8, want: []const u8 }{
.{ .line = "A dir/sub/f.txt " ++ A_CARD_HASH40 ++ "\n", .want = "f.txt" },
.{ .line = "A a/b " ++ A_CARD_HASH40 ++ "\n", .want = "b" },
.{ .line = "A x " ++ A_CARD_HASH40 ++ "\n", .want = "x" },
};
for (cases) |c| {
const card = tokenizeCardLine(c.line).?;
const parsed = parseACard(card, &buf).?;
try std.testing.expectEqualStrings(c.want, parsed.name);
}
}
test "artifact §4.7.2-r2: target is a valid hash or well-formed wiki name" {
// "syntactically either a valid artifact hash or a well-formed wiki
// name" (lines 1088-1089).
var buf: [256]u8 = undefined;
const accept = [_][]const u8{
"A x " ++ A_CARD_HASH40 ++ "\n",
"A x " ++ A_CARD_HASH64 ++ "\n",
"A x My\\sPage\n", // decodes to "My Page", a well-formed wiki name
};
for (accept) |line| {
const card = tokenizeCardLine(line).?;
try std.testing.expect(parseACard(card, &buf) != null);
}
// Fails both alternatives: not 40/64 hex, and >100 bytes (wiki r5).
const reject_line = "A x " ++ ("g" ** 101) ++ "\n";
const reject_card = tokenizeCardLine(reject_line).?;
try std.testing.expect(parseACard(reject_card, &buf) == null);
}
test "artifact §4.7.2-r3: a present source is a raw, never-decoded artifact hash" {
// "a raw (never decoded) artifact hash validated per the hash map"
// (line 1093); §4.3.1 line 680: "but not the source hash".
var buf: [256]u8 = undefined;
const accept40 = tokenizeCardLine("A x " ++ A_CARD_HASH40 ++ " " ++ A_CARD_HASH40 ++ "\n").?;
try std.testing.expectEqualStrings(A_CARD_HASH40, parseACard(accept40, &buf).?.source.?);
const accept64 = tokenizeCardLine("A x " ++ A_CARD_HASH40 ++ " " ++ A_CARD_HASH64 ++ "\n").?;
try std.testing.expectEqualStrings(A_CARD_HASH64, parseACard(accept64, &buf).?.source.?);
const reject = tokenizeCardLine("A x " ++ A_CARD_HASH40 ++ " xyz\n").?;
try std.testing.expect(parseACard(reject, &buf) == null);
// Decode asymmetry: the identical 41 wire bytes that decode to a
// 40-hex TARGET (previous test) are read RAW in the source slot -- 41
// raw bytes match neither 40 nor 64, so this rejects.
const wire = "\\a" ++ ("0" ** 39);
const asymmetry = tokenizeCardLine("A x " ++ A_CARD_HASH40 ++ " " ++ wire ++ "\n").?;
try std.testing.expect(parseACard(asymmetry, &buf) == null);
}
test "artifact §4.7.2-m2: an absent source is empty (null)" {
// "When absent, the source field is empty" (lines 1094-1095).
var buf: [256]u8 = undefined;
const card = tokenizeCardLine("A x " ++ A_CARD_HASH40 ++ "\n").?;
try std.testing.expect(parseACard(card, &buf).?.source == null);
}
test "artifact §4.7.3-p1: B card accepts one hash token, 40 or 64 hex" {
// "Exactly one hash token, validated per the hash map" (line 1129).
const forty = tokenizeCardLine("B " ++ A_CARD_HASH40 ++ "\n").?;
try std.testing.expectEqualStrings(A_CARD_HASH40, parseBCard(forty).?.hash);
const sixtyfour = tokenizeCardLine("B " ++ A_CARD_HASH64 ++ "\n").?;
try std.testing.expectEqualStrings(A_CARD_HASH64, parseBCard(sixtyfour).?.hash);
// A second token has no grammar slot to land in.
const extra = tokenizeCardLine("B " ++ A_CARD_HASH40 ++ " x\n").?;
try std.testing.expect(parseBCard(extra) == null);
}
test "artifact §4.7.3-r1: b-card grammar rejects bad arity and non-hash" {
// `b-card = %x42 SP hash LF` (line 1127).
try std.testing.expect(parseBCard(tokenizeCardLine("B\n").?) == null);
try std.testing.expect(parseBCard(tokenizeCardLine("B " ++ A_CARD_HASH40 ++ " x\n").?) == null);
try std.testing.expect(parseBCard(tokenizeCardLine("B xyz\n").?) == null);
try std.testing.expect(parseBCard(tokenizeCardLine("B " ++ A_CARD_HASH40[0..39] ++ "\n").?) == null);
}
test "artifact §4.7.4-p1: C requires exactly one escape-decoded comment" {
// A.1.2 (line 2471): "C Test\stech-note" decodes to "Test tech-note".
var buf: [256]u8 = undefined;
const card = tokenizeCardLine("C Test\\stech-note\n").?;
try std.testing.expectEqualStrings("Test tech-note", parseCCard(card, &buf).?.comment);
// Two tokens: no second grammar slot.
const two_tok = tokenizeCardLine("C a b\n").?;
try std.testing.expect(parseCCard(two_tok, &buf) == null);
}
test "artifact §4.7.4-r1: a C card with no token MUST be rejected" {
// "a C card with no token MUST be rejected" (line 1143).
var buf: [256]u8 = undefined;
try std.testing.expect(parseCCard(tokenizeCardLine("C\n").?, &buf) == null);
// Empty token: `comment = 1*tchar` (Appendix B) needs >=1 byte.
try std.testing.expect(parseCCard(tokenizeCardLine("C \n").?, &buf) == null);
}
test "artifact §4.7.4-m1: the decoded comment is otherwise unvalidated" {
// "The decoded comment content is otherwise unvalidated" (lines
// 1143-1144) -- a comment decoding to control bytes still parses.
var buf: [256]u8 = undefined;
const card = tokenizeCardLine("C \\t\\n\n").?;
try std.testing.expectEqualStrings("\t\n", parseCCard(card, &buf).?.comment);
}
const D_CARD_TS = "2017-12-06T15:36:24.287"; // A.1.2 line 2472
const E_CARD_ID = "bfdf42305400f6caa441fa816cb89231e90f8c82"; // A.1.2 line 2473, 40 hex
test "artifact §4.7.5-p1: D and E timestamps validate per §4.6.4 (raw)" {
// "Each timestamp is a single raw token validated per Section 4.6.4"
// (line 1153) -- .timestamp equals the wire bytes verbatim, never
// decoded.
const d = tokenizeCardLine("D " ++ D_CARD_TS ++ "\n").?;
try std.testing.expectEqualStrings(D_CARD_TS, parseDCard(d).?.timestamp);
const e = tokenizeCardLine("E " ++ D_CARD_TS ++ " " ++ E_CARD_ID ++ "\n").?;
try std.testing.expectEqualStrings(D_CARD_TS, parseECard(e).?.timestamp);
}
test "artifact §4.7.5-p2: E requires a technote-id per the hash map" {
// `e-card = ... SP technote-id LF` (line 1151). EVENT-type marking is
// §5.3 type resolution, deferred to the manifest-assembly batch.
const with_id = tokenizeCardLine("E " ++ D_CARD_TS ++ " " ++ E_CARD_ID ++ "\n").?;
try std.testing.expectEqualStrings(E_CARD_ID, parseECard(with_id).?.technote_id);
const missing_id = tokenizeCardLine("E " ++ D_CARD_TS ++ "\n").?;
try std.testing.expect(parseECard(missing_id) == null);
}
test "artifact §4.7.5-r1: d-card / e-card grammars reject bad arity" {
// Lines 1150-1151.
try std.testing.expect(parseDCard(tokenizeCardLine("D\n").?) == null);
try std.testing.expect(parseDCard(tokenizeCardLine("D " ++ D_CARD_TS ++ " b\n").?) == null);
try std.testing.expect(parseECard(tokenizeCardLine("E\n").?) == null);
try std.testing.expect(parseECard(tokenizeCardLine("E " ++ D_CARD_TS ++ "\n").?) == null);
try std.testing.expect(parseECard(tokenizeCardLine("E " ++ D_CARD_TS ++ " " ++ E_CARD_ID ++ " x\n").?) == null);
}
test "artifact §4.7.5-r2: a timestamp failing §4.6.4 is rejected" {
// "validated per Section 4.6.4" (line 1152).
try std.testing.expect(parseDCard(tokenizeCardLine("D \n").?) == null); // empty
try std.testing.expect(parseDCard(tokenizeCardLine("D banana\n").?) == null);
try std.testing.expect(parseDCard(tokenizeCardLine("D 2017-12-06t15:36:24\n").?) == null); // r1 lowercase t
// RESOLVED date-granularity gate (§4.6.4, ambiguities.md): reject at or
// before -4713-11-24 regardless of time-of-day.
try std.testing.expect(parseDCard(tokenizeCardLine("D -4713-11-24T18:00:00\n").?) == null);
}
test "artifact §4.7.5-r3: E technote-id validated per the hash map (raw)" {
// "a technote identifier validated per the hash map" (line 1152);
// §4.7.1 -- hash/checksum positions are never escape-decoded.
const accept40 = tokenizeCardLine("E " ++ D_CARD_TS ++ " " ++ A_CARD_HASH40 ++ "\n").?;
try std.testing.expect(parseECard(accept40) != null);
const accept64 = tokenizeCardLine("E " ++ D_CARD_TS ++ " " ++ A_CARD_HASH64 ++ "\n").?;
try std.testing.expect(parseECard(accept64) != null);
try std.testing.expect(parseECard(tokenizeCardLine("E " ++ D_CARD_TS ++ " " ++ A_CARD_HASH40[0..39] ++ "\n").?) == null);
try std.testing.expect(parseECard(tokenizeCardLine("E " ++ D_CARD_TS ++ " " ++ A_CARD_HASH40 ++ "1\n").?) == null);
try std.testing.expect(parseECard(tokenizeCardLine("E " ++ D_CARD_TS ++ " xyz\n").?) == null);
// A wire token whose escape decode would be 40 hex is read RAW here --
// 41 raw bytes match neither 40 nor 64, so it rejects (raw != decoded).
const wire_id = "\\a" ++ ("0" ** 39);
try std.testing.expect(parseECard(tokenizeCardLine("E " ++ D_CARD_TS ++ " " ++ wire_id ++ "\n").?) == null);
}
test "artifact §4.7.5-w1: A.1.2 D and E cards parse" {
// A.1.2, lines 2472-2473, exact bytes.
const d = tokenizeCardLine("D 2017-12-06T15:36:24.287\n").?;
const d_parsed = parseDCard(d).?;
try std.testing.expectEqualStrings("2017-12-06T15:36:24.287", d_parsed.timestamp);
const e = tokenizeCardLine("E 2013-08-26T07:35:00 bfdf42305400f6caa441fa816cb89231e90f8c82\n").?;
const e_parsed = parseECard(e).?;
try std.testing.expectEqualStrings("2013-08-26T07:35:00", e_parsed.timestamp);
try std.testing.expectEqualStrings("bfdf42305400f6caa441fa816cb89231e90f8c82", e_parsed.technote_id);
try std.testing.expectEqual(@as(usize, 40), e_parsed.technote_id.len); // SHA-1
}
test "artifact §4.7.6-p1: F parses across its positional forms" {
// `f-card = "F" SP filename [ SP hash [ SP perm [ SP old-name ] ] ] LF`
// (line 1160-1161) -- 1/2/3/4-token forms all valid; a 5th rejects.
var buf: [256]u8 = undefined;
const one = tokenizeCardLine("F f\n").?;
try std.testing.expect(parseFCard(one, &buf) != null);
const two = tokenizeCardLine("F f " ++ A_CARD_HASH40 ++ "\n").?;
try std.testing.expect(parseFCard(two, &buf) != null);
const three = tokenizeCardLine("F f " ++ A_CARD_HASH40 ++ " w\n").?;
try std.testing.expect(parseFCard(three, &buf) != null);
const four = tokenizeCardLine("F f " ++ A_CARD_HASH40 ++ " w old\n").?;
try std.testing.expect(parseFCard(four, &buf) != null);
const five = tokenizeCardLine("F f " ++ A_CARD_HASH40 ++ " w old extra\n").?;
try std.testing.expect(parseFCard(five, &buf) == null);
}
test "artifact §4.7.6-p1: an empty perm token is rejected (token = 1*tchar)" {
// `perm = token`, `token = 1*tchar` (Appendix B, lines 2846, 2852) -- no
// production admits a zero-length token, so a trailing SP after the hash
// (tokenizes to an empty 3rd token) must reject rather than silently
// become perm=="". Same slip via a double SP ahead of old-name: the 3rd
// token is still empty even though a 4th token follows.
var buf: [256]u8 = undefined;
try std.testing.expect(parseFCard(tokenizeCardLine("F f " ++ A_CARD_HASH40 ++ " \n").?, &buf) == null);
try std.testing.expect(parseFCard(tokenizeCardLine("F f " ++ A_CARD_HASH40 ++ " old\n").?, &buf) == null);
}
test "artifact §4.7.6-r1: the F filename is not optional" {
var buf: [256]u8 = undefined;
try std.testing.expect(parseFCard(tokenizeCardLine("F\n").?, &buf) == null);
try std.testing.expect(parseFCard(tokenizeCardLine("F \n").?, &buf) == null); // empty
}
test "artifact §4.7.6-m1/r2: filename escape-decoded and held to §4.6.1" {
// Line 1163-1164.
var buf: [256]u8 = undefined;
const good = tokenizeCardLine("F a\\sb " ++ A_CARD_HASH40 ++ "\n").?;
try std.testing.expectEqualStrings("a b", parseFCard(good, &buf).?.filename);
const bad_cases = [_][]const u8{
"F /etc " ++ A_CARD_HASH40 ++ "\n", // §4.6.1-r2 leading '/'
"F a/../b " ++ A_CARD_HASH40 ++ "\n", // §4.6.1-r7 '..'
"F " ++ A_CARD_HASH40 ++ "\n", // empty filename token
};
for (bad_cases) |line| {
try std.testing.expect(parseFCard(tokenizeCardLine(line).?, &buf) == null);
}
}
test "artifact §4.7.6-r3: F hash, when present, is exactly 40 or 64 hex" {
var buf: [256]u8 = undefined;
try std.testing.expect(parseFCard(tokenizeCardLine("F f " ++ A_CARD_HASH40 ++ "\n").?, &buf) != null);
try std.testing.expect(parseFCard(tokenizeCardLine("F f " ++ A_CARD_HASH64 ++ "\n").?, &buf) != null);
try std.testing.expect(parseFCard(tokenizeCardLine("F f xyz\n").?, &buf) == null);
try std.testing.expect(parseFCard(tokenizeCardLine("F f " ++ A_CARD_HASH40[0..39] ++ "\n").?, &buf) == null);
try std.testing.expect(parseFCard(tokenizeCardLine("F f " ++ A_CARD_HASH40 ++ "1\n").?, &buf) == null);
}
test "artifact §4.7.6-m2/r4: old-name decoded and held to §4.6.1" {
var buf: [256]u8 = undefined;
const good = tokenizeCardLine("F f " ++ A_CARD_HASH40 ++ " w a\\sb\n").?;
try std.testing.expectEqualStrings("a b", parseFCard(good, &buf).?.old_name.?);
const bad_cases = [_][]const u8{
"F f " ++ A_CARD_HASH40 ++ " w /etc\n",
"F f " ++ A_CARD_HASH40 ++ " w a/../b\n",
};
for (bad_cases) |line| {
try std.testing.expect(parseFCard(tokenizeCardLine(line).?, &buf) == null);
}
}
test "artifact §4.7.6-m3: hash and permission tokens are never decoded" {
// Line 1166: "the hash and permission tokens are never decoded."
var buf: [256]u8 = undefined;
const card = tokenizeCardLine("F f " ++ A_CARD_HASH40 ++ " w\\sx\n").?;
const parsed = parseFCard(card, &buf).?;
try std.testing.expectEqualStrings(A_CARD_HASH40, parsed.hash.?);
try std.testing.expectEqualStrings("w\\sx", parsed.perm.?); // backslash preserved raw
}
test "artifact §4.7.6-m4: third token is raw perm (no {w,x,l} check); fourth is old-name" {
// Lines 1192-1196.
var buf: [256]u8 = undefined;
const weird_perm = tokenizeCardLine("F f " ++ A_CARD_HASH40 ++ " zzz\n").?;
const parsed_weird = parseFCard(weird_perm, &buf).?;
try std.testing.expectEqualStrings("zzz", parsed_weird.perm.?);
try std.testing.expect(parsed_weird.old_name == null); // 3-token: no old-name
const four_tok = tokenizeCardLine("F f " ++ A_CARD_HASH40 ++ " w old\n").?;
try std.testing.expectEqualStrings("old", parseFCard(four_tok, &buf).?.old_name.?);
}
test "artifact §4.7.6-w1: a hash-less F card parses (A.4.2)" {
// A.4.2, line 2652, exact bytes: "F file-c.txt" with no continuation.
var buf: [256]u8 = undefined;
const card = tokenizeCardLine("F file-c.txt\n").?;
const parsed = parseFCard(card, &buf).?;
try std.testing.expectEqualStrings("file-c.txt", parsed.filename);
try std.testing.expect(parsed.hash == null);
try std.testing.expect(parsed.perm == null);
try std.testing.expect(parsed.old_name == null);
try std.testing.expectEqual(FileMode.regular, parsed.mode);
}
test "artifact §4.7.6-w2: F alpha.txt with SHA3-256 hash parses (A.3)" {
// A.3, lines 2561-2562, exact bytes (unwrapped).
var buf: [256]u8 = undefined;
const card = tokenizeCardLine(
"F alpha.txt a8009a7a528d87778c356da3a55d964719e818666a04e4f960c9e2439e35f138\n",
).?;
const parsed = parseFCard(card, &buf).?;
try std.testing.expectEqualStrings("alpha.txt", parsed.filename);
try std.testing.expectEqualStrings(
"a8009a7a528d87778c356da3a55d964719e818666a04e4f960c9e2439e35f138",
parsed.hash.?,
);
try std.testing.expectEqual(@as(usize, 64), parsed.hash.?.len);
}
test "artifact §4.7.7-g-p1: G card accepts a raw 40- and 64-hex hash, never decoded" {
// Line 1207, 1212-1214: raw hash, validated per the hash map.
try std.testing.expectEqualStrings(
A_CARD_HASH40,
parseGCard(tokenizeCardLine("G " ++ A_CARD_HASH40 ++ "\n").?).?.hash,
);
try std.testing.expectEqualStrings(
A_CARD_HASH64,
parseGCard(tokenizeCardLine("G " ++ A_CARD_HASH64 ++ "\n").?).?.hash,
);
try std.testing.expect(parseGCard(tokenizeCardLine("G xyz\n").?) == null); // non-hex
try std.testing.expect(parseGCard(tokenizeCardLine("G " ++ A_CARD_HASH40[0..39] ++ "\n").?) == null); // wrong length
}
test "artifact §4.7.7-g-r1: G card rejects zero tokens and a second token" {
// Line 1211: "single-occurrence with exactly one required token" --
// occurrence/type-marking are deferred; arity is checked here.
try std.testing.expect(parseGCard(tokenizeCardLine("G\n").?) == null);
try std.testing.expect(parseGCard(tokenizeCardLine("G " ++ A_CARD_HASH40 ++ " x\n").?) == null);
}
test "artifact §4.7.7-i-p1: I card accepts a raw hash, never decoded" {
// Line 1209, 1214-1215.
try std.testing.expectEqualStrings(
A_CARD_HASH40,
parseICard(tokenizeCardLine("I " ++ A_CARD_HASH40 ++ "\n").?).?.hash,
);
try std.testing.expectEqualStrings(
A_CARD_HASH64,
parseICard(tokenizeCardLine("I " ++ A_CARD_HASH64 ++ "\n").?).?.hash,
);
try std.testing.expect(parseICard(tokenizeCardLine("I xyz\n").?) == null); // non-hex
}
test "artifact §4.7.7-i-r1: I card rejects zero tokens and a second token" {
try std.testing.expect(parseICard(tokenizeCardLine("I\n").?) == null);
try std.testing.expect(parseICard(tokenizeCardLine("I " ++ A_CARD_HASH40 ++ " x\n").?) == null);
}
test "artifact §4.7.7-h-p1: H card decodes the title and accepts any decoded content" {
// Line 1216-1217: "no content validation -- any decoded string is
// accepted." A title that fails the wiki-name predicate (leading space,
// double space, >100 bytes) is still accepted by H; contrast the
// parseLCard rejection test below.
var buf: [256]u8 = undefined;
try std.testing.expectEqualStrings(
"thread title",
parseHCard(tokenizeCardLine("H thread\\stitle\n").?, &buf).?.title,
);
try std.testing.expectEqualStrings(
" leading",
parseHCard(tokenizeCardLine("H \\sleading\n").?, &buf).?.title,
);
try std.testing.expectEqualStrings(
"a b",
parseHCard(tokenizeCardLine("H a\\s\\sb\n").?, &buf).?.title,
);
const over_100 = "x" ** 101;
try std.testing.expectEqualStrings(
over_100,
parseHCard(tokenizeCardLine("H " ++ over_100 ++ "\n").?, &buf).?.title,
);
}
test "artifact §4.7.7-h-r1: H card rejects zero tokens, an empty token, and a second token" {
var buf: [256]u8 = undefined;
try std.testing.expect(parseHCard(tokenizeCardLine("H\n").?, &buf) == null);
try std.testing.expect(parseHCard(tokenizeCardLine("H \n").?, &buf) == null); // empty token
try std.testing.expect(parseHCard(tokenizeCardLine("H a b\n").?, &buf) == null);
}
test "artifact §4.7.8-p1: J card parses fieldname and optional value" {
var buf: [256]u8 = undefined;
const with_value = parseJCard(tokenizeCardLine("J status open\n").?, &buf).?;
try std.testing.expectEqualStrings("status", with_value.field);
try std.testing.expectEqualStrings("open", with_value.value);
const no_value = parseJCard(tokenizeCardLine("J foo\n").?, &buf).?;
try std.testing.expectEqualStrings("foo", no_value.field);
try std.testing.expectEqualStrings("", no_value.value);
}
test "artifact §4.7.8-m1: J value is escape-decoded, field name stored verbatim" {
var buf: [256]u8 = undefined;
const parsed = parseJCard(tokenizeCardLine("J comment a\\sb\n").?, &buf).?;
try std.testing.expectEqualStrings("comment", parsed.field);
try std.testing.expectEqualStrings("a b", parsed.value);
}
test "artifact §4.7.8-m2: a leading + in the J field name is preserved" {
var buf: [256]u8 = undefined;
const parsed = parseJCard(tokenizeCardLine("J +comment hi\n").?, &buf).?;
try std.testing.expectEqualStrings("+comment", parsed.field);
}
test "artifact §4.7.8-m3: an absent J value is the empty string, not null" {
var buf: [256]u8 = undefined;
const parsed = parseJCard(tokenizeCardLine("J foo\n").?, &buf).?;
try std.testing.expectEqual(@as(usize, 0), parsed.value.len);
}
test "artifact §4.7.8-r1: J card rejects a missing field, an empty value token, and a third token" {
var buf: [256]u8 = undefined;
try std.testing.expect(parseJCard(tokenizeCardLine("J\n").?, &buf) == null);
try std.testing.expect(parseJCard(tokenizeCardLine("J \n").?, &buf) == null); // empty fieldname
try std.testing.expect(parseJCard(tokenizeCardLine("J foo \n").?, &buf) == null); // empty value token
try std.testing.expect(parseJCard(tokenizeCardLine("J foo bar baz\n").?, &buf) == null); // third token
}
test "artifact §4.7.9-p1: K card accepts a 40-hex ticket id, raw" {
try std.testing.expectEqualStrings(
A_CARD_HASH40,
parseKCard(tokenizeCardLine("K " ++ A_CARD_HASH40 ++ "\n").?).?.ticket_id,
);
}
test "artifact §4.7.9-r1: K card rejects a 64-char SHA3-length id" {
try std.testing.expect(parseKCard(tokenizeCardLine("K " ++ A_CARD_HASH64 ++ "\n").?) == null);
}
test "artifact §4.7.9-r2: K card rejects zero tokens and a second token" {
try std.testing.expect(parseKCard(tokenizeCardLine("K\n").?) == null);
try std.testing.expect(parseKCard(tokenizeCardLine("K " ++ A_CARD_HASH40 ++ " x\n").?) == null);
}
test "artifact §4.7.9-s1: K card takes the MAY as lowercase-only, rejecting uppercase" {
// Mirrors validateHashToken(.ticket_id)'s lowercase-only decision
// (line 1648 above) -- a1_1_name has hex letters, unlike A_CARD_HASH40's
// all-digit fixture, so the uppercase transform is observable.
var upper: [40]u8 = undefined;
for (a1_1_name, 0..) |c, i| upper[i] = std.ascii.toUpper(c);
var line: [43]u8 = undefined; // "K " + 40 + "\n"
line[0] = 'K';
line[1] = ' ';
@memcpy(line[2..42], &upper);
line[42] = '\n';
try std.testing.expect(parseKCard(tokenizeCardLine(&line).?) == null);
// Lowercase original still parses -- proving the reject is case-specific.
try std.testing.expect(parseKCard(tokenizeCardLine("K " ++ a1_1_name ++ "\n").?) != null);
}
test "artifact §4.7.10-p1: L card decodes the title and requires the wiki-name predicate" {
var buf: [256]u8 = undefined;
try std.testing.expectEqualStrings(
"Wiki Page",
parseLCard(tokenizeCardLine("L Wiki\\sPage\n").?, &buf).?.title,
);
}
test "artifact §4.7.10-r1: L card rejects a decoded title failing the wiki-name predicate" {
var buf: [256]u8 = undefined;
try std.testing.expect(parseLCard(tokenizeCardLine("L \\sWiki\n").?, &buf) == null); // leading space (§4.6.2 r1)
const over_100 = "x" ** 101;
try std.testing.expect(parseLCard(tokenizeCardLine("L " ++ over_100 ++ "\n").?, &buf) == null); // §4.6.2 r5
}
test "artifact §4.7.10-r2: L card rejects zero tokens and a second token" {
var buf: [256]u8 = undefined;
try std.testing.expect(parseLCard(tokenizeCardLine("L\n").?, &buf) == null);
try std.testing.expect(parseLCard(tokenizeCardLine("L Wiki Page\n").?, &buf) == null); // second token
}
test "artifact §4.7.11-p1: M card accepts a raw 40- and 64-hex hash, never decoded" {
// `m-card = %x4D SP hash LF` (line 1263); "validated per the hash map"
// (line 1265-1266). Ordering (strict-ascending run) is §4.5.2-r3,
// already tested there.
const forty = tokenizeCardLine("M " ++ A_CARD_HASH40 ++ "\n").?;
try std.testing.expectEqualStrings(A_CARD_HASH40, parseMCard(forty).?.hash);
const sixtyfour = tokenizeCardLine("M " ++ A_CARD_HASH64 ++ "\n").?;
try std.testing.expectEqualStrings(A_CARD_HASH64, parseMCard(sixtyfour).?.hash);
}
test "artifact §4.7.11-r1: M card rejects zero tokens, a second token, and a non-hash" {
const bad = [_][]const u8{
"M\n",
"M " ++ A_CARD_HASH40 ++ " x\n",
"M xyz\n",
"M " ++ A_CARD_HASH40[0..39] ++ "\n", // wrong length
};
for (bad) |line| {
try std.testing.expect(parseMCard(tokenizeCardLine(line).?) == null);
}
}
test "artifact §4.7.12-p1: N card decodes the mimetype token" {
// `n-card = %x4E SP mimetype LF` (line 1270).
var buf: [256]u8 = undefined;
const card = tokenizeCardLine("N application/json\n").?;
try std.testing.expectEqualStrings("application/json", parseNCard(card, &buf).?.mimetype);
}
test "artifact §4.7.12-m1: N mimetype is escape-decoded with no mime-syntax validation" {
// Escape decode applies (§4.3 Table 1 \s -> SP); a non-mimetype token
// is accepted verbatim -- "no mime-syntax validation" (line 1272).
var buf: [256]u8 = undefined;
try std.testing.expectEqualStrings(
"a b",
parseNCard(tokenizeCardLine("N a\\sb\n").?, &buf).?.mimetype,
);
try std.testing.expectEqualStrings(
"zzz",
parseNCard(tokenizeCardLine("N zzz\n").?, &buf).?.mimetype,
);
}
test "artifact §4.7.12-r1: N card rejects zero tokens, an empty token, and a second token" {
var buf: [256]u8 = undefined;
try std.testing.expect(parseNCard(tokenizeCardLine("N\n").?, &buf) == null);
try std.testing.expect(parseNCard(tokenizeCardLine("N \n").?, &buf) == null); // token = 1*tchar
try std.testing.expect(parseNCard(tokenizeCardLine("N a b\n").?, &buf) == null);
}
test "artifact §4.7.12-w1: N card of the §A.1.2 technote decodes to text/x-fossil-wiki" {
// §A.1.2 line 2475: "N text/x-fossil-wiki" -- no escapes, identity decode.
var buf: [256]u8 = undefined;
const card = tokenizeCardLine("N text/x-fossil-wiki\n").?;
try std.testing.expectEqualStrings("text/x-fossil-wiki", parseNCard(card, &buf).?.mimetype);
}
test "artifact §4.7.13-p1: P card parses parents in order, neither sorted nor deduped" {
// "Tokens are neither sorted nor deduplicated; their order is
// semantic" (line 1296).
const H_FF = "f" ** 40; // sorts after A_CARD_HASH40 lexically
const unsorted = tokenizeCardLine("P " ++ H_FF ++ " " ++ A_CARD_HASH40 ++ "\n").?;
const p1 = parsePCard(unsorted).?;
try std.testing.expectEqual(@as(usize, 2), p1.count);
var it1 = p1.hashes();
try std.testing.expectEqualStrings(H_FF, it1.next().?);
try std.testing.expectEqualStrings(A_CARD_HASH40, it1.next().?);
try std.testing.expect(it1.next() == null);
const dup = tokenizeCardLine("P " ++ A_CARD_HASH40 ++ " " ++ A_CARD_HASH40 ++ "\n").?;
try std.testing.expectEqual(@as(usize, 2), parsePCard(dup).?.count);
}
test "artifact §4.7.13-m1: an empty P card (zero tokens) MUST be accepted" {
// "An empty P card (zero tokens) MUST be accepted" (line 1293-1295).
const card = parsePCard(tokenizeCardLine("P\n").?).?;
try std.testing.expectEqual(@as(usize, 0), card.count);
try std.testing.expect(card.parents == null);
}
test "artifact §4.7.13-r1: P card rejects any token failing the hash map" {
const bad = [_][]const u8{
"P xyz\n", // non-hash
"P \n", // empty token is not a hash
"P " ++ A_CARD_HASH40 ++ " zzz\n", // second token bad
};
for (bad) |line| {
try std.testing.expect(parsePCard(tokenizeCardLine(line).?) == null);
}
}
test "artifact §4.7.13-w1: the §A.1.1 root check-in's empty P card parses to zero parents" {
// §A.1.1 line 2421: the bare "P" line, described at 2430-2432 as the
// root check-in's parent-less card.
const card = parsePCard(tokenizeCardLine("P\n").?).?;
try std.testing.expectEqual(@as(usize, 0), card.count);
}
test "artifact §4.7.14-p1: Q card parses q-sign, target hash, and optional baseline" {
// `q-card = %x51 SP q-sign hash [ SP hash ] LF` (line 1325-1326).
const cherry = parseQCard(tokenizeCardLine("Q +" ++ A_CARD_HASH40 ++ "\n").?).?;
try std.testing.expectEqual(QSign.cherrypick, cherry.sign);
try std.testing.expectEqualStrings(A_CARD_HASH40, cherry.target);
try std.testing.expect(cherry.baseline == null);
const backout = parseQCard(tokenizeCardLine("Q -" ++ A_CARD_HASH40 ++ " " ++ A_CARD_HASH64 ++ "\n").?).?;
try std.testing.expectEqual(QSign.backout, backout.sign);
try std.testing.expectEqualStrings(A_CARD_HASH40, backout.target);
try std.testing.expectEqualStrings(A_CARD_HASH64, backout.baseline.?);
}
test "artifact §4.7.14-m1: the sign byte is stripped and the remainder validated as a hash" {
// Lines 1329-1332: sign byte checked, remainder validated per the
// hash map; the '+'/'-' byte itself never appears in `target`.
const plus = parseQCard(tokenizeCardLine("Q +" ++ A_CARD_HASH40 ++ "\n").?).?;
try std.testing.expectEqual(@as(usize, 40), plus.target.len);
try std.testing.expectEqualStrings(A_CARD_HASH40, plus.target);
try std.testing.expectEqual(QSign.cherrypick, plus.sign);
const minus = parseQCard(tokenizeCardLine("Q -" ++ A_CARD_HASH64 ++ "\n").?).?;
try std.testing.expectEqualStrings(A_CARD_HASH64, minus.target);
try std.testing.expectEqual(QSign.backout, minus.sign);
}
test "artifact §4.7.14-m2: the optional second token is a plain unprefixed baseline hash" {
// Line 1332-1334: baseline is a plain unprefixed hash.
const card = parseQCard(tokenizeCardLine("Q +" ++ A_CARD_HASH40 ++ " " ++ A_CARD_HASH64 ++ "\n").?).?;
try std.testing.expectEqualStrings(A_CARD_HASH64, card.baseline.?);
// A '+'-prefixed second token is a 41-byte string, not a valid hash.
const prefixed = tokenizeCardLine("Q +" ++ A_CARD_HASH40 ++ " +" ++ A_CARD_HASH40 ++ "\n").?;
try std.testing.expect(parseQCard(prefixed) == null);
}
test "artifact §4.7.14-m3: an omitted baseline is left null, not resolved" {
// Lines 1334-1336: "the field is left null" -- not resolved to the
// primary parent at parse time.
const card = parseQCard(tokenizeCardLine("Q +" ++ A_CARD_HASH40 ++ "\n").?).?;
try std.testing.expect(card.baseline == null);
}
test "artifact §4.7.14-r1: Q card rejects a q-sign other than + or -" {
// `q-sign = "+" / "-"` (line 1327) -- Q forbids the '*' the T card
// allows; a bare hash with no sign byte also rejects.
try std.testing.expect(parseQCard(tokenizeCardLine("Q *" ++ A_CARD_HASH40 ++ "\n").?) == null);
try std.testing.expect(parseQCard(tokenizeCardLine("Q " ++ A_CARD_HASH40 ++ "\n").?) == null);
// `Q \n` tokenizes to one empty token (no sign byte at all, not even a
// bad one) -- the length guard ahead of the sign switch, not the
// switch's `else` arm, is what rejects this.
try std.testing.expect(parseQCard(tokenizeCardLine("Q \n").?) == null);
}
test "artifact §4.7.14-r2: Q card rejects zero tokens and a third token" {
try std.testing.expect(parseQCard(tokenizeCardLine("Q\n").?) == null);
const three = tokenizeCardLine("Q +" ++ A_CARD_HASH40 ++ " " ++ A_CARD_HASH64 ++ " " ++ A_CARD_HASH40 ++ "\n").?;
try std.testing.expect(parseQCard(three) == null);
}
test "artifact §4.7.14-r3: Q card rejects a remainder or baseline failing the hash map" {
const bad = [_][]const u8{
"Q +xyz\n", // bad remainder
"Q +\n", // sign only, empty remainder
"Q +" ++ A_CARD_HASH40 ++ " zzz\n", // bad baseline
};
for (bad) |line| {
try std.testing.expect(parseQCard(tokenizeCardLine(line).?) == null);
}
}
const MD5_32: []const u8 = "0123456789abcdef" ** 2; // 32 hex chars
comptime {
std.debug.assert(MD5_32.len == 32);
}
test "artifact §4.7.15-p1: R card accepts a 32-hex checksum, raw and never decoded" {
const card = tokenizeCardLine("R " ++ MD5_32 ++ "\n").?;
const parsed = parseRCard(card).?;
try std.testing.expectEqualStrings(MD5_32, parsed.checksum);
try std.testing.expectEqual(@as(usize, 32), parsed.checksum.len);
// §6.3.1-p1/-m1: no file content is present here at all, yet the card
// still parses -- the parse path never recomputes against file content.
}
test "artifact §4.7.15-r1: R card grammar rejects bad arity and a non-checksum" {
try std.testing.expect(parseRCard(tokenizeCardLine("R\n").?) == null); // zero tokens
const second = tokenizeCardLine("R " ++ MD5_32 ++ " " ++ MD5_32 ++ "\n").?;
try std.testing.expect(parseRCard(second) == null); // second token
try std.testing.expect(parseRCard(tokenizeCardLine("R " ++ A_CARD_HASH40 ++ "\n").?) == null); // 40 hex, not 32
try std.testing.expect(parseRCard(tokenizeCardLine("R " ++ MD5_32[0..31] ++ "g\n").?) == null); // non-hex
}
test "artifact §4.7.16-p1: T card parses name, target, and value in order" {
var buf: [256]u8 = undefined;
const card = tokenizeCardLine("T +tag " ++ A_CARD_HASH40 ++ " hi\n").?;
const parsed = parseTCard(card, &buf).?;
try std.testing.expectEqual(TSign.add, parsed.sign);
try std.testing.expectEqualStrings("tag", parsed.name);
try std.testing.expectEqualStrings(A_CARD_HASH40, parsed.target.?);
try std.testing.expectEqualStrings("hi", parsed.value.?);
}
test "artifact §4.7.16-p2: T target '*' is a self-referential tag with a null target" {
var buf: [256]u8 = undefined;
const star = tokenizeCardLine("T +tag *\n").?;
try std.testing.expect(parseTCard(star, &buf).?.target == null);
const hash = tokenizeCardLine("T +tag " ++ A_CARD_HASH64 ++ "\n").?;
try std.testing.expectEqualStrings(A_CARD_HASH64, parseTCard(hash, &buf).?.target.?);
}
test "artifact §4.7.16-r1: T card rejects a name not prefixed +/-/*" {
var buf: [256]u8 = undefined;
try std.testing.expect(parseTCard(tokenizeCardLine("T xtag " ++ A_CARD_HASH40 ++ "\n").?, &buf) == null);
// Bare hash as the name: first decoded char is a hex digit, not a sign.
const bare_hash_name = tokenizeCardLine("T " ++ A_CARD_HASH40 ++ " " ++ A_CARD_HASH40 ++ "\n").?;
try std.testing.expect(parseTCard(bare_hash_name, &buf) == null);
}
test "artifact §4.7.16-r2: T card rejects a remainder that is entirely hex digits" {
var buf: [256]u8 = undefined;
const bad = [_][]const u8{
"T +deadbeef " ++ A_CARD_HASH40 ++ "\n",
"T +dead " ++ A_CARD_HASH40 ++ "\n",
"T +DEAD " ++ A_CARD_HASH40 ++ "\n", // hexdig-ci: uppercase still all-hex
};
for (bad) |line| {
try std.testing.expect(parseTCard(tokenizeCardLine(line).?, &buf) == null);
}
// A non-hex byte in the remainder breaks the all-hex reject.
const ok = tokenizeCardLine("T +dea-d " ++ A_CARD_HASH40 ++ "\n").?;
try std.testing.expect(parseTCard(ok, &buf) != null);
}
test "artifact §4.7.16-r3: T card rejects a target failing '*' and the hash map" {
var buf: [256]u8 = undefined;
try std.testing.expect(parseTCard(tokenizeCardLine("T +tag xyz\n").?, &buf) == null);
// A decoded '*' does not count -- target is never decoded (m3).
try std.testing.expect(parseTCard(tokenizeCardLine("T +tag \\*\n").?, &buf) == null);
}
test "artifact §4.7.16-r4: T card rejects a fourth token" {
var buf: [256]u8 = undefined;
const four = tokenizeCardLine("T +tag " ++ A_CARD_HASH40 ++ " val extra\n").?;
try std.testing.expect(parseTCard(four, &buf) == null);
}
test "artifact §4.7.16-m1: an absent T value is null; a present empty value is not" {
var buf: [256]u8 = undefined;
const absent = tokenizeCardLine("T +tag " ++ A_CARD_HASH40 ++ "\n").?;
try std.testing.expect(parseTCard(absent, &buf).?.value == null);
const present_empty = tokenizeCardLine("T +tag " ++ A_CARD_HASH40 ++ " \n").?;
const parsed = parseTCard(present_empty, &buf).?;
try std.testing.expect(parsed.value != null);
try std.testing.expectEqualStrings("", parsed.value.?);
}
test "artifact §4.7.16-m2: the T value is escape-decoded" {
var buf: [256]u8 = undefined;
const card = tokenizeCardLine("T +tag " ++ A_CARD_HASH40 ++ " a\\sb\n").?;
try std.testing.expectEqualStrings("a b", parseTCard(card, &buf).?.value.?);
}
test "artifact §4.7.16-m3: the T target token is never decoded" {
var buf: [256]u8 = undefined;
// "\*" decodes to "*" but the raw wire token is 2 bytes, so it never
// matches the literal self-referential target and fails the hash map.
try std.testing.expect(parseTCard(tokenizeCardLine("T +tag \\*\n").?, &buf) == null);
}
test "artifact §4.7.16-m4: the T name is escape-decoded before the sign check" {
var buf: [256]u8 = undefined;
const cancel = tokenizeCardLine("T \\-tag " ++ A_CARD_HASH40 ++ "\n").?;
const parsed_cancel = parseTCard(cancel, &buf).?;
try std.testing.expectEqual(TSign.cancel, parsed_cancel.sign);
try std.testing.expectEqualStrings("tag", parsed_cancel.name);
const spaced = tokenizeCardLine("T +a\\sb " ++ A_CARD_HASH40 ++ "\n").?;
const parsed_spaced = parseTCard(spaced, &buf).?;
try std.testing.expectEqual(TSign.add, parsed_spaced.sign);
try std.testing.expectEqualStrings("a b", parsed_spaced.name);
}
test "artifact §4.7.17-p1: U card decodes a present login" {
var buf: [64]u8 = undefined;
const plain = tokenizeCardLine("U alice\n").?;
try std.testing.expectEqualStrings("alice", parseUCard(plain, &buf).?.login);
const escaped = tokenizeCardLine("U a\\sb\n").?;
try std.testing.expectEqualStrings("a b", parseUCard(escaped, &buf).?.login);
}
test "artifact §4.7.17-m1: a bare or empty U login substitutes 'anonymous'" {
var buf: [64]u8 = undefined;
const bare = tokenizeCardLine("U\n").?;
try std.testing.expectEqualStrings("anonymous", parseUCard(bare, &buf).?.login);
const empty = tokenizeCardLine("U \n").?;
try std.testing.expectEqualStrings("anonymous", parseUCard(empty, &buf).?.login);
}
test "artifact §4.7.17-m2: an empty U token returns a card, unlike parseNCard's empty-token reject" {
var buf: [64]u8 = undefined;
const empty_u = tokenizeCardLine("U \n").?;
try std.testing.expect(parseUCard(empty_u, &buf) != null);
const empty_n = tokenizeCardLine("N \n").?;
try std.testing.expect(parseNCard(empty_n, &buf) == null);
}
test "artifact §4.7.17-r1: U card rejects a second token" {
var buf: [64]u8 = undefined;
try std.testing.expect(parseUCard(tokenizeCardLine("U a b\n").?, &buf) == null);
}
test "artifact §4.7.18-p1/m1: body located by byte count after the size LF" {
const artifact = "W 5\nhello\nX";
const w = parseWCard(artifact, 0).?;
try std.testing.expectEqualStrings("hello", w.body);
try std.testing.expectEqual(@as(usize, 10), w.next);
}
test "artifact §4.7.18-p2/w1: leading digits accumulated, suffix not re-checked (12x)" {
const artifact = "W 12x\nabcdefghijkl\nT";
const w = parseWCard(artifact, 0).?;
try std.testing.expectEqualStrings("abcdefghijkl", w.body);
try std.testing.expectEqual(@as(usize, 12), w.body.len);
}
test "artifact §4.7.18-r1: size accumulator overflow is rejected" {
const artifact = "W " ++ ("9" ** 25) ++ "\nbody\nX";
try std.testing.expect(parseWCard(artifact, 0) == null);
}
test "artifact §4.7.18-r2: declared size past remaining length is rejected" {
const artifact = "W 100\nshort\n";
try std.testing.expect(parseWCard(artifact, 0) == null);
}
test "artifact §4.7.18-m1: body consumed by count, not scanned for a card boundary" {
const artifact = "W 4\na\nbc\nX";
const w = parseWCard(artifact, 0).?;
try std.testing.expectEqualStrings("a\nbc", w.body);
try std.testing.expectEqual(@as(usize, 9), w.next);
}
test "artifact §4.7.18-r3: byte after declared-length body must be LF" {
const artifact = "W 3\nabcXY";
try std.testing.expect(parseWCard(artifact, 0) == null);
}
test "artifact §4.7.18-r4: position one past the body LF must remain within the artifact" {
const artifact = "W 0\n\n";
try std.testing.expect(parseWCard(artifact, 0) == null);
}
test "artifact §4.7.18-w2: no-leading-digit size parses as zero, accepted only if next byte is LF" {
const accepted = "W abc\n\nX";
const w = parseWCard(accepted, 0).?;
try std.testing.expectEqual(@as(usize, 0), w.body.len);
try std.testing.expectEqual(@as(usize, 7), w.next);
const rejected = "W abc\nZY";
try std.testing.expect(parseWCard(rejected, 0) == null);
}
test "artifact §4.7.18: W grammar guards -- missing SP or unterminated size line" {
try std.testing.expect(parseWCard("W\n", 0) == null); // no SP
try std.testing.expect(parseWCard("W 5", 0) == null); // no size-line LF before end
}
test "artifact §4.7.19-p1/m1: Z card accepts a 32-hex md5 token, raw and never decoded" {
const card = tokenizeCardLine("Z " ++ MD5_32 ++ "\n").?;
const z = parseZCard(card).?;
try std.testing.expectEqualStrings(MD5_32, z.checksum);
try std.testing.expectEqual(@as(usize, 32), z.checksum.len);
}
test "artifact §4.7.19-r1: Z/R checksum token must be exactly 32 hex" {
try std.testing.expect(parseZCard(tokenizeCardLine("Z " ++ MD5_32[0..31] ++ "\n").?) == null); // 31
try std.testing.expect(parseZCard(tokenizeCardLine("Z " ++ A_CARD_HASH40 ++ "\n").?) == null); // 40, not 32
try std.testing.expect(parseZCard(tokenizeCardLine("Z " ++ MD5_32[0..31] ++ "g\n").?) == null); // non-hex
try std.testing.expect(parseZCard(tokenizeCardLine("Z " ++ MD5_32 ++ "\n").?) != null); // exactly 32
}
test "artifact §4.7.19-r2: Z grammar takes a single token -- zero or two rejected" {
try std.testing.expect(parseZCard(tokenizeCardLine("Z\n").?) == null); // zero tokens
const second = tokenizeCardLine("Z " ++ MD5_32 ++ " " ++ MD5_32 ++ "\n").?;
try std.testing.expect(parseZCard(second) == null); // second token
}
test "artifact §4.7.19-m1: Z card occupies the final 35 bytes of stripped content" {
// z-card = "Z" SP md5-token LF: 1 + 1 + 32 + 1 == 35 (§4.4 l.742-743).
try std.testing.expectEqual(@as(usize, 35), 1 + 1 + 32 + 1);
// Ties to the §6.2 positional framing verifyZCard already implements:
// marker at n-35, digest at n-33..n-1, terminating LF at n-1 untouched.
// Empty-input MD5 digest (matches the self-checksum vector above) makes
// this a genuine positional match, not just a length check.
const stripped = "Z d41d8cd98f00b204e9800998ecf8427e\n";
try std.testing.expectEqual(@as(usize, 35), stripped.len);
try std.testing.expectEqual(ZVerdict.match, verifyZCard(stripped));
}
test "artifact §6.3.2-p1: R aggregate matches A.3 and empty-checkin vectors" {
// A.2: zero tracked files -> MD5("") = the "initial empty baseline" R.
var out: [32]u8 = undefined;
try std.testing.expectEqualStrings(
"d41d8cd98f00b204e9800998ecf8427e",
aggregateChecksum(&.{}, &out),
);
// A.3: three files, filename order, 85-byte input -> matches the R card.
const files = [_]TrackedFile{
.{ .name = "alpha.txt", .content = "hello world\n" },
.{ .name = "beta.txt", .content = "fossil artifact format\n" },
.{ .name = "gamma.txt", .content = "R card test\n" },
};
try std.testing.expectEqualStrings(
"95feb799536dc053e7968efb7cdd3ec3",
aggregateChecksum(&files, &out),
);
}
test "artifact §6.3.2-m1: size header is minimal decimal (no leading zeros)" {
// A.3 sizes 12/23 are already two-digit, so the p1 match above pins
// minimal-decimal for those. A zero-byte file pins the size-0 case: the
// record is "f" SP "0" LF with nothing after -- MD5("f 0\n"), a public
// MD5 constant, not a leading-zero "f 00\n" or an omitted header.
const files = [_]TrackedFile{.{ .name = "f", .content = "" }};
var out: [32]u8 = undefined;
try std.testing.expectEqualStrings(
"bad208e121af55e1d62088c7ed0699f9",
aggregateChecksum(&files, &out),
);
}
test "artifact §6.3.2-m2: checksum covers name+size-header+content" {
const files = [_]TrackedFile{
.{ .name = "alpha.txt", .content = "hello world\n" },
.{ .name = "beta.txt", .content = "fossil artifact format\n" },
.{ .name = "gamma.txt", .content = "R card test\n" },
};
var out: [32]u8 = undefined;
const got = aggregateChecksum(&files, &out);
try std.testing.expectEqualStrings("95feb799536dc053e7968efb7cdd3ec3", got);
// Raw-content-only concatenation (no filename, no size header) is a
// different, well-known MD5 value -- proving the header/name matter.
var raw_digest: [16]u8 = undefined;
std.crypto.hash.Md5.hash("hello world\nfossil artifact format\nR card test\n", &raw_digest, .{});
const raw_hex = std.fmt.bytesToHex(raw_digest, .lower);
try std.testing.expect(!std.mem.eql(u8, got, &raw_hex));
}
test "artifact §6.3.2-m3: filename order is the §4.5.3 comparator" {
// The A.3 vector's order is itself the canonical order under the §4.5.3
// comparator -- assert that, then reuse it to reconfirm the p1 match.
try std.testing.expectEqual(std.math.Order.lt, compareBytes("alpha.txt", "beta.txt"));
try std.testing.expectEqual(std.math.Order.lt, compareBytes("beta.txt", "gamma.txt"));
const files = [_]TrackedFile{
.{ .name = "alpha.txt", .content = "hello world\n" },
.{ .name = "beta.txt", .content = "fossil artifact format\n" },
.{ .name = "gamma.txt", .content = "R card test\n" },
};
var out: [32]u8 = undefined;
try std.testing.expectEqualStrings("95feb799536dc053e7968efb7cdd3ec3", aggregateChecksum(&files, &out));
}
test "artifact §6.2-p1: Z compute over first n-35 matches the A.2 vector" {
// A.2 offset table: 92 bytes checksummed (0..91), marker@92, digest
// 94..125, LF@126; n = 127.
try std.testing.expectEqual(@as(usize, 127), a1_1_manifest.len);
var out: [32]u8 = undefined;
try std.testing.expectEqualStrings(
"c62e3619f7c41ee77b33f00df8ec26f4",
selfChecksum(a1_1_manifest[0..92], &out),
);
try std.testing.expectEqual(ZVerdict.match, verifyZCard(a1_1_manifest));
}
test "artifact §6.2-m2: verification never reads byte n-1" {
// Mutate the artifact's terminating byte (n-1); the verdict is
// unaffected because Z verification's compare range is n-33..n-2.
var mutated: [127]u8 = undefined;
@memcpy(&mutated, a1_1_manifest);
try std.testing.expectEqual(@as(u8, '\n'), mutated[126]);
mutated[126] = 'X';
try std.testing.expectEqual(ZVerdict.match, verifyZCard(&mutated));
}
test "artifact §6.2-s1/-m1: absent/too-short Z is not a verification reject" {
const table = [_][]const u8{
"C comment\n", // 10 bytes: no Z card at all
"D 2007-07-21T14:09:59\n", // 22 bytes: still short of the 35-byte Z minimum
};
for (table) |bytes| {
try std.testing.expect(bytes.len < 35);
try std.testing.expectEqual(ZVerdict.absent, verifyZCard(bytes));
// The absence itself never fails the framing gate here -- §5.2's
// required-card rule is the one that rejects a missing Z.
try std.testing.expect(frame(bytes) != null);
}
}
test "artifact §6.1-p2/-r1: R/Z parse-accept is 32 hex, case-insensitive" {
const md5_upper = "0123456789ABCDEF" ** 2;
comptime std.debug.assert(md5_upper.len == 32);
// Accept: all-lower and all-upper.
try std.testing.expect(parseRCard(tokenizeCardLine("R " ++ MD5_32 ++ "\n").?) != null);
try std.testing.expect(parseRCard(tokenizeCardLine("R " ++ md5_upper ++ "\n").?) != null);
try std.testing.expect(parseZCard(tokenizeCardLine("Z " ++ MD5_32 ++ "\n").?) != null);
try std.testing.expect(parseZCard(tokenizeCardLine("Z " ++ md5_upper ++ "\n").?) != null);
// Reject: 31 chars, 33 chars, one non-hex byte, embedded space (splits
// into two tokens, so arity rejects it).
try std.testing.expect(parseRCard(tokenizeCardLine("R " ++ MD5_32[0..31] ++ "\n").?) == null);
try std.testing.expect(parseRCard(tokenizeCardLine("R " ++ MD5_32 ++ "a\n").?) == null);
try std.testing.expect(parseRCard(tokenizeCardLine("R " ++ MD5_32[0..16] ++ " " ++ MD5_32[16..32] ++ "\n").?) == null);
try std.testing.expect(parseZCard(tokenizeCardLine("Z " ++ MD5_32[0..31] ++ "\n").?) == null);
try std.testing.expect(parseZCard(tokenizeCardLine("Z " ++ MD5_32 ++ "a\n").?) == null);
try std.testing.expect(parseZCard(tokenizeCardLine("Z " ++ MD5_32[0..16] ++ " " ++ MD5_32[16..32] ++ "\n").?) == null);
// One non-hex byte, built at runtime (can't ++ a mutated buffer).
var bad_hex: [32]u8 = undefined;
@memcpy(&bad_hex, MD5_32);
bad_hex[0] = 'g';
var r_line: [35]u8 = undefined;
r_line[0] = 'R';
r_line[1] = ' ';
@memcpy(r_line[2..34], &bad_hex);
r_line[34] = '\n';
try std.testing.expect(parseRCard(tokenizeCardLine(&r_line).?) == null);
var z_line: [35]u8 = undefined;
z_line[0] = 'Z';
z_line[1] = ' ';
@memcpy(z_line[2..34], &bad_hex);
z_line[34] = '\n';
try std.testing.expect(parseZCard(tokenizeCardLine(&z_line).?) == null);
}
test "artifact §6.1-m1: the parse path never case-folds the token" {
const md5_upper = "0123456789ABCDEF" ** 2;
comptime std.debug.assert(md5_upper.len == 32);
const r = parseRCard(tokenizeCardLine("R " ++ md5_upper ++ "\n").?).?;
try std.testing.expectEqualStrings(md5_upper, r.checksum); // verbatim, not lowered
const z = parseZCard(tokenizeCardLine("Z " ++ md5_upper ++ "\n").?).?;
try std.testing.expectEqualStrings(md5_upper, z.checksum); // verbatim, not lowered
}
fn parseNoLeak(allocator: std.mem.Allocator, bytes: []const u8) !void {
if (parse(allocator, bytes)) |m| {
var mm = m;
mm.deinit(); // success owns the arena now -- release it, or the leak check misfires
} else |err| switch (err) {
ParseError.OutOfMemory => return err,
else => {}, // SyntaxError + the 7 ClassifyError variants: not a leak signal
}
}
test "artifact §4.2: parse propagates allocation failure (arena-per-parse)" {
try std.testing.checkAllAllocationFailures(std.testing.allocator, parseNoLeak, .{a1_1_manifest});
}
test "artifact fuzz: parse on arbitrary bytes never crashes (§4 robustness)" {
const a = std.testing.allocator;
var prng = std.Random.DefaultPrng.init(0xA47F_0001);
const rand = prng.random();
var round: usize = 0;
while (round < 400) : (round += 1) {
var buf: [512]u8 = undefined;
const len = rand.uintLessThan(usize, buf.len + 1);
for (buf[0..len]) |*b| b.* = rand.int(u8);
// Contract: a declared ParseError or a Manifest value -- never a crash or UB.
if (parse(a, buf[0..len])) |m| {
var mm = m;
mm.deinit();
} else |_| {}
}
}
test "artifact fuzz: tokenizeCardLine + decodeEscapes never crash (§4.2/§4.3 robustness)" {
var prng = std.Random.DefaultPrng.init(0xA47F_0002);
const rand = prng.random();
var round: usize = 0;
while (round < 400) : (round += 1) {
var buf: [128]u8 = undefined;
// tokenizeCardLine's precondition: non-empty, no embedded LF, exactly
// one trailing LF.
const body_len = 1 + rand.uintLessThan(usize, buf.len - 1);
for (buf[0 .. body_len - 1]) |*b| {
b.* = rand.int(u8);
if (b.* == '\n') b.* = ' ';
}
buf[body_len - 1] = '\n';
const line = buf[0..body_len];
if (tokenizeCardLine(line)) |card| {
try std.testing.expect(isCardLetter(card.letter));
if (card.args) |args| {
try std.testing.expect(args.len <= line.len);
var out_buf: [128]u8 = undefined;
const out = decodeEscapes(args, out_buf[0..args.len]);
try std.testing.expect(out.len <= args.len);
}
// §4.7.15-4.7.19: the per-card parsers built on this batch's
// tokenizer/decoder must also never crash on adversarial input,
// regardless of card.letter (a rejecting null is fine). W is
// excluded here -- it bypasses tokenizeCardLine entirely and
// gets its own fuzz target below.
var decode_buf: [128]u8 = undefined;
const decode_bound = if (card.args) |a| a.len else 0;
switch (card.letter) {
'R' => _ = parseRCard(card),
'T' => _ = parseTCard(card, decode_buf[0..decode_bound]),
'U' => _ = parseUCard(card, decode_buf[0..decode_bound]),
'Z' => _ = parseZCard(card),
else => {},
}
}
}
}
test "artifact fuzz: parseWCard never crashes and honors its postconditions (§4.7.18 robustness)" {
// W bypasses tokenizeCardLine (its body may embed LF), so it needs its
// own target: random bytes with the 'W' letter forced at start.
var prng = std.Random.DefaultPrng.init(0xA47F_0006);
const rand = prng.random();
var round: usize = 0;
while (round < 400) : (round += 1) {
var buf: [128]u8 = undefined;
const len = 1 + rand.uintLessThan(usize, buf.len); // start < artifact.len
for (buf[0..len]) |*b| b.* = rand.int(u8);
buf[0] = 'W'; // precondition: artifact[start] == 'W'
const artifact = buf[0..len];
// Contract: a null (rejection) or a WCard honoring next < len and
// the terminator LF sitting one byte before next -- never a crash.
if (parseWCard(artifact, 0)) |w| {
try std.testing.expect(w.next < artifact.len);
try std.testing.expect(w.next >= 1);
try std.testing.expect(artifact[w.next - 1] == '\n');
try std.testing.expect(w.body.len <= artifact.len);
}
}
}
test "artifact fuzz: classifyTimestamp on arbitrary bytes never crashes (§4.6.4 robustness)" {
var prng = std.Random.DefaultPrng.init(0xA47F_0003);
const rand = prng.random();
var round: usize = 0;
while (round < 400) : (round += 1) {
var buf: [64]u8 = undefined;
const len = rand.uintLessThan(usize, buf.len + 1);
for (buf[0..len]) |*b| b.* = if (rand.boolean())
rand.int(u8)
else
"0123456789-:.Tt"[rand.uintLessThan(usize, 15)];
_ = classifyTimestamp(buf[0..len]);
}
}
test "artifact fuzz: framing trio never crash (§4.1/§4.1.1/§6.2 robustness)" {
var prng = std.Random.DefaultPrng.init(0xA47F_0004);
const rand = prng.random();
var round: usize = 0;
while (round < 400) : (round += 1) {
var buf: [256]u8 = undefined;
const len = rand.uintLessThan(usize, buf.len + 1);
for (buf[0..len]) |*b| b.* = rand.int(u8);
const bytes = buf[0..len];
// Contract: never grows the input (§4.1.1, §4.1), never crashes.
const stripped = stripSignatureWrapper(bytes);
try std.testing.expect(stripped.len <= bytes.len);
if (frame(bytes)) |framed| try std.testing.expect(framed.len <= bytes.len);
_ = verifyZCard(bytes);
}
}
fn invertOrder(o: std.math.Order) std.math.Order {
return switch (o) {
.lt => .gt,
.gt => .lt,
.eq => .eq,
};
}
test "artifact fuzz: pathname/wiki-name/reserved/compare predicates never crash (§4.5.3/§4.6 robustness)" {
var prng = std.Random.DefaultPrng.init(0xA47F_0005);
const rand = prng.random();
var round: usize = 0;
while (round < 400) : (round += 1) {
var abuf: [64]u8 = undefined;
var bbuf: [64]u8 = undefined;
const alen = rand.uintLessThan(usize, abuf.len + 1);
const blen = rand.uintLessThan(usize, bbuf.len + 1);
for (abuf[0..alen]) |*b| b.* = rand.int(u8);
for (bbuf[0..blen]) |*b| b.* = rand.int(u8);
const a = abuf[0..alen];
const b = bbuf[0..blen];
_ = isSimplePathname(a);
_ = isWikiName(a);
_ = isReservedFilename(a);
// compareBytes: reflexive, and swapping arguments exactly reverses
// the order (§4.5.3).
try std.testing.expectEqual(std.math.Order.eq, compareBytes(a, a));
try std.testing.expectEqual(compareBytes(a, b), invertOrder(compareBytes(b, a)));
}
}
test "artifact §5.1: type codes and external names match Table 3" {
const Row = struct { t: ArtifactType, code: u8, name: []const u8 };
const rows = [_]Row{
.{ .t = .manifest, .code = 1, .name = "checkin" },
.{ .t = .cluster, .code = 2, .name = "cluster" },
.{ .t = .control, .code = 3, .name = "tag" },
.{ .t = .wiki, .code = 4, .name = "wiki" },
.{ .t = .ticket, .code = 5, .name = "ticket" },
.{ .t = .attachment, .code = 6, .name = "attachment" },
.{ .t = .event, .code = 7, .name = "technote" },
.{ .t = .forum, .code = 8, .name = "forumpost" },
};
for (rows) |row| {
try std.testing.expectEqual(row.code, @intFromEnum(row.t));
try std.testing.expectEqualStrings(row.name, row.t.externalName());
}
}
test "artifact §5.3: each type-setting letter assigns its type" {
const Row = struct { letter: u8, t: ArtifactType };
const rows = [_]Row{
.{ .letter = 'A', .t = .attachment },
.{ .letter = 'B', .t = .manifest },
.{ .letter = 'E', .t = .event },
.{ .letter = 'F', .t = .manifest },
.{ .letter = 'G', .t = .forum },
.{ .letter = 'H', .t = .forum },
.{ .letter = 'I', .t = .forum },
.{ .letter = 'J', .t = .ticket },
.{ .letter = 'K', .t = .ticket },
.{ .letter = 'L', .t = .wiki },
.{ .letter = 'M', .t = .cluster },
.{ .letter = 'Q', .t = .manifest },
.{ .letter = 'R', .t = .manifest },
};
for (rows) |row| {
const c = Census{ .letters = @as(u32, 1) << @intCast(row.letter - 'A') };
try std.testing.expectEqual(row.t, determineType(c));
}
}
test "artifact fuzz: classify and determineType are total over arbitrary censuses (§5 robustness)" {
var prng = std.Random.DefaultPrng.init(0xA47F_0007);
const rand = prng.random();
var round: usize = 0;
while (round < 400) : (round += 1) {
const c = Census{
.letters = rand.int(u32),
.p_parents = rand.int(u8),
.t_self_ref = rand.boolean(),
.t_not_self_ref = rand.boolean(),
.t_not_plus = rand.boolean(),
};
const t2 = determineType(c);
try std.testing.expect(@intFromEnum(t2) >= 1 and @intFromEnum(t2) <= 8);
// Contract: classify never panics -- always a declared ClassifyError
// or a concrete code-1..8 type (§5.1-m1: never the sentinel).
if (classify(c)) |t| {
try std.testing.expect(@intFromEnum(t) >= 1 and @intFromEnum(t) <= 8);
} else |_| {}
}
}
const TEST_TS = "2007-07-21T14:09:59"; // §4.6.4 writer form, reused across bodies
const HASH40_HIGH: []const u8 = "f" ** 40; // sorts above A_CARD_HASH40 (§4.5.3)
fn buildArtifact(buf: []u8, body: []const u8) []const u8 {
std.debug.assert(buf.len >= body.len + 35);
@memcpy(buf[0..body.len], body);
var digest_buf: [32]u8 = undefined;
const digest = selfChecksum(body, &digest_buf);
buf[body.len] = 'Z';
buf[body.len + 1] = ' ';
@memcpy(buf[body.len + 2 ..][0..32], digest);
buf[body.len + 34] = '\n';
const total = body.len + 35;
std.debug.assert(buf[total - 1] == '\n');
return buf[0..total];
}
test "artifact §5: parse assembles the worked-example manifest" {
var m = try parse(std.testing.allocator, a1_1_manifest);
defer m.deinit();
try std.testing.expectEqual(ArtifactType.manifest, m.kind);
}
test "artifact §4.4/§A.1.1: parse retains every decoded card of the worked example" {
var m = try parse(std.testing.allocator, a1_1_manifest);
defer m.deinit();
try std.testing.expectEqual(ArtifactType.manifest, m.kind);
// §4.3 Table 1: the wire's \s decodes to SP.
try std.testing.expect(m.c != null);
try std.testing.expectEqualStrings("initial empty baseline", m.c.?.comment);
// §4.3.1: D is a raw, never-decoded position.
try std.testing.expect(m.d != null);
try std.testing.expectEqualStrings("2007-07-21T14:09:59", m.d.?.timestamp);
// §4.7.13 m1: bare P (zero tokens) is retained, not dropped.
try std.testing.expectEqual(@as(usize, 1), m.p.len);
try std.testing.expectEqual(@as(usize, 0), m.p[0].count);
try std.testing.expectEqual(@as(?[]const u8, null), m.p[0].parents);
// §4.7.15: R is raw md5.
try std.testing.expect(m.r != null);
try std.testing.expectEqualStrings("d41d8cd98f00b204e9800998ecf8427e", m.r.?.checksum);
// §4.7.17: U is decoded; no escapes present in this fixture.
try std.testing.expect(m.u != null);
try std.testing.expectEqualStrings("drh", m.u.?.login);
// §4.7.19: Z is raw md5.
try std.testing.expect(m.z != null);
try std.testing.expectEqualStrings("c62e3619f7c41ee77b33f00df8ec26f4", m.z.?.checksum);
// Every letter absent from this fixture retains as null/empty.
try std.testing.expect(m.a == null);
try std.testing.expect(m.b == null);
try std.testing.expect(m.e == null);
try std.testing.expect(m.g == null);
try std.testing.expect(m.h == null);
try std.testing.expect(m.i == null);
try std.testing.expect(m.k == null);
try std.testing.expect(m.l == null);
try std.testing.expect(m.n == null);
try std.testing.expect(m.w == null);
try std.testing.expectEqual(@as(usize, 0), m.f.len);
try std.testing.expectEqual(@as(usize, 0), m.j.len);
try std.testing.expectEqual(@as(usize, 0), m.m.len);
try std.testing.expectEqual(@as(usize, 0), m.q.len);
try std.testing.expectEqual(@as(usize, 0), m.t.len);
}
test "artifact §4.7.13: a P card with parents is retained with count and raw slice" {
var buf: [220]u8 = undefined;
const body = "D " ++ TEST_TS ++ "\nP " ++ A_CARD_HASH40 ++ " " ++ HASH40_HIGH ++ "\n";
const bytes = buildArtifact(buf[0..], body);
var m = try parse(std.testing.allocator, bytes);
defer m.deinit();
try std.testing.expectEqual(@as(usize, 1), m.p.len);
try std.testing.expectEqual(@as(usize, 2), m.p[0].count);
var it = m.p[0].hashes(); // §4.7.13: tokens are neither sorted nor deduped -- order is semantic
try std.testing.expectEqualStrings(A_CARD_HASH40, it.next().?);
try std.testing.expectEqualStrings(HASH40_HIGH, it.next().?);
try std.testing.expect(it.next() == null);
}
test "artifact §4.7.3: a B card is retained as the delta-manifest marker" {
var buf: [128]u8 = undefined;
const bytes = buildArtifact(buf[0..], "B " ++ A_CARD_HASH40 ++ "\nD " ++ TEST_TS ++ "\n");
var m = try parse(std.testing.allocator, bytes);
defer m.deinit();
try std.testing.expect(m.b != null);
try std.testing.expectEqualStrings(A_CARD_HASH40, m.b.?.hash);
}
test "artifact §5.2/§5.4: parse accepts a minimal well-formed artifact of each type" {
const Row = struct { body: []const u8, t: ArtifactType };
const rows = [_]Row{
.{ .body = "B " ++ A_CARD_HASH40 ++ "\nD " ++ TEST_TS ++ "\n", .t = .manifest },
.{ .body = "M " ++ A_CARD_HASH40 ++ "\n", .t = .cluster },
.{ .body = "D " ++ TEST_TS ++ "\nT +tagname " ++ A_CARD_HASH40 ++ "\nU drh\n", .t = .control },
.{ .body = "D " ++ TEST_TS ++ "\nL Wiki\\sPage\nU drh\nW 5\nhello\n", .t = .wiki },
.{ .body = "D " ++ TEST_TS ++ "\nJ field val\nK " ++ A_CARD_HASH40 ++ "\nU drh\n", .t = .ticket },
.{ .body = "A file.txt " ++ A_CARD_HASH40 ++ "\nD " ++ TEST_TS ++ "\n", .t = .attachment },
.{ .body = "D " ++ TEST_TS ++ "\nE " ++ TEST_TS ++ " " ++ A_CARD_HASH40 ++ "\nW 5\nhello\n", .t = .event },
.{ .body = "D " ++ TEST_TS ++ "\nG " ++ A_CARD_HASH40 ++ "\nU drh\nW 5\nhello\n", .t = .forum },
};
var buf: [220]u8 = undefined;
for (rows) |row| {
const bytes = buildArtifact(buf[0..], row.body);
var m = try parse(std.testing.allocator, bytes);
defer m.deinit();
try std.testing.expectEqual(row.t, m.kind);
}
}
test "artifact §5.5: an input whose first card letter is N..Z is rejected" {
// Every type's smallest required letter is <= 'M' (CLUSTER's own M) --
// an 'N' first card can satisfy no type's required set (§5.5 minima).
try std.testing.expectError(error.SyntaxError, parse(std.testing.allocator, "N text/plain\n"));
}
test "artifact §5.5: a first card letter of M (cluster minimum) passes the pre-filter" {
var buf: [80]u8 = undefined;
const bytes = buildArtifact(buf[0..], "M " ++ A_CARD_HASH40 ++ "\n");
var m = try parse(std.testing.allocator, bytes);
defer m.deinit();
try std.testing.expectEqual(ArtifactType.cluster, m.kind);
}
test "artifact §4.1: raw input not ending in LF is rejected" {
try std.testing.expectError(error.SyntaxError, parse(std.testing.allocator, "D " ++ TEST_TS));
}
test "artifact §4.1/§6.2: a present-but-mismatched Z card is rejected" {
const body = "B " ++ A_CARD_HASH40 ++ "\nD " ++ TEST_TS ++ "\n";
var buf: [128]u8 = undefined;
const good = buildArtifact(buf[0..], body);
var mutated: [128]u8 = undefined;
@memcpy(mutated[0..good.len], good);
const digest_pos = body.len + 2; // first hex digit of the digest
mutated[digest_pos] = if (mutated[digest_pos] == 'a') 'b' else 'a';
try std.testing.expectError(error.SyntaxError, parse(std.testing.allocator, mutated[0..good.len]));
}
test "artifact §4.2.2: a card line with an undefined/out-of-alphabet letter is rejected" {
// 'O' as the SECOND card (not the first) proves the tokenizer's own
// §4.2.2 rejection fires -- a first-card 'O' would instead be caught
// earlier by the §5.5 pre-filter ('O' > 'M').
try std.testing.expectError(error.SyntaxError, parse(std.testing.allocator, "D " ++ TEST_TS ++ "\nO foo\n"));
}
test "artifact §4.2.1: a card carrying excess tokens is rejected" {
try std.testing.expectError(error.SyntaxError, parse(std.testing.allocator, "B " ++ A_CARD_HASH40 ++ " extra\n"));
}
test "artifact §4.4: out-of-order cards are rejected" {
try std.testing.expectError(error.SyntaxError, parse(std.testing.allocator, "D " ++ TEST_TS ++ "\nC hello\n"));
}
test "artifact §4.5.1: a duplicate single-occurrence card is rejected" {
try std.testing.expectError(error.SyntaxError, parse(std.testing.allocator, "D " ++ TEST_TS ++ "\nD 2008-01-01T00:00:00\n"));
}
test "artifact §4.5.2: a descending F-card run is rejected" {
try std.testing.expectError(error.SyntaxError, parse(std.testing.allocator, "F beta.txt\nF alpha.txt\n"));
}
test "artifact §4.5.2: a descending J-card run is rejected" {
try std.testing.expectError(error.SyntaxError, parse(std.testing.allocator, "J field2 val\nJ field1 val\n"));
}
test "artifact §4.5.2: a descending M-card run is rejected" {
const bytes = "M " ++ HASH40_HIGH ++ "\nM " ++ A_CARD_HASH40 ++ "\n";
try std.testing.expectError(error.SyntaxError, parse(std.testing.allocator, bytes));
}
test "artifact §4.5.2: a T-card run not two-level ascending is rejected" {
// A leading D card keeps the run past the §5.5 pre-filter ('T' > 'M')
// so the rejection under test is the intra-run check, not the filter.
const bytes = "D " ++ TEST_TS ++ "\nT +tagname " ++ HASH40_HIGH ++ "\nT +tagname " ++ A_CARD_HASH40 ++ "\n";
try std.testing.expectError(error.SyntaxError, parse(std.testing.allocator, bytes));
}
test "artifact §4.7: a card whose own grammar fails is rejected" {
try std.testing.expectError(error.SyntaxError, parse(std.testing.allocator, "D not-a-timestamp\n"));
}
test "artifact §4.1: trailing bytes after the final card are rejected" {
const body = "B " ++ A_CARD_HASH40 ++ "\nD " ++ TEST_TS ++ "\n";
var buf: [128]u8 = undefined;
const good = buildArtifact(buf[0..], body);
var full: [160]u8 = undefined;
@memcpy(full[0..good.len], good);
const junk = "X garbage\n";
@memcpy(full[good.len..][0..junk.len], junk);
try std.testing.expectError(error.SyntaxError, parse(std.testing.allocator, full[0 .. good.len + junk.len]));
}
test "artifact §5.2: a card outside the finalized type's allowed set is rejected" {
var buf: [128]u8 = undefined;
const bytes = buildArtifact(buf[0..], "D " ++ TEST_TS ++ "\nM " ++ A_CARD_HASH40 ++ "\n");
try std.testing.expectError(error.DisallowedCard, parse(std.testing.allocator, bytes));
}
test "artifact §5.4: CONTROL with a self-referential T card is rejected" {
var buf: [220]u8 = undefined;
const bytes = buildArtifact(buf[0..], "D " ++ TEST_TS ++ "\nT +tagname *\nU drh\n");
try std.testing.expectError(error.ControlSelfTag, parse(std.testing.allocator, bytes));
}
test "artifact §5.4: EVENT T card not self-referential is rejected" {
var buf: [220]u8 = undefined;
const bytes = buildArtifact(buf[0..], "D " ++ TEST_TS ++ "\nE " ++ TEST_TS ++ " " ++ A_CARD_HASH40 ++ "\nT +tagname " ++ A_CARD_HASH40 ++ "\nW 5\nhello\n");
try std.testing.expectError(error.EventTagNotSelfRef, parse(std.testing.allocator, bytes));
}
test "artifact §5.4: EVENT T card lacking the + prefix is rejected" {
var buf: [220]u8 = undefined;
const bytes = buildArtifact(buf[0..], "D " ++ TEST_TS ++ "\nE " ++ TEST_TS ++ " " ++ A_CARD_HASH40 ++ "\nT -tagname *\nW 5\nhello\n");
try std.testing.expectError(error.EventTagNotPlus, parse(std.testing.allocator, bytes));
}
test "artifact §5.4: FORUM with both H and I cards is rejected" {
var buf: [220]u8 = undefined;
const bytes = buildArtifact(buf[0..], "D " ++ TEST_TS ++ "\nH My\\sThread\nI " ++ A_CARD_HASH40 ++ "\nU drh\nW 5\nhello\n");
try std.testing.expectError(error.ForumHeadAndReply, parse(std.testing.allocator, bytes));
}
test "artifact §5.4: FORUM P card with more than one parent is rejected" {
var buf: [220]u8 = undefined;
const bytes = buildArtifact(buf[0..], "D " ++ TEST_TS ++ "\nG " ++ A_CARD_HASH40 ++ "\nP " ++ A_CARD_HASH40 ++ " " ++ HASH40_HIGH ++ "\nU drh\nW 5\nhello\n");
try std.testing.expectError(error.ForumMultiParent, parse(std.testing.allocator, bytes));
}
test "artifact §7.1: policy value strings match case-sensitively and exactly" {
// §7.1-p1: "Value strings MUST be matched case-sensitively and exactly."
try std.testing.expectEqual(HashPolicy.sha1, policyFromString("sha1").?);
try std.testing.expectEqual(HashPolicy.auto, policyFromString("auto").?);
try std.testing.expectEqual(HashPolicy.sha3, policyFromString("sha3").?);
try std.testing.expectEqual(HashPolicy.sha3_only, policyFromString("sha3-only").?);
try std.testing.expectEqual(HashPolicy.shun_sha1, policyFromString("shun-sha1").?);
try std.testing.expectEqual(@as(?HashPolicy, null), policyFromString("SHA1"));
try std.testing.expectEqual(@as(?HashPolicy, null), policyFromString("Auto"));
try std.testing.expectEqual(@as(?HashPolicy, null), policyFromString("SHA3-ONLY"));
try std.testing.expectEqual(@as(?HashPolicy, null), policyFromString("Sha3-Only"));
try std.testing.expectEqual(@as(?HashPolicy, null), policyFromString(" sha1"));
try std.testing.expectEqual(@as(?HashPolicy, null), policyFromString("sha1 "));
}
test "artifact §7.1: unrecognized policy value is rejected" {
// §7.1-r1: "an unrecognized value is a fatal error" -- modeled as null.
try std.testing.expectEqual(@as(?HashPolicy, null), policyFromString("md5"));
try std.testing.expectEqual(@as(?HashPolicy, null), policyFromString(""));
try std.testing.expectEqual(@as(?HashPolicy, null), policyFromString("sha"));
try std.testing.expectEqual(@as(?HashPolicy, null), policyFromString("sha3only"));
try std.testing.expectEqual(@as(?HashPolicy, null), policyFromString("auto\n"));
}
test "artifact §7.1: policy ordinals are 0..4 in table order" {
// §7.1-w1: "The ordinal column gives each policy's stable numeric
// encoding (0 through 4, in the order listed)."
try std.testing.expectEqual(@as(u3, 0), @intFromEnum(HashPolicy.sha1));
try std.testing.expectEqual(@as(u3, 1), @intFromEnum(HashPolicy.auto));
try std.testing.expectEqual(@as(u3, 2), @intFromEnum(HashPolicy.sha3));
try std.testing.expectEqual(@as(u3, 3), @intFromEnum(HashPolicy.sha3_only));
try std.testing.expectEqual(@as(u3, 4), @intFromEnum(HashPolicy.shun_sha1));
}
test "artifact §7.1: policy string round-trips" {
// Guards the single value-string table: encode then parse is identity
// for every policy (supports p1's encode/parse pairing).
for (std.enums.values(HashPolicy)) |p| {
try std.testing.expectEqual(p, policyFromString(policyName(p)).?);
}
}
test "artifact §7.1: sha1 and auto name with the SHA-1 primary hash" {
// "Under sha1 and auto the primary hash MUST be SHA-1."
try std.testing.expectEqual(HashAlgo.sha1, primaryHash(.sha1));
try std.testing.expectEqual(HashAlgo.sha1, primaryHash(.auto));
}
test "artifact §7.1: sha3, sha3-only, shun-sha1 name with the SHA3-256 primary" {
// "under sha3, sha3-only, and shun-sha1 it MUST be SHA3-256."
try std.testing.expectEqual(HashAlgo.sha3_256, primaryHash(.sha3));
try std.testing.expectEqual(HashAlgo.sha3_256, primaryHash(.sha3_only));
try std.testing.expectEqual(HashAlgo.sha3_256, primaryHash(.shun_sha1));
}
test "artifact §7.1: alternative hash permitted only under sha1, auto, sha3" {
// "permitted only under sha1 and auto (where it is SHA3-256) and under
// sha3 (where it is SHA-1)."
try std.testing.expectEqual(HashAlgo.sha3_256, alternativeHash(.sha1).?);
try std.testing.expectEqual(HashAlgo.sha3_256, alternativeHash(.auto).?);
try std.testing.expectEqual(HashAlgo.sha1, alternativeHash(.sha3).?);
}
test "artifact §7.1: alternative hash request fails under sha3-only and shun-sha1" {
// "a request for the alternative hash MUST fail, yielding a failure
// indication and an empty result rather than a hash value."
try std.testing.expectEqual(@as(?HashAlgo, null), alternativeHash(.sha3_only));
try std.testing.expectEqual(@as(?HashAlgo, null), alternativeHash(.shun_sha1));
}
test "artifact §7.2: default is sha3 when a SHA3 name exists or no SHA-1 name exists" {
// "If the repository already contains any SHA3-named artifact ... or
// contains no SHA-1-named artifact ... at all -- including the case of
// an empty repository -- the default is sha3."
try std.testing.expectEqual(HashPolicy.sha3, defaultPolicy(false, false)); // empty repo
try std.testing.expectEqual(HashPolicy.sha3, defaultPolicy(false, true));
try std.testing.expectEqual(HashPolicy.sha3, defaultPolicy(true, true));
}
test "artifact §7.2: default is auto only with a SHA-1 name and no SHA3 name" {
// "Otherwise -- only when at least one SHA-1 artifact exists and zero
// SHA3 artifacts exist -- the default is auto."
try std.testing.expectEqual(HashPolicy.auto, defaultPolicy(true, false));
}
test "artifact §7.2: default derivation never yields shun-sha1" {
// "This derivation never yields shun-sha1."
const bools = [_]bool{ false, true };
for (bools) |has_sha1| {
for (bools) |has_sha3| {
const p = defaultPolicy(has_sha1, has_sha3);
try std.testing.expect(p == .sha3 or p == .auto);
}
}
}
test "artifact §7.3: setting the policy back to sha1 is never coerced" {
// "Nothing prevents an operator from explicitly setting the policy
// back to sha1" -- and more generally, no non-auto request is coerced.
try std.testing.expectEqual(HashPolicy.sha1, coerceAssignment(.sha1, true));
try std.testing.expectEqual(HashPolicy.sha1, coerceAssignment(.sha1, false));
try std.testing.expectEqual(HashPolicy.sha3, coerceAssignment(.sha3, true));
try std.testing.expectEqual(HashPolicy.sha3_only, coerceAssignment(.sha3_only, true));
try std.testing.expectEqual(HashPolicy.shun_sha1, coerceAssignment(.shun_sha1, true));
}
test "artifact §7.3: requesting auto with a SHA3 artifact present stores sha3" {
// "Requesting auto when the repository already holds a SHA3-named
// artifact silently stores sha3 instead."
try std.testing.expectEqual(HashPolicy.sha3, coerceAssignment(.auto, true));
try std.testing.expectEqual(HashPolicy.auto, coerceAssignment(.auto, false));
}
test "artifact §7.4: auto ratchets to sha3 when the selected name exceeds 40 characters" {
// "MUST promote the repository to sha3 ... whenever that selected name
// exceeds 40 characters."
try std.testing.expectEqual(HashPolicy.sha3, ratchet(.auto, sha3_name_64));
}
test "artifact §7.4: the ratchet applies only under auto and only above 40 chars" {
// A fresh SHA-1 name under auto is exactly 40 characters -- no
// ratchet; and the automatic forward ratchet applies only while the
// effective policy is auto (§7.3's "no demotion guard" sentence).
try std.testing.expectEqual(HashPolicy.auto, ratchet(.auto, a1_1_name));
try std.testing.expectEqual(HashPolicy.sha1, ratchet(.sha1, sha3_name_64));
try std.testing.expectEqual(HashPolicy.sha3, ratchet(.sha3, sha3_name_64));
try std.testing.expectEqual(HashPolicy.sha3_only, ratchet(.sha3_only, sha3_name_64));
try std.testing.expectEqual(HashPolicy.shun_sha1, ratchet(.shun_sha1, sha3_name_64));
}
test "artifact §7.4: a caller-supplied 64-hex SHA3-256 name triggers the ratchet" {
// §7.4 trigger 1: "a caller-supplied 64-hexadecimal SHA3-256 name
// accepted for storage during transfer or ingest."
try std.testing.expectEqual(HashPolicy.sha3, ratchet(.auto, sha3_name_64));
}
test "artifact §7.5: naming computes the alternative hash first, falls back to primary on a miss" {
// "MUST first compute the alternative-algorithm hash (where the
// policy permits one) and look it up; only if that lookup misses does
// it fall back to naming the content with the primary hash."
try std.testing.expectEqual(HashAlgo.sha3_256, dedupNameAlgo(.sha1, true));
try std.testing.expectEqual(HashAlgo.sha1, dedupNameAlgo(.sha1, false));
try std.testing.expectEqual(HashAlgo.sha1, dedupNameAlgo(.sha3, true));
try std.testing.expectEqual(HashAlgo.sha3_256, dedupNameAlgo(.sha3, false));
try std.testing.expectEqual(HashAlgo.sha3_256, dedupNameAlgo(.auto, true));
try std.testing.expectEqual(HashAlgo.sha1, dedupNameAlgo(.auto, false));
}
test "artifact §7.5: under sha3-only and shun-sha1 new content is named SHA3-256 even when a SHA-1 name exists" {
// "Because these policies disallow the alternative hash, the pre-check
// cannot discover an existing SHA-1 name for the same bytes. New
// artifacts MUST be named with SHA3-256 even when identical content
// already carries a SHA-1 name."
try std.testing.expectEqual(HashAlgo.sha3_256, dedupNameAlgo(.sha3_only, true));
try std.testing.expectEqual(HashAlgo.sha3_256, dedupNameAlgo(.sha3_only, false));
try std.testing.expectEqual(HashAlgo.sha3_256, dedupNameAlgo(.shun_sha1, true));
try std.testing.expectEqual(HashAlgo.sha3_256, dedupNameAlgo(.shun_sha1, false));
}
const d8_h1: []const u8 = A_CARD_HASH40;
const d8_h2: []const u8 = A_CARD_HASH64[0..40];
comptime {
std.debug.assert(d8_h1.len == 40 and d8_h2.len == 40);
std.debug.assert(!std.mem.eql(u8, d8_h1, d8_h2));
}
fn d8F(filename: []const u8, hash: ?[]const u8) FCard {
return .{ .filename = filename, .hash = hash, .perm = null, .old_name = null, .mode = .regular };
}
fn d8ExpectFCard(expected: FCard, actual: FCard) !void {
try std.testing.expectEqualStrings(expected.filename, actual.filename);
try std.testing.expectEqual(expected.hash == null, actual.hash == null);
if (expected.hash) |h| try std.testing.expectEqualStrings(h, actual.hash.?);
}
fn d8ExpectStream(walk: *MergeWalk, expected: []const FCard) !void {
for (expected) |exp| {
const got = walk.next() orelse return error.TestUnexpectedResult;
try d8ExpectFCard(exp, got);
}
try std.testing.expect(walk.next() == null);
}
const D8SpyResolver = struct {
result: ?ManifestFiles,
calls: usize = 0,
fn resolveImpl(ctx: *anyopaque, hash: []const u8) ?ManifestFiles {
_ = hash;
const self: *D8SpyResolver = @ptrCast(@alignCast(ctx));
self.calls += 1;
return self.result;
}
fn resolver(self: *D8SpyResolver) BaselineResolver {
return .{ .ctx = self, .resolveFn = resolveImpl };
}
};
test "artifact §8: manifest is a delta iff it carries a B card" {
// "A manifest is a delta manifest if and only if it contains a B card;
// otherwise it is a baseline manifest." (§8-p1)
const delta = ManifestFiles{ .baseline_hash = d8_h1, .files = &.{} };
const baseline = ManifestFiles{ .baseline_hash = null, .files = &.{} };
try std.testing.expect(isDelta(delta));
try std.testing.expect(!isDelta(baseline));
}
test "artifact §8: full file set is derived by merging delta with baseline" {
// "Consumers that need the full file set MUST derive it by merging the
// delta's F cards with the baseline's, as specified in this section."
// (§8-m1)
const baseline_files = [_]FCard{d8F("a", d8_h1)};
const delta_files = [_]FCard{d8F("b", d8_h2)};
var spy = D8SpyResolver{ .result = .{ .baseline_hash = null, .files = &baseline_files } };
const delta = ManifestFiles{ .baseline_hash = d8_h1, .files = &delta_files };
var walk = try enumerate(delta, spy.resolver());
try d8ExpectStream(&walk, &.{ d8F("a", d8_h1), d8F("b", d8_h2) });
}
test "artifact §8.1: merge walk relies on sorted file lists" {
// "The merge walk requires both file lists sorted by decoded filename
// under the canonical comparator (Section 4.5.3). That sortedness is a
// parse-time invariant (Section 4.5.2): every successfully parsed
// manifest satisfies it." (§8.1-p1)
const baseline_files = [_]FCard{ d8F("a", d8_h1), d8F("c", d8_h1) };
const delta_files = [_]FCard{d8F("b", d8_h2)};
var spy = D8SpyResolver{ .result = .{ .baseline_hash = null, .files = &baseline_files } };
const delta = ManifestFiles{ .baseline_hash = d8_h1, .files = &delta_files };
var walk = try enumerate(delta, spy.resolver());
try d8ExpectStream(&walk, &.{ d8F("a", d8_h1), d8F("b", d8_h2), d8F("c", d8_h1) });
}
test "artifact §8.1: hashless F card is a delete marker removing the baseline file" {
// "A delete marker is an F card that carries no hash. It is permitted
// only in a delta manifest -- in a baseline manifest every F card MUST
// carry a hash (Section 4.7.6) -- and it carries a null hash,
// interpreted by the merge walk as removal of the named file from the
// baseline's set." (§8.1-r1)
const baseline_files = [_]FCard{d8F("a", d8_h1)};
const delta_files = [_]FCard{d8F("a", null)};
var spy = D8SpyResolver{ .result = .{ .baseline_hash = null, .files = &baseline_files } };
const delta = ManifestFiles{ .baseline_hash = d8_h1, .files = &delta_files };
var walk = try enumerate(delta, spy.resolver());
var set: [1]FCard = undefined;
var n: usize = 0;
while (walk.next()) |f| {
if (f.hash == null) continue;
std.debug.assert(n < set.len);
set[n] = f;
n += 1;
}
try std.testing.expectEqual(@as(usize, 0), n); // "a" removed from the set
}
test "artifact §8.2: merge-walk over two cursors (comprehensive)" {
// "To enumerate the complete file set of a delta manifest, an
// implementation MUST perform a filename-ordered merge-walk over two
// cursors: one over the baseline's file list and one over the delta's.
// Filenames are compared byte-wise on their decoded form (Section
// 4.5.3)." (§8.2-p1)
const baseline_files = [_]FCard{ d8F("a", d8_h1), d8F("b", d8_h1), d8F("d", d8_h1) };
const delta_files = [_]FCard{ d8F("b", d8_h2), d8F("c", null), d8F("d", null) };
var spy = D8SpyResolver{ .result = .{ .baseline_hash = null, .files = &baseline_files } };
const delta = ManifestFiles{ .baseline_hash = d8_h1, .files = &delta_files };
var walk = try enumerate(delta, spy.resolver());
try d8ExpectStream(&walk, &.{ d8F("a", d8_h1), d8F("b", d8_h2), d8F("c", null) });
var walk2 = try enumerate(delta, spy.resolver());
var set: [3]FCard = undefined;
var n: usize = 0;
while (walk2.next()) |f| {
if (f.hash == null) continue;
std.debug.assert(n < set.len);
set[n] = f;
n += 1;
}
try std.testing.expectEqual(@as(usize, 2), n);
try d8ExpectFCard(d8F("a", d8_h1), set[0]);
try d8ExpectFCard(d8F("b", d8_h2), set[1]);
}
test "artifact §8.2: same name, delta carries hash, delta replaces" {
// RFC step 1: "If both cursors name the same file and the delta entry
// carries a hash, the delta entry replaces the baseline entry: both
// cursors advance and the delta entry is emitted." (§8.2-r1)
const baseline_files = [_]FCard{d8F("a", d8_h1)};
const delta_files = [_]FCard{d8F("a", d8_h2)};
var spy = D8SpyResolver{ .result = .{ .baseline_hash = null, .files = &baseline_files } };
const delta = ManifestFiles{ .baseline_hash = d8_h1, .files = &delta_files };
var walk = try enumerate(delta, spy.resolver());
try d8ExpectStream(&walk, &.{d8F("a", d8_h2)});
}
test "artifact §8.2: same name, delta delete marker removes the file" {
// RFC step 2: "If both cursors name the same file and the delta entry
// carries no hash (a delete marker), the file is removed: both cursors
// advance and no entry is emitted." (§8.2-r2)
const baseline_files = [_]FCard{d8F("a", d8_h1)};
const delta_files = [_]FCard{d8F("a", null)};
var spy = D8SpyResolver{ .result = .{ .baseline_hash = null, .files = &baseline_files } };
const delta = ManifestFiles{ .baseline_hash = d8_h1, .files = &delta_files };
var walk = try enumerate(delta, spy.resolver());
try d8ExpectStream(&walk, &.{});
}
test "artifact §8.2: differing names, sorts-first entry emitted" {
// RFC step 3: "If the names differ, the entry that sorts first is
// emitted and only its cursor advances." (§8.2-r3)
{ // (a) delta's "a" sorts before baseline's "b"
const baseline_files = [_]FCard{d8F("b", d8_h1)};
const delta_files = [_]FCard{d8F("a", d8_h2)};
var spy = D8SpyResolver{ .result = .{ .baseline_hash = null, .files = &baseline_files } };
const delta = ManifestFiles{ .baseline_hash = d8_h1, .files = &delta_files };
var walk = try enumerate(delta, spy.resolver());
try d8ExpectStream(&walk, &.{ d8F("a", d8_h2), d8F("b", d8_h1) });
}
{ // (b) baseline's "a" sorts before delta's "b"
const baseline_files = [_]FCard{d8F("a", d8_h1)};
const delta_files = [_]FCard{d8F("b", d8_h2)};
var spy = D8SpyResolver{ .result = .{ .baseline_hash = null, .files = &baseline_files } };
const delta = ManifestFiles{ .baseline_hash = d8_h1, .files = &delta_files };
var walk = try enumerate(delta, spy.resolver());
try d8ExpectStream(&walk, &.{ d8F("a", d8_h1), d8F("b", d8_h2) });
}
}
test "artifact §8.2: unmatched delete marker emitted but adds no file" {
// "A delta delete marker whose name is not present in the baseline (an
// unmatched deletion) is emitted here like any other sorts-first entry,
// but because it carries no hash it adds no file to the reconstructed
// set. An unmatched deletion is therefore a no-op for materialization,
// not an error." (§8.2-w1)
const baseline_files = [_]FCard{d8F("b", d8_h1)};
const delta_files = [_]FCard{d8F("a", null)};
var spy = D8SpyResolver{ .result = .{ .baseline_hash = null, .files = &baseline_files } };
const delta = ManifestFiles{ .baseline_hash = d8_h1, .files = &delta_files };
var walk = try enumerate(delta, spy.resolver());
try d8ExpectStream(&walk, &.{ d8F("a", null), d8F("b", d8_h1) });
var walk2 = try enumerate(delta, spy.resolver());
var set: [2]FCard = undefined;
var n: usize = 0;
while (walk2.next()) |f| {
if (f.hash == null) continue;
std.debug.assert(n < set.len);
set[n] = f;
n += 1;
}
try std.testing.expectEqual(@as(usize, 1), n);
try d8ExpectFCard(d8F("b", d8_h1), set[0]);
}
test "artifact §8.2: enumeration terminates only when both cursors exhausted (tail delete markers)" {
// "Enumeration terminates only when both cursors are exhausted. Once
// either list runs out, the remaining entries of the other list are
// emitted verbatim -- including any delete markers remaining in the
// delta's tail." (§8.2-m1)
{ // delta tail, including a trailing delete marker
const baseline_files = [_]FCard{d8F("a", d8_h1)};
const delta_files = [_]FCard{d8F("z", null)};
var spy = D8SpyResolver{ .result = .{ .baseline_hash = null, .files = &baseline_files } };
const delta = ManifestFiles{ .baseline_hash = d8_h1, .files = &delta_files };
var walk = try enumerate(delta, spy.resolver());
try d8ExpectStream(&walk, &.{ d8F("a", d8_h1), d8F("z", null) });
}
{ // baseline tail, delta exhausted first
const baseline_files = [_]FCard{ d8F("a", d8_h1), d8F("b", d8_h1) };
var spy = D8SpyResolver{ .result = .{ .baseline_hash = null, .files = &baseline_files } };
const delta = ManifestFiles{ .baseline_hash = d8_h1, .files = &.{} };
var walk = try enumerate(delta, spy.resolver());
try d8ExpectStream(&walk, &.{ d8F("a", d8_h1), d8F("b", d8_h1) });
}
}
test "artifact §8.2: consumers filter hash-absent entries" {
// "Consumers of the merged stream MUST therefore filter out entries
// whose hash is absent when computing derived results, as the
// aggregate-checksum computation over the merged file set does
// (Section 6.3.3)." (§8.2-m2)
const baseline_files = [_]FCard{d8F("a", d8_h1)};
const delta_files = [_]FCard{d8F("z", null)};
var spy = D8SpyResolver{ .result = .{ .baseline_hash = null, .files = &baseline_files } };
const delta = ManifestFiles{ .baseline_hash = d8_h1, .files = &delta_files };
var walk = try enumerate(delta, spy.resolver());
var tracked: [1]TrackedFile = undefined;
var n: usize = 0;
while (walk.next()) |f| {
if (f.hash == null) continue; // m2: filter the hash-absent marker
std.debug.assert(n < tracked.len);
tracked[n] = .{ .name = f.filename, .content = "" };
n += 1;
}
try std.testing.expectEqual(@as(usize, 1), n); // marker excluded, not fed forward
var out: [32]u8 = undefined;
_ = aggregateChecksum(tracked[0..n], &out); // §6.3.3: no crash on the filtered set
}
test "artifact §8.3: baseline resolved lazily, only when needed" {
// "The baseline identified by the B card hash is resolved lazily: it
// is located and parsed as a manifest artifact the first time the
// baseline is needed." (§8.3-p1)
const baseline_files = [_]FCard{d8F("a", d8_h1)};
var delta_spy = D8SpyResolver{ .result = .{ .baseline_hash = null, .files = &baseline_files } };
const delta = ManifestFiles{ .baseline_hash = d8_h1, .files = &.{} };
_ = try enumerate(delta, delta_spy.resolver());
try std.testing.expectEqual(@as(usize, 1), delta_spy.calls);
var baseline_spy = D8SpyResolver{ .result = null };
const baseline_manifest = ManifestFiles{ .baseline_hash = null, .files = &baseline_files };
_ = try enumerate(baseline_manifest, baseline_spy.resolver());
try std.testing.expectEqual(@as(usize, 0), baseline_spy.calls);
}
test "artifact §8.3: unresolvable baseline is a fatal error of the enumeration" {
// "Hard failure, for the file-enumeration entry points: beginning full
// file-set enumeration, and single-name search (Section 8.4), MUST
// treat an unresolvable baseline as a fatal error of the enumeration."
// (§8.3-m1, both entry points)
var spy = D8SpyResolver{ .result = null };
const delta_files = [_]FCard{d8F("b", d8_h2)};
const delta = ManifestFiles{ .baseline_hash = d8_h1, .files = &delta_files };
try std.testing.expectError(error.BaselineUnresolvable, enumerate(delta, spy.resolver()));
try std.testing.expectError(error.BaselineUnresolvable, lookup(delta, "a", spy.resolver()));
}
test "artifact §8.3: baseline resolved before iteration, no silent delta-only degrade" {
// "Per-step enumeration reports no error for an unresolved baseline...
// Implementations MUST therefore ensure the baseline is resolved,
// under the hard-failure discipline, before iteration begins."
// (§8.3-m2) -- `enumerate` returns the error itself (not an ok
// `MergeWalk` that would silently yield only the delta's own entries),
// and the resolver was actually consulted before that error surfaced.
var spy = D8SpyResolver{ .result = null };
const delta_files = [_]FCard{d8F("b", d8_h2)};
const delta = ManifestFiles{ .baseline_hash = d8_h1, .files = &delta_files };
try std.testing.expectError(error.BaselineUnresolvable, enumerate(delta, spy.resolver()));
try std.testing.expectEqual(@as(usize, 1), spy.calls); // resolution attempted, not skipped
}
test "artifact §4.7.3/§8.3: resolved baseline that is itself a delta manifest is rejected" {
// "If the resolved baseline is itself a delta manifest (it carries a B
// card), the delta manifest naming it is invalid and MUST be
// rejected." (§4.7.3, enforced at §8's resolution point, §8.3-m1)
const cases = [_][]const FCard{
&.{d8F("a", null)}, // (a) delta baseline carries a delete marker
&.{d8F("a", d8_h1)}, // (b) delta baseline carries only hashed cards
};
for (cases) |case_files| {
var spy = D8SpyResolver{ .result = .{ .baseline_hash = d8_h2, .files = case_files } };
const delta_files = [_]FCard{d8F("b", d8_h2)};
const delta = ManifestFiles{ .baseline_hash = d8_h1, .files = &delta_files };
try std.testing.expectError(error.BaselineIsDelta, enumerate(delta, spy.resolver()));
try std.testing.expectError(error.BaselineIsDelta, lookup(delta, "a", spy.resolver()));
}
}
test "artifact §8.4: delta's own F cards searched first; best-match suppressed" {
// "When resolving a single filename against a delta manifest, an
// implementation MUST search the delta's own F cards first, and
// best-match (closest-name) lookup MUST be suppressed for delta
// manifests even when the caller requests it." (§8.4-m1)
const baseline_files = [_]FCard{d8F("a", d8_h1)};
var spy = D8SpyResolver{ .result = .{ .baseline_hash = null, .files = &baseline_files } };
const delta_files = [_]FCard{d8F("a", d8_h2)};
const delta = ManifestFiles{ .baseline_hash = d8_h1, .files = &delta_files };
const result = try lookup(delta, "a", spy.resolver());
switch (result) {
.found => |f| try d8ExpectFCard(d8F("a", d8_h2), f),
.not_found => return error.TestUnexpectedResult,
}
try std.testing.expectEqual(@as(usize, 0), spy.calls); // delta's own card wins, baseline untouched
// Near-miss: "ab" is not an exact match for delta's "abc" -- exact-only
// means it falls through to the baseline rather than fuzzy-matching,
// and the baseline (queried) has no "ab" either.
const delta_files2 = [_]FCard{d8F("abc", d8_h2)};
const baseline_files2 = [_]FCard{d8F("abd", d8_h1)};
var spy2 = D8SpyResolver{ .result = .{ .baseline_hash = null, .files = &baseline_files2 } };
const delta2 = ManifestFiles{ .baseline_hash = d8_h1, .files = &delta_files2 };
const result2 = try lookup(delta2, "ab", spy2.resolver());
try std.testing.expectEqual(LookupResult.not_found, result2);
try std.testing.expectEqual(@as(usize, 1), spy2.calls); // fell through to baseline, found nothing there either
}
test "artifact §8.4: delta delete marker means not-found without consulting baseline" {
// "If the delta contains an entry for the name whose hash is absent (a
// delete marker), the lookup MUST report not-found without consulting
// the baseline." (§8.4-m2)
const baseline_files = [_]FCard{d8F("a", d8_h1)};
var spy = D8SpyResolver{ .result = .{ .baseline_hash = null, .files = &baseline_files } };
const delta_files = [_]FCard{d8F("a", null)};
const delta = ManifestFiles{ .baseline_hash = d8_h1, .files = &delta_files };
const result = try lookup(delta, "a", spy.resolver());
try std.testing.expectEqual(LookupResult.not_found, result);
try std.testing.expectEqual(@as(usize, 0), spy.calls);
}
test "artifact §8.4: name absent from delta means the baseline is resolved and searched" {
// "Only when the name is entirely absent from the delta is the
// baseline resolved and searched." (§8.4-p1)
const baseline_files = [_]FCard{d8F("a", d8_h1)};
var spy = D8SpyResolver{ .result = .{ .baseline_hash = null, .files = &baseline_files } };
const delta_files = [_]FCard{d8F("b", d8_h2)};
const delta = ManifestFiles{ .baseline_hash = d8_h1, .files = &delta_files };
const found = try lookup(delta, "a", spy.resolver());
switch (found) {
.found => |f| try d8ExpectFCard(d8F("a", d8_h1), f),
.not_found => return error.TestUnexpectedResult,
}
try std.testing.expectEqual(@as(usize, 1), spy.calls);
const missing = try lookup(delta, "c", spy.resolver());
try std.testing.expectEqual(LookupResult.not_found, missing);
}
test "artifact §9: name and Z-verify operate over different byte extents" {
// Lines 2209-2213: name computation covers the complete stored bytes,
// wrapper included; Z verification covers the stripped stream. The
// third extent the section names -- signature verification over the
// pre-strip bytes -- is out of scope for this module (lines 2224-2230
// leave signer trust and verification mechanism outside the document),
// so its absence here is not a gap.
const s = "-----BEGIN PGP SIGNED MESSAGE-----\nHash: SHA256\n\n" ++
a1_1_manifest ++ "-----BEGIN PGP SIGNATURE-----\nabc123\n-----END PGP SIGNATURE-----\n";
const stripped = stripSignatureWrapper(s);
try std.testing.expect(stripped.len < s.len); // wrapper strictly shrinks the input
try std.testing.expectEqual(ZVerdict.match, verifyZCard(stripped));
}
test "artifact §9: Z-verify input excludes wrapper, name input includes it" {
// Lines 2214-2217: feeding the wrong extent to each operation.
const s = "-----BEGIN PGP SIGNED MESSAGE-----\nHash: SHA256\n\n" ++
a1_1_manifest ++ "-----BEGIN PGP SIGNATURE-----\nabc123\n-----END PGP SIGNATURE-----\n";
// The wrapped bytes' final 35 bytes are "abc123\n" (7) plus the 28-byte
// "-----END PGP SIGNATURE-----\n" armor line, so s[n-35] == 'a', not
// 'Z' -- the positional gate rejects before any digest compare runs.
try std.testing.expectEqual(ZVerdict.absent, verifyZCard(s));
try std.testing.expectEqual(ZVerdict.match, verifyZCard(stripSignatureWrapper(s)));
// Name computation is fed the wrapper on purpose: the inner-content
// name never verifies the wrapped bytes, and vice versa.
try std.testing.expect(verify(a1_1_name, a1_1_manifest));
try std.testing.expect(!verify(a1_1_name, s));
}
test "artifact §9: signed and inner artifacts have distinct independent names" {
// Lines 2217-2219: a signed artifact and its unsigned inner content are
// distinct artifacts with distinct names, handled independently -- not
// merely unequal names but names that bind only their own bytes.
const s = "-----BEGIN PGP SIGNED MESSAGE-----\nHash: SHA256\n\n" ++
a1_1_manifest ++ "-----BEGIN PGP SIGNATURE-----\nabc123\n-----END PGP SIGNATURE-----\n";
var buf: [64]u8 = undefined;
const n_signed = artifactName(.sha1, s, &buf);
try std.testing.expect(!std.mem.eql(u8, n_signed, a1_1_name));
try std.testing.expect(verify(n_signed, s));
try std.testing.expect(!verify(n_signed, a1_1_manifest));
try std.testing.expect(!verify(a1_1_name, s));
}
test "artifact §9: token-validation acceptance is not verified integrity" {
// Lines 2254-2258: a token that passes format validation (correct
// length, all lowercase hex) MUST NOT be treated as verified content --
// that conjunction, ACCEPT-at-token-stage paired with REJECT-at-verify,
// is the property under test, not re-tested by either check alone.
const zero40: []const u8 = "0" ** 40;
const name_rows = [_]struct { tok: []const u8, content: []const u8 }{
.{ .tok = zero40, .content = a1_1_manifest }, // valid 40-hex, wrong value
.{ .tok = a1_1_name, .content = "hello world\n" }, // valid name, wrong content
};
for (name_rows) |row| {
try std.testing.expect(validateName(row.tok)); // token stage: ACCEPT
try std.testing.expect(!verify(row.tok, row.content)); // verify stage: REJECT
}
// Z row: flip one digest digit of the a1_1 manifest (still 32-hex, so
// still token-valid), leaving the recomputed checksum mismatched.
var flipped: [127]u8 = undefined;
@memcpy(&flipped, a1_1_manifest);
flipped[94] = 'd'; // digest[0] was 'c' (§A.2 offset table); still hex
const digest = flipped[94..126];
var z_line: [35]u8 = undefined;
z_line[0] = 'Z';
z_line[1] = ' ';
@memcpy(z_line[2..34], digest);
z_line[34] = '\n';
try std.testing.expect(parseZCard(tokenizeCardLine(&z_line).?) != null); // token stage: ACCEPT
try std.testing.expectEqual(ZVerdict.mismatch, verifyZCard(&flipped)); // verify stage: REJECT
}
test "artifact §9: positional Z framing wins over a textual decoy" {
// Lines 2276-2280: a repeated "Z " earlier in the artifact is ordinary
// content covered by the final checksum; only the final 35 bytes
// participate in verification, so a textual scan for "Z " is misled by
// the earlier occurrence while the positional check is not.
const decoy_digest: []const u8 = "1" ** 32;
const prefix = "Z " ++ decoy_digest ++ "\nC filler\n";
var digest_buf: [32]u8 = undefined;
const digest = selfChecksum(prefix, &digest_buf);
var content_buf: [prefix.len + 35]u8 = undefined;
@memcpy(content_buf[0..prefix.len], prefix);
content_buf[prefix.len] = 'Z';
content_buf[prefix.len + 1] = ' ';
@memcpy(content_buf[prefix.len + 2 .. prefix.len + 34], digest);
content_buf[prefix.len + 34] = '\n';
const content: []const u8 = &content_buf;
try std.testing.expectEqual(ZVerdict.match, verifyZCard(content));
// The first textual "Z " hit is the decoy at offset 0, strictly before
// the real positional Z at content.len - 35.
const first_hit = std.mem.indexOf(u8, content, "Z ").?;
try std.testing.expect(first_hit < content.len - 35);
}
test "artifact §9: parse never reads past the buffer on truncation/W-edge" {
// Lines 2269-2274: the W card's size-bound and overflow rules, plus
// the general "MUST NOT read beyond the end of the artifact buffer
// under any malformed input". The uniform-random 0..N sweep is already
// covered by the line-5413 full-parse fuzz and the line-5470 parseWCard
// fuzz; this sweep targets the exact boundaries those rarely land on.
const a = std.testing.allocator;
// Every exact prefix of a well-formed artifact: a declared ParseError
// or a Manifest, never a trap -- an out-of-bounds slice panics under
// Debug/ReleaseSafe, the property under test.
var k: usize = 0;
while (k <= a1_1_manifest.len) : (k += 1) {
if (parse(a, a1_1_manifest[0..k])) |m| {
var mm = m;
mm.deinit();
} else |_| {}
}
// W-edge: an 8-byte body followed by its terminator LF and one
// trailing filler byte -- "remaining" (bytes after the size line) is
// fixed at 10. Only size == remaining-2 can land the r3 check on the
// real LF; remaining-1 and remaining put body_end one step from (or
// at) the buffer's end, where r4's bound check MUST fire before
// artifact[body_end] is ever read. remaining+1 rejects at r2, before
// body_end is even computed. The final row overflows the accumulator
// (r1) partway through 25 digits, well short of the digit span's own
// bound.
const body = "orangeee";
comptime std.debug.assert(body.len == 8);
const size_rows = [_][]const u8{ "8", "9", "10", "11", "9" ** 25 };
for (size_rows) |digits| {
var buf: [64]u8 = undefined;
var w: usize = 0;
buf[w] = 'W';
w += 1;
buf[w] = ' ';
w += 1;
std.debug.assert(w + digits.len < buf.len - body.len - 2);
@memcpy(buf[w .. w + digits.len], digits);
w += digits.len;
buf[w] = '\n';
w += 1;
@memcpy(buf[w .. w + body.len], body);
w += body.len;
buf[w] = '\n';
w += 1;
buf[w] = 'X';
w += 1;
const art = buf[0..w];
if (parseWCard(art, 0)) |card| {
try std.testing.expect(card.next < art.len);
try std.testing.expect(art[card.next - 1] == '\n');
try std.testing.expect(card.body.len <= art.len);
}
}
}
fn deepEql(a: anytype, b: @TypeOf(a)) bool {
const T = @TypeOf(a);
return switch (@typeInfo(T)) {
.@"struct" => |info| blk: {
inline for (info.fields) |f| {
if (!deepEql(@field(a, f.name), @field(b, f.name))) break :blk false;
}
break :blk true;
},
.optional => blk: {
const av = a orelse break :blk (b == null);
const bv = b orelse break :blk false;
break :blk deepEql(av, bv);
},
.pointer => |info| switch (info.size) {
.slice => if (info.child == u8) std.mem.eql(u8, a, b) else blk2: {
if (a.len != b.len) break :blk2 false;
for (a, b) |x, y| {
if (!deepEql(x, y)) break :blk2 false;
}
break :blk2 true;
},
else => a == b,
},
else => a == b, // enums, bools, ints: value equality
};
}
fn expectManifestEqual(a_m: Manifest, b_m: Manifest) !void {
try std.testing.expectEqual(a_m.kind, b_m.kind);
const fields = [_][]const u8{
"a", "b", "c", "d", "e", "g", "h", "i", "k", "l",
"n", "r", "u", "w", "f", "j", "m", "p", "q", "t",
};
inline for (fields) |name| {
try std.testing.expect(deepEql(@field(a_m, name), @field(b_m, name)));
}
}
test "artifact §6.2/§A.1.1/§A.2: create round-trips the worked-example manifest byte-for-byte" {
const a = std.testing.allocator;
var m = try parse(a, a1_1_manifest);
defer m.deinit();
const out = try create(&m, a);
defer a.free(out);
try std.testing.expectEqualSlices(u8, a1_1_manifest, out);
try std.testing.expectEqualStrings("Z c62e3619f7c41ee77b33f00df8ec26f4\n", out[out.len - 35 ..]);
try std.testing.expectEqual(ZVerdict.match, verifyZCard(out));
}
const a3_manifest = @embedFile("rfc_vectors/artifact_a3_rcard_manifest.txt");
test "artifact §6.3.4/§A.3: create round-trips the R-card worked example byte-for-byte" {
const a = std.testing.allocator;
var m = try parse(a, a3_manifest);
defer m.deinit();
const out = try create(&m, a);
defer a.free(out);
try std.testing.expectEqualSlices(u8, a3_manifest, out);
try std.testing.expect(std.mem.indexOf(u8, out, "R 95feb799536dc053e7968efb7cdd3ec3\n") != null);
try std.testing.expectEqual(ZVerdict.match, verifyZCard(out));
}
test "artifact §6.3.4: create emits the R token as 32 lowercase hex even when the parse-accepted token was uppercase" {
const a = std.testing.allocator;
const upper = "D41D8CD98F00B204E9800998ECF8427E"; // §6.1: case-insensitive on read
var prefix_buf: [128]u8 = undefined;
const prefix = try std.fmt.bufPrint(&prefix_buf, "C x\nD 2007-07-21T14:09:59\nP\nR {s}\nU drh\n", .{upper});
var digest: [32]u8 = undefined;
_ = selfChecksum(prefix, &digest);
var art_buf: [256]u8 = undefined;
const art = try std.fmt.bufPrint(&art_buf, "{s}Z {s}\n", .{ prefix, digest });
var m = try parse(a, art);
defer m.deinit();
const out = try create(&m, a);
defer a.free(out);
try std.testing.expect(std.mem.indexOf(u8, out, "R d41d8cd98f00b204e9800998ecf8427e\n") != null);
try std.testing.expect(std.mem.indexOf(u8, out, "R " ++ upper) == null);
}
const a1_2_technote = @embedFile("rfc_vectors/artifact_a1_2_sha3_technote.txt");
test "artifact §4.7.18/§A.1.2: create round-trips the technote Manifest across parse->create->parse" {
const a = std.testing.allocator;
var m0 = try parse(a, a1_2_technote);
defer m0.deinit();
const out1 = try create(&m0, a);
defer a.free(out1);
var m1 = try parse(a, out1);
defer m1.deinit();
// §A.1.2 stores N-after-P (§4.4 compat exception); the writer always
// emits strict-lexical N-before-P -- same Manifest, different bytes, so
// this is a data comparison, not a byte comparison.
try expectManifestEqual(m0, m1);
try std.testing.expect(std.mem.indexOf(u8, out1, "W 414\n") != null);
try std.testing.expectEqual(ZVerdict.match, verifyZCard(out1));
// Re-create identity: a second pass over the re-parsed data yields the
// exact same bytes -- `create` is a pure function of the Manifest.
const out2 = try create(&m1, a);
defer a.free(out2);
try std.testing.expectEqualSlices(u8, out1, out2);
}
test "artifact §4.4/§4.7: create emits cards in non-decreasing card-type order" {
const a = std.testing.allocator;
var m = try parse(a, a3_manifest);
defer m.deinit();
const out = try create(&m, a);
defer a.free(out);
var prev: u8 = 0;
var pos: usize = 0;
// Bounded by out.len: `frame`'s own §4.1 grammar guarantees every line
// (including a W body, opaque here since a3 carries none) ends in LF.
while (pos < out.len) {
const letter = out[pos];
try std.testing.expect(isCardLetter(letter));
try std.testing.expect(letter >= prev);
prev = letter;
const lf = std.mem.indexOfScalarPos(u8, out, pos, '\n').?;
pos = lf + 1;
}
}
test "artifact §4.5.2: create->parse->deinit of a large manifest crossing an arena node boundary leaks nothing" {
const a = std.testing.allocator;
var buf: std.ArrayList(u8) = .empty;
defer buf.deinit(a);
try buf.appendSlice(a, "C large\\stest\\smanifest\n");
try buf.appendSlice(a, "D 2026-07-02T11:18:42.940\n");
const hash64 = "a" ** 64; // any well-formed 64-hex placeholder (§4.7.6 baseline hash)
var i: usize = 0;
while (i < 300) : (i += 1) {
try buf.print(a, "F f{d:0>4}.txt {s}\n", .{ i, hash64 });
}
try buf.appendSlice(a, "P " ++ ("b" ** 64) ++ "\n");
i = 0;
while (i < 300) : (i += 1) {
try buf.print(a, "T +t{d:0>4} *\n", .{i});
}
try buf.appendSlice(a, "U drh\n");
var digest: [32]u8 = undefined;
_ = selfChecksum(buf.items, &digest);
try buf.print(a, "Z {s}\n", .{digest});
const canonical = buf.items;
var m = try parse(a, canonical);
const out = try create(&m, a);
defer a.free(out);
try std.testing.expectEqualSlices(u8, canonical, out);
var m2 = try parse(a, out);
m2.deinit();
m.deinit();
}
fn createNoLeak(allocator: std.mem.Allocator, m: *const Manifest) !void {
const out = try create(m, allocator);
allocator.free(out);
}
test "artifact §6/§4: create propagates allocation failure" {
const a = std.testing.allocator;
var m = try parse(a, a1_1_manifest);
defer m.deinit();
try std.testing.checkAllAllocationFailures(a, createNoLeak, .{&m});
}