crates/gunnar-store/src/object.rs
//! **The git object decoder that pairs with [`GitServe::read`].**
//!
//! [`GitServe::read`] answers `(ObjType, Vec<u8>)`: the *resolved* type, and
//! the object's **undecorated payload** — no `"commit 1234\0"` header, no zlib
//! frame, no delta. What those bytes mean is not the storage backend's
//! business and the contract does not say; it is git's object format, and this
//! module is where gunnar reads it.
//!
//! # Why this is not `gix-object`
//!
//! It was, until 2026-08-10. `gix-object` is a complete, correct object model —
//! and it is also seven crates (`gix-object`, `gix-actor`, `gix-date`,
//! `gix-error`, `gix-hashtable`, `gix-utils`, `gix-validate`) in every build of
//! the appliance, for four grammars gunnar reads and never writes. What gunnar
//! actually asks of it is small enough to list:
//!
//! * a commit's `tree` and its `parent`s — the traversal, once per commit in
//! the repository;
//! * a commit's committer timestamp — negotiation's ordering, and nothing else
//! depends on it being right;
//! * a tag's `object` — peeling;
//! * a tree's entries as `(oid, kind)` — the closure walk;
//! * the whole of a commit, once, for the control plane's `ListCommits`.
//!
//! That is this file. It is a **reader**: nothing here encodes an object, and
//! nothing here validates one beyond what it must to answer the question asked
//! of it. Object *writing* stays in `gunnar-pack`, on the client side of the
//! fence, where `gix-object` is still the right answer.
//!
//! # What "tolerant" means here
//!
//! Two deliberate leniencies, both matching what the call sites already relied
//! on:
//!
//! * **An unparsable signature time is `0`, not an error.** A commit with a
//! broken committer date costs the walk its ordering for that one commit; it
//! is not a reason to fail a clone. `graph.rs`'s `commit_facts_into` said so
//! in a comment before this module existed, and `control_plane.rs` spelled it
//! `.unwrap_or_default()`.
//! * **Unknown headers are skipped, including `gpgsig`'s continuation lines.**
//! A continuation line begins with a space, so it can never be mistaken for a
//! `parent ` or a `committer ` line, and the scanner does not have to
//! understand folding to be safe against it.
//! * **A commit or tag with no blank line is all headers and no message.**
//! `gix-object` rejects that object outright; this module reads it. Nothing
//! here writes an object, so no malformation can be inherited from the
//! lenient reading, and on the serve path the bytes are copied into the pack
//! verbatim whatever the decoder concluded. Refusing would turn one odd
//! object in a repository's history into a failed clone of the whole
//! repository. `tests/object_decode.rs` pins the divergence in both
//! directions, so it cannot drift unnoticed.
//!
//! Everything else is an error: a missing `tree`, a short or non-hex oid, a
//! tree entry that runs off the end of the buffer, a mode that is not octal.
//!
//! [`GitServe::read`]: git_storage_trait::GitServe::read
use gix_hash::{Kind as HashKind, ObjectId};
/// The four **resolved** git object types.
///
/// The contract's [`ObjType`] has six codes — these four plus the two delta
/// encodings, which are a property of how an object was *stored* and cannot
/// come back out of `read` or `header`. [`Kind::resolved`] is the one place
/// that conversion happens, and it refuses the delta codes rather than
/// silently mapping them onto something.
///
/// [`ObjType`]: git_storage_trait::ObjType
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Kind {
/// A directory listing.
Tree,
/// File content.
Blob,
/// A commit.
Commit,
/// An annotated tag.
Tag,
}
impl Kind {
/// The contract's resolved type as an object kind, or `None` for a delta
/// code.
///
/// `None` means the backend broke its own contract: `GitServe::read` and
/// `GitServe::header` are defined to answer the resolved type. The caller
/// decides what to say about that, because only the caller knows which
/// object it was asking for.
pub fn resolved(ty: git_storage_trait::ObjType) -> Option<Self> {
use git_storage_trait::ObjType;
Some(match ty {
ObjType::Commit => Kind::Commit,
ObjType::Tree => Kind::Tree,
ObjType::Blob => Kind::Blob,
ObjType::Tag => Kind::Tag,
_ => return None,
})
}
/// The same four names git writes into a loose object header and a tag's
/// `type` line.
pub fn as_str(self) -> &'static str {
match self {
Kind::Tree => "tree",
Kind::Blob => "blob",
Kind::Commit => "commit",
Kind::Tag => "tag",
}
}
/// Parse the spelling `as_str` produces.
pub fn from_bytes(b: &[u8]) -> Option<Self> {
Some(match b {
b"tree" => Kind::Tree,
b"blob" => Kind::Blob,
b"commit" => Kind::Commit,
b"tag" => Kind::Tag,
_ => return None,
})
}
}
impl std::fmt::Display for Kind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl From<Kind> for git_storage_trait::ObjType {
fn from(k: Kind) -> Self {
use git_storage_trait::ObjType;
match k {
Kind::Commit => ObjType::Commit,
Kind::Tree => ObjType::Tree,
Kind::Blob => ObjType::Blob,
Kind::Tag => ObjType::Tag,
}
}
}
/// What a tree entry's mode says the entry *is*.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum EntryKind {
/// A subtree.
Tree,
/// A regular file.
Blob,
/// A regular file with the execute bit.
BlobExecutable,
/// A symbolic link.
Link,
/// A gitlink: a commit id naming an object in *another* repository.
Commit,
}
/// One decoded tree entry.
///
/// The filename is deliberately **not** here. The callers this type was written
/// for walk a tree for its `(oid, kind)` pairs — the closure, the type index,
/// the blob filter — and none of them names a path, so carrying the name would
/// have cost an allocation per entry for nothing.
///
/// A caller that *does* need names — `Refs.Diff` reports paths — takes
/// [`NamedTreeEntry`] from [`TreeIter::named`] instead. It borrows the name out
/// of the caller's own buffer, so it still allocates nothing; what it costs is
/// a lifetime, which is why it is a second type rather than two more fields
/// here.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TreeEntry {
/// The object this entry points at.
pub oid: ObjectId,
/// What the mode says it is.
pub kind: EntryKind,
}
/// A [`TreeEntry`] with the name it is filed under and its raw mode.
///
/// The name is a **borrow of the tree object's own bytes**: git stores it
/// inline, so there is nothing to copy and no encoding to decide. It is `[u8]`
/// and not `str` for the same reason [`Signature`]'s fields are raw — git does
/// not say a path is UTF-8, and this module is not the place that gets to
/// pretend otherwise.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct NamedTreeEntry<'a> {
/// The object this entry points at.
pub oid: ObjectId,
/// What the mode says it is.
pub kind: EntryKind,
/// The entry's name within its tree. Never contains `/` and never empty.
pub name: &'a [u8],
/// The mode exactly as written, e.g. `0o100644`.
///
/// [`EntryKind`] is the *interpretation*; this is the value. A diff has to
/// report a file that only changed its execute bit, and the interpretation
/// alone cannot tell `100644` from `100755` after the fact.
pub mode: u32,
}
/// A name, an address, and a moment: git's `author` / `committer` / `tagger`.
///
/// The two byte fields are **raw**. git does not say a name is UTF-8 and this
/// module does not decide it is; the control plane lossily converts at its own
/// boundary, which is where a lossy conversion belongs.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Signature {
/// The name, as written, without the trailing space before `<`.
pub name: Vec<u8>,
/// The address, without the surrounding angle brackets.
pub email: Vec<u8>,
/// Seconds since the unix epoch, or `0` if the line had no readable time.
pub seconds: i64,
/// The timezone offset in **seconds** east of UTC, or `0`.
pub offset_seconds: i32,
}
/// A whole commit, decoded.
///
/// Built only by [`commit`], which exists for the control plane's
/// `ListCommits`. The traversal does not use it: [`commit_links`] and
/// [`commit_facts`] stop reading as soon as they have their answer, and on a
/// million-commit walk that difference is the point.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Commit {
/// The root tree.
pub tree: ObjectId,
/// The parents, in order.
pub parents: Vec<ObjectId>,
/// Who wrote it.
pub author: Signature,
/// Who committed it.
pub committer: Signature,
/// Everything after the blank line that ends the headers.
pub message: Vec<u8>,
}
/// What a tag object points at.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Tag {
/// The `object` line.
pub target: ObjectId,
/// The `type` line, if it named one of the four kinds.
pub target_kind: Option<Kind>,
}
// ───────────────────────────────────────────────────────────────────────────
// The error
// ───────────────────────────────────────────────────────────────────────────
/// A git object that does not decode.
///
/// One type for all four grammars, because every caller does the same thing
/// with it: names the oid it was reading and reports a corrupt object. The
/// message says which field failed, which is the part a human needs.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DecodeError {
kind: Kind,
what: &'static str,
}
impl DecodeError {
fn new(kind: Kind, what: &'static str) -> Self {
DecodeError { kind, what }
}
/// Which grammar was being read.
pub fn object_kind(&self) -> Kind {
self.kind
}
}
impl std::fmt::Display for DecodeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "malformed {}: {}", self.kind, self.what)
}
}
impl std::error::Error for DecodeError {}
/// The result of every decode in this module.
pub type DecodeResult<T> = std::result::Result<T, DecodeError>;
// ───────────────────────────────────────────────────────────────────────────
// Header-line scanning, shared by commit and tag
// ───────────────────────────────────────────────────────────────────────────
/// Split `bytes` at the blank line that ends the header block.
///
/// Returns `(headers, body)`. A commit or tag with no blank line is all
/// headers and an empty body, which is what git's own readers do with a
/// message-less object.
fn split_headers(bytes: &[u8]) -> (&[u8], &[u8]) {
let mut i = 0;
while i < bytes.len() {
match memchr(b'\n', &bytes[i..]) {
// A `\n` immediately followed by another `\n`: the blank line.
Some(rel) => {
let nl = i + rel;
if bytes.get(nl + 1) == Some(&b'\n') {
return (&bytes[..nl + 1], &bytes[nl + 2..]);
}
i = nl + 1;
}
None => break,
}
}
(bytes, &[])
}
/// Iterate the `\n`-terminated lines of a header block, without their `\n`.
struct HeaderLines<'a> {
rest: &'a [u8],
}
impl<'a> Iterator for HeaderLines<'a> {
type Item = &'a [u8];
fn next(&mut self) -> Option<&'a [u8]> {
if self.rest.is_empty() {
return None;
}
match memchr(b'\n', self.rest) {
Some(nl) => {
let line = &self.rest[..nl];
self.rest = &self.rest[nl + 1..];
Some(line)
}
None => {
let line = self.rest;
self.rest = &[];
Some(line)
}
}
}
}
fn header_lines(headers: &[u8]) -> HeaderLines<'_> {
HeaderLines { rest: headers }
}
/// `bytes[..]`'s first `needle`, as an index.
///
/// A three-line loop rather than the `memchr` crate: this module's whole
/// argument is that it costs the appliance no dependencies, and the buffers it
/// scans are one object each. `slice::iter().position()` compiles to a
/// vectorised byte search on every target gunnar builds for.
#[inline]
fn memchr(needle: u8, haystack: &[u8]) -> Option<usize> {
haystack.iter().position(|&b| b == needle)
}
/// `line` with `prefix` removed, if it is there.
#[inline]
fn strip<'a>(line: &'a [u8], prefix: &[u8]) -> Option<&'a [u8]> {
line.strip_prefix(prefix)
}
/// Decode one hex oid, rejecting the wrong length as firmly as a bad digit.
#[inline]
fn hex_oid(field: &[u8], hash: HashKind, kind: Kind, what: &'static str) -> DecodeResult<ObjectId> {
if field.len() != hash.len_in_hex() {
return Err(DecodeError::new(kind, what));
}
ObjectId::from_hex(field).map_err(|_| DecodeError::new(kind, what))
}
// ───────────────────────────────────────────────────────────────────────────
// Signatures
// ───────────────────────────────────────────────────────────────────────────
/// `Name <email> 1723200000 +0200`.
///
/// The split is on the **last** `>` in the line, not the first: a name may
/// contain angle brackets and git does not escape them, so scanning forwards
/// finds the wrong one. Everything after that `>` is `<seconds> <offset>`, and
/// either may be missing — see the module docs on why that is a `0` and not an
/// error.
fn signature(line: &[u8]) -> Signature {
let close = line.iter().rposition(|&b| b == b'>');
let (name_email, tail) = match close {
Some(at) => (&line[..at], &line[at + 1..]),
None => (line, &line[line.len()..]),
};
let open = name_email.iter().position(|&b| b == b'<');
let (name, email) = match open {
Some(at) => (&name_email[..at], &name_email[at + 1..]),
None => (name_email, &name_email[name_email.len()..]),
};
let mut fields = tail.split(|&b| b == b' ').filter(|f| !f.is_empty());
let seconds = fields
.next()
.and_then(|f| std::str::from_utf8(f).ok())
.and_then(|s| s.parse::<i64>().ok())
.unwrap_or(0);
let offset_seconds = fields.next().map(tz_offset).unwrap_or(0);
Signature {
name: trim_ascii_end(name).to_vec(),
email: email.to_vec(),
seconds,
offset_seconds,
}
}
/// `+0200` / `-0730` as seconds east of UTC.
///
/// The minutes field is **two digits taken as minutes**, not a base-100 number:
/// `-0730` is minus seven and a half hours, and reading it as `-730 * 60` is
/// the classic way to be twelve hours wrong twice a year.
fn tz_offset(field: &[u8]) -> i32 {
let (sign, digits) = match field.first() {
Some(b'-') => (-1, &field[1..]),
Some(b'+') => (1, &field[1..]),
_ => (1, field),
};
if digits.len() != 4 || !digits.iter().all(|b| b.is_ascii_digit()) {
return 0;
}
let hours = (digits[0] - b'0') as i32 * 10 + (digits[1] - b'0') as i32;
let minutes = (digits[2] - b'0') as i32 * 10 + (digits[3] - b'0') as i32;
sign * (hours * 3600 + minutes * 60)
}
#[inline]
fn trim_ascii_end(mut b: &[u8]) -> &[u8] {
while let Some((&last, rest)) = b.split_last() {
if last.is_ascii_whitespace() {
b = rest;
} else {
break;
}
}
b
}
// ───────────────────────────────────────────────────────────────────────────
// Commits
// ───────────────────────────────────────────────────────────────────────────
/// **A commit's tree and parents, and nothing else read.**
///
/// The hot one: this runs once per commit in the repository on every clone
/// that is not served from a bitmap. git's commit grammar is `tree`, then
/// `parent`*, then `author` — so the scan stops at the first line that is not
/// a parent, and the two signatures, the encoding, the extra headers and the
/// message are never looked at.
///
/// `parents` is cleared and refilled. It belongs to the caller so that a walk
/// is one allocation and not one per commit.
pub fn commit_links(
bytes: &[u8],
hash: HashKind,
parents: &mut Vec<ObjectId>,
) -> DecodeResult<ObjectId> {
let (headers, _) = split_headers(bytes);
let mut lines = header_lines(headers);
let tree = lines
.next()
.and_then(|l| strip(l, b"tree "))
.ok_or_else(|| DecodeError::new(Kind::Commit, "no tree header"))?;
let tree = hex_oid(tree, hash, Kind::Commit, "tree header")?;
parents.clear();
for line in lines {
match strip(line, b"parent ") {
Some(field) => parents.push(hex_oid(field, hash, Kind::Commit, "parent header")?),
None => break,
}
}
Ok(tree)
}
/// [`commit_links`], plus the committer's timestamp.
///
/// Negotiation orders its walk by this. It costs one more pass over the header
/// lines and exactly one signature split; the four other headers are still
/// never parsed.
///
/// A commit whose committer line has no readable time gets `0`, which costs
/// that one commit its place in the ordering and nothing else. `SLOP` in
/// `negotiate.rs` absorbs precisely this.
pub fn commit_facts(
bytes: &[u8],
hash: HashKind,
parents: &mut Vec<ObjectId>,
) -> DecodeResult<(ObjectId, i64)> {
let (headers, _) = split_headers(bytes);
let mut lines = header_lines(headers);
let tree = lines
.next()
.and_then(|l| strip(l, b"tree "))
.ok_or_else(|| DecodeError::new(Kind::Commit, "no tree header"))?;
let tree = hex_oid(tree, hash, Kind::Commit, "tree header")?;
parents.clear();
let mut committed_at = 0;
let mut in_parents = true;
for line in lines {
if in_parents {
if let Some(field) = strip(line, b"parent ") {
parents.push(hex_oid(field, hash, Kind::Commit, "parent header")?);
continue;
}
in_parents = false;
}
if let Some(field) = strip(line, b"committer ") {
committed_at = signature(field).seconds;
break;
}
}
Ok((tree, committed_at))
}
/// The whole commit: both signatures, the parents, the tree and the message.
///
/// The control plane's `ListCommits` and nothing else. Everything on the serve
/// path uses [`commit_links`] or [`commit_facts`].
pub fn commit(bytes: &[u8], hash: HashKind) -> DecodeResult<Commit> {
let (headers, body) = split_headers(bytes);
let mut tree = None;
let mut parents = Vec::new();
let mut author = None;
let mut committer = None;
for line in header_lines(headers) {
if let Some(field) = strip(line, b"tree ") {
if tree.is_none() {
tree = Some(hex_oid(field, hash, Kind::Commit, "tree header")?);
}
} else if let Some(field) = strip(line, b"parent ") {
parents.push(hex_oid(field, hash, Kind::Commit, "parent header")?);
} else if let Some(field) = strip(line, b"author ") {
if author.is_none() {
author = Some(signature(field));
}
} else if let Some(field) = strip(line, b"committer ") {
if committer.is_none() {
committer = Some(signature(field));
}
}
}
Ok(Commit {
tree: tree.ok_or_else(|| DecodeError::new(Kind::Commit, "no tree header"))?,
parents,
author: author.ok_or_else(|| DecodeError::new(Kind::Commit, "no author header"))?,
committer: committer
.ok_or_else(|| DecodeError::new(Kind::Commit, "no committer header"))?,
message: body.to_vec(),
})
}
// ───────────────────────────────────────────────────────────────────────────
// Tags
// ───────────────────────────────────────────────────────────────────────────
/// A tag's `object` and `type` lines.
///
/// Peeling needs the target; the type is returned because it is free once the
/// line is in hand, and `None` there means the tag named a type git does not
/// have, which is the tag's problem and not the peeler's.
pub fn tag(bytes: &[u8], hash: HashKind) -> DecodeResult<Tag> {
let (headers, _) = split_headers(bytes);
let mut target = None;
let mut target_kind = None;
for line in header_lines(headers) {
if let Some(field) = strip(line, b"object ") {
if target.is_none() {
target = Some(hex_oid(field, hash, Kind::Tag, "object header")?);
}
} else if let Some(field) = strip(line, b"type ") {
if target_kind.is_none() {
target_kind = Kind::from_bytes(field);
}
}
}
Ok(Tag {
target: target.ok_or_else(|| DecodeError::new(Kind::Tag, "no object header"))?,
target_kind,
})
}
/// The one line of a tag that peeling reads.
pub fn tag_target(bytes: &[u8], hash: HashKind) -> DecodeResult<ObjectId> {
tag(bytes, hash).map(|t| t.target)
}
// ───────────────────────────────────────────────────────────────────────────
// Trees
// ───────────────────────────────────────────────────────────────────────────
/// `<mode> <name>\0<raw oid>`, repeated to the end of the buffer.
///
/// Nothing is allocated: the iterator holds the caller's buffer and copies one
/// oid per entry into the yielded [`TreeEntry`].
pub struct TreeIter<'a> {
inner: NamedTreeIter<'a>,
}
impl<'a> TreeIter<'a> {
/// Read `bytes` as a tree object's payload.
pub fn from_bytes(bytes: &'a [u8], hash: HashKind) -> Self {
TreeIter {
inner: NamedTreeIter::from_bytes(bytes, hash),
}
}
/// The same walk, yielding each entry's **name and raw mode** as well.
///
/// Not a second parser: [`TreeIter`] is this one with the two extra fields
/// dropped, so a tree either decodes the same way for both or for neither
/// (one implementation).
pub fn named(self) -> NamedTreeIter<'a> {
self.inner
}
}
impl Iterator for TreeIter<'_> {
type Item = DecodeResult<TreeEntry>;
fn next(&mut self) -> Option<DecodeResult<TreeEntry>> {
Some(self.inner.next()?.map(|entry| TreeEntry {
oid: entry.oid,
kind: entry.kind,
}))
}
}
/// [`TreeIter`], with the name and the raw mode kept.
///
/// The one decoder for git's tree grammar in this crate; see [`TreeIter::named`].
pub struct NamedTreeIter<'a> {
rest: &'a [u8],
hash_len: usize,
failed: bool,
}
impl<'a> NamedTreeIter<'a> {
/// Read `bytes` as a tree object's payload.
pub fn from_bytes(bytes: &'a [u8], hash: HashKind) -> Self {
NamedTreeIter {
rest: bytes,
hash_len: hash.len_in_bytes(),
failed: false,
}
}
}
impl<'a> Iterator for NamedTreeIter<'a> {
type Item = DecodeResult<NamedTreeEntry<'a>>;
fn next(&mut self) -> Option<DecodeResult<NamedTreeEntry<'a>>> {
if self.failed || self.rest.is_empty() {
return None;
}
match self.entry() {
Ok(entry) => Some(Ok(entry)),
Err(err) => {
// One bad entry ends the tree. Resynchronising inside a
// corrupt buffer would mean guessing where the next record
// starts, and a guess that lands mid-oid yields entries that
// look perfectly well-formed.
self.failed = true;
Some(Err(err))
}
}
}
}
impl<'a> NamedTreeIter<'a> {
fn entry(&mut self) -> DecodeResult<NamedTreeEntry<'a>> {
let bad = |what| DecodeError::new(Kind::Tree, what);
let space =
memchr(b' ', self.rest).ok_or_else(|| bad("an entry with no mode delimiter"))?;
let mode =
octal(&self.rest[..space]).ok_or_else(|| bad("an entry whose mode is not octal"))?;
let after_mode = &self.rest[space + 1..];
let nul = memchr(0, after_mode).ok_or_else(|| bad("an entry with no name terminator"))?;
let name = &after_mode[..nul];
if name.is_empty() {
return Err(bad("an entry with an empty name"));
}
let after_name = &after_mode[nul + 1..];
if after_name.len() < self.hash_len {
return Err(bad("an entry whose oid runs off the end"));
}
let (raw, rest) = after_name.split_at(self.hash_len);
self.rest = rest;
Ok(NamedTreeEntry {
// Length-checked one line up, which is the only way the conversion
// can fail.
oid: ObjectId::from_bytes_or_panic(raw),
kind: entry_kind(mode),
name,
mode,
})
}
}
/// The entry a tree holds under exactly `name`, with its kind — or `None`.
///
/// [`TreeIter`] deliberately drops names (its callers walk edges), and the log
/// path filter is the caller that cannot: "did this commit change `src/lib.rs`"
/// is answered by comparing what NAME an id sits under across two trees. Same
/// parse, same corruption rule — a malformed entry is an error, never a
/// resynchronised guess. Byte comparison, no UTF-8 assumption: git names are
/// bytes and so is the needle.
pub fn tree_entry_named(
bytes: &[u8],
hash: HashKind,
name: &[u8],
) -> DecodeResult<Option<TreeEntry>> {
let bad = |what| DecodeError::new(Kind::Tree, what);
let hash_len = hash.len_in_bytes();
let mut rest = bytes;
while !rest.is_empty() {
let space = memchr(b' ', rest).ok_or_else(|| bad("an entry with no mode delimiter"))?;
let mode = octal(&rest[..space]).ok_or_else(|| bad("an entry whose mode is not octal"))?;
let after_mode = &rest[space + 1..];
let nul = memchr(0, after_mode).ok_or_else(|| bad("an entry with no name terminator"))?;
let entry_name = &after_mode[..nul];
let after_name = &after_mode[nul + 1..];
if after_name.len() < hash_len {
return Err(bad("an entry whose oid runs off the end"));
}
let (raw, tail) = after_name.split_at(hash_len);
if entry_name == name {
return Ok(Some(TreeEntry {
oid: ObjectId::from_bytes_or_panic(raw),
kind: entry_kind(mode),
}));
}
rest = tail;
}
Ok(None)
}
/// Up to six ASCII octal digits.
///
/// Bounded at six because git's modes are, and because an unbounded loop over
/// a corrupt buffer is how a decoder turns a bad object into a hang.
fn octal(field: &[u8]) -> Option<u32> {
if field.is_empty() || field.len() > 6 {
return None;
}
let mut mode = 0u32;
for &b in field {
let d = b.wrapping_sub(b'0');
if d > 7 {
return None;
}
mode = (mode << 3) | d as u32;
}
Some(mode)
}
/// git's `S_IFMT` split of a tree entry mode.
///
/// The `0o140000` arm is not a type: it is the socket bit, which git never
/// writes and `gix-object` folds onto a tree. Anything that is not a known
/// format is a gitlink, which is also what git itself concludes.
fn entry_kind(mode: u32) -> EntryKind {
const IFMT: u32 = 0o170000;
match mode & IFMT {
0o100000 => {
if mode & 0o100 == 0o100 {
EntryKind::BlobExecutable
} else {
EntryKind::Blob
}
}
0o120000 => EntryKind::Link,
0o040000 | 0o140000 => EntryKind::Tree,
_ => EntryKind::Commit,
}
}