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