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