Skip to content
zit Artifact [23169447d7]
Login

Artifact [23169447d7]

Artifact 23169447d7dd3e5573243fc1badf61a924be57cfe6345742b6367738b46f8021:


const std = @import("std");

pub const Limits = struct {
    max_line: usize,
    max_data: u64,
    max_path: usize,
};

pub const Error = error{
    ActiveLease,
    Malformed,
    LimitExceeded,
    MissingDone,
    UnexpectedEof,
    TrailingData,
    UnsupportedFeature,
    UnsupportedMode,
    UnknownMode,
    OutOfMemory,
    Io,
};

pub const ObjectRef = union(enum) {
    mark: u64,
    oid: []const u8,
};

pub const FileMode = enum {
    regular,
    executable,
    symlink,
};

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,

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

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

        self.event_arena = std.heap.ArenaAllocator.init(self.gpa);
        errdefer {
            self.event_arena.?.deinit();
            self.event_arena = null;
        }
        const allocator = self.eventAllocator();

        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;
                continue;
            }
            if (!self.feature_done_seen) return error.Malformed;

            self.records_started = true;
            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 return error.Malformed;

            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 readTopLevelLine(self: *Reader) Error![]const u8 {
        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.LimitExceeded;
            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.readData(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;

        while (true) {
            const line = try self.requiredLine();
            if (line.len == 0) break;
            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 ")) {
                merges.append(allocator, try parseObjectRef(allocator, line["merge ".len..])) catch return error.OutOfMemory;
            } else {
                commands.append(allocator, try self.parseFileCommand(allocator, line)) 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 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 (!std.mem.startsWith(u8, line, "from ")) return error.Malformed;
        const target = try parseObjectRef(allocator, line["from ".len..]);
        if ((try self.requiredLine()).len != 0) return error.Malformed;
        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.LimitExceeded;
        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 {
        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.LimitExceeded;
        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,
        };
        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 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.LimitExceeded;
    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 error.UnsupportedMode;
    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.*];
}