//! Fossil's sterilization of tracked files whose bytes are a structural
//! artifact, as `fossil import --git` applies it (src/import.c
//! `check_and_add_file`, src/manifest.c `sterilize_manifest`).
//!
//! A store names a blob by its content hash and nothing else, so a file whose
//! bytes parse as a check-in, technote, tag or any other structural artifact
//! is crosslinked as one by every reader -- zit, and Fossil after a clone or a
//! rebuild. A Git repository that keeps raw Fossil artifacts as test fixtures
//! would otherwise come out of an import with phantom check-ins. Fossil's
//! import appends a comment line after the final card, before a trailing
//! signature block, which the card-order rule rejects
//! (draft-fossil-artifact-format ยง4.1.2), so the stored blob is file content
//! only, on every peer, with no out-of-band marking to lose on sync.
//!
//! The marker is always the control-artifact variant, for a check-in too:
//! `check_and_add_file` passes -1, not the check-in type. That choice is also
//! what keeps the mapping invertible. The manifest variant is what a Fossil
//! mirror's own `manifest` file already carries, and a file that ends in it is
//! not an artifact, so it is stored untouched; had the import used the same
//! line, export could not tell that file from a sterilized raw check-in.
//!
//! export-git undoes exactly what the import did. A file is ARTIFACT-SHAPED
//! when it parses as a structural artifact once every control marker at its
//! card boundary is peeled off. The import adds one marker to every
//! artifact-shaped file and the export removes one from every artifact-shaped
//! blob that ends in one. A Git file that already ends in the marker over an
//! artifact (a Fossil import exported back to Git) is artifact-shaped too, so
//! it gains a second marker and loses exactly that one on the way out: the
//! round trip is byte-identical for every file, and no stored blob parses.
const std = @import("std");
const artifact = @import("../artifact.zig");
/// Fossil's `sterilize_manifest(p, -1)` line, byte for byte.
pub const control_sterilizer = "# Remove this line to create a well-formed Fossil control artifact.\n";
const pgp_clearsign_open = "-----BEGIN PGP SIGNED MESSAGE-----";
const ssh_clearsign_open = "-----BEGIN SSH SIGNED MESSAGE-----";
/// The bytes the import stores for a file, or null when they are the file's
/// own: an artifact-shaped file comes back with the marker spliced in at its
/// card boundary. The caller owns the result.
pub fn sterilize(gpa: std.mem.Allocator, bytes: []const u8) error{OutOfMemory}!?[]u8 {
if (!try isArtifactShaped(gpa, bytes)) return null;
const end = cardsEnd(bytes);
return try std.mem.concat(gpa, u8, &.{ bytes[0..end], control_sterilizer, bytes[end..] });
}
/// The file bytes the import was given for a stored blob, or null when the
/// blob is them already: one marker removed from an artifact-shaped blob that
/// ends in one. The caller owns the result.
pub fn unsterilize(gpa: std.mem.Allocator, bytes: []const u8) error{OutOfMemory}!?[]u8 {
const end = cardsEnd(bytes);
if (!std.mem.endsWith(u8, bytes[0..end], control_sterilizer)) return null;
if (!try isArtifactShaped(gpa, bytes)) return null;
return try std.mem.concat(gpa, u8, &.{ bytes[0 .. end - control_sterilizer.len], bytes[end..] });
}
/// Whether `bytes`, with every control marker at the card boundary peeled
/// off, parse as a structural artifact. The parse is the import-tolerant one,
/// the widest any reader of the store applies, so nothing a reader would
/// crosslink escapes.
///
/// Two bytes reject essentially all real file content before anything is
/// parsed: an artifact opens with a card, an uppercase letter followed by SP
/// or LF, or with a clearsign header.
pub fn isArtifactShaped(gpa: std.mem.Allocator, bytes: []const u8) error{OutOfMemory}!bool {
if (bytes.len < 2) return false;
const card_shaped = bytes[0] >= 'A' and bytes[0] <= 'Z' and (bytes[1] == ' ' or bytes[1] == '\n');
if (!card_shaped and !std.mem.startsWith(u8, bytes, pgp_clearsign_open) and
!std.mem.startsWith(u8, bytes, ssh_clearsign_open)) return false;
const end = cardsEnd(bytes);
var cards_end = end;
while (std.mem.endsWith(u8, bytes[0..cards_end], control_sterilizer)) cards_end -= control_sterilizer.len;
if (cards_end == end) return parses(gpa, bytes);
const peeled = try std.mem.concat(gpa, u8, &.{ bytes[0..cards_end], bytes[end..] });
defer gpa.free(peeled);
return parses(gpa, peeled);
}
fn parses(gpa: std.mem.Allocator, bytes: []const u8) error{OutOfMemory}!bool {
var parsed = artifact.parseForImport(gpa, bytes) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
else => return false,
};
parsed.deinit(gpa);
return true;
}
/// The offset just past the final card: the end of the signed body of a
/// clearsigned artifact, where `sterilize_manifest` splices its line, and the
/// end of the bytes otherwise.
fn cardsEnd(bytes: []const u8) usize {
const body = artifact.stripSignatureWrapper(bytes);
if (body.len == 0) return bytes.len;
const end = @intFromPtr(body.ptr) - @intFromPtr(bytes.ptr) + body.len;
std.debug.assert(end <= bytes.len);
return end;
}
// Real artifacts, written by fossil 2.x: a check-in naming a parent and a file
// this store never holds, a technote, and a tag control artifact.
const fixture_checkin =
\\C sparse\scommit
\\D 2026-10-23T00:47:51.328
\\F README a57d4ad9fb250f5a211a3ed89d378cd5aaa6fc8da2d626cac4868d9964840965
\\P 973a9910b48160590ebea3ee7f2a4ef4dccea66a0609afa636303ce984ab4df7
\\R 4a2f3eb03fa2d776c4ce076c13863f30
\\U u
\\Z af1e769c68cc3c121e2da2b27964c4ff
\\
;
const fixture_technote =
\\C a\snote
\\D 2026-10-23T00:47:57.876
\\E 2026-01-02T03:04:05 768fb88c0f4a48b522c937ce3f31ae41c223b65d
\\N text/plain
\\U u
\\W 10
\\note body
\\
\\Z 885540e498e2ad8549c9b5f87808e67f
\\
;
const fixture_tag =
\\D 2026-10-23T00:47:51.343
\\T +sym-release c887f9cb7f5508a9c80a768cca0b61f2735fc7fbfe553117a0389bb08cc2a88a
\\U u
\\Z 01fc88bc79d1f34680020972308023a3
\\
;
const manifest_sterilizer = "# Remove this line to create a well-formed Fossil manifest.\n";
const signature =
"-----BEGIN PGP SIGNATURE-----\n" ++
"iEYEARECAAYFAk507VMACgkQoxKgR168RlEAVgCfXvTVgVXWPGlRVI4ZDt0OzYbj\n" ++
"=LZzm\n" ++
"-----END PGP SIGNATURE-----\n";
const signed_checkin = "-----BEGIN PGP SIGNED MESSAGE-----\nHash: SHA1\n\n" ++ fixture_checkin ++ signature;
fn expectParses(bytes: []const u8, want: bool) !void {
try std.testing.expectEqual(want, try parses(std.testing.allocator, bytes));
}
test "file sterilizer: every artifact-shaped file is stored unparseable and exported byte-identical" {
const gpa = std.testing.allocator;
const twice = control_sterilizer ++ control_sterilizer;
const Case = struct { file: []const u8, stored: ?[]const u8 };
const cases = [_]Case{
// Each structural type gains the control-artifact line after its Z card.
.{ .file = fixture_checkin, .stored = fixture_checkin ++ control_sterilizer },
.{ .file = fixture_technote, .stored = fixture_technote ++ control_sterilizer },
.{ .file = fixture_tag, .stored = fixture_tag ++ control_sterilizer },
// A signature stays trailing: the line goes before it, as Fossil splices it.
.{
.file = signed_checkin,
.stored = "-----BEGIN PGP SIGNED MESSAGE-----\nHash: SHA1\n\n" ++ fixture_checkin ++ control_sterilizer ++ signature,
},
// Already sterilized by Fossil's import: a second line, so export can
// take exactly one back off.
.{ .file = fixture_tag ++ control_sterilizer, .stored = fixture_tag ++ twice },
.{ .file = fixture_checkin ++ twice, .stored = fixture_checkin ++ twice ++ control_sterilizer },
// A Fossil mirror's `manifest` file is not an artifact, and stays as is.
.{ .file = fixture_checkin ++ manifest_sterilizer, .stored = null },
// Not artifacts: a damaged Z card, ordinary text, text that merely
// ends in the line, and the line alone.
.{ .file = fixture_tag[0 .. fixture_tag.len - 3] ++ "00\n", .stored = null },
.{ .file = "hello\n", .stored = null },
.{ .file = "A note\n" ++ control_sterilizer, .stored = null },
.{ .file = control_sterilizer, .stored = null },
.{ .file = "", .stored = null },
};
for (cases) |case| {
const stored = try sterilize(gpa, case.file);
defer if (stored) |bytes| gpa.free(bytes);
if (case.stored) |want| {
try std.testing.expectEqualStrings(want, stored orelse return error.NotSterilized);
} else try std.testing.expect(stored == null);
const kept = stored orelse case.file;
// No stored blob is a structural artifact.
try expectParses(kept, false);
// Export gives back the file exactly.
const exported = try unsterilize(gpa, kept);
defer if (exported) |bytes| gpa.free(bytes);
try std.testing.expectEqualStrings(case.file, exported orelse kept);
try std.testing.expectEqual(stored != null, exported != null);
}
// The fixtures themselves are what a reader would crosslink.
for ([_][]const u8{ fixture_checkin, fixture_technote, fixture_tag, signed_checkin }) |bytes| try expectParses(bytes, true);
}