Artifact ae055fc388c690114200838fcd0e73f4992740244c686c5deedc4b87010289ef:
- File
src/git_fast_export_test.zig
— part of check-in
[1f21f96558]
at
2026-07-31 03:13:01
on branch sub-manifest
— fast-export: accept an author with an empty email
parseIdentity refused an identity carrying an empty address twice over, once as `open + 1 == close` and again as `email.len == 0`. That is not malformed input. Setting `user.email` to the empty string is ordinary git configuration; `git fast-export` writes the identity with an empty angle-bracket pair, and `git fast-import` reads that same stream back to the same commit. The construct is valid by git's own definition on both sides of its own tooling, and we rejected it.
The cost was not cosmetic. Two such commits, reachable only through a merge's second parent, aborted the import of an entire 437 commit repository with `import-git: import failed: Malformed`, naming neither the line nor the rule.
Only the email clauses are relaxed. The name clauses stay: git's own porcelain refuses an empty ident name, and the importer maps the identity's name onto the U card login, so a nameless identity would produce a nameless check-in. The two questions were conflated in a single condition, which is a large part of why this went unnoticed, so the guard now separates them and says why.
This is the third defect on this branch of the same shape: a property of our own fixtures promoted into a rule about the world. The first two were bounds tuned to the width of our benchmark corpus. This one is sharper, a refusal of input the tool we integrate with emits by default. Every identity fixture in the parser tests carried a non-empty email, so a uniform corpus could not falsify a rule that keys on emptiness.
Both tests are witnesses rather than decoration: each fails with Malformed against the previous parser and passes against this one. The unit test pins the construct and the empty-name refusal beside it; the differential test has git itself produce the commit and then asserts the imported check-in carries the login mohye77 rather than an empty user. (user: dmestas, size: 30434) [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 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, } }