Artifact 5245b5eaa71894e3f9747445f665fa21aace744239b41bba1251cf4b3154fcaf:
- File
src/git_fast_export.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: 32612) [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, max_events: u64 = 1_000_000, max_total_data: u64 = 1 << 40, }; /// 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. /// /// `max_total_data` is the parser's authorization for bytes it may hold in an /// event arena, but `admitData` charges only `data <n>` payloads against it. /// A file command's list slot and duplicated path are arena bytes no budget /// sees, so an unbounded command loop grows the arena without ever touching /// `max_total_data`. The ceiling closes that gap by restating the same /// authorization in commands: the most whose minimum retention still fits /// inside the budget. It moves when the byte budget moves and at no other time. pub fn fileCommandCeiling(limits: Limits) usize { const ceiling = limits.max_total_data / 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 — which knows the /// flag that raises the budget — 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, payload_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, .payload_budget_bytes = self.limits.max_total_data, }; 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.*]; }