Artifact 5ce982e47159005c6cde2bfb81b62d5329a2bc4353659ce7fe70cccc21b9208a:
- File
src/git/git_fast_export.zig
— part of check-in
[ce3c87e25c]
at
2026-08-10 23:48:29
on branch sub-manifest
— Move the git-interop implementation into src/git/
Seven files relocate with unchanged basenames: git_fast_export(+test), git_import, git_import_impl, git_import_cli, git_import_verify, git_import_benchmark_support. The six standalone module roots (test, refs_test, cli_test, e2e, benchmark, pipe_helper) stay flat because a Zig module cannot import above its root file's directory — the same rule that keeps main/e2e/dst/smoke at src/. Future export-git files follow the same split: roots flat, implementation in src/git/. Path-string edits only; build.zig unchanged. (user: dmestas, size: 33603) [annotate] [blame] [check-ins using] [more...]
- File
src/git_fast_export.zig
— part of check-in
[367539bec4]
at
2026-08-05 17:30:55
on branch sub-manifest
— stat: report per-kind bytes; unpin two import ceilings
Issue #32. `zit stat` gains full-form-bytes and delta-bytes, a partition of content-bytes. The counts alone mislead: a real history is 23% full-form by artifact and 84% by byte, because an artifact is stored whole exactly when delta did not pay.
tree_cache_bytes was 1 MiB, smaller than a wide tree's root fan-out, so nodes were evicted and re-decoded on the next commit that touched them. 64 MiB: byte-identical output, 67.4s to 56.5s importing 45,000 files. Unmeasurable below the old budget, which is why it went unnoticed.
fast_export.Limits.max_events loses its 1,000,000 default rather than gaining a new one. #29 removed that cap at the CLI and left it armed here; six callers silently re-inherited it, the benchmark among them.
test 3495/3495, fmt. (user: dmestas, size: 33603) [annotate] [blame] [check-ins using]
const std = @import("std"); pub const Limits = struct { max_line: usize, max_data: u64, max_path: usize, max_merges_per_commit: usize = 1_024, /// No default: this is a runaway-stream tripwire, and the right ceiling is /// the caller's to state. It carried 1,000,000 -- a round number with no /// basis, which #29 removed at the CLI only to leave armed here, so any new /// caller silently re-inherited the exact cap that had just been deleted. /// A required field makes that impossible: a caller that wants no ceiling /// says `maxInt`, and says it where a reader can see it. max_events: u64, max_total_data: u64 = 1 << 40, /// Bytes one commit's retained file commands may occupy. See /// `fileCommandCeiling` for why this is its own budget and not a share of /// `max_total_data`. max_command_arena_bytes: u64 = @as(u64, 4) * 1024 * 1024 * 1024, }; /// Floor on the bytes one retained file command occupies: its slot in the /// per-commit command list plus the shortest path it can carry. `parsePathToken` /// rejects the empty path, so one byte is that floor. Nothing smaller is /// representable, which is what makes it safe to divide a byte budget by. pub const min_retained_bytes_per_file_command: u64 = @sizeOf(FileCommand) + 1; /// The per-commit file-command ceiling, derived rather than declared. /// /// A file command's list slot and duplicated path are arena bytes no byte /// budget charges: `admitData` sees only `data <n>` payloads, so an unbounded /// command loop grows the arena without ever touching one. The ceiling closes /// that gap by restating a byte authorization in commands — the most whose /// minimum retention still fits. /// /// It draws on `max_command_arena_bytes` rather than `max_total_data` because /// the two bound different things and only one of them is memory. Cumulative /// payload is streamed and released per event; the per-commit command list is /// held whole. While they shared a number, raising the cumulative budget so an /// ordinary repository could import also raised how much one commit may hold — /// a 1 TiB payload allowance silently became a 1 TiB arena allowance. Separated, /// each can take the value its own risk justifies. pub fn fileCommandCeiling(limits: Limits) usize { const ceiling = limits.max_command_arena_bytes / min_retained_bytes_per_file_command; return std.math.cast(usize, ceiling) orelse std.math.maxInt(usize); } /// The numbers a `FileCommandLimitExceeded` diagnostic must name. The parser /// has no output channel, so it retains them and the caller does the /// formatting. pub const FileCommandOverrun = struct { /// The refused commit carried at least this many commands: the ceiling /// plus the one that did not fit. Parsing stops there, so the true count /// is unknown and this is a lower bound, never an estimate. observed_at_least: usize, ceiling: usize, arena_budget_bytes: u64, min_bytes_per_command: u64 = min_retained_bytes_per_file_command, }; /// The numbers a `DataLimitExceeded` diagnostic must name. Two unrelated bounds /// share that one error, and they differ in the only respect an operator cares /// about: whether a flag raises them. `max_total_data` is the operator's /// `--max-export-payload-bytes`; `max_data` is a fixed per-payload ceiling no /// flag reaches. Reporting the wrong one sends an operator to a knob that /// cannot help. Retained here for the same reason `FileCommandOverrun` is: the /// parser has no output channel. pub const PayloadOverrun = struct { pub const Bound = enum { /// One `data <n>` payload against the fixed per-payload ceiling. single_payload, /// The running charged total against the operator's byte budget. cumulative_total, }; bound: Bound, /// For `single_payload` the refused payload's exact size. For /// `cumulative_total` the total the refused payload would have reached: /// parsing stops there, so the stream's true payload is larger and this is /// a lower bound, never an estimate. observed_at_least: u64, ceiling: u64, }; pub const Error = error{ ActiveLease, Malformed, DataLimitExceeded, EventLimitExceeded, FileCommandLimitExceeded, LineLimitExceeded, MergeLimitExceeded, PathLimitExceeded, MissingDone, UnexpectedEof, TrailingData, UnsupportedFeature, UnknownMode, OutOfMemory, Io, }; pub const ObjectRef = union(enum) { mark: u64, oid: []const u8, }; /// Modes a Git tree entry can carry. `gitlink` is mode 160000 -- a recorded /// submodule commit. Zit has no equivalent, but that is an import-policy /// question, not a parsing one: the parser reports what the stream said and /// the importer decides what to do with it. Only modes Git never emits reach /// `error.UnknownMode`. pub const FileMode = enum { regular, executable, symlink, gitlink, }; pub const FileCommand = union(enum) { modify: struct { mode: FileMode, data: ObjectRef, path: []const u8, }, delete: []const u8, rename: struct { old: []const u8, new: []const u8, }, copy: struct { old: []const u8, new: []const u8, }, delete_all, }; pub const Identity = struct { name: []const u8, email: []const u8, timestamp: i64, timezone_offset_minutes: i16, }; pub const Blob = struct { mark: u64, original_oid: ?[]const u8, data: []const u8, }; pub const Commit = struct { ref: []const u8, mark: u64, original_oid: ?[]const u8, author: Identity, committer: Identity, message: []const u8, from: ?ObjectRef, merges: []const ObjectRef, commands: []const FileCommand, }; pub const Reset = struct { ref: []const u8, target: ?ObjectRef, }; pub const Tag = struct { name: []const u8, mark: ?u64, target: ObjectRef, original_oid: ?[]const u8, tagger: Identity, message: []const u8, }; pub const Event = union(enum) { blob: Blob, commit: Commit, reset: Reset, tag: Tag, done, }; pub const EventLease = struct { value: Event, reader: *Reader, released: bool = false, pub fn deinit(self: *EventLease) void { if (self.released) return; self.released = true; self.reader.releaseLease(); } }; pub const Reader = struct { gpa: std.mem.Allocator, input: std.Io.Reader, limits: Limits, line: std.ArrayList(u8), event_arena: ?std.heap.ArenaAllocator = null, active_lease: bool = false, feature_done_seen: bool = false, records_started: bool = false, done_seen: bool = false, eof_seen: bool = false, pending_top_level: ?[]u8 = null, events_seen: u64 = 0, data_bytes_seen: u64 = 0, /// Retained by the bounds whose error names cannot carry their own /// numbers. Null until the matching error is returned. file_command_overrun: ?FileCommandOverrun = null, payload_overrun: ?PayloadOverrun = null, pub fn init(gpa: std.mem.Allocator, input: std.Io.Reader, limits: Limits) Reader { return .{ .gpa = gpa, .input = input, .limits = limits, .line = .empty, }; } pub fn deinit(self: *Reader) void { if (self.event_arena) |arena| arena.deinit(); self.event_arena = null; self.active_lease = false; self.line.deinit(self.gpa); if (self.pending_top_level) |line| self.gpa.free(line); self.pending_top_level = null; } pub fn next(self: *Reader) Error!?EventLease { if (self.active_lease) return error.ActiveLease; if (self.eof_seen) return null; if (self.done_seen) { try self.requireEof(); self.eof_seen = true; return null; } while (true) { const line = try self.readTopLevelLine(); if (std.mem.startsWith(u8, line, "feature ")) { if (self.records_started or self.feature_done_seen) return error.Malformed; if (!std.mem.eql(u8, line, "feature done")) return error.UnsupportedFeature; self.feature_done_seen = true; continue; } if (std.mem.startsWith(u8, line, "progress ")) { if (!self.feature_done_seen) return error.Malformed; try self.admitEvent(); continue; } if (!self.feature_done_seen) return error.Malformed; const is_event = std.mem.eql(u8, line, "blob") or std.mem.startsWith(u8, line, "commit ") or std.mem.startsWith(u8, line, "reset ") or std.mem.startsWith(u8, line, "tag ") or std.mem.eql(u8, line, "done"); if (!is_event) return error.Malformed; self.records_started = true; try self.admitEvent(); self.event_arena = std.heap.ArenaAllocator.init(self.gpa); errdefer { self.event_arena.?.deinit(); self.event_arena = null; } const allocator = self.eventAllocator(); const event: Event = if (std.mem.eql(u8, line, "blob")) .{ .blob = try self.parseBlob(allocator) } else if (std.mem.startsWith(u8, line, "commit ")) .{ .commit = try self.parseCommit(allocator, line["commit ".len..]) } else if (std.mem.startsWith(u8, line, "reset ")) .{ .reset = try self.parseReset(allocator, line["reset ".len..]) } else if (std.mem.startsWith(u8, line, "tag ")) .{ .tag = try self.parseTag(allocator, line["tag ".len..]) } else if (std.mem.eql(u8, line, "done")) blk: { self.done_seen = true; break :blk .done; } else unreachable; return self.lease(event); } } fn lease(self: *Reader, value: Event) EventLease { self.active_lease = true; return .{ .value = value, .reader = self }; } fn releaseLease(self: *Reader) void { std.debug.assert(self.active_lease); self.event_arena.?.deinit(); self.event_arena = null; self.active_lease = false; } fn eventAllocator(self: *Reader) std.mem.Allocator { return self.event_arena.?.allocator(); } fn admitEvent(self: *Reader) Error!void { const next_events_seen = std.math.add(u64, self.events_seen, 1) catch return error.EventLimitExceeded; if (next_events_seen > self.limits.max_events) return error.EventLimitExceeded; self.events_seen = next_events_seen; } fn admitData(self: *Reader, size: u64) Error!void { // Saturating on overflow rather than propagating it: the report wants // the total this payload reached for, and a u64 wrap is not that // number. The bound is exceeded either way. const next_data_bytes_seen = std.math.add(u64, self.data_bytes_seen, size) catch return self.refusePayload(.cumulative_total, std.math.maxInt(u64), self.limits.max_total_data); if (next_data_bytes_seen > self.limits.max_total_data) return self.refusePayload(.cumulative_total, next_data_bytes_seen, self.limits.max_total_data); self.data_bytes_seen = next_data_bytes_seen; } fn refusePayload( self: *Reader, bound: PayloadOverrun.Bound, observed_at_least: u64, ceiling: u64, ) Error { self.payload_overrun = .{ .bound = bound, .observed_at_least = observed_at_least, .ceiling = ceiling, }; return error.DataLimitExceeded; } fn readTopLevelLine(self: *Reader) Error![]const u8 { if (self.pending_top_level) |owned| { defer self.gpa.free(owned); self.pending_top_level = null; self.line.clearRetainingCapacity(); self.line.appendSlice(self.gpa, owned) catch return error.OutOfMemory; return self.line.items; } return self.readLine() catch |err| switch (err) { error.EndOfStream => return error.MissingDone, else => |e| return e, }; } fn requiredLine(self: *Reader) Error![]const u8 { return self.readLine() catch |err| switch (err) { error.EndOfStream => return error.UnexpectedEof, else => |e| return e, }; } fn readLine(self: *Reader) (Error || error{EndOfStream})![]const u8 { self.line.clearRetainingCapacity(); while (true) { const byte = self.input.takeByte() catch |err| switch (err) { error.EndOfStream => return error.EndOfStream, error.ReadFailed => return error.Io, }; if (byte == '\n') return self.line.items; if (self.line.items.len == self.limits.max_line) return error.LineLimitExceeded; self.line.append(self.gpa, byte) catch return error.OutOfMemory; } } fn requireEof(self: *Reader) Error!void { _ = self.input.takeByte() catch |err| switch (err) { error.EndOfStream => return, error.ReadFailed => return error.Io, }; return error.TrailingData; } fn duplicate(_: *Reader, allocator: std.mem.Allocator, bytes: []const u8) Error![]const u8 { return allocator.dupe(u8, bytes) catch return error.OutOfMemory; } fn parseBlob(self: *Reader, allocator: std.mem.Allocator) Error!Blob { const mark = try parseMarkLine(try self.requiredLine()); var original_oid: ?[]const u8 = null; while (true) { const line = try self.requiredLine(); if (std.mem.startsWith(u8, line, "original-oid ")) { if (original_oid != null) return error.Malformed; original_oid = try parseOid(allocator, line["original-oid ".len..]); continue; } if (std.mem.startsWith(u8, line, "data ")) { return .{ .mark = mark, .original_oid = original_oid, .data = try self.readData(allocator, line), }; } return error.Malformed; } } fn parseCommit(self: *Reader, allocator: std.mem.Allocator, ref_text: []const u8) Error!Commit { const ref = try self.parseRef(allocator, ref_text); const mark = try parseMarkLine(try self.requiredLine()); var original_oid: ?[]const u8 = null; var author: ?Identity = null; var committer: ?Identity = null; var message: ?[]const u8 = null; while (true) { const line = try self.requiredLine(); if (std.mem.startsWith(u8, line, "original-oid ")) { if (original_oid != null) return error.Malformed; original_oid = try parseOid(allocator, line["original-oid ".len..]); } else if (std.mem.startsWith(u8, line, "author ")) { if (author != null) return error.Malformed; author = try parseIdentity(self, allocator, line["author ".len..]); } else if (std.mem.startsWith(u8, line, "committer ")) { if (committer != null) return error.Malformed; committer = try parseIdentity(self, allocator, line["committer ".len..]); } else if (std.mem.startsWith(u8, line, "data ")) { message = try self.readCommitData(allocator, line); break; } else return error.Malformed; } const parsed_author = author orelse return error.Malformed; const parsed_committer = committer orelse return error.Malformed; const parsed_message = message orelse return error.Malformed; var from: ?ObjectRef = null; var merges: std.ArrayList(ObjectRef) = .empty; var commands: std.ArrayList(FileCommand) = .empty; const file_command_ceiling = fileCommandCeiling(self.limits); // Exact-count data may have an optional trailing LF. Only the first // following empty line is ambiguous with the commit terminator. var first_command_line = true; command_loop: while (true) { var line = try self.requiredLine(); if (line.len == 0) { if (!first_command_line) break; line = self.readLine() catch |err| switch (err) { error.EndOfStream => break :command_loop, else => |e| return e, }; if (line.len == 0) break; if (isTopLevelRecord(line)) { self.pending_top_level = self.gpa.dupe(u8, line) catch return error.OutOfMemory; break; } } first_command_line = false; if (std.mem.startsWith(u8, line, "from ")) { if (from != null) return error.Malformed; from = try parseObjectRef(allocator, line["from ".len..]); } else if (std.mem.startsWith(u8, line, "merge ")) { if (merges.items.len >= self.limits.max_merges_per_commit) return error.MergeLimitExceeded; const merge = try parseObjectRef(allocator, line["merge ".len..]); merges.append(allocator, merge) catch return error.OutOfMemory; } else { if (commands.items.len >= file_command_ceiling) { self.file_command_overrun = .{ .observed_at_least = file_command_ceiling +| 1, .ceiling = file_command_ceiling, .arena_budget_bytes = self.limits.max_command_arena_bytes, }; return error.FileCommandLimitExceeded; } const command = try self.parseFileCommand(allocator, line); commands.append(allocator, command) catch return error.OutOfMemory; } } return .{ .ref = ref, .mark = mark, .original_oid = original_oid, .author = parsed_author, .committer = parsed_committer, .message = parsed_message, .from = from, .merges = merges.toOwnedSlice(allocator) catch return error.OutOfMemory, .commands = commands.toOwnedSlice(allocator) catch return error.OutOfMemory, }; } fn isTopLevelRecord(line: []const u8) bool { return std.mem.eql(u8, line, "blob") or std.mem.startsWith(u8, line, "commit ") or std.mem.startsWith(u8, line, "reset ") or std.mem.startsWith(u8, line, "tag ") or std.mem.startsWith(u8, line, "progress ") or std.mem.eql(u8, line, "done"); } fn parseReset(self: *Reader, allocator: std.mem.Allocator, ref_text: []const u8) Error!Reset { const ref = try self.parseRef(allocator, ref_text); const line = try self.requiredLine(); if (line.len == 0) return .{ .ref = ref, .target = null }; if (isTopLevelRecord(line)) { self.pending_top_level = self.gpa.dupe(u8, line) catch return error.OutOfMemory; return .{ .ref = ref, .target = null }; } if (!std.mem.startsWith(u8, line, "from ")) return error.Malformed; const target = try parseObjectRef(allocator, line["from ".len..]); const separator = try self.requiredLine(); if (separator.len == 0) return .{ .ref = ref, .target = target }; if (!isTopLevelRecord(separator)) return error.Malformed; self.pending_top_level = self.gpa.dupe(u8, separator) catch return error.OutOfMemory; return .{ .ref = ref, .target = target }; } fn parseTag(self: *Reader, allocator: std.mem.Allocator, name_text: []const u8) Error!Tag { const name = try self.parseRef(allocator, name_text); var mark: ?u64 = null; var target: ?ObjectRef = null; var original_oid: ?[]const u8 = null; var tagger: ?Identity = null; while (true) { const line = try self.requiredLine(); if (std.mem.startsWith(u8, line, "mark ")) { if (mark != null) return error.Malformed; mark = try parseMarkLine(line); } else if (std.mem.startsWith(u8, line, "from ")) { if (target != null) return error.Malformed; target = try parseObjectRef(allocator, line["from ".len..]); } else if (std.mem.startsWith(u8, line, "original-oid ")) { if (original_oid != null) return error.Malformed; original_oid = try parseOid(allocator, line["original-oid ".len..]); } else if (std.mem.startsWith(u8, line, "tagger ")) { if (tagger != null) return error.Malformed; tagger = try parseIdentity(self, allocator, line["tagger ".len..]); } else if (std.mem.startsWith(u8, line, "data ")) { return .{ .name = name, .mark = mark, .target = target orelse return error.Malformed, .original_oid = original_oid, .tagger = tagger orelse return error.Malformed, .message = try self.readData(allocator, line), }; } else return error.Malformed; } } fn parseFileCommand(self: *Reader, allocator: std.mem.Allocator, line: []const u8) Error!FileCommand { if (std.mem.eql(u8, line, "deleteall")) return .delete_all; if (std.mem.startsWith(u8, line, "M ")) return self.parseModify(allocator, line[2..]); if (std.mem.startsWith(u8, line, "D ")) return .{ .delete = try self.parseSinglePath(allocator, line[2..]) }; if (std.mem.startsWith(u8, line, "R ")) return self.parsePair(allocator, line[2..], .rename); if (std.mem.startsWith(u8, line, "C ")) return self.parsePair(allocator, line[2..], .copy); return error.Malformed; } fn parseModify(self: *Reader, allocator: std.mem.Allocator, body: []const u8) Error!FileCommand { const mode_end = std.mem.indexOfScalar(u8, body, ' ') orelse return error.Malformed; const mode = try parseMode(body[0..mode_end]); var index = mode_end + 1; const data_text = takeBareToken(body, &index) orelse return error.Malformed; const data = try parseObjectRef(allocator, data_text); if (index >= body.len or body[index] != ' ') return error.Malformed; index += 1; const path = try self.parsePathToken(allocator, body, &index); if (index != body.len) return error.Malformed; return .{ .modify = .{ .mode = mode, .data = data, .path = path } }; } fn parseSinglePath(self: *Reader, allocator: std.mem.Allocator, body: []const u8) Error![]const u8 { var index: usize = 0; const path = try self.parsePathToken(allocator, body, &index); if (index != body.len) return error.Malformed; return path; } fn parsePair(self: *Reader, allocator: std.mem.Allocator, body: []const u8, comptime kind: enum { rename, copy }) Error!FileCommand { var index: usize = 0; const old = try self.parsePathToken(allocator, body, &index); if (index >= body.len or body[index] != ' ') return error.Malformed; index += 1; const new = try self.parsePathToken(allocator, body, &index); if (index != body.len) return error.Malformed; return switch (kind) { .rename => .{ .rename = .{ .old = old, .new = new } }, .copy => .{ .copy = .{ .old = old, .new = new } }, }; } fn parsePathToken(self: *Reader, allocator: std.mem.Allocator, input: []const u8, index: *usize) Error![]const u8 { if (index.* >= input.len) return error.Malformed; const start = index.*; if (input[start] == '"') { index.* += 1; var escaped = false; while (index.* < input.len) : (index.* += 1) { const byte = input[index.*]; if (escaped) { escaped = false; continue; } if (byte == '\\') { escaped = true; continue; } if (byte == '"') { index.* += 1; return self.decodeQuotedPath(allocator, input[start + 1 .. index.* - 1]); } } return error.Malformed; } while (index.* < input.len and input[index.*] != ' ') : (index.* += 1) {} return self.copyPath(allocator, input[start..index.*]); } fn copyPath(self: *Reader, allocator: std.mem.Allocator, path: []const u8) Error![]const u8 { if (path.len == 0 or std.mem.indexOfScalar(u8, path, 0) != null) return error.Malformed; if (path.len > self.limits.max_path) return error.PathLimitExceeded; return self.duplicate(allocator, path); } fn decodeQuotedPath(self: *Reader, allocator: std.mem.Allocator, encoded: []const u8) Error![]const u8 { var decoded: std.ArrayList(u8) = .empty; var index: usize = 0; while (index < encoded.len) { const byte = encoded[index]; index += 1; if (byte != '\\') { try appendPathByte(self, &decoded, byte); continue; } if (index == encoded.len) return error.Malformed; const escape = encoded[index]; index += 1; const value: u8 = switch (escape) { '"', '\\' => escape, 'a' => '\x07', 'b' => '\x08', 'f' => '\x0c', 'n' => '\n', 'r' => '\r', 't' => '\t', 'v' => '\x0b', '0'...'7' => blk: { if (index + 2 > encoded.len) return error.Malformed; const second = encoded[index]; const third = encoded[index + 1]; if (second < '0' or second > '7' or third < '0' or third > '7') return error.Malformed; index += 2; const octal: u16 = @as(u16, escape - '0') * 64 + @as(u16, second - '0') * 8 + @as(u16, third - '0'); if (octal > @as(u16, std.math.maxInt(u8))) return error.Malformed; break :blk @intCast(octal); }, else => return error.Malformed, }; try appendPathByte(self, &decoded, value); } if (decoded.items.len == 0) return error.Malformed; return decoded.toOwnedSlice(allocator) catch return error.OutOfMemory; } fn parseRef(self: *Reader, allocator: std.mem.Allocator, text: []const u8) Error![]const u8 { if (text.len == 0 or std.mem.indexOfScalar(u8, text, ' ') != null or std.mem.indexOfScalar(u8, text, 0) != null) return error.Malformed; return self.duplicate(allocator, text); } fn readData(self: *Reader, allocator: std.mem.Allocator, line: []const u8) Error![]const u8 { const data = try self.readDataExact(allocator, line); const terminator = self.input.takeByte() catch |err| switch (err) { error.EndOfStream => return error.UnexpectedEof, error.ReadFailed => return error.Io, }; if (terminator != '\n') return error.Malformed; return data; } fn readCommitData(self: *Reader, allocator: std.mem.Allocator, line: []const u8) Error![]const u8 { return self.readDataExact(allocator, line); } fn readDataExact(self: *Reader, allocator: std.mem.Allocator, line: []const u8) Error![]const u8 { if (!std.mem.startsWith(u8, line, "data ")) return error.Malformed; const size = try parseUnsigned(line["data ".len..]); // `maxInt(usize)` is the addressability floor on a 32-bit host, not a // policy; on 64-bit it never binds. Either way `max_data` is the // ceiling worth naming, since it is the one the code declares. if (size > self.limits.max_data or size > @as(u64, std.math.maxInt(usize))) return self.refusePayload(.single_payload, size, self.limits.max_data); try self.admitData(size); const count: usize = @intCast(size); const data = allocator.alloc(u8, count) catch return error.OutOfMemory; self.input.readSliceAll(data) catch |err| switch (err) { error.EndOfStream => return error.UnexpectedEof, error.ReadFailed => return error.Io, }; return data; } }; fn appendPathByte(reader: *Reader, list: *std.ArrayList(u8), byte: u8) Error!void { if (byte == 0) return error.Malformed; if (list.items.len == reader.limits.max_path) return error.PathLimitExceeded; list.append(reader.eventAllocator(), byte) catch return error.OutOfMemory; } fn parseMarkLine(line: []const u8) Error!u64 { if (!std.mem.startsWith(u8, line, "mark :")) return error.Malformed; const mark = try parseUnsigned(line["mark :".len..]); if (mark == 0) return error.Malformed; return mark; } fn parseObjectRef(allocator: std.mem.Allocator, text: []const u8) Error!ObjectRef { if (text.len == 0) return error.Malformed; if (text[0] == ':') return .{ .mark = try parseMark(text) }; return .{ .oid = try parseOid(allocator, text) }; } fn parseMark(text: []const u8) Error!u64 { if (text.len < 2 or text[0] != ':') return error.Malformed; const mark = try parseUnsigned(text[1..]); if (mark == 0) return error.Malformed; return mark; } fn parseOid(allocator: std.mem.Allocator, text: []const u8) Error![]const u8 { if (text.len != 40 and text.len != 64) return error.Malformed; for (text) |byte| if (!std.ascii.isHex(byte)) return error.Malformed; return allocator.dupe(u8, text) catch return error.OutOfMemory; } fn parseUnsigned(text: []const u8) Error!u64 { if (text.len == 0) return error.Malformed; var value: u64 = 0; for (text) |byte| { if (byte < '0' or byte > '9') return error.Malformed; const digit: u64 = byte - '0'; if (value > (std.math.maxInt(u64) - digit) / 10) return error.Malformed; value = value * 10 + digit; } return value; } fn parseSigned(text: []const u8) Error!i64 { if (text.len == 0) return error.Malformed; const negative = text[0] == '-'; const digits = if (negative or text[0] == '+') text[1..] else text; const unsigned = try parseUnsigned(digits); if (negative) { const minimum_magnitude = @as(u64, std.math.maxInt(i64)) + 1; if (unsigned > minimum_magnitude) return error.Malformed; if (unsigned == minimum_magnitude) return std.math.minInt(i64); return -@as(i64, @intCast(unsigned)); } if (unsigned > std.math.maxInt(i64)) return error.Malformed; return @intCast(unsigned); } fn parseIdentity(reader: *Reader, allocator: std.mem.Allocator, text: []const u8) Error!Identity { const close = std.mem.lastIndexOfScalar(u8, text, '>') orelse return error.Malformed; const open = std.mem.lastIndexOfScalar(u8, text[0..close], '<') orelse return error.Malformed; if (open == 0 or text[open - 1] != ' ' or close + 1 >= text.len or text[close + 1] != ' ') return error.Malformed; const name = text[0 .. open - 1]; const email = text[open + 1 .. close]; // An empty email is valid input: `git fast-export` writes `name <>` for a // commit whose author email is unset, and `git fast-import` round-trips // that stream back to the same commit. An empty NAME is not -- git's own // porcelain refuses it ("empty ident name not allowed") -- and the // importer maps the name onto the U card login, so it stays required. if (name.len == 0) return error.Malformed; const stamp_and_zone = text[close + 2 ..]; const separator = std.mem.indexOfScalar(u8, stamp_and_zone, ' ') orelse return error.Malformed; const timestamp = try parseSigned(stamp_and_zone[0..separator]); const zone = stamp_and_zone[separator + 1 ..]; if (zone.len != 5 or (zone[0] != '+' and zone[0] != '-')) return error.Malformed; const hours = try parseUnsigned(zone[1..3]); const minutes = try parseUnsigned(zone[3..5]); if (hours > 23 or minutes > 59) return error.Malformed; const raw_offset: i16 = @intCast(hours * 60 + minutes); return .{ .name = try reader.duplicate(allocator, name), .email = try reader.duplicate(allocator, email), .timestamp = timestamp, .timezone_offset_minutes = if (zone[0] == '-') -raw_offset else raw_offset, }; } fn parseMode(text: []const u8) Error!FileMode { if (std.mem.eql(u8, text, "100644")) return .regular; if (std.mem.eql(u8, text, "100755")) return .executable; if (std.mem.eql(u8, text, "120000")) return .symlink; if (std.mem.eql(u8, text, "160000")) return .gitlink; return error.UnknownMode; } fn takeBareToken(input: []const u8, index: *usize) ?[]const u8 { if (index.* >= input.len) return null; const start = index.*; while (index.* < input.len and input[index.*] != ' ') : (index.* += 1) {} if (start == index.*) return null; return input[start..index.*]; }