Skip to main content

grit_lib/objects/
core.rs

1//! Git object model: object IDs, kinds, and in-memory representations.
2//!
3//! # Object ID
4//!
5//! [`ObjectId`] is a 20-byte SHA-1 digest.  It implements `Display` as
6//! lowercase hex, `FromStr` from a 40-character hex string, and the standard
7//! ordering traits so it can be used as a map key.
8//!
9//! # Object Kind
10//!
11//! [`ObjectKind`] represents the four Git object types: blob, tree, commit,
12//! and tag.  The raw header byte-slice is parsed with [`ObjectKind::from_bytes`].
13//!
14//! # Parsed objects
15//!
16//! [`Object`] bundles a kind and its raw (decompressed, header-stripped) byte
17//! content.  Higher-level parsed forms (e.g. [`TreeEntry`], [`CommitData`])
18//! live in this module and are produced by fallible `TryFrom<&Object>`
19//! conversions.
20
21use std::fmt;
22use std::str::FromStr;
23
24use crate::commit_encoding;
25use crate::error::{Error, Result};
26
27/// A Git hash algorithm.
28///
29/// Git supports two object-id hash functions: the historical SHA-1 (20-byte
30/// digests, 40 hex chars) and the newer SHA-256 (32-byte digests, 64 hex
31/// chars). A repository's algorithm is recorded in `extensions.objectformat`.
32#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
33pub enum HashAlgo {
34    /// SHA-1 — 20-byte digests.
35    #[default]
36    Sha1,
37    /// SHA-256 — 32-byte digests.
38    Sha256,
39}
40
41impl HashAlgo {
42    /// The raw digest length in bytes (20 for SHA-1, 32 for SHA-256).
43    #[must_use]
44    pub const fn len(self) -> usize {
45        match self {
46            Self::Sha1 => 20,
47            Self::Sha256 => 32,
48        }
49    }
50
51    /// Always false — every hash algorithm has a non-zero digest length.
52    #[must_use]
53    pub const fn is_empty(self) -> bool {
54        false
55    }
56
57    /// The hex-encoded digest length (40 for SHA-1, 64 for SHA-256).
58    #[must_use]
59    pub const fn hex_len(self) -> usize {
60        self.len() * 2
61    }
62
63    /// The lowercase name as written in `extensions.objectformat`.
64    #[must_use]
65    pub const fn name(self) -> &'static str {
66        match self {
67            Self::Sha1 => "sha1",
68            Self::Sha256 => "sha256",
69        }
70    }
71
72    /// The `oid_version` byte used in `.idx`/multi-pack-index headers
73    /// (SHA-1 → 1, SHA-256 → 2).
74    #[must_use]
75    pub const fn oid_version(self) -> u8 {
76        match self {
77            Self::Sha1 => 1,
78            Self::Sha256 => 2,
79        }
80    }
81
82    /// Parse from the name used in `extensions.objectformat` config.
83    #[must_use]
84    pub fn from_name(name: &str) -> Option<Self> {
85        match name.trim() {
86            v if v.eq_ignore_ascii_case("sha1") => Some(Self::Sha1),
87            v if v.eq_ignore_ascii_case("sha256") => Some(Self::Sha256),
88            _ => None,
89        }
90    }
91
92    /// The algorithm implied by a raw digest length, if recognised.
93    #[must_use]
94    pub const fn from_len(len: usize) -> Option<Self> {
95        match len {
96            20 => Some(Self::Sha1),
97            32 => Some(Self::Sha256),
98            _ => None,
99        }
100    }
101}
102
103/// Error when converting an on-disk hash-version byte to [`HashAlgo`].
104#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
105#[error("unknown Git hash version byte {0} (expected 1 for SHA-1 or 2 for SHA-256)")]
106pub struct HashVersionError(pub u8);
107
108impl From<HashAlgo> for u8 {
109    fn from(algo: HashAlgo) -> Self {
110        algo.oid_version()
111    }
112}
113
114impl TryFrom<u8> for HashAlgo {
115    type Error = HashVersionError;
116
117    fn try_from(version: u8) -> std::result::Result<Self, Self::Error> {
118        match version {
119            1 => Ok(Self::Sha1),
120            2 => Ok(Self::Sha256),
121            other => Err(HashVersionError(other)),
122        }
123    }
124}
125
126/// Maximum raw digest length across supported hash algorithms (SHA-256).
127const MAX_OID_LEN: usize = 32;
128
129/// Canonical SHA-1 empty tree digest (`4b825dc642cb6eb9a060e54bf8d69288fbee4904`).
130const WELL_KNOWN_EMPTY_TREE_CANON: [u8; 20] = [
131    0x4b, 0x82, 0x5d, 0xc6, 0x42, 0xcb, 0x6e, 0xb9, 0xa0, 0x60, 0xe5, 0x4b, 0xf8, 0xd6, 0x92, 0x88,
132    0xfb, 0xee, 0x49, 0x04,
133];
134/// Legacy typo empty tree digest still referenced by some tests (`…899d69f7c6948d4`).
135const WELL_KNOWN_EMPTY_TREE_LEGACY: [u8; 20] = [
136    0x4b, 0x82, 0x5d, 0xc6, 0x42, 0xcb, 0x6e, 0xb9, 0xa0, 0x60, 0xe5, 0x4b, 0xf8, 0x99, 0xd6, 0x9f,
137    0x7c, 0x69, 0x48, 0xd4,
138];
139/// SHA-256 empty tree digest (`6ef19b41…decc5321`).
140const EMPTY_TREE_SHA256: [u8; 32] = [
141    0x6e, 0xf1, 0x9b, 0x41, 0x22, 0x5c, 0x53, 0x69, 0xf1, 0xc1, 0x04, 0xd4, 0x5d, 0x8d, 0x85, 0xef,
142    0xa9, 0xb0, 0x57, 0xb5, 0x3b, 0x14, 0xb4, 0xb9, 0xb9, 0x39, 0xdd, 0x74, 0xde, 0xcc, 0x53, 0x21,
143];
144/// SHA-1 digest of the empty blob (`e69de29bb2d1d6434b8b29ae775ad8c2e48c5391`).
145const EMPTY_BLOB_SHA1: [u8; 20] = [
146    0xe6, 0x9d, 0xe2, 0x9b, 0xb2, 0xd1, 0xd6, 0x43, 0x4b, 0x8b, 0x29, 0xae, 0x77, 0x5a, 0xd8, 0xc2,
147    0xe4, 0x8c, 0x53, 0x91,
148];
149/// SHA-256 digest of the empty blob (`473a0f4c…303721813`).
150const EMPTY_BLOB_SHA256: [u8; 32] = [
151    0x47, 0x3a, 0x0f, 0x4c, 0x3b, 0xe8, 0xa9, 0x36, 0x81, 0xa2, 0x67, 0xe3, 0xb1, 0xe9, 0xa7, 0xdc,
152    0xda, 0x11, 0x85, 0x43, 0x6f, 0xe1, 0x41, 0xf7, 0x74, 0x91, 0x20, 0xa3, 0x03, 0x72, 0x18, 0x13,
153];
154
155/// A Git object identifier: a SHA-1 (20-byte) or SHA-256 (32-byte) digest.
156///
157/// The digest is stored in a fixed 32-byte buffer with an explicit length;
158/// bytes beyond `len` are always zero, so the derived `Eq`/`Ord`/`Hash`
159/// remain correct. The hash algorithm is inferred from the length.
160///
161/// # Examples
162///
163/// ```
164/// use grit_lib::objects::ObjectId;
165///
166/// # fn main() -> grit_lib::error::Result<()> {
167/// let id = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf8d69288fbee4904")?;
168/// assert_eq!(id.to_hex(), "4b825dc642cb6eb9a060e54bf8d69288fbee4904");
169/// # Ok(())
170/// # }
171/// ```
172#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
173pub struct ObjectId {
174    bytes: [u8; MAX_OID_LEN],
175    len: u8,
176}
177
178impl ObjectId {
179    /// The all-zero SHA-1 object id (Git's "null" OID).
180    ///
181    /// Used for index placeholders such as intent-to-add entries and for
182    /// special cases in plumbing output. For an algorithm-specific null OID
183    /// (e.g. 64 zeros in a SHA-256 repo) use [`ObjectId::null`].
184    #[must_use]
185    pub const fn zero() -> Self {
186        Self {
187            bytes: [0u8; MAX_OID_LEN],
188            len: 20,
189        }
190    }
191
192    /// The all-zero ("null") object id for a given hash algorithm.
193    #[must_use]
194    pub const fn null(algo: HashAlgo) -> Self {
195        Self {
196            bytes: [0u8; MAX_OID_LEN],
197            len: algo.len() as u8,
198        }
199    }
200
201    /// Construct from a raw digest slice (20 bytes for SHA-1, 32 for SHA-256).
202    ///
203    /// # Errors
204    ///
205    /// Returns [`Error::InvalidObjectId`] when `bytes` is not a recognised
206    /// digest length.
207    pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
208        if HashAlgo::from_len(bytes.len()).is_none() {
209            return Err(Error::InvalidObjectId(hex::encode(bytes)));
210        }
211        let mut buf = [0u8; MAX_OID_LEN];
212        buf[..bytes.len()].copy_from_slice(bytes);
213        Ok(Self {
214            bytes: buf,
215            len: bytes.len() as u8,
216        })
217    }
218
219    /// Raw digest bytes (20 or 32 bytes depending on the hash algorithm).
220    #[must_use]
221    pub fn as_bytes(&self) -> &[u8] {
222        &self.bytes[..self.len as usize]
223    }
224
225    /// The hash algorithm this OID belongs to.
226    #[must_use]
227    pub fn algo(&self) -> HashAlgo {
228        HashAlgo::from_len(self.len as usize).unwrap_or(HashAlgo::Sha1)
229    }
230
231    /// Check if this is the null (all-zero) object ID.
232    #[must_use]
233    pub fn is_zero(&self) -> bool {
234        self.as_bytes().iter().all(|&b| b == 0)
235    }
236
237    /// Number of leading hex digits this id shares with `other`.
238    #[must_use]
239    pub(crate) fn common_prefix_nibbles(&self, other: &Self) -> usize {
240        let (a, b) = (self.as_bytes(), other.as_bytes());
241        match a.iter().zip(b).position(|(x, y)| x != y) {
242            Some(i) => i * 2 + usize::from((a[i] ^ b[i]) & 0xf0 == 0),
243            None => a.len().min(b.len()) * 2,
244        }
245    }
246
247    /// Whether this OID is the empty blob for its hash algorithm (SHA-1 or SHA-256).
248    #[must_use]
249    pub(crate) fn is_empty_blob(&self) -> bool {
250        let bytes = self.as_bytes();
251        bytes == EMPTY_BLOB_SHA1 || bytes == EMPTY_BLOB_SHA256
252    }
253
254    /// Whether this OID is Git's well-known empty tree (canonical or legacy typo hash).
255    #[must_use]
256    pub(crate) fn is_well_known_empty_tree(&self) -> bool {
257        self.is_canonical_empty_tree()
258            || (self.as_bytes().len() == HashAlgo::Sha1.len()
259                && self.as_bytes() == WELL_KNOWN_EMPTY_TREE_LEGACY)
260    }
261
262    /// Whether this OID is the empty tree of a repository using `algo`, including the legacy
263    /// SHA-1 typo id.
264    #[must_use]
265    pub(crate) fn is_empty_tree_for(&self, algo: HashAlgo) -> bool {
266        match algo {
267            HashAlgo::Sha1 => self.is_well_known_empty_tree(),
268            HashAlgo::Sha256 => self.as_bytes() == EMPTY_TREE_SHA256,
269        }
270    }
271
272    /// Whether this OID is the canonical SHA-1 empty tree (`4b825dc6…4904`).
273    #[must_use]
274    pub(crate) fn is_canonical_empty_tree(&self) -> bool {
275        self.as_bytes().len() == HashAlgo::Sha1.len()
276            && self.as_bytes() == WELL_KNOWN_EMPTY_TREE_CANON
277    }
278
279    /// Canonical SHA-1 empty tree object id (`4b825dc642cb6eb9a060e54bf8d69288fbee4904`).
280    ///
281    /// Returns the well-known digest without writing a loose object. On SHA-256
282    /// repositories use [`Odb::empty_tree_oid`](crate::odb::Odb::empty_tree_oid)
283    /// instead.
284    #[must_use]
285    pub const fn empty_tree() -> Self {
286        let mut bytes = [0u8; MAX_OID_LEN];
287        let mut i = 0;
288        while i < WELL_KNOWN_EMPTY_TREE_CANON.len() {
289            bytes[i] = WELL_KNOWN_EMPTY_TREE_CANON[i];
290            i += 1;
291        }
292        Self { bytes, len: 20 }
293    }
294
295    /// Append `/<xx>/<rest-of-hex>` for this OID under `path` without heap hex strings.
296    pub(crate) fn push_loose_relative(&self, path: &mut std::path::PathBuf) {
297        const HEX: &[u8; 16] = b"0123456789abcdef";
298        let bytes = self.as_bytes();
299        let prefix = [
300            HEX[(bytes[0] >> 4) as usize],
301            HEX[(bytes[0] & 0xf) as usize],
302        ];
303        // SAFETY: `prefix` and `suffix` are ASCII hex digits only.
304        path.push(unsafe { std::str::from_utf8_unchecked(&prefix) });
305        let suffix_len = (bytes.len() - 1) * 2;
306        let mut suffix = [0u8; 62];
307        for (i, &b) in bytes[1..].iter().enumerate() {
308            suffix[i * 2] = HEX[(b >> 4) as usize];
309            suffix[i * 2 + 1] = HEX[(b & 0xf) as usize];
310        }
311        path.push(unsafe { std::str::from_utf8_unchecked(&suffix[..suffix_len]) });
312    }
313
314    /// Build `<objects-dir>/<xx>/<rest>` for this OID without intermediate hex [`String`]s.
315    #[must_use]
316    pub(crate) fn loose_path_in(&self, objects_dir: &std::path::Path) -> std::path::PathBuf {
317        let mut path = objects_dir.to_path_buf();
318        self.push_loose_relative(&mut path);
319        path
320    }
321
322    /// Lowercase hex representation (40 or 64 characters).
323    #[must_use]
324    pub fn to_hex(&self) -> String {
325        self.hex_digits().as_str().to_owned()
326    }
327
328    /// The lowercase hex digits of this id, without allocating.
329    fn hex_digits(&self) -> HexDigits {
330        const DIGITS: &[u8; 16] = b"0123456789abcdef";
331        let mut buf = [0u8; MAX_OID_LEN * 2];
332        for (pair, &byte) in buf.as_chunks_mut::<2>().0.iter_mut().zip(self.as_bytes()) {
333            *pair = [
334                DIGITS[usize::from(byte >> 4)],
335                DIGITS[usize::from(byte & 0x0f)],
336            ];
337        }
338        HexDigits {
339            buf,
340            len: usize::from(self.len) * 2,
341        }
342    }
343
344    /// The two-character directory prefix used by the loose object store.
345    ///
346    /// Returns the first two hex chars (e.g. `"ab"` for `"ab3f…"`).
347    #[must_use]
348    pub fn loose_prefix(&self) -> String {
349        hex::encode(&self.bytes[..1])
350    }
351
352    /// Parse an object ID from a hex string (40 chars for SHA-1, 64 for
353    /// SHA-256).
354    ///
355    /// # Errors
356    ///
357    /// Returns [`Error::InvalidObjectId`] if the string is not a valid hex OID.
358    pub fn from_hex(s: &str) -> Result<Self> {
359        Self::decode_hex(s.as_bytes()).ok_or_else(|| Error::InvalidObjectId(s.to_owned()))
360    }
361
362    /// Parse a fixed-width lowercase/uppercase ASCII hex OID (40 chars for SHA-1).
363    ///
364    /// # Errors
365    ///
366    /// Returns [`Error::InvalidObjectId`] when `bytes` is not exactly 40 hex digits.
367    pub fn from_hex_ascii(bytes: &[u8]) -> Result<Self> {
368        if bytes.len() != HashAlgo::Sha1.hex_len() {
369            return Err(Error::InvalidObjectId(
370                String::from_utf8_lossy(bytes).into_owned(),
371            ));
372        }
373        Self::decode_hex(bytes)
374            .ok_or_else(|| Error::InvalidObjectId(String::from_utf8_lossy(bytes).into_owned()))
375    }
376
377    /// Decode 40 or 64 ASCII hex digits (either case); `None` for any other input.
378    ///
379    /// Ref listings and tree walks parse ids in bulk, so this avoids per-digit
380    /// branching and allocation: invalid digits are folded into one check at the end.
381    fn decode_hex(hex: &[u8]) -> Option<Self> {
382        let len = hex.len() / 2;
383        if !Self::is_hex_len(hex.len()) {
384            return None;
385        }
386        let mut bytes = [0u8; MAX_OID_LEN];
387        let mut invalid = 0u8;
388        for (out, pair) in bytes.iter_mut().zip(hex.as_chunks::<2>().0) {
389            let hi = HEX_DECODE[usize::from(pair[0])];
390            let lo = HEX_DECODE[usize::from(pair[1])];
391            invalid |= hi | lo;
392            *out = (hi << 4) | lo;
393        }
394        (invalid & HEX_INVALID == 0).then_some(Self {
395            bytes,
396            len: len as u8,
397        })
398    }
399
400    /// The suffix used as the filename inside the loose prefix dir (the digest
401    /// minus its first byte: 38 hex chars for SHA-1, 62 for SHA-256).
402    #[must_use]
403    pub fn loose_suffix(&self) -> String {
404        hex::encode(&self.bytes[1..self.len as usize])
405    }
406
407    /// Whether `s` is a full-length hex OID for a supported hash algorithm
408    /// (40 hex chars for SHA-1 or 64 for SHA-256), case-insensitive.
409    #[must_use]
410    pub fn is_full_hex(s: &str) -> bool {
411        (s.len() == HashAlgo::Sha1.hex_len() || s.len() == HashAlgo::Sha256.hex_len())
412            && s.bytes().all(|b| b.is_ascii_hexdigit())
413    }
414
415    /// Whether `len` is a valid full hex-OID length (40 or 64).
416    #[must_use]
417    pub const fn is_hex_len(len: usize) -> bool {
418        len == HashAlgo::Sha1.hex_len() || len == HashAlgo::Sha256.hex_len()
419    }
420
421    /// Whether `len` is a valid loose-object filename suffix length, i.e. a full
422    /// hex OID minus its first byte (38 for SHA-1, 62 for SHA-256).
423    #[must_use]
424    pub const fn is_loose_suffix_len(len: usize) -> bool {
425        len == HashAlgo::Sha1.hex_len() - 2 || len == HashAlgo::Sha256.hex_len() - 2
426    }
427}
428
429impl std::hash::Hash for ObjectId {
430    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
431        state.write(self.as_bytes());
432    }
433}
434
435impl fmt::Display for ObjectId {
436    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
437        f.write_str(self.hex_digits().as_str())
438    }
439}
440
441/// Stack buffer holding an [`ObjectId`]'s hex digits.
442struct HexDigits {
443    buf: [u8; MAX_OID_LEN * 2],
444    len: usize,
445}
446
447impl HexDigits {
448    fn as_str(&self) -> &str {
449        std::str::from_utf8(&self.buf[..self.len]).unwrap_or_default()
450    }
451}
452
453impl fmt::Debug for ObjectId {
454    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
455        write!(f, "ObjectId({})", self.to_hex())
456    }
457}
458
459#[cfg(feature = "serde-models")]
460impl serde::Serialize for ObjectId {
461    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
462    where
463        S: serde::Serializer,
464    {
465        serializer.serialize_str(&self.to_hex())
466    }
467}
468
469#[cfg(feature = "serde-models")]
470impl<'de> serde::Deserialize<'de> for ObjectId {
471    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
472    where
473        D: serde::Deserializer<'de>,
474    {
475        let hex = String::deserialize(deserializer)?;
476        Self::from_hex(&hex).map_err(serde::de::Error::custom)
477    }
478}
479
480/// Marker bit in [`HEX_DECODE`] for bytes that are not hex digits.
481const HEX_INVALID: u8 = 0x10;
482
483/// Nibble value of each ASCII hex digit; every other byte maps to [`HEX_INVALID`].
484const HEX_DECODE: [u8; 256] = {
485    let mut table = [HEX_INVALID; 256];
486    let mut i = 0u8;
487    while i < 10 {
488        table[(b'0' + i) as usize] = i;
489        i += 1;
490    }
491    let mut i = 0u8;
492    while i < 6 {
493        table[(b'a' + i) as usize] = 10 + i;
494        table[(b'A' + i) as usize] = 10 + i;
495        i += 1;
496    }
497    table
498};
499
500impl FromStr for ObjectId {
501    type Err = Error;
502
503    fn from_str(s: &str) -> Result<Self> {
504        Self::from_hex(s)
505    }
506}
507
508/// A Git file mode (`100644`, `100755`, `120000`, `160000`, `040000`), or
509/// [`FileMode::ABSENT`] for the missing side of an add or delete.
510///
511/// Displays and serializes as the six-digit octal text Git prints in raw diff
512/// output, so the zero mode reads `000000`.
513///
514/// # Examples
515///
516/// ```
517/// use grit_lib::objects::FileMode;
518///
519/// assert_eq!(FileMode::EXECUTABLE.to_string(), "100755");
520/// assert_eq!(FileMode::ABSENT.to_string(), "000000");
521/// assert_eq!(FileMode::new(0o120000), FileMode::SYMLINK);
522/// assert!(FileMode::SYMLINK.is_symlink());
523/// ```
524#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
525pub struct FileMode(u32);
526
527impl FileMode {
528    /// No entry on this side (an add's old side, a delete's new side).
529    pub const ABSENT: Self = Self(0);
530    /// Regular, non-executable file.
531    pub const REGULAR: Self = Self(0o100644);
532    /// Executable file.
533    pub const EXECUTABLE: Self = Self(0o100755);
534    /// Symbolic link.
535    pub const SYMLINK: Self = Self(0o120000);
536    /// Submodule commit (gitlink).
537    pub const GITLINK: Self = Self(0o160000);
538    /// Tree (directory).
539    pub const TREE: Self = Self(0o040000);
540
541    /// Wrap raw mode bits as stored in a tree entry or the index.
542    #[must_use]
543    pub const fn new(bits: u32) -> Self {
544        Self(bits)
545    }
546
547    /// The raw mode bits.
548    #[must_use]
549    pub const fn bits(self) -> u32 {
550        self.0
551    }
552
553    /// Whether this is [`Self::ABSENT`].
554    #[must_use]
555    pub const fn is_absent(self) -> bool {
556        self.0 == 0
557    }
558
559    /// The octal text, or `None` for [`Self::ABSENT`] (the missing side of an add or delete).
560    #[must_use]
561    pub fn octal_if_present(self) -> Option<String> {
562        (!self.is_absent()).then(|| self.to_string())
563    }
564
565    /// Whether the object-type bits name a symbolic link.
566    #[must_use]
567    pub const fn is_symlink(self) -> bool {
568        self.0 & 0o170000 == 0o120000
569    }
570
571    /// Whether the object-type bits name a gitlink (submodule commit).
572    #[must_use]
573    pub const fn is_gitlink(self) -> bool {
574        self.0 & 0o170000 == 0o160000
575    }
576}
577
578impl From<u32> for FileMode {
579    fn from(bits: u32) -> Self {
580        Self(bits)
581    }
582}
583
584impl From<FileMode> for u32 {
585    fn from(mode: FileMode) -> Self {
586        mode.0
587    }
588}
589
590impl fmt::Display for FileMode {
591    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
592        write!(f, "{:06o}", self.0)
593    }
594}
595
596impl fmt::Debug for FileMode {
597    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
598        write!(f, "FileMode({:06o})", self.0)
599    }
600}
601
602#[cfg(feature = "serde-models")]
603impl serde::Serialize for FileMode {
604    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
605    where
606        S: serde::Serializer,
607    {
608        serializer.collect_str(self)
609    }
610}
611
612/// A parsed abbreviated object id: the leading hex digits ("nibbles") of a digest.
613///
614/// Parse the user's hex once with [`ObjectIdPrefix::from_hex`], then test candidates
615/// with [`ObjectIdPrefix::matches`], which compares raw digest bytes without allocating.
616/// Sorted backends (pack indexes, key-value stores) can instead scan the inclusive id
617/// range [`ObjectIdPrefix::lowest`]..=[`ObjectIdPrefix::highest`], which holds exactly
618/// the ids that match.
619///
620/// # Examples
621///
622/// ```
623/// use grit_lib::objects::{HashAlgo, ObjectId, ObjectIdPrefix};
624///
625/// # fn main() -> grit_lib::error::Result<()> {
626/// let id = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf8d69288fbee4904")?;
627/// let prefix = ObjectIdPrefix::from_hex("4B825", HashAlgo::Sha1)?;
628/// assert_eq!(prefix.nibbles(), 5);
629/// assert!(prefix.matches(&id));
630/// assert!(!ObjectIdPrefix::from_hex("4b826", HashAlgo::Sha1)?.matches(&id));
631/// assert!(prefix.lowest() <= id && id <= prefix.highest());
632/// # Ok(())
633/// # }
634/// ```
635#[derive(Clone, Copy, PartialEq, Eq)]
636pub struct ObjectIdPrefix {
637    /// Prefix digits packed two per byte; the low nibble of an odd-length tail and
638    /// every byte past it are zero.
639    bytes: [u8; MAX_OID_LEN],
640    nibbles: u8,
641    algo: HashAlgo,
642}
643
644impl ObjectIdPrefix {
645    /// Parse `hex` (either case, possibly empty) as a prefix of `algo` object ids.
646    ///
647    /// The empty prefix matches every id.
648    ///
649    /// # Errors
650    ///
651    /// Returns [`Error::InvalidObjectId`] when `hex` contains a non-hex character or is
652    /// longer than a full `algo` id ([`HashAlgo::hex_len`]).
653    pub fn from_hex(hex: &str, algo: HashAlgo) -> Result<Self> {
654        let digits = hex.as_bytes();
655        if digits.len() > algo.hex_len() {
656            return Err(Error::InvalidObjectId(hex.to_owned()));
657        }
658        let mut bytes = [0u8; MAX_OID_LEN];
659        let mut invalid = 0u8;
660        for (i, &digit) in digits.iter().enumerate() {
661            let nibble = HEX_DECODE[usize::from(digit)];
662            invalid |= nibble;
663            let shift = if i.is_multiple_of(2) { 4 } else { 0 };
664            bytes[i / 2] |= (nibble & 0x0f) << shift;
665        }
666        if invalid & HEX_INVALID != 0 {
667            return Err(Error::InvalidObjectId(hex.to_owned()));
668        }
669        Ok(Self {
670            bytes,
671            nibbles: digits.len() as u8,
672            algo,
673        })
674    }
675
676    /// Number of hex digits in the prefix.
677    #[must_use]
678    pub fn nibbles(&self) -> usize {
679        usize::from(self.nibbles)
680    }
681
682    /// Whether the prefix has no digits (and so matches every id).
683    #[must_use]
684    pub fn is_empty(&self) -> bool {
685        self.nibbles == 0
686    }
687
688    /// Hash algorithm the prefix was parsed for.
689    #[must_use]
690    pub fn algo(&self) -> HashAlgo {
691        self.algo
692    }
693
694    /// Whether `oid` starts with this prefix.
695    ///
696    /// Compares raw digest bytes (masking the last byte for an odd digit count), so the
697    /// check never allocates. An id shorter than the prefix never matches.
698    #[must_use]
699    pub fn matches(&self, oid: &ObjectId) -> bool {
700        self.matches_digest(oid.as_bytes())
701    }
702
703    /// Like [`Self::matches`] for a raw digest slice (for example a pack index entry).
704    #[must_use]
705    pub(crate) fn matches_digest(&self, digest: &[u8]) -> bool {
706        let whole = self.nibbles() / 2;
707        let Some(head) = digest.get(..whole) else {
708            return false;
709        };
710        if head != &self.bytes[..whole] {
711            return false;
712        }
713        if self.nibbles.is_multiple_of(2) {
714            return true;
715        }
716        digest
717            .get(whole)
718            .is_some_and(|&b| b & 0xf0 == self.bytes[whole])
719    }
720
721    /// The smallest `algo` id that starts with this prefix (remaining digits all `0`).
722    #[must_use]
723    pub fn lowest(&self) -> ObjectId {
724        ObjectId {
725            bytes: self.bytes,
726            len: self.algo.len() as u8,
727        }
728    }
729
730    /// The largest `algo` id that starts with this prefix (remaining digits all `f`).
731    #[must_use]
732    pub fn highest(&self) -> ObjectId {
733        let mut bytes = self.bytes;
734        let whole = self.nibbles() / 2;
735        if !self.nibbles.is_multiple_of(2) {
736            bytes[whole] |= 0x0f;
737        }
738        let first_free = whole + usize::from(self.nibbles % 2);
739        bytes[first_free..self.algo.len()].fill(0xff);
740        ObjectId {
741            bytes,
742            len: self.algo.len() as u8,
743        }
744    }
745}
746
747impl fmt::Display for ObjectIdPrefix {
748    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
749        let hex = hex::encode(&self.bytes[..self.nibbles().div_ceil(2)]);
750        f.write_str(&hex[..self.nibbles()])
751    }
752}
753
754impl fmt::Debug for ObjectIdPrefix {
755    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
756        write!(f, "ObjectIdPrefix({self})")
757    }
758}
759
760/// The four Git object types.
761#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
762pub enum ObjectKind {
763    /// A raw file snapshot.
764    Blob,
765    /// A directory listing.
766    Tree,
767    /// A snapshot with metadata and parentage.
768    Commit,
769    /// An annotated tag.
770    Tag,
771}
772
773impl ObjectKind {
774    /// Parse from the ASCII keyword used in Git object headers.
775    ///
776    /// # Errors
777    ///
778    /// Returns [`Error::UnknownObjectType`] for unrecognised strings.
779    pub fn from_bytes(b: &[u8]) -> Result<Self> {
780        match b {
781            b"blob" => Ok(Self::Blob),
782            b"tree" => Ok(Self::Tree),
783            b"commit" => Ok(Self::Commit),
784            b"tag" => Ok(Self::Tag),
785            other => Err(Error::UnknownObjectType(
786                String::from_utf8_lossy(other).into_owned(),
787            )),
788        }
789    }
790
791    /// Parse the `type` field on an annotated tag object (Git `type_from_string_gently` rules).
792    ///
793    /// The tag header line is `type <typename>\n` where `typename` must match a known object type
794    /// keyword **exactly** (no extra characters, no strict prefix of a longer keyword).
795    #[must_use]
796    pub fn from_tag_type_field(line: &[u8]) -> Option<Self> {
797        fn keyword_matches(canonical: &[u8], field: &[u8]) -> bool {
798            if field.is_empty() {
799                return false;
800            }
801            for (i, &bc) in field.iter().enumerate() {
802                let sc = canonical.get(i).copied().unwrap_or(0);
803                if sc != bc {
804                    return false;
805                }
806            }
807            canonical.get(field.len()).copied().unwrap_or(0) == 0
808        }
809
810        const NAMES: &[(ObjectKind, &[u8])] = &[
811            (ObjectKind::Blob, b"blob"),
812            (ObjectKind::Tree, b"tree"),
813            (ObjectKind::Commit, b"commit"),
814            (ObjectKind::Tag, b"tag"),
815        ];
816        for &(kind, name) in NAMES {
817            if keyword_matches(name, line) {
818                return Some(kind);
819            }
820        }
821        None
822    }
823
824    /// The ASCII keyword for this kind (used in object headers).
825    #[must_use]
826    pub fn as_str(&self) -> &'static str {
827        match self {
828            Self::Blob => "blob",
829            Self::Tree => "tree",
830            Self::Commit => "commit",
831            Self::Tag => "tag",
832        }
833    }
834}
835
836impl fmt::Display for ObjectKind {
837    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
838        f.write_str(self.as_str())
839    }
840}
841
842impl FromStr for ObjectKind {
843    type Err = Error;
844
845    fn from_str(s: &str) -> Result<Self> {
846        Self::from_bytes(s.as_bytes())
847    }
848}
849
850/// Kind and uncompressed size of a Git object without loading its payload.
851#[derive(Debug, Clone, Copy, PartialEq, Eq)]
852pub struct ObjectInfo {
853    /// Object type after resolving any pack delta chain.
854    pub kind: ObjectKind,
855    /// Uncompressed byte length of the object body (after the header NUL).
856    pub size: u64,
857}
858
859/// A decompressed, header-stripped Git object.
860#[derive(Debug, Clone)]
861pub struct Object {
862    /// The type of this object.
863    pub kind: ObjectKind,
864    /// Raw byte content (everything after the NUL in the header).
865    pub data: Vec<u8>,
866}
867
868/// Borrowed view of an object's kind and uncompressed payload.
869///
870/// Produced by [`crate::odb::Odb::read_into`](crate::odb::Odb::read_into) and
871/// [`crate::odb::store::ObjectStore::read_into`](crate::odb::store::ObjectStore::read_into);
872/// the payload bytes live in the caller's buffer.
873#[derive(Debug, Clone, Copy, PartialEq, Eq)]
874pub struct ObjectRef<'a> {
875    /// Object type after resolving any pack delta chain.
876    pub kind: ObjectKind,
877    /// Uncompressed payload (header stripped).
878    pub data: &'a [u8],
879}
880
881impl<'a> ObjectRef<'a> {
882    /// Borrow `buf` as an object payload with the given `kind`.
883    #[must_use]
884    pub fn from_buf(kind: ObjectKind, buf: &'a [u8]) -> Self {
885        Self { kind, data: buf }
886    }
887}
888
889impl Object {
890    /// Construct a new object from its kind and raw data.
891    #[must_use]
892    pub fn new(kind: ObjectKind, data: Vec<u8>) -> Self {
893        Self { kind, data }
894    }
895
896    /// Borrowed view of this object's kind and payload.
897    #[must_use]
898    pub fn as_ref(&self) -> ObjectRef<'_> {
899        ObjectRef {
900            kind: self.kind,
901            data: self.data.as_slice(),
902        }
903    }
904}
905
906/// A single entry in a Git tree object.
907#[derive(Debug, Clone, PartialEq, Eq)]
908pub struct TreeEntry {
909    /// Unix file mode (e.g. `0o100644` for a regular file, `0o040000` for a tree).
910    pub mode: u32,
911    /// Entry name (file or directory name only, no path separators).
912    pub name: Vec<u8>,
913    /// The object ID of the blob or sub-tree.
914    pub oid: ObjectId,
915}
916
917impl TreeEntry {
918    /// Format the mode as Git does: no leading zero, minimal digits.
919    ///
920    /// Git uses `"40000"` for trees (not `"040000"`), and `"100644"` for blobs.
921    #[must_use]
922    pub fn mode_str(&self) -> String {
923        // Git omits the leading zero for tree mode
924        if self.mode == 0o040000 {
925            "40000".to_owned()
926        } else {
927            format!("{:o}", self.mode)
928        }
929    }
930}
931
932/// A single tree entry borrowing name bytes from the tree object payload.
933#[derive(Debug, Clone, Copy, PartialEq, Eq)]
934pub struct TreeEntryRef<'a> {
935    /// Unix file mode (e.g. `0o100644` for a regular file, `0o040000` for a tree).
936    pub mode: u32,
937    /// Entry name (file or directory name only, no path separators).
938    pub name: &'a [u8],
939    /// The object ID of the blob or sub-tree.
940    pub oid: ObjectId,
941}
942
943impl TreeEntryRef<'_> {
944    /// Allocate an owning [`TreeEntry`] with the same mode, name, and OID.
945    #[must_use]
946    pub fn to_owned(&self) -> TreeEntry {
947        TreeEntry {
948            mode: self.mode,
949            name: self.name.to_vec(),
950            oid: self.oid,
951        }
952    }
953}
954
955/// Iterator over tree entries borrowing from the tree object's raw payload.
956///
957/// Construct with [`Self::with_oid_len`] when the hash width is known, or
958/// [`Self::infer_oid_len`] to detect SHA-1 vs SHA-256 like [`parse_tree`].
959#[derive(Debug, Clone)]
960pub struct TreeRefIter<'a> {
961    data: &'a [u8],
962    pos: usize,
963    oid_len: usize,
964    /// Set after the first parse error so callers do not see endless `Err` items.
965    failed: bool,
966}
967
968impl<'a> TreeRefIter<'a> {
969    /// Iterate `data` assuming each entry ends with an OID of `oid_len` bytes.
970    #[must_use]
971    pub fn with_oid_len(data: &'a [u8], oid_len: usize) -> Self {
972        Self {
973            data,
974            pos: 0,
975            oid_len,
976            failed: false,
977        }
978    }
979
980    /// Detect OID width and return an iterator (SHA-1 first, then SHA-256).
981    ///
982    /// # Errors
983    ///
984    /// Returns [`Error::CorruptObject`] when the blob does not parse as a tree.
985    pub fn infer_oid_len(data: &'a [u8]) -> Result<Self> {
986        match try_parse_tree_oid_len(data, HashAlgo::Sha1.len()) {
987            Ok(()) => Ok(Self::with_oid_len(data, HashAlgo::Sha1.len())),
988            Err(sha1_err) => match try_parse_tree_oid_len(data, HashAlgo::Sha256.len()) {
989                Ok(()) => Ok(Self::with_oid_len(data, HashAlgo::Sha256.len())),
990                Err(_) => Err(sha1_err),
991            },
992        }
993    }
994
995    fn next_entry(&mut self) -> Result<Option<TreeEntryRef<'a>>> {
996        parse_tree_entry_at(self.data, &mut self.pos, self.oid_len)
997    }
998}
999
1000/// Parse one tree entry and advance `pos`. Returns `Ok(None)` when `pos` is at end of `data`.
1001fn parse_tree_entry_at<'a>(
1002    data: &'a [u8],
1003    pos: &mut usize,
1004    oid_len: usize,
1005) -> Result<Option<TreeEntryRef<'a>>> {
1006    if *pos >= data.len() {
1007        return Ok(None);
1008    }
1009    let sp = data[*pos..]
1010        .iter()
1011        .position(|&b| b == b' ')
1012        .ok_or_else(|| Error::CorruptObject("tree entry missing space".to_owned()))?;
1013    let mode_bytes = &data[*pos..*pos + sp];
1014    let mode = std::str::from_utf8(mode_bytes)
1015        .ok()
1016        .and_then(|s| u32::from_str_radix(s, 8).ok())
1017        .ok_or_else(|| {
1018            Error::CorruptObject(format!(
1019                "invalid tree mode: {}",
1020                String::from_utf8_lossy(mode_bytes)
1021            ))
1022        })?;
1023    *pos += sp + 1;
1024
1025    let nul = data[*pos..]
1026        .iter()
1027        .position(|&b| b == 0)
1028        .ok_or_else(|| Error::CorruptObject("tree entry missing NUL".to_owned()))?;
1029    let name = &data[*pos..*pos + nul];
1030    *pos += nul + 1;
1031
1032    if *pos + oid_len > data.len() {
1033        return Err(Error::CorruptObject("tree entry truncated SHA".to_owned()));
1034    }
1035    let oid = ObjectId::from_bytes(&data[*pos..*pos + oid_len])?;
1036    *pos += oid_len;
1037
1038    Ok(Some(TreeEntryRef { mode, name, oid }))
1039}
1040
1041impl<'a> Iterator for TreeRefIter<'a> {
1042    type Item = Result<TreeEntryRef<'a>>;
1043
1044    fn next(&mut self) -> Option<Self::Item> {
1045        if self.failed || self.pos >= self.data.len() {
1046            return None;
1047        }
1048        match self.next_entry() {
1049            Ok(Some(entry)) => Some(Ok(entry)),
1050            Ok(None) => None,
1051            Err(err) => {
1052                self.failed = true;
1053                self.pos = self.data.len();
1054                Some(Err(err))
1055            }
1056        }
1057    }
1058}
1059
1060/// Walk `data` once to verify it parses as a tree with the given OID width (no entry allocation).
1061fn try_parse_tree_oid_len(data: &[u8], oid_len: usize) -> Result<()> {
1062    let mut pos = 0usize;
1063    while parse_tree_entry_at(data, &mut pos, oid_len)?.is_some() {}
1064    Ok(())
1065}
1066
1067fn collect_tree_with_oid_len(data: &[u8], oid_len: usize) -> Result<Vec<TreeEntry>> {
1068    let mut pos = 0usize;
1069    let mut entries = Vec::with_capacity(data.len() / 32);
1070    while let Some(entry) = parse_tree_entry_at(data, &mut pos, oid_len)? {
1071        entries.push(TreeEntry {
1072            mode: entry.mode,
1073            name: entry.name.to_vec(),
1074            oid: entry.oid,
1075        });
1076    }
1077    Ok(entries)
1078}
1079
1080/// Parse the raw data of a tree object into its entries.
1081///
1082/// # Format
1083///
1084/// Each entry is `"<mode> <name>\0<20-byte-sha1>"` concatenated with no
1085/// separator between entries.
1086///
1087/// # Errors
1088///
1089/// Returns [`Error::CorruptObject`] if the data is malformed.
1090pub fn parse_tree(data: &[u8]) -> Result<Vec<TreeEntry>> {
1091    match collect_tree_with_oid_len(data, HashAlgo::Sha1.len()) {
1092        Ok(entries) => Ok(entries),
1093        Err(sha1_err) => {
1094            collect_tree_with_oid_len(data, HashAlgo::Sha256.len()).map_err(|_| sha1_err)
1095        }
1096    }
1097}
1098
1099/// Parse a tree blob assuming a fixed raw OID width (`oid_len` bytes).
1100///
1101/// Returns an error if the data does not parse cleanly into whole entries of
1102/// that width (used by [`parse_tree`] to detect the hash algorithm).
1103pub fn parse_tree_with_oid_len(data: &[u8], oid_len: usize) -> Result<Vec<TreeEntry>> {
1104    collect_tree_with_oid_len(data, oid_len)
1105}
1106
1107/// Build the raw bytes of a tree object from a slice of entries.
1108///
1109/// Entries **must** already be sorted in Git tree order (see [`tree_entry_cmp`]),
1110/// with unique, valid names and modes; nothing here checks that. To write a tree
1111/// from entries in any order with validation, use
1112/// [`Objects::write_tree`](crate::objects::Objects::write_tree).
1113#[must_use]
1114pub fn serialize_tree(entries: &[TreeEntry]) -> Vec<u8> {
1115    let len = entries
1116        .iter()
1117        .map(|e| 8 + e.name.len() + e.oid.as_bytes().len())
1118        .sum();
1119    let mut out = Vec::with_capacity(len);
1120    for e in entries {
1121        push_tree_mode(&mut out, e.mode);
1122        out.push(b' ');
1123        out.extend_from_slice(&e.name);
1124        out.push(0);
1125        out.extend_from_slice(e.oid.as_bytes());
1126    }
1127    out
1128}
1129
1130/// Append `mode` in octal without leading zeros, as [`TreeEntry::mode_str`] formats it.
1131fn push_tree_mode(out: &mut Vec<u8>, mode: u32) {
1132    let mut digits = [0u8; 11];
1133    let mut start = digits.len();
1134    let mut rest = mode;
1135    loop {
1136        start -= 1;
1137        digits[start] = b'0' + (rest & 7) as u8;
1138        rest >>= 3;
1139        if rest == 0 {
1140            break;
1141        }
1142    }
1143    out.extend_from_slice(&digits[start..]);
1144}
1145
1146/// Git's tree-entry sort comparator.
1147///
1148/// Trees are sorted byte-by-byte by `"<name>"` for blobs and `"<name>/"` for
1149/// sub-trees, so a directory `foo` sorts after a file `foo-bar` but before
1150/// `fooz`.  This matches `base_name_compare` in `tree.c`.
1151///
1152/// # Parameters
1153///
1154/// - `a_name`: name bytes of the first entry
1155/// - `a_is_tree`: whether the first entry is a sub-tree (`mode == 0o040000`)
1156/// - `b_name`: name bytes of the second entry
1157/// - `b_is_tree`: whether the second entry is a sub-tree
1158#[must_use]
1159pub fn tree_entry_cmp(
1160    a_name: &[u8],
1161    a_is_tree: bool,
1162    b_name: &[u8],
1163    b_is_tree: bool,
1164) -> std::cmp::Ordering {
1165    let a_trailer = if a_is_tree { b'/' } else { 0u8 };
1166    let b_trailer = if b_is_tree { b'/' } else { 0u8 };
1167
1168    let min_len = a_name.len().min(b_name.len());
1169    let cmp = a_name[..min_len].cmp(&b_name[..min_len]);
1170    if cmp != std::cmp::Ordering::Equal {
1171        return cmp;
1172    }
1173    // Names share a prefix; compare the next character (or trailer).
1174    let ac = a_name.get(min_len).copied().unwrap_or(a_trailer);
1175    let bc = b_name.get(min_len).copied().unwrap_or(b_trailer);
1176    ac.cmp(&bc)
1177}
1178
1179/// Parse an octal tree-entry mode without going through `str`.
1180fn parse_tree_mode_octal(bytes: &[u8]) -> Option<u32> {
1181    if bytes.is_empty() || bytes.len() > 7 {
1182        return None;
1183    }
1184    bytes.iter().try_fold(0u32, |acc, &b| match b {
1185        b'0'..=b'7' => Some(acc * 8 + u32::from(b - b'0')),
1186        _ => None,
1187    })
1188}
1189
1190/// Find one tree entry by name without building a full [`TreeEntry`] vector.
1191///
1192/// Scans entries in stored order and returns the first whose name equals `name`
1193/// (file or directory). Because Git sorts entries by `name` (with a `/` trailer for
1194/// directories), the scan stops at the first entry that sorts after `name/`, so only
1195/// the prefix of the tree up to the match is decoded; corruption after that point is
1196/// not reported.
1197///
1198/// # Errors
1199///
1200/// [`Error::CorruptObject`] when an entry scanned before the stop point is malformed.
1201pub(crate) fn find_tree_entry_by_name_in_data(
1202    data: &[u8],
1203    oid_len: usize,
1204    name: &[u8],
1205    name_is_tree: bool,
1206) -> Result<Option<TreeEntry>> {
1207    let mut pos = 0usize;
1208    while pos < data.len() {
1209        let rest = &data[pos..];
1210        let sp = memchr::memchr(b' ', rest)
1211            .ok_or_else(|| Error::CorruptObject("tree entry missing space".to_owned()))?;
1212        let mode = parse_tree_mode_octal(&rest[..sp])
1213            .ok_or_else(|| Error::CorruptObject("invalid tree mode".to_owned()))?;
1214        let name_start = sp + 1;
1215        let nul = memchr::memchr(0, &rest[name_start..])
1216            .ok_or_else(|| Error::CorruptObject("tree entry missing NUL".to_owned()))?;
1217        let entry_name = &rest[name_start..name_start + nul];
1218        let oid_start = name_start + nul + 1;
1219        let oid_end = oid_start + oid_len;
1220        if oid_end > rest.len() {
1221            return Err(Error::CorruptObject("tree entry truncated SHA".to_owned()));
1222        }
1223        if entry_name == name {
1224            return Ok(Some(TreeEntry {
1225                mode,
1226                name: entry_name.to_vec(),
1227                oid: ObjectId::from_bytes(&rest[oid_start..oid_end])?,
1228            }));
1229        }
1230        if tree_entry_cmp(entry_name, mode == 0o040000, name, name_is_tree)
1231            == std::cmp::Ordering::Greater
1232        {
1233            return Ok(None);
1234        }
1235        pos += oid_end;
1236    }
1237    Ok(None)
1238}
1239
1240/// Like [`find_tree_entry_by_name_in_data`], trying directory ordering first then file.
1241pub(crate) fn find_tree_entry_by_name_dir_or_file_in_data(
1242    data: &[u8],
1243    oid_len: usize,
1244    name: &[u8],
1245) -> Result<Option<TreeEntry>> {
1246    if let Some(entry) = find_tree_entry_by_name_in_data(data, oid_len, name, true)? {
1247        if entry.mode == 0o040000 {
1248            return Ok(Some(entry));
1249        }
1250    }
1251    find_tree_entry_by_name_in_data(data, oid_len, name, false)
1252}
1253
1254/// Parsed representation of a commit object.
1255///
1256/// This is the read-side shape (it round-trips stored bytes exactly). To create a
1257/// new commit, build a [`NewCommit`](crate::objects::NewCommit) and pass it to
1258/// [`Objects::write_commit`](crate::objects::Objects::write_commit).
1259#[derive(Debug, Clone, PartialEq, Eq)]
1260pub struct CommitData {
1261    /// The tree this commit points to.
1262    pub tree: ObjectId,
1263    /// Parent commit IDs (zero or more).
1264    pub parents: Vec<ObjectId>,
1265    /// Author field decoded to Unicode (using `encoding` when present, else UTF-8).
1266    pub author: String,
1267    /// Committer field decoded to Unicode.
1268    pub committer: String,
1269    /// Exact `author` header payload bytes as stored in the object (after `author `).
1270    ///
1271    /// Empty means treat [`Self::author`] as UTF-8 when serializing (new commits).
1272    pub author_raw: Vec<u8>,
1273    /// Exact `committer` header payload bytes as stored in the object.
1274    pub committer_raw: Vec<u8>,
1275    /// Optional encoding override (e.g. `"UTF-8"`).
1276    pub encoding: Option<String>,
1277    /// Commit message (everything after the blank line).
1278    pub message: String,
1279    /// Optional raw message bytes for non-UTF-8 commit messages.
1280    /// When set, `serialize_commit` uses these bytes instead of `message`.
1281    #[doc = "Optional raw message bytes for non-UTF-8 messages."]
1282    pub raw_message: Option<Vec<u8>>,
1283    /// Raw bytes for headers not represented in the fields above (`gpgsig`, `mergetag`, …),
1284    /// including each line's trailing `\n` and any continuation lines (leading space).
1285    ///
1286    /// [`serialize_commit`] always emits `tree`, `parent`, `author`, `committer`, and `encoding`
1287    /// from the structured fields, then appends these bytes before the blank line and message.
1288    pub extra_headers: Vec<u8>,
1289}
1290
1291/// First paragraph of a commit message, folded like Git `%(contents:subject)`.
1292///
1293/// Non-blank lines in the first paragraph are joined with a single space. Each line keeps
1294/// its leading whitespace and internal spacing; only trailing whitespace on each line is
1295/// removed before joining. The paragraph ends at the first blank line. An empty message
1296/// yields an empty string.
1297#[must_use]
1298pub fn commit_subject_pretty(message: &str) -> String {
1299    let mut parts = Vec::new();
1300    for line in message.lines() {
1301        if line.trim().is_empty() {
1302            break;
1303        }
1304        parts.push(line.trim_end());
1305    }
1306    parts.join(" ")
1307}
1308
1309/// Subject from commit object bytes (see [`commit_subject_pretty`]).
1310///
1311/// # Errors
1312///
1313/// Returns [`Error::CorruptObject`] when the commit headers are malformed.
1314pub fn read_commit_subject_pretty(data: &[u8]) -> Result<String> {
1315    let message_start = data.windows(2).position(|w| w == b"\n\n").map(|p| p + 2);
1316    let Some(message_start) = message_start else {
1317        return Ok(String::new());
1318    };
1319    let body = std::str::from_utf8(data.get(message_start..).unwrap_or_default()).unwrap_or("");
1320    Ok(commit_subject_pretty(body))
1321}
1322
1323/// First line of the commit message (subject), honoring the `encoding` header.
1324///
1325/// Scans headers only; does not require a full [`parse_commit`] parse.
1326///
1327/// # Errors
1328///
1329/// Returns [`Error::CorruptObject`] when commit headers are malformed.
1330pub fn read_commit_subject_line(data: &[u8]) -> Result<String> {
1331    let mut pos = 0usize;
1332    let mut encoding: Option<String> = None;
1333    while pos < data.len() {
1334        let line_start = pos;
1335        let mut line_end = pos;
1336        while line_end < data.len() && data[line_end] != b'\n' {
1337            line_end += 1;
1338        }
1339        let line = &data[line_start..line_end];
1340        let after_nl = line_end.saturating_add(1);
1341        if line.is_empty() {
1342            let body = data.get(after_nl..).unwrap_or_default();
1343            let message = commit_encoding::decode_bytes(encoding.as_deref(), body);
1344            return Ok(message
1345                .lines()
1346                .find(|l| !l.trim().is_empty())
1347                .map(str::trim)
1348                .unwrap_or("(no subject)")
1349                .to_owned());
1350        }
1351        if line.first() == Some(&b' ') {
1352            pos = after_nl;
1353            continue;
1354        }
1355        if let Some(rest) = line.strip_prefix(b"encoding ") {
1356            encoding = Some(
1357                std::str::from_utf8(rest)
1358                    .map_err(|_| Error::CorruptObject("encoding is not UTF-8".to_owned()))?
1359                    .trim()
1360                    .to_owned(),
1361            );
1362        }
1363        pos = after_nl;
1364    }
1365    Ok("(no subject)".to_owned())
1366}
1367
1368pub(crate) fn parse_oid_field(value: &[u8]) -> Result<ObjectId> {
1369    let s = std::str::from_utf8(value)
1370        .map_err(|_| Error::CorruptObject("commit header object id is not UTF-8".to_owned()))?;
1371    s.trim()
1372        .parse::<ObjectId>()
1373        .map_err(|_| Error::CorruptObject("invalid object id in commit header".to_owned()))
1374}
1375
1376/// Parse `tree` and `parent` headers from a possibly truncated leading slice of a commit.
1377///
1378/// `prefix` may be cut anywhere (for example a fixed-size inflate of the object start).
1379/// Returns `Some` only when the slice provably holds the complete parent list: the `tree`
1380/// line, every `parent` line, and the start of a following line that cannot be another
1381/// `parent` header. Returns `None` when the slice ends inside that region or the headers are
1382/// not in canonical form; parse the whole object with
1383/// [`parse_commit_tree_and_parents_lenient`] instead, which then yields the same result
1384/// whenever this returns `Some`.
1385pub(crate) fn parse_commit_tree_and_parents_prefix(
1386    prefix: &[u8],
1387    hex_len: usize,
1388) -> Option<(ObjectId, Vec<ObjectId>)> {
1389    canonical_commit_tree_and_parents(prefix, hex_len, false)
1390}
1391
1392/// Parse `tree` and `parent` headers of a whole commit object the way Git's commit parser
1393/// does: a leading `tree` line followed by any number of `parent` lines.
1394///
1395/// Nothing after the parent list is inspected, so a commit missing `author` or `committer`
1396/// (or with other malformed headers) still yields its tree and parents. Returns `None` when
1397/// the first line is not a valid `tree` header or a `parent` line is malformed.
1398pub(crate) fn parse_commit_tree_and_parents_lenient(
1399    data: &[u8],
1400    hex_len: usize,
1401) -> Option<(ObjectId, Vec<ObjectId>)> {
1402    canonical_commit_tree_and_parents(data, hex_len, true)
1403}
1404
1405fn canonical_commit_tree_and_parents(
1406    data: &[u8],
1407    hex_len: usize,
1408    complete: bool,
1409) -> Option<(ObjectId, Vec<ObjectId>)> {
1410    fn header_oid<'a>(rest: &'a [u8], key: &[u8], hex_len: usize) -> Option<(ObjectId, &'a [u8])> {
1411        let line = rest.strip_prefix(key)?;
1412        if line.len() <= hex_len || line[hex_len] != b'\n' {
1413            return None;
1414        }
1415        let oid = ObjectId::from_hex(std::str::from_utf8(&line[..hex_len]).ok()?).ok()?;
1416        Some((oid, &line[hex_len + 1..]))
1417    }
1418    const PARENT: &[u8] = b"parent ";
1419    let (tree, mut rest) = header_oid(data, b"tree ", hex_len)?;
1420    let mut parents = Vec::new();
1421    loop {
1422        if !complete && rest.len() < PARENT.len() && PARENT.starts_with(rest) {
1423            return None;
1424        }
1425        if !rest.starts_with(PARENT) {
1426            return Some((tree, parents));
1427        }
1428        let (parent, next) = header_oid(rest, PARENT, hex_len)?;
1429        parents.push(parent);
1430        rest = next;
1431    }
1432}
1433
1434/// Parse the raw data of a commit object.
1435///
1436/// # Errors
1437///
1438/// Returns [`Error::CorruptObject`] if required headers are missing.
1439pub fn parse_commit(data: &[u8]) -> Result<CommitData> {
1440    Ok(super::CommitRef::parse(data)?.to_owned())
1441}
1442
1443/// Value after `prefix` on the first header line that starts with `prefix`, scanning until a blank
1444/// line (Git tag headers). Returns `None` if no such line exists before the body.
1445#[must_use]
1446pub fn tag_header_field(data: &[u8], prefix: &[u8]) -> Option<String> {
1447    let mut pos = 0usize;
1448    while pos < data.len() {
1449        let rest = &data[pos..];
1450        let nl = rest.iter().position(|&b| b == b'\n');
1451        let line = if let Some(i) = nl { &rest[..i] } else { rest };
1452        if line.is_empty() {
1453            break;
1454        }
1455        if let Some(after) = line.strip_prefix(prefix) {
1456            return Some(String::from_utf8_lossy(after).trim().to_owned());
1457        }
1458        pos += line.len().saturating_add(nl.map(|_| 1).unwrap_or(0));
1459        if nl.is_none() {
1460            break;
1461        }
1462    }
1463    None
1464}
1465
1466/// OID from the first `object <hex>` line in the tag header block, if hex parses.
1467#[must_use]
1468pub fn tag_object_line_oid(data: &[u8]) -> Option<ObjectId> {
1469    let s = tag_header_field(data, b"object ")?;
1470    s.parse().ok()
1471}
1472
1473/// Parsed representation of an annotated tag object.
1474#[derive(Debug, Clone)]
1475pub struct TagData {
1476    /// The object this tag points to.
1477    pub object: ObjectId,
1478    /// The type of the tagged object (e.g. `"commit"`).
1479    pub object_type: String,
1480    /// The short tag name (without `refs/tags/` prefix).
1481    pub tag: String,
1482    /// The tagger identity and timestamp (raw Git format).
1483    pub tagger: Option<String>,
1484    /// Extra header lines after `tagger` and before the blank line (raw bytes, LF-terminated).
1485    pub extra_headers: Vec<u8>,
1486    /// The tag message (everything after the blank line).
1487    pub message: String,
1488}
1489
1490/// Parse the raw data of a tag object.
1491///
1492/// # Errors
1493///
1494/// Returns [`Error::CorruptObject`] if required headers are missing or malformed.
1495///
1496/// When the header block ends at EOF without a blank line, the message is empty (Git accepts this).
1497pub fn parse_tag(data: &[u8]) -> Result<TagData> {
1498    use super::extra_headers::next_header_block;
1499
1500    #[derive(Clone, Copy)]
1501    enum Continuation {
1502        None,
1503        Extra,
1504    }
1505
1506    let mut pos = 0usize;
1507    let mut object = None;
1508    let mut object_type = None;
1509    let mut tag_name = None;
1510    let mut tagger = None;
1511    let mut extra_headers = Vec::new();
1512    let mut cont = Continuation::None;
1513    let mut past_tagger = false;
1514
1515    while pos < data.len() {
1516        let line_start = pos;
1517        let mut line_end = pos;
1518        while line_end < data.len() && data[line_end] != b'\n' {
1519            line_end += 1;
1520        }
1521        let line = &data[line_start..line_end];
1522        let after_nl = (line_end + 1).min(data.len());
1523        if line.is_empty() {
1524            let body = data.get(after_nl..).unwrap_or_default();
1525            let message = std::str::from_utf8(body)
1526                .map_err(|_| Error::CorruptObject("tag message is not valid UTF-8".to_owned()))?
1527                .to_owned();
1528            return Ok(TagData {
1529                object: object
1530                    .ok_or_else(|| Error::CorruptObject("tag missing object header".to_owned()))?,
1531                object_type: object_type
1532                    .ok_or_else(|| Error::CorruptObject("tag missing type header".to_owned()))?,
1533                tag: tag_name
1534                    .ok_or_else(|| Error::CorruptObject("tag missing tag header".to_owned()))?,
1535                tagger,
1536                extra_headers,
1537                message,
1538            });
1539        }
1540
1541        if past_tagger {
1542            let (_, block_end, _) = next_header_block(data, line_start);
1543            extra_headers.extend_from_slice(&data[line_start..block_end]);
1544            pos = block_end;
1545            continue;
1546        }
1547
1548        if line.first() == Some(&b' ') {
1549            match cont {
1550                Continuation::Extra => {
1551                    extra_headers.extend_from_slice(&data[line_start..after_nl]);
1552                }
1553                Continuation::None => {}
1554            }
1555            pos = after_nl;
1556            continue;
1557        }
1558
1559        let key_end = line
1560            .iter()
1561            .position(|&b| b == b' ')
1562            .ok_or_else(|| Error::CorruptObject("malformed tag header line".to_owned()))?;
1563        let key = &line[..key_end];
1564        let rest = line.get(key_end + 1..).unwrap_or_default();
1565
1566        match key {
1567            b"object" => {
1568                let line_str = std::str::from_utf8(rest).map_err(|_| {
1569                    Error::CorruptObject("tag object line is not valid UTF-8".to_owned())
1570                })?;
1571                object = Some(line_str.trim().parse::<ObjectId>()?);
1572                cont = Continuation::None;
1573            }
1574            b"type" => {
1575                let typ = std::str::from_utf8(rest).map_err(|_| {
1576                    Error::CorruptObject("tag type line is not valid UTF-8".to_owned())
1577                })?;
1578                let typ = typ.trim();
1579                if ObjectKind::from_tag_type_field(typ.as_bytes()).is_none() {
1580                    return Err(Error::CorruptObject(format!(
1581                        "invalid 'type' value in tag: {typ}"
1582                    )));
1583                }
1584                object_type = Some(typ.to_owned());
1585                cont = Continuation::None;
1586            }
1587            b"tag" => {
1588                let line_str = std::str::from_utf8(rest).map_err(|_| {
1589                    Error::CorruptObject("tag name line is not valid UTF-8".to_owned())
1590                })?;
1591                tag_name = Some(line_str.trim().to_owned());
1592                cont = Continuation::None;
1593            }
1594            b"tagger" => {
1595                tagger = Some(
1596                    std::str::from_utf8(rest)
1597                        .map_err(|_| {
1598                            Error::CorruptObject("tag tagger line is not valid UTF-8".to_owned())
1599                        })?
1600                        .to_owned(),
1601                );
1602                past_tagger = true;
1603                cont = Continuation::None;
1604            }
1605            _ => {
1606                cont = Continuation::Extra;
1607                extra_headers.extend_from_slice(&data[line_start..after_nl]);
1608            }
1609        }
1610        pos = after_nl;
1611    }
1612
1613    Ok(TagData {
1614        object: object
1615            .ok_or_else(|| Error::CorruptObject("tag missing object header".to_owned()))?,
1616        object_type: object_type
1617            .ok_or_else(|| Error::CorruptObject("tag missing type header".to_owned()))?,
1618        tag: tag_name.ok_or_else(|| Error::CorruptObject("tag missing tag header".to_owned()))?,
1619        tagger,
1620        extra_headers,
1621        message: String::new(),
1622    })
1623}
1624
1625/// Serialize a [`TagData`] into the raw bytes suitable for storage as a tag object.
1626///
1627/// The caller is responsible for supplying a correctly-formatted `tagger` string
1628/// (including timestamp and timezone) when present.
1629#[must_use]
1630pub fn serialize_tag(t: &TagData) -> Vec<u8> {
1631    let mut out = Vec::new();
1632    out.extend_from_slice(format!("object {}\n", t.object).as_bytes());
1633    out.extend_from_slice(format!("type {}\n", t.object_type).as_bytes());
1634    out.extend_from_slice(format!("tag {}\n", t.tag).as_bytes());
1635    if let Some(ref tagger) = t.tagger {
1636        out.extend_from_slice(format!("tagger {tagger}\n").as_bytes());
1637    }
1638    out.extend_from_slice(&t.extra_headers);
1639    out.push(b'\n');
1640    let msg = t.message.trim_end_matches('\n');
1641    if !msg.is_empty() {
1642        out.extend_from_slice(msg.as_bytes());
1643        out.push(b'\n');
1644    }
1645    out
1646}
1647
1648/// Serialize a [`CommitData`] into the raw bytes suitable for storage.
1649///
1650/// The caller is responsible for supplying a correctly-formatted `author` and
1651/// `committer` string (including timestamp and timezone). To create a commit from
1652/// typed identities instead, use [`Objects::write_commit`](crate::objects::Objects::write_commit)
1653/// with a [`NewCommit`](crate::objects::NewCommit).
1654///
1655/// The message body is written exactly as given: `git commit` and `git commit-tree -m`
1656/// supply a trailing LF; `git commit-tree` reading from stdin or `-F` does not add one.
1657#[must_use]
1658pub fn serialize_commit(c: &CommitData) -> Vec<u8> {
1659    let mut out = Vec::new();
1660    out.extend_from_slice(format!("tree {}\n", c.tree).as_bytes());
1661    for p in &c.parents {
1662        out.extend_from_slice(format!("parent {p}\n").as_bytes());
1663    }
1664    out.extend_from_slice(b"author ");
1665    if c.author_raw.is_empty() {
1666        out.extend_from_slice(c.author.as_bytes());
1667    } else {
1668        out.extend_from_slice(&c.author_raw);
1669    }
1670    out.push(b'\n');
1671    out.extend_from_slice(b"committer ");
1672    if c.committer_raw.is_empty() {
1673        out.extend_from_slice(c.committer.as_bytes());
1674    } else {
1675        out.extend_from_slice(&c.committer_raw);
1676    }
1677    out.push(b'\n');
1678    if let Some(enc) = &c.encoding {
1679        out.extend_from_slice(format!("encoding {enc}\n").as_bytes());
1680    }
1681    out.extend_from_slice(&c.extra_headers);
1682    out.push(b'\n');
1683    if let Some(raw) = &c.raw_message {
1684        out.extend_from_slice(raw);
1685    } else if !c.message.is_empty() {
1686        out.extend_from_slice(c.message.as_bytes());
1687    }
1688    out
1689}
1690
1691#[cfg(test)]
1692mod object_id_prefix_tests {
1693    use super::*;
1694
1695    const ID: &str = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
1696
1697    fn id() -> ObjectId {
1698        ObjectId::from_hex(ID).unwrap()
1699    }
1700
1701    fn prefix(hex: &str) -> ObjectIdPrefix {
1702        ObjectIdPrefix::from_hex(hex, HashAlgo::Sha1).unwrap()
1703    }
1704
1705    #[test]
1706    fn matches_every_length_of_the_id_and_rejects_a_changed_last_digit() {
1707        let id = id();
1708        for n in 0..=ID.len() {
1709            let p = prefix(&ID[..n]);
1710            assert_eq!(p.nibbles(), n);
1711            assert_eq!(p.is_empty(), n == 0);
1712            assert!(p.matches(&id), "{n} digits");
1713            assert_eq!(p.to_string(), ID[..n]);
1714            if n > 0 {
1715                let mut changed = ID[..n].to_owned();
1716                let last = changed.pop().unwrap();
1717                changed.push(if last == '0' { '1' } else { '0' });
1718                assert!(!prefix(&changed).matches(&id), "{changed}");
1719            }
1720        }
1721    }
1722
1723    #[test]
1724    fn odd_prefix_ignores_the_low_nibble_of_its_last_byte() {
1725        let p = prefix("4b8");
1726        assert!(p.matches(&ObjectId::from_hex("4b80000000000000000000000000000000000000").unwrap()));
1727        assert!(p.matches(&ObjectId::from_hex("4b8fffffffffffffffffffffffffffffffffffff").unwrap()));
1728        assert!(
1729            !p.matches(&ObjectId::from_hex("4b90000000000000000000000000000000000000").unwrap())
1730        );
1731    }
1732
1733    #[test]
1734    fn parsing_is_case_insensitive_and_validates() {
1735        assert_eq!(prefix("4B825DC"), prefix("4b825dc"));
1736        assert!(matches!(
1737            ObjectIdPrefix::from_hex("4b8g", HashAlgo::Sha1),
1738            Err(Error::InvalidObjectId(_))
1739        ));
1740        assert!(matches!(
1741            ObjectIdPrefix::from_hex(&format!("{ID}0"), HashAlgo::Sha1),
1742            Err(Error::InvalidObjectId(_))
1743        ));
1744        let sha256 = "a".repeat(64);
1745        assert_eq!(
1746            ObjectIdPrefix::from_hex(&sha256, HashAlgo::Sha256)
1747                .unwrap()
1748                .nibbles(),
1749            64
1750        );
1751        assert_eq!(prefix("").algo(), HashAlgo::Sha1);
1752    }
1753
1754    #[test]
1755    fn longer_prefix_never_matches_a_shorter_digest() {
1756        let long = ObjectIdPrefix::from_hex(&format!("{ID}0000"), HashAlgo::Sha256).unwrap();
1757        assert!(!long.matches(&id()));
1758        assert!(ObjectIdPrefix::from_hex(ID, HashAlgo::Sha256)
1759            .unwrap()
1760            .matches(&id()));
1761    }
1762
1763    #[test]
1764    fn bounds_are_the_first_and_last_matching_ids() {
1765        let p = prefix("4b8");
1766        assert_eq!(
1767            p.lowest().to_hex(),
1768            "4b80000000000000000000000000000000000000"
1769        );
1770        assert_eq!(
1771            p.highest().to_hex(),
1772            "4b8fffffffffffffffffffffffffffffffffffff"
1773        );
1774        let even = prefix("4b82");
1775        assert_eq!(
1776            even.lowest().to_hex(),
1777            "4b82000000000000000000000000000000000000"
1778        );
1779        assert_eq!(
1780            even.highest().to_hex(),
1781            "4b82ffffffffffffffffffffffffffffffffffff"
1782        );
1783        assert_eq!(prefix("").lowest(), ObjectId::null(HashAlgo::Sha1));
1784        assert_eq!(prefix("").highest().to_hex(), "f".repeat(40));
1785        assert_eq!(prefix(ID).lowest(), id());
1786        assert_eq!(prefix(ID).highest(), id());
1787        let sha256 = ObjectIdPrefix::from_hex("abc", HashAlgo::Sha256).unwrap();
1788        assert_eq!(sha256.highest().to_hex(), format!("abc{}", "f".repeat(61)));
1789        assert_eq!(format!("{sha256:?}"), "ObjectIdPrefix(abc)");
1790    }
1791
1792    #[test]
1793    fn common_prefix_nibbles_counts_shared_leading_digits() {
1794        let a = id();
1795        let hex = |s: &str| ObjectId::from_hex(s).unwrap();
1796        assert_eq!(a.common_prefix_nibbles(&a), 40);
1797        assert_eq!(
1798            a.common_prefix_nibbles(&hex("4b825dc642cb6eb9a060e54bf8d69288fbee4905")),
1799            39
1800        );
1801        assert_eq!(
1802            a.common_prefix_nibbles(&hex("4b815dc642cb6eb9a060e54bf8d69288fbee4904")),
1803            3
1804        );
1805        assert_eq!(
1806            a.common_prefix_nibbles(&hex("4b925dc642cb6eb9a060e54bf8d69288fbee4904")),
1807            2
1808        );
1809        assert_eq!(
1810            a.common_prefix_nibbles(&hex("5b825dc642cb6eb9a060e54bf8d69288fbee4904")),
1811            0
1812        );
1813        assert_eq!(
1814            a.common_prefix_nibbles(&hex("4c825dc642cb6eb9a060e54bf8d69288fbee4904")),
1815            1
1816        );
1817    }
1818}
1819
1820#[cfg(test)]
1821mod commit_parse_tests {
1822    use super::*;
1823
1824    #[test]
1825    fn empty_tree_constant_is_canonical_sha1_digest() {
1826        assert_eq!(
1827            ObjectId::empty_tree().to_hex(),
1828            "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
1829        );
1830    }
1831
1832    #[test]
1833    fn is_empty_tree_for_matches_hashed_empty_tree() {
1834        for algo in [HashAlgo::Sha1, HashAlgo::Sha256] {
1835            let hashed =
1836                crate::hash::hash_object(algo, ObjectKind::Tree, b"").expect("hash empty tree");
1837            assert!(hashed.is_empty_tree_for(algo), "{algo:?}");
1838            let other = match algo {
1839                HashAlgo::Sha1 => HashAlgo::Sha256,
1840                HashAlgo::Sha256 => HashAlgo::Sha1,
1841            };
1842            assert!(!hashed.is_empty_tree_for(other), "{algo:?}");
1843            let empty_blob =
1844                crate::hash::hash_object(algo, ObjectKind::Blob, b"").expect("hash empty blob");
1845            assert!(!empty_blob.is_empty_tree_for(algo), "{algo:?}");
1846        }
1847        let legacy = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf899d69f7c6948d4").expect("hex");
1848        assert!(legacy.is_empty_tree_for(HashAlgo::Sha1));
1849        assert!(!legacy.is_empty_tree_for(HashAlgo::Sha256));
1850    }
1851
1852    #[test]
1853    fn serialize_tree_mode_matches_mode_str() {
1854        for mode in [
1855            0,
1856            0o7,
1857            0o100644,
1858            0o100755,
1859            0o120000,
1860            0o040000,
1861            0o160000,
1862            u32::MAX,
1863        ] {
1864            let mut out = Vec::new();
1865            push_tree_mode(&mut out, mode);
1866            let entry = TreeEntry {
1867                mode,
1868                name: Vec::new(),
1869                oid: ObjectId::empty_tree(),
1870            };
1871            assert_eq!(out, entry.mode_str().into_bytes(), "mode {mode:o}");
1872        }
1873    }
1874
1875    #[test]
1876    fn is_empty_blob_matches_both_hash_algorithms() {
1877        for algo in [HashAlgo::Sha1, HashAlgo::Sha256] {
1878            let oid = algo.hash_object(ObjectKind::Blob, b"").unwrap();
1879            assert!(oid.is_empty_blob(), "{algo:?} empty blob {}", oid.to_hex());
1880            let other = algo.hash_object(ObjectKind::Blob, b"x").unwrap();
1881            assert!(!other.is_empty_blob());
1882        }
1883        assert!(!ObjectId::empty_tree().is_empty_blob());
1884        assert!(!ObjectId::zero().is_empty_blob());
1885    }
1886
1887    #[test]
1888    fn commit_subject_pretty_folds_first_paragraph() {
1889        assert_eq!(commit_subject_pretty("hello\nworld\n\nbody"), "hello world");
1890        assert_eq!(commit_subject_pretty(""), "");
1891        assert_eq!(
1892            commit_subject_pretty("  hello   world\tthere  \n  next\t line  \n\nbody\n"),
1893            "  hello   world\tthere   next\t line"
1894        );
1895    }
1896
1897    #[test]
1898    fn commit_ref_matches_parse_commit_headers() {
1899        let raw = concat!(
1900            "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
1901            "parent 1111111111111111111111111111111111111111\n",
1902            "author A <a@example.com> 1 +0000\n",
1903            "committer C <c@example.com> 2 +0000\n",
1904            "\n",
1905            "ignored body\n",
1906        );
1907        let full = parse_commit(raw.as_bytes()).expect("parse");
1908        let link = crate::objects::CommitRef::parse(raw.as_bytes()).expect("CommitRef");
1909        assert_eq!(link.tree(), full.tree);
1910        assert_eq!(link.parents(), full.parents.as_slice());
1911        assert_eq!(link.author(), full.author);
1912        assert_eq!(link.committer(), full.committer);
1913    }
1914
1915    #[test]
1916    fn parse_commit_skips_multiline_gpgsig_continuation() {
1917        let raw = concat!(
1918            "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
1919            "author A U Thor <author@example.com> 1 +0000\n",
1920            "committer C O Mitter <committer@example.com> 1 +0000\n",
1921            "gpgsig -----BEGIN PGP SIGNATURE-----\n",
1922            " abcdef\n",
1923            " -----END PGP SIGNATURE-----\n",
1924            "\n",
1925            "msg\n",
1926        );
1927        let c = parse_commit(raw.as_bytes()).expect("parse signed commit");
1928        assert_eq!(c.tree.to_hex(), "4b825dc642cb6eb9a060e54bf8d69288fbee4904");
1929        assert_eq!(c.message, "msg\n");
1930        assert_eq!(serialize_commit(&c), raw.as_bytes());
1931    }
1932
1933    #[test]
1934    fn hex_parsing_accepts_both_cases_and_lengths_and_rejects_the_rest() {
1935        let sha1 = "0123456789abcdefABCDEF0123456789abcdef01";
1936        let oid = ObjectId::from_hex(sha1).unwrap();
1937        assert_eq!(oid.algo(), HashAlgo::Sha1);
1938        assert_eq!(oid.to_hex(), sha1.to_ascii_lowercase());
1939        assert_eq!(ObjectId::from_hex_ascii(sha1.as_bytes()).unwrap(), oid);
1940        assert_eq!(sha1.parse::<ObjectId>().unwrap(), oid);
1941
1942        let sha256 = "f".repeat(64);
1943        let oid = sha256.parse::<ObjectId>().unwrap();
1944        assert_eq!(oid.algo(), HashAlgo::Sha256);
1945        assert_eq!(oid.as_bytes(), [0xff; 32]);
1946        assert!(ObjectId::from_hex_ascii(sha256.as_bytes()).is_err());
1947
1948        for bad in [
1949            "",
1950            &sha1[..39],
1951            &format!("{sha1}0"),
1952            "g123456789abcdef0123456789abcdef01234567",
1953            "0123456789abcdef0123456789abcdef0123456 ",
1954            "0123456789abcdef0123456789abcdef012345/7",
1955            "0123456789abcdef0123456789abcdef012345:7",
1956            "0123456789abcdef0123456789abcdef012345`7",
1957            "0123456789abcdef0123456789abcdef012345G7",
1958        ] {
1959            assert!(
1960                matches!(ObjectId::from_hex(bad), Err(Error::InvalidObjectId(s)) if s == bad),
1961                "{bad:?}"
1962            );
1963            assert!(bad.parse::<ObjectId>().is_err(), "{bad:?}");
1964            assert!(ObjectId::from_hex_ascii(bad.as_bytes()).is_err(), "{bad:?}");
1965        }
1966        let mut non_ascii = sha1.as_bytes().to_vec();
1967        non_ascii[3] = 0xb0;
1968        assert!(ObjectId::from_hex_ascii(&non_ascii).is_err());
1969    }
1970
1971    #[test]
1972    fn hex_and_display_cover_every_byte_for_both_lengths() {
1973        let all: Vec<u8> = (0..=255).collect();
1974        for width in [20, 32] {
1975            for chunk in all.chunks(width).filter(|c| c.len() == width) {
1976                let oid = ObjectId::from_bytes(chunk).unwrap();
1977                let expected: String = chunk.iter().map(|b| format!("{b:02x}")).collect();
1978                assert_eq!(oid.to_hex(), expected);
1979                assert_eq!(oid.to_string(), expected);
1980                assert_eq!(format!("{oid:?}"), format!("ObjectId({expected})"));
1981                assert_eq!(ObjectId::from_hex(&expected).unwrap(), oid);
1982            }
1983        }
1984    }
1985
1986    #[test]
1987    fn serialize_commit_applies_public_field_edits_after_parse() {
1988        let raw = concat!(
1989            "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
1990            "author A <a@example.com> 1 +0000\n",
1991            "committer C <c@example.com> 1 +0000\n",
1992            "\n",
1993            "msg\n",
1994        );
1995        let mut commit = parse_commit(raw.as_bytes()).expect("parse");
1996        let zero = ObjectId::from_hex("0000000000000000000000000000000000000000").unwrap();
1997        commit.tree = zero;
1998        let serialized = serialize_commit(&commit);
1999        let tree_line = format!("tree {}\n", zero.to_hex());
2000        assert!(
2001            serialized.starts_with(tree_line.as_bytes()),
2002            "serialize_commit ignored the updated public tree field"
2003        );
2004    }
2005
2006    #[test]
2007    fn parse_commit_preserves_gpgsig_mergetag_bytes_exactly() {
2008        let raw = concat!(
2009            "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
2010            "author A <a@example.com> 1 +0000\n",
2011            "committer C <c@example.com> 1 +0000\n",
2012            "encoding ISO-8859-1\n",
2013            "gpgsig -----BEGIN PGP SIGNATURE-----\n",
2014            " line2\n",
2015            " -----END PGP SIGNATURE-----\n",
2016            "mergetag object 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
2017            " type commit\n",
2018            " tag merge-tag\n",
2019            " tagger T <t@example.com> 1 +0000\n",
2020            " -----BEGIN PGP SIGNATURE-----\n",
2021            " inner\n",
2022            " -----END PGP SIGNATURE-----\n",
2023            "\n",
2024            "msg-only\n",
2025        );
2026        let parsed = parse_commit(raw.as_bytes()).expect("parse");
2027        assert_eq!(serialize_commit(&parsed), raw.as_bytes());
2028    }
2029
2030    #[test]
2031    fn tree_entry_mode_str_and_tag_object_line_oid() {
2032        let oid = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap();
2033        let entry = TreeEntry {
2034            mode: 0o040000,
2035            name: b"dir".to_vec(),
2036            oid,
2037        };
2038        assert_eq!(entry.mode_str(), "40000");
2039        let tag_body = format!("object {}\ntype commit\ntag t\n\n", oid.to_hex());
2040        assert_eq!(tag_object_line_oid(tag_body.as_bytes()), Some(oid));
2041    }
2042
2043    #[test]
2044    fn parse_commit_non_commit_bytes_does_not_panic() {
2045        let tree_bytes = b"100644 blob 0123456789012345678901234567890123456789\tf";
2046        let err = parse_commit(tree_bytes).expect_err("tree bytes are not a commit");
2047        assert!(matches!(err, Error::CorruptObject(_)));
2048    }
2049
2050    #[test]
2051    fn parse_tree_with_sha256_oid_width() {
2052        let oid_bytes = [0x6eu8; 32];
2053        let oid = ObjectId::from_bytes(&oid_bytes).expect("oid");
2054        let mut raw = b"100644 file\0".to_vec();
2055        raw.extend_from_slice(oid.as_bytes());
2056        let entries = parse_tree_with_oid_len(&raw, HashAlgo::Sha256.len()).expect("tree");
2057        assert_eq!(entries.len(), 1);
2058        assert_eq!(entries[0].oid, oid);
2059    }
2060
2061    #[test]
2062    fn parse_commit_tree_and_parents_prefix_matches_full_parse_or_declines() -> Result<()> {
2063        let tree = "1111111111111111111111111111111111111111";
2064        let p1 = "2222222222222222222222222222222222222222";
2065        let p2 = "3333333333333333333333333333333333333333";
2066        let raw = format!(
2067            "tree {tree}\nparent {p1}\nparent {p2}\nauthor A <a@x> 1 +0000\n\
2068             committer C <c@x> 2 +0000\n\nmessage\n"
2069        );
2070        let commit = parse_commit(raw.as_bytes())?;
2071        let full = (commit.tree, commit.parents);
2072        assert_eq!(full.1.len(), 2);
2073        assert_eq!(
2074            parse_commit_tree_and_parents_lenient(raw.as_bytes(), HashAlgo::Sha1.hex_len()),
2075            Some(full.clone())
2076        );
2077        let hex_len = HashAlgo::Sha1.hex_len();
2078        let header_end = raw.find("author").expect("author line");
2079        for cut in 0..=raw.len() {
2080            let got = parse_commit_tree_and_parents_prefix(&raw.as_bytes()[..cut], hex_len);
2081            if cut > header_end {
2082                assert_eq!(got.as_ref(), Some(&full), "cut at {cut}");
2083            } else {
2084                assert_eq!(
2085                    got, None,
2086                    "cut at {cut} must not claim a complete parent list"
2087                );
2088            }
2089        }
2090        Ok(())
2091    }
2092
2093    #[test]
2094    fn commit_tree_and_parents_prefix_never_disagrees_with_whole_object() {
2095        let tree = "tree 1111111111111111111111111111111111111111\n";
2096        let parent = "parent 2222222222222222222222222222222222222222\n";
2097        let parent2 = "parent 3333333333333333333333333333333333333333\n";
2098        let bodies = [
2099            format!("{tree}{parent}author A <a@x> 1 +0000\n\nmessage\n"),
2100            format!("{tree}{parent}{parent2}committer C <c@x> 2 +0000\n\nno author\n"),
2101            format!("{tree}{parent}"),
2102            format!("{tree}{parent}{parent2}"),
2103            format!("{tree}author A <a@x> 1 +0000\n{parent}\nlate parent is ignored\n"),
2104            format!("{tree}{parent}not a header\n"),
2105            format!("{tree}parent zz\n\n"),
2106            format!("{tree}{parent}parent 2222"),
2107            format!("author A <a@x> 1 +0000\n{tree}\n"),
2108        ];
2109        let hex_len = HashAlgo::Sha1.hex_len();
2110        for body in &bodies {
2111            let whole = parse_commit_tree_and_parents_lenient(body.as_bytes(), hex_len);
2112            for cut in 0..=body.len() {
2113                if let Some(got) =
2114                    parse_commit_tree_and_parents_prefix(&body.as_bytes()[..cut], hex_len)
2115                {
2116                    assert_eq!(Some(got), whole, "cut {cut} of {body:?}");
2117                }
2118            }
2119        }
2120        let parents_of = |body: &str| {
2121            parse_commit_tree_and_parents_lenient(body.as_bytes(), hex_len).map(|(_, p)| p.len())
2122        };
2123        assert_eq!(parents_of(&bodies[1]), Some(2), "missing author is fine");
2124        assert_eq!(
2125            parents_of(&bodies[3]),
2126            Some(2),
2127            "object may end after parents"
2128        );
2129        assert_eq!(
2130            parents_of(&bodies[4]),
2131            Some(0),
2132            "only leading parents count"
2133        );
2134        assert_eq!(parents_of(&bodies[6]), None, "malformed parent line");
2135        assert_eq!(parents_of(&bodies[7]), None, "truncated parent line");
2136        assert_eq!(parents_of(&bodies[8]), None, "tree must come first");
2137    }
2138
2139    #[test]
2140    fn parse_commit_tree_and_parents_prefix_handles_root_and_sha256() -> Result<()> {
2141        let root = b"tree 1111111111111111111111111111111111111111\nauthor A <a@x> 1 +0000\n";
2142        let (tree, parents) =
2143            parse_commit_tree_and_parents_prefix(root, HashAlgo::Sha1.hex_len()).expect("root");
2144        assert_eq!(tree.to_hex(), "1111111111111111111111111111111111111111");
2145        assert!(parents.is_empty());
2146
2147        let t = "a".repeat(64);
2148        let p = "b".repeat(64);
2149        let raw = format!("tree {t}\nparent {p}\ncommitter C <c@x> 2 +0000\n");
2150        let (tree, parents) =
2151            parse_commit_tree_and_parents_prefix(raw.as_bytes(), HashAlgo::Sha256.hex_len())
2152                .expect("sha256");
2153        assert_eq!(tree.to_hex(), t);
2154        assert_eq!(parents.len(), 1);
2155        assert_eq!(parents[0].to_hex(), p);
2156
2157        assert!(parse_commit_tree_and_parents_prefix(raw.as_bytes(), 40).is_none());
2158        assert!(parse_commit_tree_and_parents_prefix(b"parent x\n", 40).is_none());
2159        assert!(parse_commit_tree_and_parents_prefix(
2160            b"tree zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz\nauthor\n",
2161            40
2162        )
2163        .is_none());
2164        Ok(())
2165    }
2166
2167    #[test]
2168    fn object_id_loose_path_helpers() {
2169        let oid = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap();
2170        assert_eq!(oid.loose_prefix(), "4b");
2171        assert_eq!(oid.loose_suffix().len(), 38);
2172        assert!(ObjectId::is_hex_len(40));
2173        assert!(ObjectId::is_loose_suffix_len(38));
2174    }
2175
2176    #[test]
2177    fn tree_ref_iter_yields_at_most_one_error() {
2178        let bad = b"100644 incomplete";
2179        let mut iter = TreeRefIter::with_oid_len(bad, HashAlgo::Sha1.len());
2180        assert!(iter.next().unwrap().is_err());
2181        assert!(iter.next().is_none());
2182    }
2183
2184    #[test]
2185    fn tree_ref_iter_public_api_smoke() {
2186        let data = serialize_tree(&[TreeEntry {
2187            mode: 0o100644,
2188            name: b"a".to_vec(),
2189            oid: ObjectId::from_hex("0123456789012345678901234567890123456789").unwrap(),
2190        }]);
2191        let mut iter = TreeRefIter::infer_oid_len(&data).expect("infer");
2192        let entry = iter.next().unwrap().expect("entry");
2193        assert_eq!(entry.name, b"a");
2194        let _owned = entry.to_owned();
2195        let _vec = parse_tree(&data).expect("parse");
2196        let _ = TreeRefIter::with_oid_len(&data, HashAlgo::Sha1.len());
2197    }
2198
2199    #[test]
2200    fn find_tree_entry_by_name_in_data_locates_directory_in_large_tree() -> Result<()> {
2201        let dummy = ObjectId::from_hex("0123456789012345678901234567890123456789")?;
2202        let mut entries: Vec<TreeEntry> = (1..=60)
2203            .map(|i| TreeEntry {
2204                mode: 0o100644,
2205                name: format!("f{i}.txt").into_bytes(),
2206                oid: dummy,
2207            })
2208            .collect();
2209        entries.push(TreeEntry {
2210            mode: 0o040000,
2211            name: b"sub".to_vec(),
2212            oid: dummy,
2213        });
2214        entries.sort_by(|a, b| {
2215            tree_entry_cmp(&a.name, a.mode == 0o040000, &b.name, b.mode == 0o040000)
2216        });
2217        let data = serialize_tree(&entries);
2218        assert!(entries.len() >= 48);
2219        let found = find_tree_entry_by_name_in_data(&data, HashAlgo::Sha1.len(), b"sub", true)?
2220            .expect("sub directory in wide tree");
2221        assert_eq!(found.mode, 0o040000);
2222        assert_eq!(found.name, b"sub");
2223        Ok(())
2224    }
2225
2226    fn sorted_tree_data(entries: &mut [TreeEntry]) -> Vec<u8> {
2227        entries.sort_by(|a, b| {
2228            tree_entry_cmp(&a.name, a.mode == 0o040000, &b.name, b.mode == 0o040000)
2229        });
2230        serialize_tree(entries)
2231    }
2232
2233    #[test]
2234    fn find_tree_entry_by_name_in_data_matches_parse_tree_for_every_name() -> Result<()> {
2235        let oid_a = ObjectId::from_hex("1111111111111111111111111111111111111111")?;
2236        let oid_b = ObjectId::from_hex("2222222222222222222222222222222222222222")?;
2237        let mut entries = vec![
2238            TreeEntry {
2239                mode: 0o040000,
2240                name: b"foo".to_vec(),
2241                oid: oid_a,
2242            },
2243            TreeEntry {
2244                mode: 0o100644,
2245                name: b"foo.c".to_vec(),
2246                oid: oid_b,
2247            },
2248            TreeEntry {
2249                mode: 0o100755,
2250                name: b"foo-bar".to_vec(),
2251                oid: oid_b,
2252            },
2253            TreeEntry {
2254                mode: 0o120000,
2255                name: b"link".to_vec(),
2256                oid: oid_a,
2257            },
2258            TreeEntry {
2259                mode: 0o160000,
2260                name: b"sub".to_vec(),
2261                oid: oid_b,
2262            },
2263            TreeEntry {
2264                mode: 0o100644,
2265                name: b"README".to_vec(),
2266                oid: oid_a,
2267            },
2268        ];
2269        let data = sorted_tree_data(&mut entries);
2270        for expected in parse_tree(&data)? {
2271            let name_is_tree = expected.mode == 0o040000;
2272            let found = find_tree_entry_by_name_in_data(
2273                &data,
2274                HashAlgo::Sha1.len(),
2275                &expected.name,
2276                name_is_tree,
2277            )?
2278            .expect("entry present");
2279            assert_eq!(found, expected);
2280        }
2281        for missing in [&b"fo"[..], b"foo/", b"foo.d", b"zzz", b"", b"AAA", b"lin"] {
2282            assert!(
2283                find_tree_entry_by_name_in_data(&data, HashAlgo::Sha1.len(), missing, false)?
2284                    .is_none(),
2285                "unexpected match for {:?}",
2286                String::from_utf8_lossy(missing)
2287            );
2288        }
2289        Ok(())
2290    }
2291
2292    #[test]
2293    fn find_tree_entry_by_name_in_data_stops_after_sorted_position() -> Result<()> {
2294        let oid = ObjectId::from_hex("3333333333333333333333333333333333333333")?;
2295        let mut entries = vec![
2296            TreeEntry {
2297                mode: 0o100644,
2298                name: b"a".to_vec(),
2299                oid,
2300            },
2301            TreeEntry {
2302                mode: 0o100644,
2303                name: b"c".to_vec(),
2304                oid,
2305            },
2306        ];
2307        let mut data = sorted_tree_data(&mut entries);
2308        data.extend_from_slice(b"garbage without separators");
2309        let found = find_tree_entry_by_name_in_data(&data, HashAlgo::Sha1.len(), b"a", false)?;
2310        assert_eq!(found.map(|e| e.name), Some(b"a".to_vec()));
2311        assert!(
2312            find_tree_entry_by_name_in_data(&data, HashAlgo::Sha1.len(), b"b", false)?.is_none()
2313        );
2314        assert!(matches!(
2315            find_tree_entry_by_name_in_data(&data, HashAlgo::Sha1.len(), b"d", false),
2316            Err(Error::CorruptObject(_))
2317        ));
2318        Ok(())
2319    }
2320
2321    #[test]
2322    fn find_tree_entry_by_name_in_data_rejects_bad_mode_and_truncated_oid() {
2323        let bad_mode = b"10x644 a\0aaaaaaaaaaaaaaaaaaaa";
2324        assert!(matches!(
2325            find_tree_entry_by_name_in_data(bad_mode, HashAlgo::Sha1.len(), b"a", false),
2326            Err(Error::CorruptObject(_))
2327        ));
2328        let truncated = b"100644 a\0short";
2329        assert!(matches!(
2330            find_tree_entry_by_name_in_data(truncated, HashAlgo::Sha1.len(), b"a", false),
2331            Err(Error::CorruptObject(_))
2332        ));
2333    }
2334
2335    #[test]
2336    fn find_tree_entry_by_name_in_data_reads_sha256_width() -> Result<()> {
2337        let oid = ObjectId::from_bytes(&[0x5au8; 32])?;
2338        let mut raw = b"100644 file\0".to_vec();
2339        raw.extend_from_slice(oid.as_bytes());
2340        let found = find_tree_entry_by_name_in_data(&raw, HashAlgo::Sha256.len(), b"file", false)?
2341            .expect("file");
2342        assert_eq!(found.oid, oid);
2343        assert_eq!(found.mode, 0o100644);
2344        Ok(())
2345    }
2346}