Artifact 92f8869179af67a37dae40f57860b45d912b30ea4683179483dc70f831a72ea1:
- File
src/git_fast_export_test.zig
— part of check-in
[ee703cde16]
at
2026-07-31 19:06:55
on branch sub-manifest
— import-git: the payload refusal names its numbers
ExportPayloadLimitExceeded was a bare error name. Importing sqlite cost 17 minutes to learn a single integer, and the operator still had to guess it: the stream is 12.2 GiB against a 4 GiB default.
One error carried two unrelated bounds. max_total_data is the operator's --max-export-payload-bytes; max_data is a fixed 1 GiB per-payload ceiling no flag reaches. Reporting the wrong one sends an operator to a knob that cannot help, so PayloadOverrun records which bound refused, and the single -payload arm says plainly that no option raises it.
The cumulative arm refuses to name a remedy value. Parsing stops at the overrun, so the observed total is a lower bound; 'above 9' would name a number that fails again. It reports the bound and says the full payload is larger, which is the honest shape of what the parser knows.
Also corrects the budget's own doc comment, which claimed four GiB was 'the working set an import may claim on a commodity machine'. Measured resident size importing sqlite's 12.2 GiB stream: 21 MiB. Payload is streamed and released per event. What the cumulative half actually bounds is total history, so the default over-provisions memory by three orders of magnitude while refusing a mainstream repository. (user: dmestas, size: 32636) [annotate] [blame] [check-ins using]
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, @intFromEnum(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, }; } 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 }, error.DataLimitExceeded); try expectNextError("feature done\n", .{ .max_line = 3, .max_data = 64, .max_path = 64 }, 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 }, 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. var command_limits = defaultLimits(); command_limits.max_total_data = 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_total_data, overrun.payload_budget_bytes); try std.testing.expectEqual( fast_export.min_retained_bytes_per_file_command, overrun.min_bytes_per_command, ); } // Raising the byte budget raises the ceiling with it: nothing else does. var doubled = command_limits; doubled.max_total_data *= 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, }); 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, }); 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, } }