Something went wrong. Try again.
A Zig library that reads git repositories without running git, with log, show and diff output byte-identical to git's.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! The contents of a tree, read an entry at a time without allocating.//!//! A tree is a run of entries, each `<mode> <name>\0` followed by the//! entry's object ID in binary. The rules here are git's observed ones,//! from trees written with `git hash-object --literally` and listed with//! `git ls-tree` and `git cat-file -p`://!//! - An entry without a NUL, or with fewer bytes after it than an object//! ID has, makes the tree too short (`TruncatedTree`).//! - The mode is octal digits and nothing else, at least one, before the//! first space (`MalformedMode` otherwise). A second space belongs to//! the name.//! - The name must not be empty (`EmptyName`), and may be anything else://! `.`, `..`, `.git`, or a name with a `/` in it.//! - Entries need not be sorted or unique.//! - Whatever the mode, git shows it as one of the five it means: a//! directory, a symbolic link, a submodule's commit (a gitlink), or a//! file, executable if its owner may execute it. Any mode that is none//! of the first three types and not a regular file is a gitlink.
const std = @import("std");
const object = @import("object.zig");const Format = object.Format;const Id = object.Id;const Kind = object.Kind;
/// The five modes git gives tree entries.pub const Mode = enum(u32) { file = 0o100644, executable = 0o100755, symlink = 0o120000, directory = 0o040000, gitlink = 0o160000,
/// The mode git shows for an entry whose mode is written as `raw`. pub fn fromRaw(raw: u32) Mode { return switch (raw & 0o170000) { 0o100000 => if (raw & 0o100 != 0) .executable else .file, 0o040000 => .directory, 0o120000 => .symlink, else => .gitlink, }; }
/// The kind of object an entry with this mode names. pub fn kind(m: Mode) Kind { return switch (m) { .directory => .tree, .gitlink => .commit, .file, .executable, .symlink => .blob, }; }};
/// One entry of a tree.pub const Entry = struct { /// The mode as written, which may be one git never writes itself; /// `mode()` is what git makes of it. raw_mode: u32, /// The name, which is never empty. name: []const u8, /// The object the entry names. id: Id,
/// The mode git shows for the entry. pub fn mode(e: Entry) Mode { return .fromRaw(e.raw_mode); }};
/// Steps through a tree's entries.pub const Iterator = struct { format: Format, rest: []const u8,
/// Why a tree could not be read further. git's words for them are "too-short /// tree object", "malformed mode in tree entry" and "empty filename in /// tree entry". pub const Error = error{ TruncatedTree, MalformedMode, EmptyName };
/// The next entry, or null after the last. pub fn next(it: *Iterator) Error!?Entry { if (it.rest.len == 0) return null; const nul = std.mem.findScalar(u8, it.rest, 0) orelse return error.TruncatedTree; const n = it.format.byteLength(); if (it.rest.len - nul - 1 < n) return error.TruncatedTree; const text = it.rest[0..nul]; const space = std.mem.findScalar(u8, text, ' ') orelse return error.MalformedMode; if (space == 0) return error.MalformedMode; var raw: u32 = 0; for (text[0..space]) |c| { if (c < '0' or c > '7') return error.MalformedMode; raw = raw << 3 | (c - '0'); } const name = text[space + 1 ..]; if (name.len == 0) return error.EmptyName; const id: Id = .fromBytes(it.format, it.rest[nul + 1 ..][0..n]); it.rest = it.rest[nul + 1 + n ..]; return .{ .raw_mode = raw, .name = name, .id = id }; }};
/// The entries of a tree in `format` whose contents are `contents`.pub fn iterate(format: Format, contents: []const u8) Iterator { return .{ .format = format, .rest = contents };}
const testing = std.testing;const fuzz_input = @import("testing/fuzz.zig");
test "a tree's modes are shown as git shows them" { // What `git cat-file -p` showed for entries with these modes; the // differential tests in tools/zig_git.zig ask it again. const Case = struct { raw: []const u8, mode: Mode }; for ([_]Case{ .{ .raw = "100644", .mode = .file }, .{ .raw = "100664", .mode = .file }, .{ .raw = "100600", .mode = .file }, .{ .raw = "100755", .mode = .executable }, .{ .raw = "40000", .mode = .directory }, .{ .raw = "040755", .mode = .directory }, .{ .raw = "120000", .mode = .symlink }, .{ .raw = "160000", .mode = .gitlink }, .{ .raw = "777", .mode = .gitlink }, .{ .raw = "0", .mode = .gitlink }, .{ .raw = "140000", .mode = .gitlink }, .{ .raw = "37777777777", .mode = .gitlink }, .{ .raw = "1000000000000644", .mode = .gitlink }, }) |case| { var buffer: [64]u8 = undefined; const contents = try std.fmt.bufPrint(&buffer, "{s} a\x00" ++ "01234567890123456789", .{case.raw}); var it = iterate(.sha1, contents); try testing.expectEqual(case.mode, (try it.next()).?.mode()); try testing.expectEqual(null, try it.next()); }}
test "a malformed tree is refused as git refuses it" { const id = "01234567890123456789"; const Case = struct { contents: []const u8, err: Iterator.Error }; for ([_]Case{ .{ .contents = "100648 a\x00" ++ id, .err = error.MalformedMode }, .{ .contents = "abc a\x00" ++ id, .err = error.MalformedMode }, .{ .contents = " a\x00" ++ id, .err = error.MalformedMode }, .{ .contents = "+100644 a\x00" ++ id, .err = error.MalformedMode }, .{ .contents = "100644a\x00" ++ id, .err = error.MalformedMode }, .{ .contents = "100644 \x00" ++ id, .err = error.EmptyName }, .{ .contents = "100644 a\x00" ++ id[1..], .err = error.TruncatedTree }, .{ .contents = "100644 a", .err = error.TruncatedTree }, .{ .contents = "100644 a\x00" ++ id ++ "x", .err = error.TruncatedTree }, }) |case| { var it = iterate(.sha1, case.contents); const got = while (true) { if ((it.next() catch |err| break err) == null) return error.TestExpectedError; }; try testing.expectEqual(case.err, got); }}
fn fuzzOne(_: void, smith: *testing.Smith) anyerror!void { var buffer: [1024]u8 = undefined; const contents = fuzz_input.bytes(smith, &buffer); for ([_]Format{ .sha1, .sha256 }) |format| { var it = iterate(format, contents); while (it.next() catch null) |entry| { try testing.expect(entry.name.len != 0); _ = entry.mode().kind(); } }}
test "fuzz: trees" { try fuzz_input.fuzz({}, fuzzOne, .{ .corpus = &.{ @embedFile("fuzz/tree"), @embedFile("fuzz/tree-sha256"), } });}