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        hex::encode(self.as_bytes())
326    }
327
328    /// The two-character directory prefix used by the loose object store.
329    ///
330    /// Returns the first two hex chars (e.g. `"ab"` for `"ab3f…"`).
331    #[must_use]
332    pub fn loose_prefix(&self) -> String {
333        hex::encode(&self.bytes[..1])
334    }
335
336    /// Parse an object ID from a hex string (40 chars for SHA-1, 64 for
337    /// SHA-256).
338    ///
339    /// # Errors
340    ///
341    /// Returns [`Error::InvalidObjectId`] if the string is not a valid hex OID.
342    pub fn from_hex(s: &str) -> Result<Self> {
343        Self::decode_hex(s.as_bytes()).ok_or_else(|| Error::InvalidObjectId(s.to_owned()))
344    }
345
346    /// Parse a fixed-width lowercase/uppercase ASCII hex OID (40 chars for SHA-1).
347    ///
348    /// # Errors
349    ///
350    /// Returns [`Error::InvalidObjectId`] when `bytes` is not exactly 40 hex digits.
351    pub fn from_hex_ascii(bytes: &[u8]) -> Result<Self> {
352        if bytes.len() != HashAlgo::Sha1.hex_len() {
353            return Err(Error::InvalidObjectId(
354                String::from_utf8_lossy(bytes).into_owned(),
355            ));
356        }
357        Self::decode_hex(bytes)
358            .ok_or_else(|| Error::InvalidObjectId(String::from_utf8_lossy(bytes).into_owned()))
359    }
360
361    /// Decode 40 or 64 ASCII hex digits (either case); `None` for any other input.
362    ///
363    /// Ref listings and tree walks parse ids in bulk, so this avoids per-digit
364    /// branching and allocation: invalid digits are folded into one check at the end.
365    fn decode_hex(hex: &[u8]) -> Option<Self> {
366        let len = hex.len() / 2;
367        if !Self::is_hex_len(hex.len()) {
368            return None;
369        }
370        let mut bytes = [0u8; MAX_OID_LEN];
371        let mut invalid = 0u8;
372        for (out, pair) in bytes.iter_mut().zip(hex.as_chunks::<2>().0) {
373            let hi = HEX_DECODE[usize::from(pair[0])];
374            let lo = HEX_DECODE[usize::from(pair[1])];
375            invalid |= hi | lo;
376            *out = (hi << 4) | lo;
377        }
378        (invalid & HEX_INVALID == 0).then_some(Self {
379            bytes,
380            len: len as u8,
381        })
382    }
383
384    /// The suffix used as the filename inside the loose prefix dir (the digest
385    /// minus its first byte: 38 hex chars for SHA-1, 62 for SHA-256).
386    #[must_use]
387    pub fn loose_suffix(&self) -> String {
388        hex::encode(&self.bytes[1..self.len as usize])
389    }
390
391    /// Whether `s` is a full-length hex OID for a supported hash algorithm
392    /// (40 hex chars for SHA-1 or 64 for SHA-256), case-insensitive.
393    #[must_use]
394    pub fn is_full_hex(s: &str) -> bool {
395        (s.len() == HashAlgo::Sha1.hex_len() || s.len() == HashAlgo::Sha256.hex_len())
396            && s.bytes().all(|b| b.is_ascii_hexdigit())
397    }
398
399    /// Whether `len` is a valid full hex-OID length (40 or 64).
400    #[must_use]
401    pub const fn is_hex_len(len: usize) -> bool {
402        len == HashAlgo::Sha1.hex_len() || len == HashAlgo::Sha256.hex_len()
403    }
404
405    /// Whether `len` is a valid loose-object filename suffix length, i.e. a full
406    /// hex OID minus its first byte (38 for SHA-1, 62 for SHA-256).
407    #[must_use]
408    pub const fn is_loose_suffix_len(len: usize) -> bool {
409        len == HashAlgo::Sha1.hex_len() - 2 || len == HashAlgo::Sha256.hex_len() - 2
410    }
411}
412
413impl std::hash::Hash for ObjectId {
414    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
415        state.write(self.as_bytes());
416    }
417}
418
419impl fmt::Display for ObjectId {
420    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
421        f.write_str(&self.to_hex())
422    }
423}
424
425impl fmt::Debug for ObjectId {
426    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
427        write!(f, "ObjectId({})", self.to_hex())
428    }
429}
430
431#[cfg(feature = "serde-models")]
432impl serde::Serialize for ObjectId {
433    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
434    where
435        S: serde::Serializer,
436    {
437        serializer.serialize_str(&self.to_hex())
438    }
439}
440
441/// Marker bit in [`HEX_DECODE`] for bytes that are not hex digits.
442const HEX_INVALID: u8 = 0x10;
443
444/// Nibble value of each ASCII hex digit; every other byte maps to [`HEX_INVALID`].
445const HEX_DECODE: [u8; 256] = {
446    let mut table = [HEX_INVALID; 256];
447    let mut i = 0u8;
448    while i < 10 {
449        table[(b'0' + i) as usize] = i;
450        i += 1;
451    }
452    let mut i = 0u8;
453    while i < 6 {
454        table[(b'a' + i) as usize] = 10 + i;
455        table[(b'A' + i) as usize] = 10 + i;
456        i += 1;
457    }
458    table
459};
460
461impl FromStr for ObjectId {
462    type Err = Error;
463
464    fn from_str(s: &str) -> Result<Self> {
465        Self::from_hex(s)
466    }
467}
468
469/// A parsed abbreviated object id: the leading hex digits ("nibbles") of a digest.
470///
471/// Parse the user's hex once with [`ObjectIdPrefix::from_hex`], then test candidates
472/// with [`ObjectIdPrefix::matches`], which compares raw digest bytes without allocating.
473/// Sorted backends (pack indexes, key-value stores) can instead scan the inclusive id
474/// range [`ObjectIdPrefix::lowest`]..=[`ObjectIdPrefix::highest`], which holds exactly
475/// the ids that match.
476///
477/// # Examples
478///
479/// ```
480/// use grit_lib::objects::{HashAlgo, ObjectId, ObjectIdPrefix};
481///
482/// # fn main() -> grit_lib::error::Result<()> {
483/// let id = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf8d69288fbee4904")?;
484/// let prefix = ObjectIdPrefix::from_hex("4B825", HashAlgo::Sha1)?;
485/// assert_eq!(prefix.nibbles(), 5);
486/// assert!(prefix.matches(&id));
487/// assert!(!ObjectIdPrefix::from_hex("4b826", HashAlgo::Sha1)?.matches(&id));
488/// assert!(prefix.lowest() <= id && id <= prefix.highest());
489/// # Ok(())
490/// # }
491/// ```
492#[derive(Clone, Copy, PartialEq, Eq)]
493pub struct ObjectIdPrefix {
494    /// Prefix digits packed two per byte; the low nibble of an odd-length tail and
495    /// every byte past it are zero.
496    bytes: [u8; MAX_OID_LEN],
497    nibbles: u8,
498    algo: HashAlgo,
499}
500
501impl ObjectIdPrefix {
502    /// Parse `hex` (either case, possibly empty) as a prefix of `algo` object ids.
503    ///
504    /// The empty prefix matches every id.
505    ///
506    /// # Errors
507    ///
508    /// Returns [`Error::InvalidObjectId`] when `hex` contains a non-hex character or is
509    /// longer than a full `algo` id ([`HashAlgo::hex_len`]).
510    pub fn from_hex(hex: &str, algo: HashAlgo) -> Result<Self> {
511        let digits = hex.as_bytes();
512        if digits.len() > algo.hex_len() {
513            return Err(Error::InvalidObjectId(hex.to_owned()));
514        }
515        let mut bytes = [0u8; MAX_OID_LEN];
516        let mut invalid = 0u8;
517        for (i, &digit) in digits.iter().enumerate() {
518            let nibble = HEX_DECODE[usize::from(digit)];
519            invalid |= nibble;
520            let shift = if i.is_multiple_of(2) { 4 } else { 0 };
521            bytes[i / 2] |= (nibble & 0x0f) << shift;
522        }
523        if invalid & HEX_INVALID != 0 {
524            return Err(Error::InvalidObjectId(hex.to_owned()));
525        }
526        Ok(Self {
527            bytes,
528            nibbles: digits.len() as u8,
529            algo,
530        })
531    }
532
533    /// Number of hex digits in the prefix.
534    #[must_use]
535    pub fn nibbles(&self) -> usize {
536        usize::from(self.nibbles)
537    }
538
539    /// Whether the prefix has no digits (and so matches every id).
540    #[must_use]
541    pub fn is_empty(&self) -> bool {
542        self.nibbles == 0
543    }
544
545    /// Hash algorithm the prefix was parsed for.
546    #[must_use]
547    pub fn algo(&self) -> HashAlgo {
548        self.algo
549    }
550
551    /// Whether `oid` starts with this prefix.
552    ///
553    /// Compares raw digest bytes (masking the last byte for an odd digit count), so the
554    /// check never allocates. An id shorter than the prefix never matches.
555    #[must_use]
556    pub fn matches(&self, oid: &ObjectId) -> bool {
557        self.matches_digest(oid.as_bytes())
558    }
559
560    /// Like [`Self::matches`] for a raw digest slice (for example a pack index entry).
561    #[must_use]
562    pub(crate) fn matches_digest(&self, digest: &[u8]) -> bool {
563        let whole = self.nibbles() / 2;
564        let Some(head) = digest.get(..whole) else {
565            return false;
566        };
567        if head != &self.bytes[..whole] {
568            return false;
569        }
570        if self.nibbles.is_multiple_of(2) {
571            return true;
572        }
573        digest
574            .get(whole)
575            .is_some_and(|&b| b & 0xf0 == self.bytes[whole])
576    }
577
578    /// The smallest `algo` id that starts with this prefix (remaining digits all `0`).
579    #[must_use]
580    pub fn lowest(&self) -> ObjectId {
581        ObjectId {
582            bytes: self.bytes,
583            len: self.algo.len() as u8,
584        }
585    }
586
587    /// The largest `algo` id that starts with this prefix (remaining digits all `f`).
588    #[must_use]
589    pub fn highest(&self) -> ObjectId {
590        let mut bytes = self.bytes;
591        let whole = self.nibbles() / 2;
592        if !self.nibbles.is_multiple_of(2) {
593            bytes[whole] |= 0x0f;
594        }
595        let first_free = whole + usize::from(self.nibbles % 2);
596        bytes[first_free..self.algo.len()].fill(0xff);
597        ObjectId {
598            bytes,
599            len: self.algo.len() as u8,
600        }
601    }
602}
603
604impl fmt::Display for ObjectIdPrefix {
605    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
606        let hex = hex::encode(&self.bytes[..self.nibbles().div_ceil(2)]);
607        f.write_str(&hex[..self.nibbles()])
608    }
609}
610
611impl fmt::Debug for ObjectIdPrefix {
612    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
613        write!(f, "ObjectIdPrefix({self})")
614    }
615}
616
617/// The four Git object types.
618#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
619pub enum ObjectKind {
620    /// A raw file snapshot.
621    Blob,
622    /// A directory listing.
623    Tree,
624    /// A snapshot with metadata and parentage.
625    Commit,
626    /// An annotated tag.
627    Tag,
628}
629
630impl ObjectKind {
631    /// Parse from the ASCII keyword used in Git object headers.
632    ///
633    /// # Errors
634    ///
635    /// Returns [`Error::UnknownObjectType`] for unrecognised strings.
636    pub fn from_bytes(b: &[u8]) -> Result<Self> {
637        match b {
638            b"blob" => Ok(Self::Blob),
639            b"tree" => Ok(Self::Tree),
640            b"commit" => Ok(Self::Commit),
641            b"tag" => Ok(Self::Tag),
642            other => Err(Error::UnknownObjectType(
643                String::from_utf8_lossy(other).into_owned(),
644            )),
645        }
646    }
647
648    /// Parse the `type` field on an annotated tag object (Git `type_from_string_gently` rules).
649    ///
650    /// The tag header line is `type <typename>\n` where `typename` must match a known object type
651    /// keyword **exactly** (no extra characters, no strict prefix of a longer keyword).
652    #[must_use]
653    pub fn from_tag_type_field(line: &[u8]) -> Option<Self> {
654        fn keyword_matches(canonical: &[u8], field: &[u8]) -> bool {
655            if field.is_empty() {
656                return false;
657            }
658            for (i, &bc) in field.iter().enumerate() {
659                let sc = canonical.get(i).copied().unwrap_or(0);
660                if sc != bc {
661                    return false;
662                }
663            }
664            canonical.get(field.len()).copied().unwrap_or(0) == 0
665        }
666
667        const NAMES: &[(ObjectKind, &[u8])] = &[
668            (ObjectKind::Blob, b"blob"),
669            (ObjectKind::Tree, b"tree"),
670            (ObjectKind::Commit, b"commit"),
671            (ObjectKind::Tag, b"tag"),
672        ];
673        for &(kind, name) in NAMES {
674            if keyword_matches(name, line) {
675                return Some(kind);
676            }
677        }
678        None
679    }
680
681    /// The ASCII keyword for this kind (used in object headers).
682    #[must_use]
683    pub fn as_str(&self) -> &'static str {
684        match self {
685            Self::Blob => "blob",
686            Self::Tree => "tree",
687            Self::Commit => "commit",
688            Self::Tag => "tag",
689        }
690    }
691}
692
693impl fmt::Display for ObjectKind {
694    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
695        f.write_str(self.as_str())
696    }
697}
698
699impl FromStr for ObjectKind {
700    type Err = Error;
701
702    fn from_str(s: &str) -> Result<Self> {
703        Self::from_bytes(s.as_bytes())
704    }
705}
706
707/// Kind and uncompressed size of a Git object without loading its payload.
708#[derive(Debug, Clone, Copy, PartialEq, Eq)]
709pub struct ObjectInfo {
710    /// Object type after resolving any pack delta chain.
711    pub kind: ObjectKind,
712    /// Uncompressed byte length of the object body (after the header NUL).
713    pub size: u64,
714}
715
716/// A decompressed, header-stripped Git object.
717#[derive(Debug, Clone)]
718pub struct Object {
719    /// The type of this object.
720    pub kind: ObjectKind,
721    /// Raw byte content (everything after the NUL in the header).
722    pub data: Vec<u8>,
723}
724
725/// Borrowed view of an object's kind and uncompressed payload.
726///
727/// Produced by [`crate::odb::Odb::read_into`](crate::odb::Odb::read_into) and
728/// [`crate::odb::store::ObjectStore::read_into`](crate::odb::store::ObjectStore::read_into);
729/// the payload bytes live in the caller's buffer.
730#[derive(Debug, Clone, Copy, PartialEq, Eq)]
731pub struct ObjectRef<'a> {
732    /// Object type after resolving any pack delta chain.
733    pub kind: ObjectKind,
734    /// Uncompressed payload (header stripped).
735    pub data: &'a [u8],
736}
737
738impl<'a> ObjectRef<'a> {
739    /// Borrow `buf` as an object payload with the given `kind`.
740    #[must_use]
741    pub fn from_buf(kind: ObjectKind, buf: &'a [u8]) -> Self {
742        Self { kind, data: buf }
743    }
744}
745
746impl Object {
747    /// Construct a new object from its kind and raw data.
748    #[must_use]
749    pub fn new(kind: ObjectKind, data: Vec<u8>) -> Self {
750        Self { kind, data }
751    }
752
753    /// Borrowed view of this object's kind and payload.
754    #[must_use]
755    pub fn as_ref(&self) -> ObjectRef<'_> {
756        ObjectRef {
757            kind: self.kind,
758            data: self.data.as_slice(),
759        }
760    }
761
762    /// Serialize to the canonical Git object format: `"<kind> <size>\0<data>"`.
763    #[must_use]
764    pub fn to_store_bytes(&self) -> Vec<u8> {
765        let header = format!("{} {}\0", self.kind, self.data.len());
766        let mut out = Vec::with_capacity(header.len() + self.data.len());
767        out.extend_from_slice(header.as_bytes());
768        out.extend_from_slice(&self.data);
769        out
770    }
771}
772
773/// A single entry in a Git tree object.
774#[derive(Debug, Clone, PartialEq, Eq)]
775pub struct TreeEntry {
776    /// Unix file mode (e.g. `0o100644` for a regular file, `0o040000` for a tree).
777    pub mode: u32,
778    /// Entry name (file or directory name only, no path separators).
779    pub name: Vec<u8>,
780    /// The object ID of the blob or sub-tree.
781    pub oid: ObjectId,
782}
783
784impl TreeEntry {
785    /// Format the mode as Git does: no leading zero, minimal digits.
786    ///
787    /// Git uses `"40000"` for trees (not `"040000"`), and `"100644"` for blobs.
788    #[must_use]
789    pub fn mode_str(&self) -> String {
790        // Git omits the leading zero for tree mode
791        if self.mode == 0o040000 {
792            "40000".to_owned()
793        } else {
794            format!("{:o}", self.mode)
795        }
796    }
797}
798
799/// A single tree entry borrowing name bytes from the tree object payload.
800#[derive(Debug, Clone, Copy, PartialEq, Eq)]
801pub struct TreeEntryRef<'a> {
802    /// Unix file mode (e.g. `0o100644` for a regular file, `0o040000` for a tree).
803    pub mode: u32,
804    /// Entry name (file or directory name only, no path separators).
805    pub name: &'a [u8],
806    /// The object ID of the blob or sub-tree.
807    pub oid: ObjectId,
808}
809
810impl TreeEntryRef<'_> {
811    /// Allocate an owning [`TreeEntry`] with the same mode, name, and OID.
812    #[must_use]
813    pub fn to_owned(&self) -> TreeEntry {
814        TreeEntry {
815            mode: self.mode,
816            name: self.name.to_vec(),
817            oid: self.oid,
818        }
819    }
820}
821
822/// Iterator over tree entries borrowing from the tree object's raw payload.
823///
824/// Construct with [`Self::with_oid_len`] when the hash width is known, or
825/// [`Self::infer_oid_len`] to detect SHA-1 vs SHA-256 like [`parse_tree`].
826#[derive(Debug, Clone)]
827pub struct TreeRefIter<'a> {
828    data: &'a [u8],
829    pos: usize,
830    oid_len: usize,
831    /// Set after the first parse error so callers do not see endless `Err` items.
832    failed: bool,
833}
834
835impl<'a> TreeRefIter<'a> {
836    /// Iterate `data` assuming each entry ends with an OID of `oid_len` bytes.
837    #[must_use]
838    pub fn with_oid_len(data: &'a [u8], oid_len: usize) -> Self {
839        Self {
840            data,
841            pos: 0,
842            oid_len,
843            failed: false,
844        }
845    }
846
847    /// Detect OID width and return an iterator (SHA-1 first, then SHA-256).
848    ///
849    /// # Errors
850    ///
851    /// Returns [`Error::CorruptObject`] when the blob does not parse as a tree.
852    pub fn infer_oid_len(data: &'a [u8]) -> Result<Self> {
853        match try_parse_tree_oid_len(data, HashAlgo::Sha1.len()) {
854            Ok(()) => Ok(Self::with_oid_len(data, HashAlgo::Sha1.len())),
855            Err(sha1_err) => match try_parse_tree_oid_len(data, HashAlgo::Sha256.len()) {
856                Ok(()) => Ok(Self::with_oid_len(data, HashAlgo::Sha256.len())),
857                Err(_) => Err(sha1_err),
858            },
859        }
860    }
861
862    fn next_entry(&mut self) -> Result<Option<TreeEntryRef<'a>>> {
863        parse_tree_entry_at(self.data, &mut self.pos, self.oid_len)
864    }
865}
866
867/// Parse one tree entry and advance `pos`. Returns `Ok(None)` when `pos` is at end of `data`.
868fn parse_tree_entry_at<'a>(
869    data: &'a [u8],
870    pos: &mut usize,
871    oid_len: usize,
872) -> Result<Option<TreeEntryRef<'a>>> {
873    if *pos >= data.len() {
874        return Ok(None);
875    }
876    let sp = data[*pos..]
877        .iter()
878        .position(|&b| b == b' ')
879        .ok_or_else(|| Error::CorruptObject("tree entry missing space".to_owned()))?;
880    let mode_bytes = &data[*pos..*pos + sp];
881    let mode = std::str::from_utf8(mode_bytes)
882        .ok()
883        .and_then(|s| u32::from_str_radix(s, 8).ok())
884        .ok_or_else(|| {
885            Error::CorruptObject(format!(
886                "invalid tree mode: {}",
887                String::from_utf8_lossy(mode_bytes)
888            ))
889        })?;
890    *pos += sp + 1;
891
892    let nul = data[*pos..]
893        .iter()
894        .position(|&b| b == 0)
895        .ok_or_else(|| Error::CorruptObject("tree entry missing NUL".to_owned()))?;
896    let name = &data[*pos..*pos + nul];
897    *pos += nul + 1;
898
899    if *pos + oid_len > data.len() {
900        return Err(Error::CorruptObject("tree entry truncated SHA".to_owned()));
901    }
902    let oid = ObjectId::from_bytes(&data[*pos..*pos + oid_len])?;
903    *pos += oid_len;
904
905    Ok(Some(TreeEntryRef { mode, name, oid }))
906}
907
908impl<'a> Iterator for TreeRefIter<'a> {
909    type Item = Result<TreeEntryRef<'a>>;
910
911    fn next(&mut self) -> Option<Self::Item> {
912        if self.failed || self.pos >= self.data.len() {
913            return None;
914        }
915        match self.next_entry() {
916            Ok(Some(entry)) => Some(Ok(entry)),
917            Ok(None) => None,
918            Err(err) => {
919                self.failed = true;
920                self.pos = self.data.len();
921                Some(Err(err))
922            }
923        }
924    }
925}
926
927/// Walk `data` once to verify it parses as a tree with the given OID width (no entry allocation).
928fn try_parse_tree_oid_len(data: &[u8], oid_len: usize) -> Result<()> {
929    let mut pos = 0usize;
930    while parse_tree_entry_at(data, &mut pos, oid_len)?.is_some() {}
931    Ok(())
932}
933
934fn collect_tree_with_oid_len(data: &[u8], oid_len: usize) -> Result<Vec<TreeEntry>> {
935    let mut pos = 0usize;
936    let mut entries = Vec::with_capacity(data.len() / 32);
937    while let Some(entry) = parse_tree_entry_at(data, &mut pos, oid_len)? {
938        entries.push(TreeEntry {
939            mode: entry.mode,
940            name: entry.name.to_vec(),
941            oid: entry.oid,
942        });
943    }
944    Ok(entries)
945}
946
947/// Parse the raw data of a tree object into its entries.
948///
949/// # Format
950///
951/// Each entry is `"<mode> <name>\0<20-byte-sha1>"` concatenated with no
952/// separator between entries.
953///
954/// # Errors
955///
956/// Returns [`Error::CorruptObject`] if the data is malformed.
957pub fn parse_tree(data: &[u8]) -> Result<Vec<TreeEntry>> {
958    match collect_tree_with_oid_len(data, HashAlgo::Sha1.len()) {
959        Ok(entries) => Ok(entries),
960        Err(sha1_err) => {
961            collect_tree_with_oid_len(data, HashAlgo::Sha256.len()).map_err(|_| sha1_err)
962        }
963    }
964}
965
966/// Parse a tree blob assuming a fixed raw OID width (`oid_len` bytes).
967///
968/// Returns an error if the data does not parse cleanly into whole entries of
969/// that width (used by [`parse_tree`] to detect the hash algorithm).
970pub fn parse_tree_with_oid_len(data: &[u8], oid_len: usize) -> Result<Vec<TreeEntry>> {
971    collect_tree_with_oid_len(data, oid_len)
972}
973
974/// Build the raw bytes of a tree object from a slice of entries.
975///
976/// Entries **must** already be sorted in Git tree order (see [`tree_entry_cmp`]),
977/// with unique, valid names and modes; nothing here checks that. To write a tree
978/// from entries in any order with validation, use
979/// [`Objects::write_tree`](crate::objects::Objects::write_tree).
980#[must_use]
981pub fn serialize_tree(entries: &[TreeEntry]) -> Vec<u8> {
982    let len = entries
983        .iter()
984        .map(|e| 8 + e.name.len() + e.oid.as_bytes().len())
985        .sum();
986    let mut out = Vec::with_capacity(len);
987    for e in entries {
988        push_tree_mode(&mut out, e.mode);
989        out.push(b' ');
990        out.extend_from_slice(&e.name);
991        out.push(0);
992        out.extend_from_slice(e.oid.as_bytes());
993    }
994    out
995}
996
997/// Append `mode` in octal without leading zeros, as [`TreeEntry::mode_str`] formats it.
998fn push_tree_mode(out: &mut Vec<u8>, mode: u32) {
999    let mut digits = [0u8; 11];
1000    let mut start = digits.len();
1001    let mut rest = mode;
1002    loop {
1003        start -= 1;
1004        digits[start] = b'0' + (rest & 7) as u8;
1005        rest >>= 3;
1006        if rest == 0 {
1007            break;
1008        }
1009    }
1010    out.extend_from_slice(&digits[start..]);
1011}
1012
1013/// Git's tree-entry sort comparator.
1014///
1015/// Trees are sorted byte-by-byte by `"<name>"` for blobs and `"<name>/"` for
1016/// sub-trees, so a directory `foo` sorts after a file `foo-bar` but before
1017/// `fooz`.  This matches `base_name_compare` in `tree.c`.
1018///
1019/// # Parameters
1020///
1021/// - `a_name`: name bytes of the first entry
1022/// - `a_is_tree`: whether the first entry is a sub-tree (`mode == 0o040000`)
1023/// - `b_name`: name bytes of the second entry
1024/// - `b_is_tree`: whether the second entry is a sub-tree
1025#[must_use]
1026pub fn tree_entry_cmp(
1027    a_name: &[u8],
1028    a_is_tree: bool,
1029    b_name: &[u8],
1030    b_is_tree: bool,
1031) -> std::cmp::Ordering {
1032    let a_trailer = if a_is_tree { b'/' } else { 0u8 };
1033    let b_trailer = if b_is_tree { b'/' } else { 0u8 };
1034
1035    let min_len = a_name.len().min(b_name.len());
1036    let cmp = a_name[..min_len].cmp(&b_name[..min_len]);
1037    if cmp != std::cmp::Ordering::Equal {
1038        return cmp;
1039    }
1040    // Names share a prefix; compare the next character (or trailer).
1041    let ac = a_name.get(min_len).copied().unwrap_or(a_trailer);
1042    let bc = b_name.get(min_len).copied().unwrap_or(b_trailer);
1043    ac.cmp(&bc)
1044}
1045
1046/// Parse an octal tree-entry mode without going through `str`.
1047fn parse_tree_mode_octal(bytes: &[u8]) -> Option<u32> {
1048    if bytes.is_empty() || bytes.len() > 7 {
1049        return None;
1050    }
1051    bytes.iter().try_fold(0u32, |acc, &b| match b {
1052        b'0'..=b'7' => Some(acc * 8 + u32::from(b - b'0')),
1053        _ => None,
1054    })
1055}
1056
1057/// Find one tree entry by name without building a full [`TreeEntry`] vector.
1058///
1059/// Scans entries in stored order and returns the first whose name equals `name`
1060/// (file or directory). Because Git sorts entries by `name` (with a `/` trailer for
1061/// directories), the scan stops at the first entry that sorts after `name/`, so only
1062/// the prefix of the tree up to the match is decoded; corruption after that point is
1063/// not reported.
1064///
1065/// # Errors
1066///
1067/// [`Error::CorruptObject`] when an entry scanned before the stop point is malformed.
1068pub(crate) fn find_tree_entry_by_name_in_data(
1069    data: &[u8],
1070    oid_len: usize,
1071    name: &[u8],
1072    name_is_tree: bool,
1073) -> Result<Option<TreeEntry>> {
1074    let mut pos = 0usize;
1075    while pos < data.len() {
1076        let rest = &data[pos..];
1077        let sp = memchr::memchr(b' ', rest)
1078            .ok_or_else(|| Error::CorruptObject("tree entry missing space".to_owned()))?;
1079        let mode = parse_tree_mode_octal(&rest[..sp])
1080            .ok_or_else(|| Error::CorruptObject("invalid tree mode".to_owned()))?;
1081        let name_start = sp + 1;
1082        let nul = memchr::memchr(0, &rest[name_start..])
1083            .ok_or_else(|| Error::CorruptObject("tree entry missing NUL".to_owned()))?;
1084        let entry_name = &rest[name_start..name_start + nul];
1085        let oid_start = name_start + nul + 1;
1086        let oid_end = oid_start + oid_len;
1087        if oid_end > rest.len() {
1088            return Err(Error::CorruptObject("tree entry truncated SHA".to_owned()));
1089        }
1090        if entry_name == name {
1091            return Ok(Some(TreeEntry {
1092                mode,
1093                name: entry_name.to_vec(),
1094                oid: ObjectId::from_bytes(&rest[oid_start..oid_end])?,
1095            }));
1096        }
1097        if tree_entry_cmp(entry_name, mode == 0o040000, name, name_is_tree)
1098            == std::cmp::Ordering::Greater
1099        {
1100            return Ok(None);
1101        }
1102        pos += oid_end;
1103    }
1104    Ok(None)
1105}
1106
1107/// Like [`find_tree_entry_by_name_in_data`], trying directory ordering first then file.
1108pub(crate) fn find_tree_entry_by_name_dir_or_file_in_data(
1109    data: &[u8],
1110    oid_len: usize,
1111    name: &[u8],
1112) -> Result<Option<TreeEntry>> {
1113    if let Some(entry) = find_tree_entry_by_name_in_data(data, oid_len, name, true)? {
1114        if entry.mode == 0o040000 {
1115            return Ok(Some(entry));
1116        }
1117    }
1118    find_tree_entry_by_name_in_data(data, oid_len, name, false)
1119}
1120
1121/// Parsed representation of a commit object.
1122///
1123/// This is the read-side shape (it round-trips stored bytes exactly). To create a
1124/// new commit, build a [`NewCommit`](crate::objects::NewCommit) and pass it to
1125/// [`Objects::write_commit`](crate::objects::Objects::write_commit).
1126#[derive(Debug, Clone, PartialEq, Eq)]
1127pub struct CommitData {
1128    /// The tree this commit points to.
1129    pub tree: ObjectId,
1130    /// Parent commit IDs (zero or more).
1131    pub parents: Vec<ObjectId>,
1132    /// Author field decoded to Unicode (using `encoding` when present, else UTF-8).
1133    pub author: String,
1134    /// Committer field decoded to Unicode.
1135    pub committer: String,
1136    /// Exact `author` header payload bytes as stored in the object (after `author `).
1137    ///
1138    /// Empty means treat [`Self::author`] as UTF-8 when serializing (new commits).
1139    pub author_raw: Vec<u8>,
1140    /// Exact `committer` header payload bytes as stored in the object.
1141    pub committer_raw: Vec<u8>,
1142    /// Optional encoding override (e.g. `"UTF-8"`).
1143    pub encoding: Option<String>,
1144    /// Commit message (everything after the blank line).
1145    pub message: String,
1146    /// Optional raw message bytes for non-UTF-8 commit messages.
1147    /// When set, `serialize_commit` uses these bytes instead of `message`.
1148    #[doc = "Optional raw message bytes for non-UTF-8 messages."]
1149    pub raw_message: Option<Vec<u8>>,
1150    /// Raw bytes for headers not represented in the fields above (`gpgsig`, `mergetag`, …),
1151    /// including each line's trailing `\n` and any continuation lines (leading space).
1152    ///
1153    /// [`serialize_commit`] always emits `tree`, `parent`, `author`, `committer`, and `encoding`
1154    /// from the structured fields, then appends these bytes before the blank line and message.
1155    pub extra_headers: Vec<u8>,
1156}
1157
1158/// First paragraph of a commit message, folded like Git `%(contents:subject)`.
1159///
1160/// Non-blank lines in the first paragraph are joined with a single space. Each line keeps
1161/// its leading whitespace and internal spacing; only trailing whitespace on each line is
1162/// removed before joining. The paragraph ends at the first blank line. An empty message
1163/// yields an empty string.
1164#[must_use]
1165pub fn commit_subject_pretty(message: &str) -> String {
1166    let mut parts = Vec::new();
1167    for line in message.lines() {
1168        if line.trim().is_empty() {
1169            break;
1170        }
1171        parts.push(line.trim_end());
1172    }
1173    parts.join(" ")
1174}
1175
1176/// Subject from commit object bytes (see [`commit_subject_pretty`]).
1177///
1178/// # Errors
1179///
1180/// Returns [`Error::CorruptObject`] when the commit headers are malformed.
1181pub fn read_commit_subject_pretty(data: &[u8]) -> Result<String> {
1182    let message_start = data
1183        .windows(2)
1184        .position(|w| w == b"\n\n")
1185        .map(|p| p + 2)
1186        .ok_or_else(|| {
1187            Error::CorruptObject("commit missing blank line before message".to_owned())
1188        })?;
1189    let body = std::str::from_utf8(data.get(message_start..).unwrap_or_default()).unwrap_or("");
1190    Ok(commit_subject_pretty(body))
1191}
1192
1193/// First line of the commit message (subject), honoring the `encoding` header.
1194///
1195/// Scans headers only; does not require a full [`parse_commit`] parse.
1196///
1197/// # Errors
1198///
1199/// Returns [`Error::CorruptObject`] when commit headers are malformed.
1200pub fn read_commit_subject_line(data: &[u8]) -> Result<String> {
1201    let mut pos = 0usize;
1202    let mut encoding: Option<String> = None;
1203    while pos < data.len() {
1204        let line_start = pos;
1205        let mut line_end = pos;
1206        while line_end < data.len() && data[line_end] != b'\n' {
1207            line_end += 1;
1208        }
1209        let line = &data[line_start..line_end];
1210        let after_nl = line_end.saturating_add(1);
1211        if line.is_empty() {
1212            let body = data.get(after_nl..).unwrap_or_default();
1213            let message = commit_encoding::decode_bytes(encoding.as_deref(), body);
1214            return Ok(message
1215                .lines()
1216                .find(|l| !l.trim().is_empty())
1217                .map(str::trim)
1218                .unwrap_or("(no subject)")
1219                .to_owned());
1220        }
1221        if line.first() == Some(&b' ') {
1222            pos = after_nl;
1223            continue;
1224        }
1225        if let Some(rest) = line.strip_prefix(b"encoding ") {
1226            encoding = Some(
1227                std::str::from_utf8(rest)
1228                    .map_err(|_| Error::CorruptObject("encoding is not UTF-8".to_owned()))?
1229                    .trim()
1230                    .to_owned(),
1231            );
1232        }
1233        pos = after_nl;
1234    }
1235    Err(Error::CorruptObject(
1236        "commit missing blank line before message".to_owned(),
1237    ))
1238}
1239
1240pub(crate) fn parse_oid_field(value: &[u8]) -> Result<ObjectId> {
1241    let s = std::str::from_utf8(value)
1242        .map_err(|_| Error::CorruptObject("commit header object id is not UTF-8".to_owned()))?;
1243    s.trim()
1244        .parse::<ObjectId>()
1245        .map_err(|_| Error::CorruptObject("invalid object id in commit header".to_owned()))
1246}
1247
1248/// Parse `tree` and `parent` headers from a possibly truncated leading slice of a commit.
1249///
1250/// `prefix` may be cut anywhere (for example a fixed-size inflate of the object start).
1251/// Returns `Some` only when the slice provably holds the complete parent list: the `tree`
1252/// line, every `parent` line, and the start of a following line that cannot be another
1253/// `parent` header. Returns `None` when the slice ends inside that region or the headers are
1254/// not in canonical form; parse the whole object with
1255/// [`parse_commit_tree_and_parents_lenient`] instead, which then yields the same result
1256/// whenever this returns `Some`.
1257pub(crate) fn parse_commit_tree_and_parents_prefix(
1258    prefix: &[u8],
1259    hex_len: usize,
1260) -> Option<(ObjectId, Vec<ObjectId>)> {
1261    canonical_commit_tree_and_parents(prefix, hex_len, false)
1262}
1263
1264/// Parse `tree` and `parent` headers of a whole commit object the way Git's commit parser
1265/// does: a leading `tree` line followed by any number of `parent` lines.
1266///
1267/// Nothing after the parent list is inspected, so a commit missing `author` or `committer`
1268/// (or with other malformed headers) still yields its tree and parents. Returns `None` when
1269/// the first line is not a valid `tree` header or a `parent` line is malformed.
1270pub(crate) fn parse_commit_tree_and_parents_lenient(
1271    data: &[u8],
1272    hex_len: usize,
1273) -> Option<(ObjectId, Vec<ObjectId>)> {
1274    canonical_commit_tree_and_parents(data, hex_len, true)
1275}
1276
1277fn canonical_commit_tree_and_parents(
1278    data: &[u8],
1279    hex_len: usize,
1280    complete: bool,
1281) -> Option<(ObjectId, Vec<ObjectId>)> {
1282    fn header_oid<'a>(rest: &'a [u8], key: &[u8], hex_len: usize) -> Option<(ObjectId, &'a [u8])> {
1283        let line = rest.strip_prefix(key)?;
1284        if line.len() <= hex_len || line[hex_len] != b'\n' {
1285            return None;
1286        }
1287        let oid = ObjectId::from_hex(std::str::from_utf8(&line[..hex_len]).ok()?).ok()?;
1288        Some((oid, &line[hex_len + 1..]))
1289    }
1290    const PARENT: &[u8] = b"parent ";
1291    let (tree, mut rest) = header_oid(data, b"tree ", hex_len)?;
1292    let mut parents = Vec::new();
1293    loop {
1294        if !complete && rest.len() < PARENT.len() && PARENT.starts_with(rest) {
1295            return None;
1296        }
1297        if !rest.starts_with(PARENT) {
1298            return Some((tree, parents));
1299        }
1300        let (parent, next) = header_oid(rest, PARENT, hex_len)?;
1301        parents.push(parent);
1302        rest = next;
1303    }
1304}
1305
1306/// Parse the raw data of a commit object.
1307///
1308/// # Errors
1309///
1310/// Returns [`Error::CorruptObject`] if required headers are missing.
1311pub fn parse_commit(data: &[u8]) -> Result<CommitData> {
1312    Ok(super::CommitRef::parse(data)?.to_owned())
1313}
1314
1315/// Value after `prefix` on the first header line that starts with `prefix`, scanning until a blank
1316/// line (Git tag headers). Returns `None` if no such line exists before the body.
1317#[must_use]
1318pub fn tag_header_field(data: &[u8], prefix: &[u8]) -> Option<String> {
1319    let mut pos = 0usize;
1320    while pos < data.len() {
1321        let rest = &data[pos..];
1322        let nl = rest.iter().position(|&b| b == b'\n');
1323        let line = if let Some(i) = nl { &rest[..i] } else { rest };
1324        if line.is_empty() {
1325            break;
1326        }
1327        if let Some(after) = line.strip_prefix(prefix) {
1328            return Some(String::from_utf8_lossy(after).trim().to_owned());
1329        }
1330        pos += line.len().saturating_add(nl.map(|_| 1).unwrap_or(0));
1331        if nl.is_none() {
1332            break;
1333        }
1334    }
1335    None
1336}
1337
1338/// OID from the first `object <hex>` line in the tag header block, if hex parses.
1339#[must_use]
1340pub fn tag_object_line_oid(data: &[u8]) -> Option<ObjectId> {
1341    let s = tag_header_field(data, b"object ")?;
1342    s.parse().ok()
1343}
1344
1345/// Parsed representation of an annotated tag object.
1346#[derive(Debug, Clone)]
1347pub struct TagData {
1348    /// The object this tag points to.
1349    pub object: ObjectId,
1350    /// The type of the tagged object (e.g. `"commit"`).
1351    pub object_type: String,
1352    /// The short tag name (without `refs/tags/` prefix).
1353    pub tag: String,
1354    /// The tagger identity and timestamp (raw Git format).
1355    pub tagger: Option<String>,
1356    /// Extra header lines after `tagger` and before the blank line (raw bytes, LF-terminated).
1357    pub extra_headers: Vec<u8>,
1358    /// The tag message (everything after the blank line).
1359    pub message: String,
1360}
1361
1362/// Parse the raw data of a tag object.
1363///
1364/// # Errors
1365///
1366/// Returns [`Error::CorruptObject`] if required headers are missing or malformed.
1367pub fn parse_tag(data: &[u8]) -> Result<TagData> {
1368    use super::extra_headers::next_header_block;
1369
1370    #[derive(Clone, Copy)]
1371    enum Continuation {
1372        None,
1373        Extra,
1374    }
1375
1376    let mut pos = 0usize;
1377    let mut object = None;
1378    let mut object_type = None;
1379    let mut tag_name = None;
1380    let mut tagger = None;
1381    let mut extra_headers = Vec::new();
1382    let mut cont = Continuation::None;
1383    let mut past_tagger = false;
1384
1385    while pos < data.len() {
1386        let line_start = pos;
1387        let mut line_end = pos;
1388        while line_end < data.len() && data[line_end] != b'\n' {
1389            line_end += 1;
1390        }
1391        let line = &data[line_start..line_end];
1392        let after_nl = (line_end + 1).min(data.len());
1393        if line.is_empty() {
1394            let body = data.get(after_nl..).unwrap_or_default();
1395            let message = std::str::from_utf8(body)
1396                .map_err(|_| Error::CorruptObject("tag message is not valid UTF-8".to_owned()))?
1397                .to_owned();
1398            return Ok(TagData {
1399                object: object
1400                    .ok_or_else(|| Error::CorruptObject("tag missing object header".to_owned()))?,
1401                object_type: object_type
1402                    .ok_or_else(|| Error::CorruptObject("tag missing type header".to_owned()))?,
1403                tag: tag_name
1404                    .ok_or_else(|| Error::CorruptObject("tag missing tag header".to_owned()))?,
1405                tagger,
1406                extra_headers,
1407                message,
1408            });
1409        }
1410
1411        if past_tagger {
1412            let (_, block_end, _) = next_header_block(data, line_start);
1413            extra_headers.extend_from_slice(&data[line_start..block_end]);
1414            pos = block_end;
1415            continue;
1416        }
1417
1418        if line.first() == Some(&b' ') {
1419            match cont {
1420                Continuation::Extra => {
1421                    extra_headers.extend_from_slice(&data[line_start..after_nl]);
1422                }
1423                Continuation::None => {}
1424            }
1425            pos = after_nl;
1426            continue;
1427        }
1428
1429        let key_end = line
1430            .iter()
1431            .position(|&b| b == b' ')
1432            .ok_or_else(|| Error::CorruptObject("malformed tag header line".to_owned()))?;
1433        let key = &line[..key_end];
1434        let rest = line.get(key_end + 1..).unwrap_or_default();
1435
1436        match key {
1437            b"object" => {
1438                let line_str = std::str::from_utf8(rest).map_err(|_| {
1439                    Error::CorruptObject("tag object line is not valid UTF-8".to_owned())
1440                })?;
1441                object = Some(line_str.trim().parse::<ObjectId>()?);
1442                cont = Continuation::None;
1443            }
1444            b"type" => {
1445                let typ = std::str::from_utf8(rest).map_err(|_| {
1446                    Error::CorruptObject("tag type line is not valid UTF-8".to_owned())
1447                })?;
1448                let typ = typ.trim();
1449                if ObjectKind::from_tag_type_field(typ.as_bytes()).is_none() {
1450                    return Err(Error::CorruptObject(format!(
1451                        "invalid 'type' value in tag: {typ}"
1452                    )));
1453                }
1454                object_type = Some(typ.to_owned());
1455                cont = Continuation::None;
1456            }
1457            b"tag" => {
1458                let line_str = std::str::from_utf8(rest).map_err(|_| {
1459                    Error::CorruptObject("tag name line is not valid UTF-8".to_owned())
1460                })?;
1461                tag_name = Some(line_str.trim().to_owned());
1462                cont = Continuation::None;
1463            }
1464            b"tagger" => {
1465                tagger = Some(
1466                    std::str::from_utf8(rest)
1467                        .map_err(|_| {
1468                            Error::CorruptObject("tag tagger line is not valid UTF-8".to_owned())
1469                        })?
1470                        .to_owned(),
1471                );
1472                past_tagger = true;
1473                cont = Continuation::None;
1474            }
1475            _ => {
1476                cont = Continuation::Extra;
1477                extra_headers.extend_from_slice(&data[line_start..after_nl]);
1478            }
1479        }
1480        pos = after_nl;
1481    }
1482
1483    Err(Error::CorruptObject(
1484        "tag missing blank line before message".to_owned(),
1485    ))
1486}
1487
1488/// Serialize a [`TagData`] into the raw bytes suitable for storage as a tag object.
1489///
1490/// The caller is responsible for supplying a correctly-formatted `tagger` string
1491/// (including timestamp and timezone) when present.
1492#[must_use]
1493pub fn serialize_tag(t: &TagData) -> Vec<u8> {
1494    let mut out = Vec::new();
1495    out.extend_from_slice(format!("object {}\n", t.object).as_bytes());
1496    out.extend_from_slice(format!("type {}\n", t.object_type).as_bytes());
1497    out.extend_from_slice(format!("tag {}\n", t.tag).as_bytes());
1498    if let Some(ref tagger) = t.tagger {
1499        out.extend_from_slice(format!("tagger {tagger}\n").as_bytes());
1500    }
1501    out.extend_from_slice(&t.extra_headers);
1502    out.push(b'\n');
1503    let msg = t.message.trim_end_matches('\n');
1504    if !msg.is_empty() {
1505        out.extend_from_slice(msg.as_bytes());
1506        out.push(b'\n');
1507    }
1508    out
1509}
1510
1511/// Serialize a [`CommitData`] into the raw bytes suitable for storage.
1512///
1513/// The caller is responsible for supplying a correctly-formatted `author` and
1514/// `committer` string (including timestamp and timezone). To create a commit from
1515/// typed identities instead, use [`Objects::write_commit`](crate::objects::Objects::write_commit)
1516/// with a [`NewCommit`](crate::objects::NewCommit).
1517///
1518/// The message body is written exactly as given: `git commit` and `git commit-tree -m`
1519/// supply a trailing LF; `git commit-tree` reading from stdin or `-F` does not add one.
1520#[must_use]
1521pub fn serialize_commit(c: &CommitData) -> Vec<u8> {
1522    let mut out = Vec::new();
1523    out.extend_from_slice(format!("tree {}\n", c.tree).as_bytes());
1524    for p in &c.parents {
1525        out.extend_from_slice(format!("parent {p}\n").as_bytes());
1526    }
1527    out.extend_from_slice(b"author ");
1528    if c.author_raw.is_empty() {
1529        out.extend_from_slice(c.author.as_bytes());
1530    } else {
1531        out.extend_from_slice(&c.author_raw);
1532    }
1533    out.push(b'\n');
1534    out.extend_from_slice(b"committer ");
1535    if c.committer_raw.is_empty() {
1536        out.extend_from_slice(c.committer.as_bytes());
1537    } else {
1538        out.extend_from_slice(&c.committer_raw);
1539    }
1540    out.push(b'\n');
1541    if let Some(enc) = &c.encoding {
1542        out.extend_from_slice(format!("encoding {enc}\n").as_bytes());
1543    }
1544    out.extend_from_slice(&c.extra_headers);
1545    out.push(b'\n');
1546    if let Some(raw) = &c.raw_message {
1547        out.extend_from_slice(raw);
1548    } else if !c.message.is_empty() {
1549        out.extend_from_slice(c.message.as_bytes());
1550    }
1551    out
1552}
1553
1554#[cfg(test)]
1555mod object_id_prefix_tests {
1556    use super::*;
1557
1558    const ID: &str = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
1559
1560    fn id() -> ObjectId {
1561        ObjectId::from_hex(ID).unwrap()
1562    }
1563
1564    fn prefix(hex: &str) -> ObjectIdPrefix {
1565        ObjectIdPrefix::from_hex(hex, HashAlgo::Sha1).unwrap()
1566    }
1567
1568    #[test]
1569    fn matches_every_length_of_the_id_and_rejects_a_changed_last_digit() {
1570        let id = id();
1571        for n in 0..=ID.len() {
1572            let p = prefix(&ID[..n]);
1573            assert_eq!(p.nibbles(), n);
1574            assert_eq!(p.is_empty(), n == 0);
1575            assert!(p.matches(&id), "{n} digits");
1576            assert_eq!(p.to_string(), ID[..n]);
1577            if n > 0 {
1578                let mut changed = ID[..n].to_owned();
1579                let last = changed.pop().unwrap();
1580                changed.push(if last == '0' { '1' } else { '0' });
1581                assert!(!prefix(&changed).matches(&id), "{changed}");
1582            }
1583        }
1584    }
1585
1586    #[test]
1587    fn odd_prefix_ignores_the_low_nibble_of_its_last_byte() {
1588        let p = prefix("4b8");
1589        assert!(p.matches(&ObjectId::from_hex("4b80000000000000000000000000000000000000").unwrap()));
1590        assert!(p.matches(&ObjectId::from_hex("4b8fffffffffffffffffffffffffffffffffffff").unwrap()));
1591        assert!(
1592            !p.matches(&ObjectId::from_hex("4b90000000000000000000000000000000000000").unwrap())
1593        );
1594    }
1595
1596    #[test]
1597    fn parsing_is_case_insensitive_and_validates() {
1598        assert_eq!(prefix("4B825DC"), prefix("4b825dc"));
1599        assert!(matches!(
1600            ObjectIdPrefix::from_hex("4b8g", HashAlgo::Sha1),
1601            Err(Error::InvalidObjectId(_))
1602        ));
1603        assert!(matches!(
1604            ObjectIdPrefix::from_hex(&format!("{ID}0"), HashAlgo::Sha1),
1605            Err(Error::InvalidObjectId(_))
1606        ));
1607        let sha256 = "a".repeat(64);
1608        assert_eq!(
1609            ObjectIdPrefix::from_hex(&sha256, HashAlgo::Sha256)
1610                .unwrap()
1611                .nibbles(),
1612            64
1613        );
1614        assert_eq!(prefix("").algo(), HashAlgo::Sha1);
1615    }
1616
1617    #[test]
1618    fn longer_prefix_never_matches_a_shorter_digest() {
1619        let long = ObjectIdPrefix::from_hex(&format!("{ID}0000"), HashAlgo::Sha256).unwrap();
1620        assert!(!long.matches(&id()));
1621        assert!(ObjectIdPrefix::from_hex(ID, HashAlgo::Sha256)
1622            .unwrap()
1623            .matches(&id()));
1624    }
1625
1626    #[test]
1627    fn bounds_are_the_first_and_last_matching_ids() {
1628        let p = prefix("4b8");
1629        assert_eq!(
1630            p.lowest().to_hex(),
1631            "4b80000000000000000000000000000000000000"
1632        );
1633        assert_eq!(
1634            p.highest().to_hex(),
1635            "4b8fffffffffffffffffffffffffffffffffffff"
1636        );
1637        let even = prefix("4b82");
1638        assert_eq!(
1639            even.lowest().to_hex(),
1640            "4b82000000000000000000000000000000000000"
1641        );
1642        assert_eq!(
1643            even.highest().to_hex(),
1644            "4b82ffffffffffffffffffffffffffffffffffff"
1645        );
1646        assert_eq!(prefix("").lowest(), ObjectId::null(HashAlgo::Sha1));
1647        assert_eq!(prefix("").highest().to_hex(), "f".repeat(40));
1648        assert_eq!(prefix(ID).lowest(), id());
1649        assert_eq!(prefix(ID).highest(), id());
1650        let sha256 = ObjectIdPrefix::from_hex("abc", HashAlgo::Sha256).unwrap();
1651        assert_eq!(sha256.highest().to_hex(), format!("abc{}", "f".repeat(61)));
1652        assert_eq!(format!("{sha256:?}"), "ObjectIdPrefix(abc)");
1653    }
1654
1655    #[test]
1656    fn common_prefix_nibbles_counts_shared_leading_digits() {
1657        let a = id();
1658        let hex = |s: &str| ObjectId::from_hex(s).unwrap();
1659        assert_eq!(a.common_prefix_nibbles(&a), 40);
1660        assert_eq!(
1661            a.common_prefix_nibbles(&hex("4b825dc642cb6eb9a060e54bf8d69288fbee4905")),
1662            39
1663        );
1664        assert_eq!(
1665            a.common_prefix_nibbles(&hex("4b815dc642cb6eb9a060e54bf8d69288fbee4904")),
1666            3
1667        );
1668        assert_eq!(
1669            a.common_prefix_nibbles(&hex("4b925dc642cb6eb9a060e54bf8d69288fbee4904")),
1670            2
1671        );
1672        assert_eq!(
1673            a.common_prefix_nibbles(&hex("5b825dc642cb6eb9a060e54bf8d69288fbee4904")),
1674            0
1675        );
1676        assert_eq!(
1677            a.common_prefix_nibbles(&hex("4c825dc642cb6eb9a060e54bf8d69288fbee4904")),
1678            1
1679        );
1680    }
1681}
1682
1683#[cfg(test)]
1684mod commit_parse_tests {
1685    use super::*;
1686
1687    #[test]
1688    fn empty_tree_constant_is_canonical_sha1_digest() {
1689        assert_eq!(
1690            ObjectId::empty_tree().to_hex(),
1691            "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
1692        );
1693    }
1694
1695    #[test]
1696    fn is_empty_tree_for_matches_hashed_empty_tree() {
1697        for algo in [HashAlgo::Sha1, HashAlgo::Sha256] {
1698            let hashed =
1699                crate::hash::hash_object(algo, ObjectKind::Tree, b"").expect("hash empty tree");
1700            assert!(hashed.is_empty_tree_for(algo), "{algo:?}");
1701            let other = match algo {
1702                HashAlgo::Sha1 => HashAlgo::Sha256,
1703                HashAlgo::Sha256 => HashAlgo::Sha1,
1704            };
1705            assert!(!hashed.is_empty_tree_for(other), "{algo:?}");
1706            let empty_blob =
1707                crate::hash::hash_object(algo, ObjectKind::Blob, b"").expect("hash empty blob");
1708            assert!(!empty_blob.is_empty_tree_for(algo), "{algo:?}");
1709        }
1710        let legacy = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf899d69f7c6948d4").expect("hex");
1711        assert!(legacy.is_empty_tree_for(HashAlgo::Sha1));
1712        assert!(!legacy.is_empty_tree_for(HashAlgo::Sha256));
1713    }
1714
1715    #[test]
1716    fn serialize_tree_mode_matches_mode_str() {
1717        for mode in [
1718            0,
1719            0o7,
1720            0o100644,
1721            0o100755,
1722            0o120000,
1723            0o040000,
1724            0o160000,
1725            u32::MAX,
1726        ] {
1727            let mut out = Vec::new();
1728            push_tree_mode(&mut out, mode);
1729            let entry = TreeEntry {
1730                mode,
1731                name: Vec::new(),
1732                oid: ObjectId::empty_tree(),
1733            };
1734            assert_eq!(out, entry.mode_str().into_bytes(), "mode {mode:o}");
1735        }
1736    }
1737
1738    #[test]
1739    fn is_empty_blob_matches_both_hash_algorithms() {
1740        for algo in [HashAlgo::Sha1, HashAlgo::Sha256] {
1741            let oid = algo.hash_object(ObjectKind::Blob, b"").unwrap();
1742            assert!(oid.is_empty_blob(), "{algo:?} empty blob {}", oid.to_hex());
1743            let other = algo.hash_object(ObjectKind::Blob, b"x").unwrap();
1744            assert!(!other.is_empty_blob());
1745        }
1746        assert!(!ObjectId::empty_tree().is_empty_blob());
1747        assert!(!ObjectId::zero().is_empty_blob());
1748    }
1749
1750    #[test]
1751    fn commit_subject_pretty_folds_first_paragraph() {
1752        assert_eq!(commit_subject_pretty("hello\nworld\n\nbody"), "hello world");
1753        assert_eq!(commit_subject_pretty(""), "");
1754        assert_eq!(
1755            commit_subject_pretty("  hello   world\tthere  \n  next\t line  \n\nbody\n"),
1756            "  hello   world\tthere   next\t line"
1757        );
1758    }
1759
1760    #[test]
1761    fn commit_ref_matches_parse_commit_headers() {
1762        let raw = concat!(
1763            "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
1764            "parent 1111111111111111111111111111111111111111\n",
1765            "author A <a@example.com> 1 +0000\n",
1766            "committer C <c@example.com> 2 +0000\n",
1767            "\n",
1768            "ignored body\n",
1769        );
1770        let full = parse_commit(raw.as_bytes()).expect("parse");
1771        let link = crate::objects::CommitRef::parse(raw.as_bytes()).expect("CommitRef");
1772        assert_eq!(link.tree(), full.tree);
1773        assert_eq!(link.parents(), full.parents.as_slice());
1774        assert_eq!(link.author(), full.author);
1775        assert_eq!(link.committer(), full.committer);
1776    }
1777
1778    #[test]
1779    fn parse_commit_skips_multiline_gpgsig_continuation() {
1780        let raw = concat!(
1781            "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
1782            "author A U Thor <author@example.com> 1 +0000\n",
1783            "committer C O Mitter <committer@example.com> 1 +0000\n",
1784            "gpgsig -----BEGIN PGP SIGNATURE-----\n",
1785            " abcdef\n",
1786            " -----END PGP SIGNATURE-----\n",
1787            "\n",
1788            "msg\n",
1789        );
1790        let c = parse_commit(raw.as_bytes()).expect("parse signed commit");
1791        assert_eq!(c.tree.to_hex(), "4b825dc642cb6eb9a060e54bf8d69288fbee4904");
1792        assert_eq!(c.message, "msg\n");
1793        assert_eq!(serialize_commit(&c), raw.as_bytes());
1794    }
1795
1796    #[test]
1797    fn hex_parsing_accepts_both_cases_and_lengths_and_rejects_the_rest() {
1798        let sha1 = "0123456789abcdefABCDEF0123456789abcdef01";
1799        let oid = ObjectId::from_hex(sha1).unwrap();
1800        assert_eq!(oid.algo(), HashAlgo::Sha1);
1801        assert_eq!(oid.to_hex(), sha1.to_ascii_lowercase());
1802        assert_eq!(ObjectId::from_hex_ascii(sha1.as_bytes()).unwrap(), oid);
1803        assert_eq!(sha1.parse::<ObjectId>().unwrap(), oid);
1804
1805        let sha256 = "f".repeat(64);
1806        let oid = sha256.parse::<ObjectId>().unwrap();
1807        assert_eq!(oid.algo(), HashAlgo::Sha256);
1808        assert_eq!(oid.as_bytes(), [0xff; 32]);
1809        assert!(ObjectId::from_hex_ascii(sha256.as_bytes()).is_err());
1810
1811        for bad in [
1812            "",
1813            &sha1[..39],
1814            &format!("{sha1}0"),
1815            "g123456789abcdef0123456789abcdef01234567",
1816            "0123456789abcdef0123456789abcdef0123456 ",
1817            "0123456789abcdef0123456789abcdef012345/7",
1818            "0123456789abcdef0123456789abcdef012345:7",
1819            "0123456789abcdef0123456789abcdef012345`7",
1820            "0123456789abcdef0123456789abcdef012345G7",
1821        ] {
1822            assert!(
1823                matches!(ObjectId::from_hex(bad), Err(Error::InvalidObjectId(s)) if s == bad),
1824                "{bad:?}"
1825            );
1826            assert!(bad.parse::<ObjectId>().is_err(), "{bad:?}");
1827            assert!(ObjectId::from_hex_ascii(bad.as_bytes()).is_err(), "{bad:?}");
1828        }
1829        let mut non_ascii = sha1.as_bytes().to_vec();
1830        non_ascii[3] = 0xb0;
1831        assert!(ObjectId::from_hex_ascii(&non_ascii).is_err());
1832    }
1833
1834    #[test]
1835    fn serialize_commit_applies_public_field_edits_after_parse() {
1836        let raw = concat!(
1837            "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
1838            "author A <a@example.com> 1 +0000\n",
1839            "committer C <c@example.com> 1 +0000\n",
1840            "\n",
1841            "msg\n",
1842        );
1843        let mut commit = parse_commit(raw.as_bytes()).expect("parse");
1844        let zero = ObjectId::from_hex("0000000000000000000000000000000000000000").unwrap();
1845        commit.tree = zero;
1846        let serialized = serialize_commit(&commit);
1847        let tree_line = format!("tree {}\n", zero.to_hex());
1848        assert!(
1849            serialized.starts_with(tree_line.as_bytes()),
1850            "serialize_commit ignored the updated public tree field"
1851        );
1852    }
1853
1854    #[test]
1855    fn parse_commit_preserves_gpgsig_mergetag_bytes_exactly() {
1856        let raw = concat!(
1857            "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
1858            "author A <a@example.com> 1 +0000\n",
1859            "committer C <c@example.com> 1 +0000\n",
1860            "encoding ISO-8859-1\n",
1861            "gpgsig -----BEGIN PGP SIGNATURE-----\n",
1862            " line2\n",
1863            " -----END PGP SIGNATURE-----\n",
1864            "mergetag object 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
1865            " type commit\n",
1866            " tag merge-tag\n",
1867            " tagger T <t@example.com> 1 +0000\n",
1868            " -----BEGIN PGP SIGNATURE-----\n",
1869            " inner\n",
1870            " -----END PGP SIGNATURE-----\n",
1871            "\n",
1872            "msg-only\n",
1873        );
1874        let parsed = parse_commit(raw.as_bytes()).expect("parse");
1875        assert_eq!(serialize_commit(&parsed), raw.as_bytes());
1876    }
1877
1878    #[test]
1879    fn tree_entry_mode_str_and_tag_object_line_oid() {
1880        let oid = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap();
1881        let entry = TreeEntry {
1882            mode: 0o040000,
1883            name: b"dir".to_vec(),
1884            oid,
1885        };
1886        assert_eq!(entry.mode_str(), "40000");
1887        let tag_body = format!("object {}\ntype commit\ntag t\n\n", oid.to_hex());
1888        assert_eq!(tag_object_line_oid(tag_body.as_bytes()), Some(oid));
1889    }
1890
1891    #[test]
1892    fn parse_commit_non_commit_bytes_does_not_panic() {
1893        let tree_bytes = b"100644 blob 0123456789012345678901234567890123456789\tf";
1894        let err = parse_commit(tree_bytes).expect_err("tree bytes are not a commit");
1895        assert!(matches!(err, Error::CorruptObject(_)));
1896    }
1897
1898    #[test]
1899    fn parse_tree_with_sha256_oid_width() {
1900        let oid_bytes = [0x6eu8; 32];
1901        let oid = ObjectId::from_bytes(&oid_bytes).expect("oid");
1902        let mut raw = b"100644 file\0".to_vec();
1903        raw.extend_from_slice(oid.as_bytes());
1904        let entries = parse_tree_with_oid_len(&raw, HashAlgo::Sha256.len()).expect("tree");
1905        assert_eq!(entries.len(), 1);
1906        assert_eq!(entries[0].oid, oid);
1907    }
1908
1909    #[test]
1910    fn parse_commit_tree_and_parents_prefix_matches_full_parse_or_declines() -> Result<()> {
1911        let tree = "1111111111111111111111111111111111111111";
1912        let p1 = "2222222222222222222222222222222222222222";
1913        let p2 = "3333333333333333333333333333333333333333";
1914        let raw = format!(
1915            "tree {tree}\nparent {p1}\nparent {p2}\nauthor A <a@x> 1 +0000\n\
1916             committer C <c@x> 2 +0000\n\nmessage\n"
1917        );
1918        let commit = parse_commit(raw.as_bytes())?;
1919        let full = (commit.tree, commit.parents);
1920        assert_eq!(full.1.len(), 2);
1921        assert_eq!(
1922            parse_commit_tree_and_parents_lenient(raw.as_bytes(), HashAlgo::Sha1.hex_len()),
1923            Some(full.clone())
1924        );
1925        let hex_len = HashAlgo::Sha1.hex_len();
1926        let header_end = raw.find("author").expect("author line");
1927        for cut in 0..=raw.len() {
1928            let got = parse_commit_tree_and_parents_prefix(&raw.as_bytes()[..cut], hex_len);
1929            if cut > header_end {
1930                assert_eq!(got.as_ref(), Some(&full), "cut at {cut}");
1931            } else {
1932                assert_eq!(
1933                    got, None,
1934                    "cut at {cut} must not claim a complete parent list"
1935                );
1936            }
1937        }
1938        Ok(())
1939    }
1940
1941    #[test]
1942    fn commit_tree_and_parents_prefix_never_disagrees_with_whole_object() {
1943        let tree = "tree 1111111111111111111111111111111111111111\n";
1944        let parent = "parent 2222222222222222222222222222222222222222\n";
1945        let parent2 = "parent 3333333333333333333333333333333333333333\n";
1946        let bodies = [
1947            format!("{tree}{parent}author A <a@x> 1 +0000\n\nmessage\n"),
1948            format!("{tree}{parent}{parent2}committer C <c@x> 2 +0000\n\nno author\n"),
1949            format!("{tree}{parent}"),
1950            format!("{tree}{parent}{parent2}"),
1951            format!("{tree}author A <a@x> 1 +0000\n{parent}\nlate parent is ignored\n"),
1952            format!("{tree}{parent}not a header\n"),
1953            format!("{tree}parent zz\n\n"),
1954            format!("{tree}{parent}parent 2222"),
1955            format!("author A <a@x> 1 +0000\n{tree}\n"),
1956        ];
1957        let hex_len = HashAlgo::Sha1.hex_len();
1958        for body in &bodies {
1959            let whole = parse_commit_tree_and_parents_lenient(body.as_bytes(), hex_len);
1960            for cut in 0..=body.len() {
1961                if let Some(got) =
1962                    parse_commit_tree_and_parents_prefix(&body.as_bytes()[..cut], hex_len)
1963                {
1964                    assert_eq!(Some(got), whole, "cut {cut} of {body:?}");
1965                }
1966            }
1967        }
1968        let parents_of = |body: &str| {
1969            parse_commit_tree_and_parents_lenient(body.as_bytes(), hex_len).map(|(_, p)| p.len())
1970        };
1971        assert_eq!(parents_of(&bodies[1]), Some(2), "missing author is fine");
1972        assert_eq!(
1973            parents_of(&bodies[3]),
1974            Some(2),
1975            "object may end after parents"
1976        );
1977        assert_eq!(
1978            parents_of(&bodies[4]),
1979            Some(0),
1980            "only leading parents count"
1981        );
1982        assert_eq!(parents_of(&bodies[6]), None, "malformed parent line");
1983        assert_eq!(parents_of(&bodies[7]), None, "truncated parent line");
1984        assert_eq!(parents_of(&bodies[8]), None, "tree must come first");
1985    }
1986
1987    #[test]
1988    fn parse_commit_tree_and_parents_prefix_handles_root_and_sha256() -> Result<()> {
1989        let root = b"tree 1111111111111111111111111111111111111111\nauthor A <a@x> 1 +0000\n";
1990        let (tree, parents) =
1991            parse_commit_tree_and_parents_prefix(root, HashAlgo::Sha1.hex_len()).expect("root");
1992        assert_eq!(tree.to_hex(), "1111111111111111111111111111111111111111");
1993        assert!(parents.is_empty());
1994
1995        let t = "a".repeat(64);
1996        let p = "b".repeat(64);
1997        let raw = format!("tree {t}\nparent {p}\ncommitter C <c@x> 2 +0000\n");
1998        let (tree, parents) =
1999            parse_commit_tree_and_parents_prefix(raw.as_bytes(), HashAlgo::Sha256.hex_len())
2000                .expect("sha256");
2001        assert_eq!(tree.to_hex(), t);
2002        assert_eq!(parents.len(), 1);
2003        assert_eq!(parents[0].to_hex(), p);
2004
2005        assert!(parse_commit_tree_and_parents_prefix(raw.as_bytes(), 40).is_none());
2006        assert!(parse_commit_tree_and_parents_prefix(b"parent x\n", 40).is_none());
2007        assert!(parse_commit_tree_and_parents_prefix(
2008            b"tree zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz\nauthor\n",
2009            40
2010        )
2011        .is_none());
2012        Ok(())
2013    }
2014
2015    #[test]
2016    fn object_id_loose_path_helpers() {
2017        let oid = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap();
2018        assert_eq!(oid.loose_prefix(), "4b");
2019        assert_eq!(oid.loose_suffix().len(), 38);
2020        assert!(ObjectId::is_hex_len(40));
2021        assert!(ObjectId::is_loose_suffix_len(38));
2022    }
2023
2024    #[test]
2025    fn tree_ref_iter_yields_at_most_one_error() {
2026        let bad = b"100644 incomplete";
2027        let mut iter = TreeRefIter::with_oid_len(bad, HashAlgo::Sha1.len());
2028        assert!(iter.next().unwrap().is_err());
2029        assert!(iter.next().is_none());
2030    }
2031
2032    #[test]
2033    fn tree_ref_iter_public_api_smoke() {
2034        let data = serialize_tree(&[TreeEntry {
2035            mode: 0o100644,
2036            name: b"a".to_vec(),
2037            oid: ObjectId::from_hex("0123456789012345678901234567890123456789").unwrap(),
2038        }]);
2039        let mut iter = TreeRefIter::infer_oid_len(&data).expect("infer");
2040        let entry = iter.next().unwrap().expect("entry");
2041        assert_eq!(entry.name, b"a");
2042        let _owned = entry.to_owned();
2043        let _vec = parse_tree(&data).expect("parse");
2044        let _ = TreeRefIter::with_oid_len(&data, HashAlgo::Sha1.len());
2045    }
2046
2047    #[test]
2048    fn find_tree_entry_by_name_in_data_locates_directory_in_large_tree() -> Result<()> {
2049        let dummy = ObjectId::from_hex("0123456789012345678901234567890123456789")?;
2050        let mut entries: Vec<TreeEntry> = (1..=60)
2051            .map(|i| TreeEntry {
2052                mode: 0o100644,
2053                name: format!("f{i}.txt").into_bytes(),
2054                oid: dummy,
2055            })
2056            .collect();
2057        entries.push(TreeEntry {
2058            mode: 0o040000,
2059            name: b"sub".to_vec(),
2060            oid: dummy,
2061        });
2062        entries.sort_by(|a, b| {
2063            tree_entry_cmp(&a.name, a.mode == 0o040000, &b.name, b.mode == 0o040000)
2064        });
2065        let data = serialize_tree(&entries);
2066        assert!(entries.len() >= 48);
2067        let found = find_tree_entry_by_name_in_data(&data, HashAlgo::Sha1.len(), b"sub", true)?
2068            .expect("sub directory in wide tree");
2069        assert_eq!(found.mode, 0o040000);
2070        assert_eq!(found.name, b"sub");
2071        Ok(())
2072    }
2073
2074    fn sorted_tree_data(entries: &mut [TreeEntry]) -> Vec<u8> {
2075        entries.sort_by(|a, b| {
2076            tree_entry_cmp(&a.name, a.mode == 0o040000, &b.name, b.mode == 0o040000)
2077        });
2078        serialize_tree(entries)
2079    }
2080
2081    #[test]
2082    fn find_tree_entry_by_name_in_data_matches_parse_tree_for_every_name() -> Result<()> {
2083        let oid_a = ObjectId::from_hex("1111111111111111111111111111111111111111")?;
2084        let oid_b = ObjectId::from_hex("2222222222222222222222222222222222222222")?;
2085        let mut entries = vec![
2086            TreeEntry {
2087                mode: 0o040000,
2088                name: b"foo".to_vec(),
2089                oid: oid_a,
2090            },
2091            TreeEntry {
2092                mode: 0o100644,
2093                name: b"foo.c".to_vec(),
2094                oid: oid_b,
2095            },
2096            TreeEntry {
2097                mode: 0o100755,
2098                name: b"foo-bar".to_vec(),
2099                oid: oid_b,
2100            },
2101            TreeEntry {
2102                mode: 0o120000,
2103                name: b"link".to_vec(),
2104                oid: oid_a,
2105            },
2106            TreeEntry {
2107                mode: 0o160000,
2108                name: b"sub".to_vec(),
2109                oid: oid_b,
2110            },
2111            TreeEntry {
2112                mode: 0o100644,
2113                name: b"README".to_vec(),
2114                oid: oid_a,
2115            },
2116        ];
2117        let data = sorted_tree_data(&mut entries);
2118        for expected in parse_tree(&data)? {
2119            let name_is_tree = expected.mode == 0o040000;
2120            let found = find_tree_entry_by_name_in_data(
2121                &data,
2122                HashAlgo::Sha1.len(),
2123                &expected.name,
2124                name_is_tree,
2125            )?
2126            .expect("entry present");
2127            assert_eq!(found, expected);
2128        }
2129        for missing in [&b"fo"[..], b"foo/", b"foo.d", b"zzz", b"", b"AAA", b"lin"] {
2130            assert!(
2131                find_tree_entry_by_name_in_data(&data, HashAlgo::Sha1.len(), missing, false)?
2132                    .is_none(),
2133                "unexpected match for {:?}",
2134                String::from_utf8_lossy(missing)
2135            );
2136        }
2137        Ok(())
2138    }
2139
2140    #[test]
2141    fn find_tree_entry_by_name_in_data_stops_after_sorted_position() -> Result<()> {
2142        let oid = ObjectId::from_hex("3333333333333333333333333333333333333333")?;
2143        let mut entries = vec![
2144            TreeEntry {
2145                mode: 0o100644,
2146                name: b"a".to_vec(),
2147                oid,
2148            },
2149            TreeEntry {
2150                mode: 0o100644,
2151                name: b"c".to_vec(),
2152                oid,
2153            },
2154        ];
2155        let mut data = sorted_tree_data(&mut entries);
2156        data.extend_from_slice(b"garbage without separators");
2157        let found = find_tree_entry_by_name_in_data(&data, HashAlgo::Sha1.len(), b"a", false)?;
2158        assert_eq!(found.map(|e| e.name), Some(b"a".to_vec()));
2159        assert!(
2160            find_tree_entry_by_name_in_data(&data, HashAlgo::Sha1.len(), b"b", false)?.is_none()
2161        );
2162        assert!(matches!(
2163            find_tree_entry_by_name_in_data(&data, HashAlgo::Sha1.len(), b"d", false),
2164            Err(Error::CorruptObject(_))
2165        ));
2166        Ok(())
2167    }
2168
2169    #[test]
2170    fn find_tree_entry_by_name_in_data_rejects_bad_mode_and_truncated_oid() {
2171        let bad_mode = b"10x644 a\0aaaaaaaaaaaaaaaaaaaa";
2172        assert!(matches!(
2173            find_tree_entry_by_name_in_data(bad_mode, HashAlgo::Sha1.len(), b"a", false),
2174            Err(Error::CorruptObject(_))
2175        ));
2176        let truncated = b"100644 a\0short";
2177        assert!(matches!(
2178            find_tree_entry_by_name_in_data(truncated, HashAlgo::Sha1.len(), b"a", false),
2179            Err(Error::CorruptObject(_))
2180        ));
2181    }
2182
2183    #[test]
2184    fn find_tree_entry_by_name_in_data_reads_sha256_width() -> Result<()> {
2185        let oid = ObjectId::from_bytes(&[0x5au8; 32])?;
2186        let mut raw = b"100644 file\0".to_vec();
2187        raw.extend_from_slice(oid.as_bytes());
2188        let found = find_tree_entry_by_name_in_data(&raw, HashAlgo::Sha256.len(), b"file", false)?
2189            .expect("file");
2190        assert_eq!(found.oid, oid);
2191        assert_eq!(found.mode, 0o100644);
2192        Ok(())
2193    }
2194}