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