const std = @import("std");
const fast_export = @import("git_fast_export.zig");
const Limits = fast_export.Limits;
const Reader = fast_export.Reader;
const Event = fast_export.Event;
const oid = "0123456789abcdef0123456789abcdef01234567";
const second_oid = "89abcdef0123456789abcdef0123456789abcdef";
var stream_bytes: []const u8 = &.{};
var stream_split: usize = 0;
var stream_offset: usize = 0;
fn splitStream(_: *std.Io.Reader, writer: *std.Io.Writer, limit: std.Io.Limit) std.Io.Reader.StreamError!usize {
if (stream_offset == stream_bytes.len) return error.EndOfStream;
const boundary = if (stream_offset < stream_split) stream_split else stream_bytes.len;
const available = boundary - stream_offset;
const count = @min(available, @backingInt(limit));
if (count == 0) return error.EndOfStream;
try writer.writeAll(stream_bytes[stream_offset..][0..count]);
stream_offset += count;
return count;
}
fn initSplitInput(bytes: []const u8, split: usize, buffer: *[1]u8) std.Io.Reader {
stream_bytes = bytes;
stream_split = split;
stream_offset = 0;
return .{
.vtable = &.{ .stream = splitStream },
.buffer = buffer,
.seek = 0,
.end = 0,
};
}
fn defaultLimits() Limits {
return .{
.max_line = 4096,
.max_data = 4096,
.max_path = 4096,
// Not what these tests bound; the two that DO exercise the event
// ceiling raise it from here explicitly.
.max_events = std.math.maxInt(u64),
};
}
fn expectBlobThenDone(reader: *Reader) !void {
var blob_lease = (try reader.next()).?;
defer blob_lease.deinit();
switch (blob_lease.value) {
.blob => |blob| {
try std.testing.expectEqual(@as(u64, 1), blob.mark);
try std.testing.expectEqualStrings(oid, blob.original_oid.?);
try std.testing.expectEqualSlices(u8, "a\n\x00bc", blob.data);
},
else => return error.TestUnexpectedResult,
}
blob_lease.deinit();
var done_lease = (try reader.next()).?;
defer done_lease.deinit();
switch (done_lease.value) {
.done => {},
else => return error.TestUnexpectedResult,
}
done_lease.deinit();
try std.testing.expect((try reader.next()) == null);
}
test "git fast-export blob framing survives every split position" {
const stream = "feature done\n" ++
"blob\n" ++
"mark :1\n" ++
"original-oid " ++ oid ++ "\n" ++
"data 5\n" ++
"a\n\x00bc\n" ++
"done\n";
for (0..stream.len + 1) |split| {
var input_buffer: [1]u8 = undefined;
var reader = Reader.init(std.testing.allocator, initSplitInput(stream, split, &input_buffer), defaultLimits());
defer reader.deinit();
try expectBlobThenDone(&reader);
}
}
test "git fast-export leases block advancement and release event storage" {
const stream = "feature done\nblob\nmark :1\ndata 1\nx\ndone\n";
const input = std.Io.Reader.fixed(stream);
var reader = Reader.init(std.testing.allocator, input, defaultLimits());
defer reader.deinit();
var lease = (try reader.next()).?;
try std.testing.expectError(error.ActiveLease, reader.next());
lease.deinit();
var done_lease = (try reader.next()).?;
defer done_lease.deinit();
switch (done_lease.value) {
.done => {},
else => return error.TestUnexpectedResult,
}
}
test "git fast-export skips top-level progress directives" {
const stream = "feature done\n" ++
"progress before blob\n" ++
"blob\nmark :1\ndata 0\n\n" ++
"progress after blob\n" ++
"done\n";
const input = std.Io.Reader.fixed(stream);
var reader = Reader.init(std.testing.allocator, input, defaultLimits());
defer reader.deinit();
var blob_lease = (try reader.next()).?;
switch (blob_lease.value) {
.blob => {},
else => return error.TestUnexpectedResult,
}
blob_lease.deinit();
var done_lease = (try reader.next()).?;
defer done_lease.deinit();
switch (done_lease.value) {
.done => {},
else => return error.TestUnexpectedResult,
}
done_lease.deinit();
try std.testing.expect((try reader.next()) == null);
}
test "git fast-export commit decodes identities modes paths and ordered commands" {
const stream = "feature done\n" ++
"commit refs/heads/main\n" ++
"mark :2\n" ++
"original-oid " ++ oid ++ "\n" ++
"author Alice Example <[email protected]> 1700000000 -0230\n" ++
"committer Bob Example <[email protected]> 1700000001 +0530\n" ++
"data 4\n" ++
"msg\n" ++
"from :1\n" ++
"merge " ++ second_oid ++ "\n" ++
"R \"old\\\\path\" \"space path\\t\\377\"\n" ++
"M 100755 :2 \"space path\\t\\377\"\n" ++
"M 100644 :1 plain\n" ++
"M 120000 :3 link\n" ++
"D -leading-dash\n" ++
"D \"line\\nfeed\"\n" ++
"D \"bells\\a\\v\"\n" ++
"C plain copy\n" ++
"deleteall\n" ++
"\n" ++
"done\n";
const input = std.Io.Reader.fixed(stream);
var reader = Reader.init(std.testing.allocator, input, defaultLimits());
defer reader.deinit();
var lease = (try reader.next()).?;
defer lease.deinit();
switch (lease.value) {
.commit => |commit| {
try std.testing.expectEqualStrings("refs/heads/main", commit.ref);
try std.testing.expectEqual(@as(u64, 2), commit.mark);
try std.testing.expectEqualStrings(oid, commit.original_oid.?);
try std.testing.expectEqualStrings("Alice Example", commit.author.name);
try std.testing.expectEqualStrings("[email protected]", commit.author.email);
try std.testing.expectEqual(@as(i64, 1700000000), commit.author.timestamp);
try std.testing.expectEqual(@as(i16, -150), commit.author.timezone_offset_minutes);
try std.testing.expectEqualStrings("Bob Example", commit.committer.name);
try std.testing.expectEqual(@as(i16, 330), commit.committer.timezone_offset_minutes);
try std.testing.expectEqualSlices(u8, "msg\n", commit.message);
try std.testing.expectEqual(@as(usize, 1), commit.merges.len);
try std.testing.expectEqualStrings(second_oid, commit.merges[0].oid);
try std.testing.expectEqual(@as(usize, 9), commit.commands.len);
switch (commit.commands[0]) {
.rename => |rename| {
try std.testing.expectEqualStrings("old\\path", rename.old);
try std.testing.expectEqualSlices(u8, "space path\t\xff", rename.new);
},
else => return error.TestUnexpectedResult,
}
switch (commit.commands[1]) {
.modify => |modify| {
try std.testing.expectEqual(.executable, modify.mode);
try std.testing.expectEqual(@as(u64, 2), modify.data.mark);
try std.testing.expectEqualSlices(u8, "space path\t\xff", modify.path);
},
else => return error.TestUnexpectedResult,
}
switch (commit.commands[5]) {
.delete => |path| try std.testing.expectEqualSlices(u8, "line\nfeed", path),
else => return error.TestUnexpectedResult,
}
switch (commit.commands[6]) {
.delete => |path| try std.testing.expectEqualSlices(u8, "bells\x07\x0b", path),
else => return error.TestUnexpectedResult,
}
switch (commit.commands[8]) {
.delete_all => {},
else => return error.TestUnexpectedResult,
}
},
else => return error.TestUnexpectedResult,
}
}
test "git fast-export accepts an empty reset directly before a commit" {
const stream = "feature done\n" ++
"reset refs/heads/main\n" ++
"commit refs/heads/main\n" ++
"mark :2\n" ++
"author A <[email protected]> 1 +0000\n" ++
"committer C <[email protected]> 2 +0000\n" ++
"data 0\n\n" ++
"done\n";
const input = std.Io.Reader.fixed(stream);
var reader = Reader.init(std.testing.allocator, input, defaultLimits());
defer reader.deinit();
var reset_lease = (try reader.next()).?;
switch (reset_lease.value) {
.reset => |reset| {
try std.testing.expectEqualStrings("refs/heads/main", reset.ref);
try std.testing.expect(reset.target == null);
},
else => return error.TestUnexpectedResult,
}
reset_lease.deinit();
var commit_lease = (try reader.next()).?;
defer commit_lease.deinit();
switch (commit_lease.value) {
.commit => |commit| try std.testing.expectEqual(@as(u64, 2), commit.mark),
else => return error.TestUnexpectedResult,
}
}
test "git fast-export accepts a reset target directly before a commit" {
const stream = "feature done\n" ++
"reset refs/heads/main\n" ++
"from :2\n" ++
"commit refs/heads/main\n" ++
"mark :3\n" ++
"author A <[email protected]> 1 +0000\n" ++
"committer C <[email protected]> 2 +0000\n" ++
"data 0\n\n" ++
"done\n";
const input = std.Io.Reader.fixed(stream);
var reader = Reader.init(std.testing.allocator, input, defaultLimits());
defer reader.deinit();
var reset_lease = (try reader.next()).?;
switch (reset_lease.value) {
.reset => |reset| try std.testing.expectEqual(@as(u64, 2), reset.target.?.mark),
else => return error.TestUnexpectedResult,
}
reset_lease.deinit();
var commit_lease = (try reader.next()).?;
defer commit_lease.deinit();
switch (commit_lease.value) {
.commit => |commit| try std.testing.expectEqual(@as(u64, 3), commit.mark),
else => return error.TestUnexpectedResult,
}
}
test "git fast-export reset and tag retain metadata" {
const stream = "feature done\n" ++
"reset refs/heads/main\n" ++
"from :2\n\n" ++
"tag v1.0\n" ++
"mark :3\n" ++
"from :2\n" ++
"original-oid " ++ oid ++ "\n" ++
"tagger Release Bot <[email protected]> 1700000002 +0000\n" ++
"data 3\n" ++
"tag\n" ++
"done\n";
const input = std.Io.Reader.fixed(stream);
var reader = Reader.init(std.testing.allocator, input, defaultLimits());
defer reader.deinit();
var reset_lease = (try reader.next()).?;
defer reset_lease.deinit();
switch (reset_lease.value) {
.reset => |reset| {
try std.testing.expectEqualStrings("refs/heads/main", reset.ref);
try std.testing.expectEqual(@as(u64, 2), reset.target.?.mark);
},
else => return error.TestUnexpectedResult,
}
reset_lease.deinit();
var tag_lease = (try reader.next()).?;
defer tag_lease.deinit();
switch (tag_lease.value) {
.tag => |tag| {
try std.testing.expectEqualStrings("v1.0", tag.name);
try std.testing.expectEqual(@as(u64, 3), tag.mark.?);
try std.testing.expectEqual(@as(u64, 2), tag.target.mark);
try std.testing.expectEqualStrings(oid, tag.original_oid.?);
try std.testing.expectEqualStrings("Release Bot", tag.tagger.name);
try std.testing.expectEqualSlices(u8, "tag", tag.message);
},
else => return error.TestUnexpectedResult,
}
}
fn expectNextError(bytes: []const u8, limits: Limits, expected: anyerror) !void {
const input = std.Io.Reader.fixed(bytes);
var reader = Reader.init(std.testing.allocator, input, limits);
defer reader.deinit();
try std.testing.expectError(expected, reader.next());
}
test "git fast-export rejects malformed framing and configured bounds" {
try expectNextError("feature done\nblob\nmark :1\ndata -1\n", defaultLimits(), error.Malformed);
try expectNextError("feature done\nblob\nmark :1\ndata 18446744073709551616\n", defaultLimits(), error.Malformed);
try expectNextError("feature done\nblob\nmark :1\ndata 1\nx", defaultLimits(), error.UnexpectedEof);
try expectNextError("feature done\nblob\nmark :1\ndata 1\nxx", defaultLimits(), error.Malformed);
try expectNextError("feature done\nblob\nmark :x\ndata 0\n\n", defaultLimits(), error.Malformed);
try expectNextError("feature relative-marks\n", defaultLimits(), error.UnsupportedFeature);
try expectNextError("feature done\nblob\nmark :1\ndata 2\nxx\n", .{ .max_line = 64, .max_data = 1, .max_path = 64, .max_events = std.math.maxInt(u64) }, error.DataLimitExceeded);
try expectNextError("feature done\n", .{ .max_line = 3, .max_data = 64, .max_path = 64, .max_events = std.math.maxInt(u64) }, error.LineLimitExceeded);
try expectNextError(
"feature done\ncommit refs/heads/main\nmark :2\nauthor A <[email protected]> 1 +0000\ncommitter C <[email protected]> 2 +0000\ndata 0\nD \"\\400\"\n\n",
defaultLimits(),
error.Malformed,
);
try expectNextError(
"feature done\ncommit refs/heads/main\nmark :2\nauthor A <[email protected]> 1 +0000\ncommitter C <[email protected]> 2 +0000\ndata 0\nD four\n\n",
.{ .max_line = 128, .max_data = 64, .max_path = 3, .max_events = std.math.maxInt(u64) },
error.PathLimitExceeded,
);
const trailing = "feature done\ndone\ntrailing";
const input = std.Io.Reader.fixed(trailing);
var reader = Reader.init(std.testing.allocator, input, defaultLimits());
defer reader.deinit();
var done_lease = (try reader.next()).?;
done_lease.deinit();
try std.testing.expectError(error.TrailingData, reader.next());
}
test "git fast-export requires a terminal done followed by eof" {
const stream = "feature done\nblob\nmark :1\ndata 0\n\n";
const input = std.Io.Reader.fixed(stream);
var reader = Reader.init(std.testing.allocator, input, defaultLimits());
defer reader.deinit();
var blob_lease = (try reader.next()).?;
blob_lease.deinit();
try std.testing.expectError(error.MissingDone, reader.next());
}
test "git fast-export rejects invalid commit semantics at the grammar boundary" {
const prefix = "feature done\ncommit refs/heads/main\nmark :2\nauthor A <[email protected]> 1 +0000\ncommitter C <[email protected]> 2 +0000\ndata 0\n";
try expectNextError(prefix ++ "M 100600 :1 unknown\n\n", defaultLimits(), error.UnknownMode);
try expectNextError(prefix ++ "M 100644 nonsense file\n\n", defaultLimits(), error.Malformed);
try expectNextError("feature done\ncommit refs/heads/main\nmark :2\nauthor A <[email protected]> 999999999999999999999999 +0000\n", defaultLimits(), error.Malformed);
try expectNextError("feature done\ncommit refs/heads/main\nmark :2\nauthor A <[email protected]> 1 +2460\n", defaultLimits(), error.Malformed);
try expectNextError("feature done\ncommit refs/heads/main\nmark :2\nauthor A <[email protected]> 1 +0000\ncommitter C <[email protected]> 2 +0000\n\n", defaultLimits(), error.Malformed);
try expectNextError("feature done\ncommit refs/heads/main\nmark :2\nauthor A <[email protected]> 1 +0000\ncommitter C <[email protected]> 2 +0000\ndata 0\n", defaultLimits(), error.UnexpectedEof);
try expectNextError("feature done\ncommit refs/heads/main\nmark :2\nauthor A <[email protected]> 1 +0000\ncommitter C <[email protected]> 2 +0000\ndata 4\nabc", defaultLimits(), error.UnexpectedEof);
// The name half of the identity guard stays strict: git's own porcelain
// refuses an empty ident name, and the importer maps the name onto the
// U card login.
try expectNextError("feature done\ncommit refs/heads/main\nmark :2\nauthor <[email protected]> 1 +0000\n", defaultLimits(), error.Malformed);
}
test "git fast-export accepts an author with an empty email" {
// `git config user.email ""` makes git export `mohye77 <>`, and `git
// fast-import` reads that same stream back to the same commit, so the
// construct is valid by git's definition on both sides. Refusing it
// rejected input git writes by default. The 437-commit repository that
// surfaced this reduces to these bytes.
const stream = "feature done\n" ++
"blob\nmark :1\ndata 3\nhi\n\n" ++
"commit refs/heads/main\nmark :2\n" ++
"author mohye77 <> 1785464161 -0500\n" ++
"committer mohye77 <> 1785464161 -0500\n" ++
"data 19\nempty email commit\n" ++
"M 100644 :1 f.txt\n\n" ++
"done\n";
const input = std.Io.Reader.fixed(stream);
var reader = Reader.init(std.testing.allocator, input, defaultLimits());
defer reader.deinit();
var blob_lease = (try reader.next()).?;
blob_lease.deinit();
var lease = (try reader.next()).?;
defer lease.deinit();
switch (lease.value) {
.commit => |parsed| {
try std.testing.expectEqualStrings("mohye77", parsed.author.name);
try std.testing.expectEqualStrings("", parsed.author.email);
try std.testing.expectEqualStrings("mohye77", parsed.committer.name);
try std.testing.expectEqualStrings("", parsed.committer.email);
try std.testing.expectEqual(@as(usize, 1), parsed.commands.len);
},
else => return error.TestUnexpectedResult,
}
}
test "git fast-export reports a submodule entry as a gitlink mode" {
// Mode 160000 is a real thing a healthy repository exports. Refusing it is
// an import decision, so the parser hands it to the importer intact.
const stream = "feature done\n" ++
"commit refs/heads/main\nmark :2\nauthor A <[email protected]> 1 +0000\ncommitter C <[email protected]> 2 +0000\ndata 0\n" ++
"M 160000 " ++ oid ++ " nested\nM 100644 :1 keep\n\n" ++
"done\n";
const input = std.Io.Reader.fixed(stream);
var reader = Reader.init(std.testing.allocator, input, defaultLimits());
defer reader.deinit();
var lease = (try reader.next()).?;
defer lease.deinit();
switch (lease.value) {
.commit => |parsed| {
try std.testing.expectEqual(@as(usize, 2), parsed.commands.len);
const gitlink = parsed.commands[0].modify;
try std.testing.expectEqual(fast_export.FileMode.gitlink, gitlink.mode);
try std.testing.expectEqualStrings("nested", gitlink.path);
try std.testing.expectEqualStrings(oid, gitlink.data.oid);
// The rest of the commit still parses normally around it.
try std.testing.expectEqual(fast_export.FileMode.regular, parsed.commands[1].modify.mode);
try std.testing.expectEqualStrings("keep", parsed.commands[1].modify.path);
},
else => return error.TestUnexpectedResult,
}
}
test "git fast-export leaves mark declaration and reference resolution to the importer" {
const stream = "feature done\n" ++
"blob\nmark :1\ndata 0\n\n" ++
"blob\nmark :1\ndata 0\n\n" ++
"commit refs/heads/main\nmark :2\nauthor A <[email protected]> 1 +0000\ncommitter C <[email protected]> 2 +0000\ndata 0\nfrom :999\n\n" ++
"done\n";
const input = std.Io.Reader.fixed(stream);
var reader = Reader.init(std.testing.allocator, input, defaultLimits());
defer reader.deinit();
for (0..2) |_| {
var blob_lease = (try reader.next()).?;
switch (blob_lease.value) {
.blob => {},
else => return error.TestUnexpectedResult,
}
blob_lease.deinit();
}
var commit_lease = (try reader.next()).?;
defer commit_lease.deinit();
switch (commit_lease.value) {
.commit => |commit| try std.testing.expectEqual(@as(u64, 999), commit.from.?.mark),
else => return error.TestUnexpectedResult,
}
}
test "git fast-export accepts exact per-commit merge and derived file command limits" {
const commit_prefix = "feature done\n" ++
"commit refs/heads/main\n" ++
"mark :1\n" ++
"author A <[email protected]> 1 +0000\n" ++
"committer C <[email protected]> 2 +0000\n" ++
"data 0\n";
var merge_limits = defaultLimits();
merge_limits.max_merges_per_commit = 2;
const merge_stream = commit_prefix ++ "merge :2\nmerge :3\n\n";
{
const input = std.Io.Reader.fixed(merge_stream);
var reader = Reader.init(std.testing.allocator, input, merge_limits);
defer reader.deinit();
var lease = (try reader.next()).?;
defer lease.deinit();
switch (lease.value) {
.commit => |commit| try std.testing.expectEqual(@as(usize, 2), commit.merges.len),
else => return error.TestUnexpectedResult,
}
}
try expectNextError(
commit_prefix ++ "merge :2\nmerge :3\nmerge :4\n\n",
merge_limits,
error.MergeLimitExceeded,
);
// The file-command ceiling is derived, so the way to pin it at two is to
// authorize exactly two commands' worth of minimum retention. No count
// appears in the limits at all. It draws on the per-commit arena budget,
// not the cumulative payload one: a commit's command list is held whole
// while payload is released per event, so the two cannot share a number.
var command_limits = defaultLimits();
command_limits.max_command_arena_bytes = 2 * fast_export.min_retained_bytes_per_file_command;
try std.testing.expectEqual(
@as(usize, 2),
fast_export.fileCommandCeiling(command_limits),
);
const command_stream = commit_prefix ++ "D one\nD two\n\n";
{
const input = std.Io.Reader.fixed(command_stream);
var reader = Reader.init(std.testing.allocator, input, command_limits);
defer reader.deinit();
var lease = (try reader.next()).?;
defer lease.deinit();
switch (lease.value) {
.commit => |commit| try std.testing.expectEqual(@as(usize, 2), commit.commands.len),
else => return error.TestUnexpectedResult,
}
try std.testing.expect(reader.file_command_overrun == null);
}
{
const input = std.Io.Reader.fixed(commit_prefix ++ "D one\nD two\nD three\n\n");
var reader = Reader.init(std.testing.allocator, input, command_limits);
defer reader.deinit();
try std.testing.expectError(error.FileCommandLimitExceeded, reader.next());
// The refusal retains the numbers its diagnostic must name.
const overrun = reader.file_command_overrun.?;
try std.testing.expectEqual(@as(usize, 3), overrun.observed_at_least);
try std.testing.expectEqual(@as(usize, 2), overrun.ceiling);
try std.testing.expectEqual(command_limits.max_command_arena_bytes, overrun.arena_budget_bytes);
try std.testing.expectEqual(
fast_export.min_retained_bytes_per_file_command,
overrun.min_bytes_per_command,
);
}
// Raising the arena budget raises the ceiling with it: nothing else does,
// and in particular the cumulative payload budget no longer moves it.
var doubled = command_limits;
doubled.max_total_data *= 2;
try std.testing.expectEqual(@as(usize, 2), fast_export.fileCommandCeiling(doubled));
doubled.max_command_arena_bytes *= 2;
try std.testing.expectEqual(@as(usize, 4), fast_export.fileCommandCeiling(doubled));
}
test "git fast-export imports a commit far above the retired 200,000 file wall" {
// The retired ceiling was the benchmark corpus width promoted to policy.
// A root commit that drops a whole monorepo in at once is an ordinary
// source shape, and `git fast-export` lists the entire tree for it, so a
// commit past that number must import rather than fail.
const retired_ceiling: usize = 200_000;
const command_count: usize = retired_ceiling + 1;
var stream: std.ArrayList(u8) = .empty;
defer stream.deinit(std.testing.allocator);
try stream.appendSlice(std.testing.allocator, "feature done\n" ++
"commit refs/heads/main\n" ++
"mark :1\n" ++
"author A <[email protected]> 1 +0000\n" ++
"committer C <[email protected]> 2 +0000\n" ++
"data 0\n");
var line_buffer: [32]u8 = undefined;
for (0..command_count) |index| {
const line = try std.fmt.bufPrint(&line_buffer, "D f{d}\n", .{index});
try stream.appendSlice(std.testing.allocator, line);
}
try stream.appendSlice(std.testing.allocator, "\ndone\n");
var limits = defaultLimits();
// The production payload budget, not a value picked to make this pass.
limits.max_total_data = @as(u64, 4) * 1024 * 1024 * 1024;
try std.testing.expect(fast_export.fileCommandCeiling(limits) > command_count);
const input = std.Io.Reader.fixed(stream.items);
var reader = Reader.init(std.testing.allocator, input, limits);
defer reader.deinit();
var lease = (try reader.next()).?;
switch (lease.value) {
.commit => |commit| try std.testing.expectEqual(command_count, commit.commands.len),
else => return error.TestUnexpectedResult,
}
lease.deinit();
try std.testing.expect(reader.file_command_overrun == null);
var done_lease = (try reader.next()).?;
defer done_lease.deinit();
switch (done_lease.value) {
.done => {},
else => return error.TestUnexpectedResult,
}
}
test "git fast-export bounds total events and payload bytes before allocation" {
var event_limits = defaultLimits();
event_limits.max_events = 4;
// `feature done` configures the stream; progress, blobs, and done each
// consume one event slot.
const event_stream = "feature done\n" ++
"progress before blobs\n" ++
"blob\nmark :1\ndata 0\n\n" ++
"blob\nmark :2\ndata 0\n\n" ++
"done\n";
{
const input = std.Io.Reader.fixed(event_stream);
var reader = Reader.init(std.testing.allocator, input, event_limits);
defer reader.deinit();
for (0..2) |_| {
var lease = (try reader.next()).?;
switch (lease.value) {
.blob => {},
else => return error.TestUnexpectedResult,
}
lease.deinit();
}
var done_lease = (try reader.next()).?;
switch (done_lease.value) {
.done => {},
else => return error.TestUnexpectedResult,
}
done_lease.deinit();
try std.testing.expect((try reader.next()) == null);
}
{
const overflow_stream = "feature done\n" ++
"progress before blobs\n" ++
"blob\nmark :1\ndata 0\n\n" ++
"blob\nmark :2\ndata 0\n\n" ++
"blob\nmark :3\ndata 0\n\n" ++
"done\n";
const input = std.Io.Reader.fixed(overflow_stream);
var reader = Reader.init(std.testing.allocator, input, event_limits);
defer reader.deinit();
for (0..3) |_| {
var lease = (try reader.next()).?;
lease.deinit();
}
try std.testing.expectError(error.EventLimitExceeded, reader.next());
}
var payload_limits = defaultLimits();
payload_limits.max_events = 3;
payload_limits.max_total_data = 3;
const payload_stream = "feature done\n" ++
"blob\nmark :1\ndata 2\nab\n" ++
"blob\nmark :2\ndata 1\nc\n" ++
"done\n";
for (0..payload_stream.len + 1) |split| {
var input_buffer: [1]u8 = undefined;
var reader = Reader.init(
std.testing.allocator,
initSplitInput(payload_stream, split, &input_buffer),
payload_limits,
);
defer reader.deinit();
for (0..2) |_| {
var lease = (try reader.next()).?;
switch (lease.value) {
.blob => {},
else => return error.TestUnexpectedResult,
}
lease.deinit();
}
var done_lease = (try reader.next()).?;
switch (done_lease.value) {
.done => {},
else => return error.TestUnexpectedResult,
}
done_lease.deinit();
try std.testing.expect((try reader.next()) == null);
}
{
const input = std.Io.Reader.fixed(
"feature done\nblob\nmark :1\ndata 2\nab\nblob\nmark :2\ndata 2\ncd\n",
);
var reader = Reader.init(std.testing.allocator, input, payload_limits);
defer reader.deinit();
var first_blob = (try reader.next()).?;
first_blob.deinit();
try std.testing.expectError(error.DataLimitExceeded, reader.next());
}
}
test "git fast-export payload refusals name which bound refused them" {
// One error carries both bounds, and only one of them has a flag behind
// it. A caller that reports the wrong one sends an operator to a knob that
// cannot help, so the record — not the error name — is the contract.
{
// Cumulative: the first blob's two bytes admit, the second reaches four
// against a budget of three. Parsing stops there, so four is a lower
// bound on the stream's payload rather than its total.
const input = std.Io.Reader.fixed(
"feature done\nblob\nmark :1\ndata 2\nab\nblob\nmark :2\ndata 2\ncd\n",
);
var reader = Reader.init(std.testing.allocator, input, .{
.max_line = 64,
.max_data = 64,
.max_path = 64,
.max_total_data = 3,
.max_events = std.math.maxInt(u64),
});
defer reader.deinit();
var first_blob = (try reader.next()).?;
first_blob.deinit();
try std.testing.expectError(error.DataLimitExceeded, reader.next());
const overrun = reader.payload_overrun.?;
try std.testing.expectEqual(fast_export.PayloadOverrun.Bound.cumulative_total, overrun.bound);
try std.testing.expectEqual(@as(u64, 4), overrun.observed_at_least);
try std.testing.expectEqual(@as(u64, 3), overrun.ceiling);
}
{
// Single payload: refused on its own size before it is charged against
// the cumulative budget, which here is wide open.
const input = std.Io.Reader.fixed("feature done\nblob\nmark :1\ndata 2\nab\n");
var reader = Reader.init(std.testing.allocator, input, .{
.max_line = 64,
.max_data = 1,
.max_path = 64,
.max_total_data = 1 << 20,
.max_events = std.math.maxInt(u64),
});
defer reader.deinit();
try std.testing.expectError(error.DataLimitExceeded, reader.next());
const overrun = reader.payload_overrun.?;
try std.testing.expectEqual(fast_export.PayloadOverrun.Bound.single_payload, overrun.bound);
try std.testing.expectEqual(@as(u64, 2), overrun.observed_at_least);
try std.testing.expectEqual(@as(u64, 1), overrun.ceiling);
}
}
test "git fast-export commit data framing accepts both legal forms across every split" {
const prefix = "feature done\n" ++
"commit refs/heads/main\n" ++
"mark :1\n" ++
"author A <[email protected]> 1 +0000\n" ++
"committer C <[email protected]> 2 +0000\n";
const suffix = "from :2\n\ndone\n";
const Form = struct {
stream: []const u8,
message: []const u8,
};
const forms = [_]Form{
.{ .stream = prefix ++ "data 3\nabc\n" ++ suffix, .message = "abc" },
.{ .stream = prefix ++ "data 4\nabc\n" ++ suffix, .message = "abc\n" },
.{ .stream = prefix ++ "data 0\n" ++ suffix, .message = "" },
};
for (forms) |form| {
const stream = form.stream;
for (0..stream.len + 1) |split| {
var input_buffer: [1]u8 = undefined;
var reader = Reader.init(std.testing.allocator, initSplitInput(stream, split, &input_buffer), defaultLimits());
defer reader.deinit();
var commit_lease = (try reader.next()).?;
switch (commit_lease.value) {
.commit => |commit| {
try std.testing.expectEqualStrings(form.message, commit.message);
try std.testing.expectEqual(@as(u64, 2), commit.from.?.mark);
},
else => return error.TestUnexpectedResult,
}
commit_lease.deinit();
var done_lease = (try reader.next()).?;
defer done_lease.deinit();
switch (done_lease.value) {
.done => {},
else => return error.TestUnexpectedResult,
}
}
}
}
test "git fast-export commit data count permits an immediately following command" {
const stream = "feature done\n" ++
"commit refs/heads/main\n" ++
"mark :1\n" ++
"author A <[email protected]> 1 +0000\n" ++
"committer C <[email protected]> 2 +0000\n" ++
"data 1\nxfrom :2\n\n" ++
"done\n";
const input = std.Io.Reader.fixed(stream);
var reader = Reader.init(std.testing.allocator, input, defaultLimits());
defer reader.deinit();
var lease = (try reader.next()).?;
defer lease.deinit();
switch (lease.value) {
.commit => |commit| {
try std.testing.expectEqualStrings("x", commit.message);
try std.testing.expectEqual(@as(u64, 2), commit.from.?.mark);
},
else => return error.TestUnexpectedResult,
}
}