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.*];
}