// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-License-Identifier: MIT //! An object database: where a repository's objects are found by object //! ID, whatever the storage behind it. //! //! This is the seam the rest of the library reads objects through. Loose //! objects (`loose.Store`) are one implementation; packs, the //! multi-pack-index and alternates sit behind the same interface, and a //! caller may supply its own. It is a runtime interface rather than a //! closed union so that adding a kind of storage does not mean editing //! everything that reads objects. //! //! Every read takes an optional `*Diagnostics`, filled in with the object, //! the file and a detail in git's words when the read fails on the //! repository's account. const std = @import("std"); const Allocator = std.mem.Allocator; const Io = std.Io; const object = @import("object.zig"); const Id = object.Id; const Kind = object.Kind; const Header = object.Header; const Diagnostics = @import("Diagnostics.zig"); const ObjectDatabase = @This(); ptr: *anyopaque, vtable: *const VTable, /// What an implementation provides. pub const VTable = struct { contains: *const fn (ptr: *anyopaque, io: Io, id: Id, diag: ?*Diagnostics) ContainsError!bool, readHeader: *const fn (ptr: *anyopaque, io: Io, id: Id, diag: ?*Diagnostics) ReadError!Header, read: *const fn (ptr: *anyopaque, io: Io, gpa: Allocator, id: Id, diag: ?*Diagnostics) ReadError!Object, stream: *const fn (ptr: *anyopaque, io: Io, gpa: Allocator, id: Id, out: *Io.Writer, diag: ?*Diagnostics) StreamError!Header, }; /// An object read whole: its kind and its contents, which the caller owns. pub const Object = struct { kind: Kind, /// The contents, without the header. data: []const u8, /// What to free: `data` may be only part of it. allocation: []u8, /// Frees the contents. pub fn deinit(o: *Object, gpa: Allocator) void { gpa.free(o.allocation); o.* = undefined; } }; /// Why an object could not be read. Each is a corruption git reports in /// words of its own, and the names follow what it found wrong: /// /// - `ObjectNotFound`: there is no such object. /// - `ObjectUnreadable`: the storage could not be read; the diagnostic's /// detail names the system's error. /// - `EmptyObjectFile`: "object file … is empty". /// - `HeaderInflateFailed`: the zlib stream was corrupt before the end of /// the object's header ("unable to unpack … header"); the detail is /// zlib's description, such as `data stream error (incorrect header /// check)`. /// - `HeaderTooLong`, `MalformedHeader`, `ObjectSizeOverflow`, /// `UnknownObjectKind`: as `Header.parse` says. /// - `ContentInflateFailed`: the zlib stream was corrupt after the header; /// the detail is zlib's description. /// - `TruncatedObject`: the stored bytes ran out before the zlib stream /// ended. /// - `ContentTooLong`: the object holds more than its header says ("corrupt /// loose object"). /// - `ContentTooShort`: the object holds less than its header says. git /// does not notice, and shows whatever its buffer held past the end; /// this library refuses rather than invent the missing bytes. /// - `TrailingGarbage`: bytes after the end of the zlib stream ("garbage /// at end of loose object"). /// - `ObjectTooLarge`: the object, or a delta it is built from, claims /// more bytes than the reader was told it may build in memory /// (`DiskDatabase.Options.max_object_size`). Not a corruption: git has /// no such limit and would read it. Only reads that build an object /// whole refuse it; see `DiskDatabase.Options.max_object_size`. /// - `ReplaceDepthTooHigh`: read through replace refs, a chain of more /// replacements than git follows ("replace depth too high for object /// …"); the diagnostic's object ID is the original. /// - `ReplacementNotFound`: read through replace refs, the replacement is /// not there ("replacement … not found for …"); the diagnostic's object /// ID is the original and its detail the replacement. /// /// Reading a packed object can also fail with the errors `PackError` /// lists, and finding one with those `PackOpenError` does: see there. pub const ReadError = PackError || PackOpenError || error{ ObjectNotFound, ObjectUnreadable, OutOfMemory, EmptyObjectFile, HeaderInflateFailed, HeaderTooLong, MalformedHeader, ObjectSizeOverflow, UnknownObjectKind, ContentInflateFailed, TruncatedObject, ContentTooLong, ContentTooShort, TrailingGarbage, ObjectTooLarge, ReplaceDepthTooHigh, ReplacementNotFound, }; /// Why a packed object could not be read, beyond the errors a loose one /// shares (`ContentInflateFailed`, `TruncatedObject`, `ContentTooLong` and /// `ContentTooShort` mean the same of a pack entry's zlib stream): /// /// - `TruncatedPackEntry`: an entry's header, or an OFS_DELTA's offset or /// a REF_DELTA's base, runs into the pack's checksum. /// - `BadPackEntryType`: an entry of type 0 or 5, which mean nothing. /// - `PackEntrySizeOverflow`: an entry's size too big for 64 bits. /// - `BadDeltaBaseOffset`: an OFS_DELTA whose base would be at or before /// the start of the pack, or not before the delta itself. /// - `BadLargeOffset`: a pack index or multi-pack-index entry pointing /// past the end of its large-offset table. /// - `BadMultiPackIndexEntry`: a multi-pack-index entry naming a pack it /// does not list. /// - `DeltaBaseMissing`: a REF_DELTA whose base is nowhere in the object /// database; the diagnostic's detail is the base's object ID. /// - `DeltaChainTooDeep`: a chain of more than `pack.max_delta_depth` /// deltas, which a chain looping back on itself always is. /// - The errors of `delta.Error`: a delta that cannot be applied to its /// base. pub const PackError = error{ TruncatedPackEntry, BadPackEntryType, PackEntrySizeOverflow, BadDeltaBaseOffset, BadLargeOffset, BadMultiPackIndexEntry, DeltaBaseMissing, DeltaChainTooDeep, TruncatedDelta, DeltaSizeOverflow, DeltaBaseSizeMismatch, DeltaReservedInstruction, DeltaCopyOutOfRange, DeltaResultTooLong, DeltaResultTooShort, }; /// Why a pack could not be used, each in git's words. git meets these /// when it first needs the pack, says so, and goes on looking elsewhere; /// so an object database returns one only for an object it found nowhere /// else, in place of `ObjectNotFound`, since the broken pack might have /// held it. The diagnostic's path names the file. /// /// - The errors of `PackIndex.Error`, for its pack index. /// - `PackIndexMissing`: a pack the multi-pack-index names has no index /// ("packfile … index unavailable"). /// - `PackTooSmall`: a pack too short to hold its header and checksum. /// - `NotAPackfile`: "file … is not a GIT packfile". /// - `UnsupportedPackVersion`: "packfile … is version N and not /// supported"; git reads versions 2 and 3. The detail is the version. /// - `PackDoesNotMatchIndex`: "packfile … does not match index": the /// checksum the pack ends with is not the one its index copied. /// - `PackObjectCountMismatch`: the pack's header and its index disagree /// about how many objects it holds. pub const PackOpenError = error{ PackIndexTooSmall, UnsupportedPackIndexVersion, NonMonotonicPackIndex, WrongPackIndexSize, PackIndexMissing, PackTooSmall, NotAPackfile, UnsupportedPackVersion, PackDoesNotMatchIndex, PackObjectCountMismatch, }; /// Why an object could not be streamed: a read error, or the writer's. pub const StreamError = ReadError || error{WriteFailed}; /// Why the database could not say whether it holds an object: the /// storage could not be read, or the object is in none of the places that /// could be read while a pack that might hold it could not be /// (`PackOpenError`). pub const ContainsError = PackOpenError || error{ObjectUnreadable}; /// Whether the database holds the object, without reading it: an object /// that is there but corrupt is held, as `git cat-file -e` says. `diag` /// says what a failure was about. pub fn contains(db: ObjectDatabase, io: Io, id: Id, diag: ?*Diagnostics) ContainsError!bool { return db.vtable.contains(db.ptr, io, id, diag); } /// The object's kind and size, reading no more of it than that takes, as /// `git cat-file -t` and `-s` do; so an object whose contents are corrupt /// may still have a header. pub fn readHeader(db: ObjectDatabase, io: Io, id: Id, diag: ?*Diagnostics) ReadError!Header { return db.vtable.readHeader(db.ptr, io, id, diag); } /// The whole object, checked as git checks an object it reads whole: the /// contents must be exactly as long as the header says, and nothing may /// follow them. The object ID is not recomputed; `object.hash` does that /// for a caller that wants it, as `git cat-file -p` does for a tree. pub fn read(db: ObjectDatabase, io: Io, gpa: Allocator, id: Id, diag: ?*Diagnostics) ReadError!Object { return db.vtable.read(db.ptr, io, gpa, id, diag); } /// Writes the object's contents to `out` as they are inflated, in memory /// bounded whatever the object's size, and returns its header. This is /// how git prints a blob, and it checks what git checks then: the stream /// must be whole, but its length is not compared with the header's size /// and anything after it is ignored. If the stream turns out to be corrupt /// part way, everything inflated before that has been written by the time /// the error is returned. pub fn stream(db: ObjectDatabase, io: Io, gpa: Allocator, id: Id, out: *Io.Writer, diag: ?*Diagnostics) StreamError!Header { return db.vtable.stream(db.ptr, io, gpa, id, out, diag); }