Skip to content
zit Artifact [75d951bcbb]
Login

Artifact [75d951bcbb]

Artifact 75d951bcbbc1834e1399d6ca7c00e2507cdcb1e752f2b551ef9b3aa0114ecec5:


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,
};

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 one bound whose error name cannot carry its own
    /// numbers. Null until `FileCommandLimitExceeded` is returned.
    file_command_overrun: ?FileCommandOverrun = 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 {
        const next_data_bytes_seen = std.math.add(u64, self.data_bytes_seen, size) catch return error.DataLimitExceeded;
        if (next_data_bytes_seen > self.limits.max_total_data) return error.DataLimitExceeded;
        self.data_bytes_seen = next_data_bytes_seen;
    }

    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..]);
        if (size > self.limits.max_data or size > @as(u64, std.math.maxInt(usize))) return error.DataLimitExceeded;
        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 open + 1 == close or close + 1 >= text.len or text[close + 1] != ' ') return error.Malformed;
    const name = text[0 .. open - 1];
    const email = text[open + 1 .. close];
    if (name.len == 0 or email.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.*];
}