1use std::fmt;
22use std::str::FromStr;
23
24use crate::commit_encoding;
25use crate::error::{Error, Result};
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
33pub enum HashAlgo {
34 #[default]
36 Sha1,
37 Sha256,
39}
40
41impl HashAlgo {
42 #[must_use]
44 pub const fn len(self) -> usize {
45 match self {
46 Self::Sha1 => 20,
47 Self::Sha256 => 32,
48 }
49 }
50
51 #[must_use]
53 pub const fn is_empty(self) -> bool {
54 false
55 }
56
57 #[must_use]
59 pub const fn hex_len(self) -> usize {
60 self.len() * 2
61 }
62
63 #[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 #[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 #[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 #[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#[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
126const MAX_OID_LEN: usize = 32;
128
129const WELL_KNOWN_EMPTY_TREE_CANON: [u8; 20] = [
131 0x4b, 0x82, 0x5d, 0xc6, 0x42, 0xcb, 0x6e, 0xb9, 0xa0, 0x60, 0xe5, 0x4b, 0xf8, 0xd6, 0x92, 0x88,
132 0xfb, 0xee, 0x49, 0x04,
133];
134const WELL_KNOWN_EMPTY_TREE_LEGACY: [u8; 20] = [
136 0x4b, 0x82, 0x5d, 0xc6, 0x42, 0xcb, 0x6e, 0xb9, 0xa0, 0x60, 0xe5, 0x4b, 0xf8, 0x99, 0xd6, 0x9f,
137 0x7c, 0x69, 0x48, 0xd4,
138];
139const EMPTY_TREE_SHA256: [u8; 32] = [
141 0x6e, 0xf1, 0x9b, 0x41, 0x22, 0x5c, 0x53, 0x69, 0xf1, 0xc1, 0x04, 0xd4, 0x5d, 0x8d, 0x85, 0xef,
142 0xa9, 0xb0, 0x57, 0xb5, 0x3b, 0x14, 0xb4, 0xb9, 0xb9, 0x39, 0xdd, 0x74, 0xde, 0xcc, 0x53, 0x21,
143];
144const EMPTY_BLOB_SHA1: [u8; 20] = [
146 0xe6, 0x9d, 0xe2, 0x9b, 0xb2, 0xd1, 0xd6, 0x43, 0x4b, 0x8b, 0x29, 0xae, 0x77, 0x5a, 0xd8, 0xc2,
147 0xe4, 0x8c, 0x53, 0x91,
148];
149const EMPTY_BLOB_SHA256: [u8; 32] = [
151 0x47, 0x3a, 0x0f, 0x4c, 0x3b, 0xe8, 0xa9, 0x36, 0x81, 0xa2, 0x67, 0xe3, 0xb1, 0xe9, 0xa7, 0xdc,
152 0xda, 0x11, 0x85, 0x43, 0x6f, 0xe1, 0x41, 0xf7, 0x74, 0x91, 0x20, 0xa3, 0x03, 0x72, 0x18, 0x13,
153];
154
155#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
173pub struct ObjectId {
174 bytes: [u8; MAX_OID_LEN],
175 len: u8,
176}
177
178impl ObjectId {
179 #[must_use]
185 pub const fn zero() -> Self {
186 Self {
187 bytes: [0u8; MAX_OID_LEN],
188 len: 20,
189 }
190 }
191
192 #[must_use]
194 pub const fn null(algo: HashAlgo) -> Self {
195 Self {
196 bytes: [0u8; MAX_OID_LEN],
197 len: algo.len() as u8,
198 }
199 }
200
201 pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
208 if HashAlgo::from_len(bytes.len()).is_none() {
209 return Err(Error::InvalidObjectId(hex::encode(bytes)));
210 }
211 let mut buf = [0u8; MAX_OID_LEN];
212 buf[..bytes.len()].copy_from_slice(bytes);
213 Ok(Self {
214 bytes: buf,
215 len: bytes.len() as u8,
216 })
217 }
218
219 #[must_use]
221 pub fn as_bytes(&self) -> &[u8] {
222 &self.bytes[..self.len as usize]
223 }
224
225 #[must_use]
227 pub fn algo(&self) -> HashAlgo {
228 HashAlgo::from_len(self.len as usize).unwrap_or(HashAlgo::Sha1)
229 }
230
231 #[must_use]
233 pub fn is_zero(&self) -> bool {
234 self.as_bytes().iter().all(|&b| b == 0)
235 }
236
237 #[must_use]
239 pub(crate) fn common_prefix_nibbles(&self, other: &Self) -> usize {
240 let (a, b) = (self.as_bytes(), other.as_bytes());
241 match a.iter().zip(b).position(|(x, y)| x != y) {
242 Some(i) => i * 2 + usize::from((a[i] ^ b[i]) & 0xf0 == 0),
243 None => a.len().min(b.len()) * 2,
244 }
245 }
246
247 #[must_use]
249 pub(crate) fn is_empty_blob(&self) -> bool {
250 let bytes = self.as_bytes();
251 bytes == EMPTY_BLOB_SHA1 || bytes == EMPTY_BLOB_SHA256
252 }
253
254 #[must_use]
256 pub(crate) fn is_well_known_empty_tree(&self) -> bool {
257 self.is_canonical_empty_tree()
258 || (self.as_bytes().len() == HashAlgo::Sha1.len()
259 && self.as_bytes() == WELL_KNOWN_EMPTY_TREE_LEGACY)
260 }
261
262 #[must_use]
265 pub(crate) fn is_empty_tree_for(&self, algo: HashAlgo) -> bool {
266 match algo {
267 HashAlgo::Sha1 => self.is_well_known_empty_tree(),
268 HashAlgo::Sha256 => self.as_bytes() == EMPTY_TREE_SHA256,
269 }
270 }
271
272 #[must_use]
274 pub(crate) fn is_canonical_empty_tree(&self) -> bool {
275 self.as_bytes().len() == HashAlgo::Sha1.len()
276 && self.as_bytes() == WELL_KNOWN_EMPTY_TREE_CANON
277 }
278
279 #[must_use]
285 pub const fn empty_tree() -> Self {
286 let mut bytes = [0u8; MAX_OID_LEN];
287 let mut i = 0;
288 while i < WELL_KNOWN_EMPTY_TREE_CANON.len() {
289 bytes[i] = WELL_KNOWN_EMPTY_TREE_CANON[i];
290 i += 1;
291 }
292 Self { bytes, len: 20 }
293 }
294
295 pub(crate) fn push_loose_relative(&self, path: &mut std::path::PathBuf) {
297 const HEX: &[u8; 16] = b"0123456789abcdef";
298 let bytes = self.as_bytes();
299 let prefix = [
300 HEX[(bytes[0] >> 4) as usize],
301 HEX[(bytes[0] & 0xf) as usize],
302 ];
303 path.push(unsafe { std::str::from_utf8_unchecked(&prefix) });
305 let suffix_len = (bytes.len() - 1) * 2;
306 let mut suffix = [0u8; 62];
307 for (i, &b) in bytes[1..].iter().enumerate() {
308 suffix[i * 2] = HEX[(b >> 4) as usize];
309 suffix[i * 2 + 1] = HEX[(b & 0xf) as usize];
310 }
311 path.push(unsafe { std::str::from_utf8_unchecked(&suffix[..suffix_len]) });
312 }
313
314 #[must_use]
316 pub(crate) fn loose_path_in(&self, objects_dir: &std::path::Path) -> std::path::PathBuf {
317 let mut path = objects_dir.to_path_buf();
318 self.push_loose_relative(&mut path);
319 path
320 }
321
322 #[must_use]
324 pub fn to_hex(&self) -> String {
325 self.hex_digits().as_str().to_owned()
326 }
327
328 fn hex_digits(&self) -> HexDigits {
330 const DIGITS: &[u8; 16] = b"0123456789abcdef";
331 let mut buf = [0u8; MAX_OID_LEN * 2];
332 for (pair, &byte) in buf.as_chunks_mut::<2>().0.iter_mut().zip(self.as_bytes()) {
333 *pair = [
334 DIGITS[usize::from(byte >> 4)],
335 DIGITS[usize::from(byte & 0x0f)],
336 ];
337 }
338 HexDigits {
339 buf,
340 len: usize::from(self.len) * 2,
341 }
342 }
343
344 #[must_use]
348 pub fn loose_prefix(&self) -> String {
349 hex::encode(&self.bytes[..1])
350 }
351
352 pub fn from_hex(s: &str) -> Result<Self> {
359 Self::decode_hex(s.as_bytes()).ok_or_else(|| Error::InvalidObjectId(s.to_owned()))
360 }
361
362 pub fn from_hex_ascii(bytes: &[u8]) -> Result<Self> {
368 if bytes.len() != HashAlgo::Sha1.hex_len() {
369 return Err(Error::InvalidObjectId(
370 String::from_utf8_lossy(bytes).into_owned(),
371 ));
372 }
373 Self::decode_hex(bytes)
374 .ok_or_else(|| Error::InvalidObjectId(String::from_utf8_lossy(bytes).into_owned()))
375 }
376
377 fn decode_hex(hex: &[u8]) -> Option<Self> {
382 let len = hex.len() / 2;
383 if !Self::is_hex_len(hex.len()) {
384 return None;
385 }
386 let mut bytes = [0u8; MAX_OID_LEN];
387 let mut invalid = 0u8;
388 for (out, pair) in bytes.iter_mut().zip(hex.as_chunks::<2>().0) {
389 let hi = HEX_DECODE[usize::from(pair[0])];
390 let lo = HEX_DECODE[usize::from(pair[1])];
391 invalid |= hi | lo;
392 *out = (hi << 4) | lo;
393 }
394 (invalid & HEX_INVALID == 0).then_some(Self {
395 bytes,
396 len: len as u8,
397 })
398 }
399
400 #[must_use]
403 pub fn loose_suffix(&self) -> String {
404 hex::encode(&self.bytes[1..self.len as usize])
405 }
406
407 #[must_use]
410 pub fn is_full_hex(s: &str) -> bool {
411 (s.len() == HashAlgo::Sha1.hex_len() || s.len() == HashAlgo::Sha256.hex_len())
412 && s.bytes().all(|b| b.is_ascii_hexdigit())
413 }
414
415 #[must_use]
417 pub const fn is_hex_len(len: usize) -> bool {
418 len == HashAlgo::Sha1.hex_len() || len == HashAlgo::Sha256.hex_len()
419 }
420
421 #[must_use]
424 pub const fn is_loose_suffix_len(len: usize) -> bool {
425 len == HashAlgo::Sha1.hex_len() - 2 || len == HashAlgo::Sha256.hex_len() - 2
426 }
427}
428
429impl std::hash::Hash for ObjectId {
430 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
431 state.write(self.as_bytes());
432 }
433}
434
435impl fmt::Display for ObjectId {
436 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
437 f.write_str(self.hex_digits().as_str())
438 }
439}
440
441struct HexDigits {
443 buf: [u8; MAX_OID_LEN * 2],
444 len: usize,
445}
446
447impl HexDigits {
448 fn as_str(&self) -> &str {
449 std::str::from_utf8(&self.buf[..self.len]).unwrap_or_default()
450 }
451}
452
453impl fmt::Debug for ObjectId {
454 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
455 write!(f, "ObjectId({})", self.to_hex())
456 }
457}
458
459#[cfg(feature = "serde-models")]
460impl serde::Serialize for ObjectId {
461 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
462 where
463 S: serde::Serializer,
464 {
465 serializer.serialize_str(&self.to_hex())
466 }
467}
468
469#[cfg(feature = "serde-models")]
470impl<'de> serde::Deserialize<'de> for ObjectId {
471 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
472 where
473 D: serde::Deserializer<'de>,
474 {
475 let hex = String::deserialize(deserializer)?;
476 Self::from_hex(&hex).map_err(serde::de::Error::custom)
477 }
478}
479
480const HEX_INVALID: u8 = 0x10;
482
483const HEX_DECODE: [u8; 256] = {
485 let mut table = [HEX_INVALID; 256];
486 let mut i = 0u8;
487 while i < 10 {
488 table[(b'0' + i) as usize] = i;
489 i += 1;
490 }
491 let mut i = 0u8;
492 while i < 6 {
493 table[(b'a' + i) as usize] = 10 + i;
494 table[(b'A' + i) as usize] = 10 + i;
495 i += 1;
496 }
497 table
498};
499
500impl FromStr for ObjectId {
501 type Err = Error;
502
503 fn from_str(s: &str) -> Result<Self> {
504 Self::from_hex(s)
505 }
506}
507
508#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
525pub struct FileMode(u32);
526
527impl FileMode {
528 pub const ABSENT: Self = Self(0);
530 pub const REGULAR: Self = Self(0o100644);
532 pub const EXECUTABLE: Self = Self(0o100755);
534 pub const SYMLINK: Self = Self(0o120000);
536 pub const GITLINK: Self = Self(0o160000);
538 pub const TREE: Self = Self(0o040000);
540
541 #[must_use]
543 pub const fn new(bits: u32) -> Self {
544 Self(bits)
545 }
546
547 #[must_use]
549 pub const fn bits(self) -> u32 {
550 self.0
551 }
552
553 #[must_use]
555 pub const fn is_absent(self) -> bool {
556 self.0 == 0
557 }
558
559 #[must_use]
561 pub fn octal_if_present(self) -> Option<String> {
562 (!self.is_absent()).then(|| self.to_string())
563 }
564
565 #[must_use]
567 pub const fn is_symlink(self) -> bool {
568 self.0 & 0o170000 == 0o120000
569 }
570
571 #[must_use]
573 pub const fn is_gitlink(self) -> bool {
574 self.0 & 0o170000 == 0o160000
575 }
576}
577
578impl From<u32> for FileMode {
579 fn from(bits: u32) -> Self {
580 Self(bits)
581 }
582}
583
584impl From<FileMode> for u32 {
585 fn from(mode: FileMode) -> Self {
586 mode.0
587 }
588}
589
590impl fmt::Display for FileMode {
591 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
592 write!(f, "{:06o}", self.0)
593 }
594}
595
596impl fmt::Debug for FileMode {
597 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
598 write!(f, "FileMode({:06o})", self.0)
599 }
600}
601
602#[cfg(feature = "serde-models")]
603impl serde::Serialize for FileMode {
604 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
605 where
606 S: serde::Serializer,
607 {
608 serializer.collect_str(self)
609 }
610}
611
612#[derive(Clone, Copy, PartialEq, Eq)]
636pub struct ObjectIdPrefix {
637 bytes: [u8; MAX_OID_LEN],
640 nibbles: u8,
641 algo: HashAlgo,
642}
643
644impl ObjectIdPrefix {
645 pub fn from_hex(hex: &str, algo: HashAlgo) -> Result<Self> {
654 let digits = hex.as_bytes();
655 if digits.len() > algo.hex_len() {
656 return Err(Error::InvalidObjectId(hex.to_owned()));
657 }
658 let mut bytes = [0u8; MAX_OID_LEN];
659 let mut invalid = 0u8;
660 for (i, &digit) in digits.iter().enumerate() {
661 let nibble = HEX_DECODE[usize::from(digit)];
662 invalid |= nibble;
663 let shift = if i.is_multiple_of(2) { 4 } else { 0 };
664 bytes[i / 2] |= (nibble & 0x0f) << shift;
665 }
666 if invalid & HEX_INVALID != 0 {
667 return Err(Error::InvalidObjectId(hex.to_owned()));
668 }
669 Ok(Self {
670 bytes,
671 nibbles: digits.len() as u8,
672 algo,
673 })
674 }
675
676 #[must_use]
678 pub fn nibbles(&self) -> usize {
679 usize::from(self.nibbles)
680 }
681
682 #[must_use]
684 pub fn is_empty(&self) -> bool {
685 self.nibbles == 0
686 }
687
688 #[must_use]
690 pub fn algo(&self) -> HashAlgo {
691 self.algo
692 }
693
694 #[must_use]
699 pub fn matches(&self, oid: &ObjectId) -> bool {
700 self.matches_digest(oid.as_bytes())
701 }
702
703 #[must_use]
705 pub(crate) fn matches_digest(&self, digest: &[u8]) -> bool {
706 let whole = self.nibbles() / 2;
707 let Some(head) = digest.get(..whole) else {
708 return false;
709 };
710 if head != &self.bytes[..whole] {
711 return false;
712 }
713 if self.nibbles.is_multiple_of(2) {
714 return true;
715 }
716 digest
717 .get(whole)
718 .is_some_and(|&b| b & 0xf0 == self.bytes[whole])
719 }
720
721 #[must_use]
723 pub fn lowest(&self) -> ObjectId {
724 ObjectId {
725 bytes: self.bytes,
726 len: self.algo.len() as u8,
727 }
728 }
729
730 #[must_use]
732 pub fn highest(&self) -> ObjectId {
733 let mut bytes = self.bytes;
734 let whole = self.nibbles() / 2;
735 if !self.nibbles.is_multiple_of(2) {
736 bytes[whole] |= 0x0f;
737 }
738 let first_free = whole + usize::from(self.nibbles % 2);
739 bytes[first_free..self.algo.len()].fill(0xff);
740 ObjectId {
741 bytes,
742 len: self.algo.len() as u8,
743 }
744 }
745}
746
747impl fmt::Display for ObjectIdPrefix {
748 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
749 let hex = hex::encode(&self.bytes[..self.nibbles().div_ceil(2)]);
750 f.write_str(&hex[..self.nibbles()])
751 }
752}
753
754impl fmt::Debug for ObjectIdPrefix {
755 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
756 write!(f, "ObjectIdPrefix({self})")
757 }
758}
759
760#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
762pub enum ObjectKind {
763 Blob,
765 Tree,
767 Commit,
769 Tag,
771}
772
773impl ObjectKind {
774 pub fn from_bytes(b: &[u8]) -> Result<Self> {
780 match b {
781 b"blob" => Ok(Self::Blob),
782 b"tree" => Ok(Self::Tree),
783 b"commit" => Ok(Self::Commit),
784 b"tag" => Ok(Self::Tag),
785 other => Err(Error::UnknownObjectType(
786 String::from_utf8_lossy(other).into_owned(),
787 )),
788 }
789 }
790
791 #[must_use]
796 pub fn from_tag_type_field(line: &[u8]) -> Option<Self> {
797 fn keyword_matches(canonical: &[u8], field: &[u8]) -> bool {
798 if field.is_empty() {
799 return false;
800 }
801 for (i, &bc) in field.iter().enumerate() {
802 let sc = canonical.get(i).copied().unwrap_or(0);
803 if sc != bc {
804 return false;
805 }
806 }
807 canonical.get(field.len()).copied().unwrap_or(0) == 0
808 }
809
810 const NAMES: &[(ObjectKind, &[u8])] = &[
811 (ObjectKind::Blob, b"blob"),
812 (ObjectKind::Tree, b"tree"),
813 (ObjectKind::Commit, b"commit"),
814 (ObjectKind::Tag, b"tag"),
815 ];
816 for &(kind, name) in NAMES {
817 if keyword_matches(name, line) {
818 return Some(kind);
819 }
820 }
821 None
822 }
823
824 #[must_use]
826 pub fn as_str(&self) -> &'static str {
827 match self {
828 Self::Blob => "blob",
829 Self::Tree => "tree",
830 Self::Commit => "commit",
831 Self::Tag => "tag",
832 }
833 }
834}
835
836impl fmt::Display for ObjectKind {
837 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
838 f.write_str(self.as_str())
839 }
840}
841
842impl FromStr for ObjectKind {
843 type Err = Error;
844
845 fn from_str(s: &str) -> Result<Self> {
846 Self::from_bytes(s.as_bytes())
847 }
848}
849
850#[derive(Debug, Clone, Copy, PartialEq, Eq)]
852pub struct ObjectInfo {
853 pub kind: ObjectKind,
855 pub size: u64,
857}
858
859#[derive(Debug, Clone)]
861pub struct Object {
862 pub kind: ObjectKind,
864 pub data: Vec<u8>,
866}
867
868#[derive(Debug, Clone, Copy, PartialEq, Eq)]
874pub struct ObjectRef<'a> {
875 pub kind: ObjectKind,
877 pub data: &'a [u8],
879}
880
881impl<'a> ObjectRef<'a> {
882 #[must_use]
884 pub fn from_buf(kind: ObjectKind, buf: &'a [u8]) -> Self {
885 Self { kind, data: buf }
886 }
887}
888
889impl Object {
890 #[must_use]
892 pub fn new(kind: ObjectKind, data: Vec<u8>) -> Self {
893 Self { kind, data }
894 }
895
896 #[must_use]
898 pub fn as_ref(&self) -> ObjectRef<'_> {
899 ObjectRef {
900 kind: self.kind,
901 data: self.data.as_slice(),
902 }
903 }
904}
905
906#[derive(Debug, Clone, PartialEq, Eq)]
908pub struct TreeEntry {
909 pub mode: u32,
911 pub name: Vec<u8>,
913 pub oid: ObjectId,
915}
916
917impl TreeEntry {
918 #[must_use]
922 pub fn mode_str(&self) -> String {
923 if self.mode == 0o040000 {
925 "40000".to_owned()
926 } else {
927 format!("{:o}", self.mode)
928 }
929 }
930}
931
932#[derive(Debug, Clone, Copy, PartialEq, Eq)]
934pub struct TreeEntryRef<'a> {
935 pub mode: u32,
937 pub name: &'a [u8],
939 pub oid: ObjectId,
941}
942
943impl TreeEntryRef<'_> {
944 #[must_use]
946 pub fn to_owned(&self) -> TreeEntry {
947 TreeEntry {
948 mode: self.mode,
949 name: self.name.to_vec(),
950 oid: self.oid,
951 }
952 }
953}
954
955#[derive(Debug, Clone)]
960pub struct TreeRefIter<'a> {
961 data: &'a [u8],
962 pos: usize,
963 oid_len: usize,
964 failed: bool,
966}
967
968impl<'a> TreeRefIter<'a> {
969 #[must_use]
971 pub fn with_oid_len(data: &'a [u8], oid_len: usize) -> Self {
972 Self {
973 data,
974 pos: 0,
975 oid_len,
976 failed: false,
977 }
978 }
979
980 pub fn infer_oid_len(data: &'a [u8]) -> Result<Self> {
986 match try_parse_tree_oid_len(data, HashAlgo::Sha1.len()) {
987 Ok(()) => Ok(Self::with_oid_len(data, HashAlgo::Sha1.len())),
988 Err(sha1_err) => match try_parse_tree_oid_len(data, HashAlgo::Sha256.len()) {
989 Ok(()) => Ok(Self::with_oid_len(data, HashAlgo::Sha256.len())),
990 Err(_) => Err(sha1_err),
991 },
992 }
993 }
994
995 fn next_entry(&mut self) -> Result<Option<TreeEntryRef<'a>>> {
996 parse_tree_entry_at(self.data, &mut self.pos, self.oid_len)
997 }
998}
999
1000fn parse_tree_entry_at<'a>(
1002 data: &'a [u8],
1003 pos: &mut usize,
1004 oid_len: usize,
1005) -> Result<Option<TreeEntryRef<'a>>> {
1006 if *pos >= data.len() {
1007 return Ok(None);
1008 }
1009 let sp = data[*pos..]
1010 .iter()
1011 .position(|&b| b == b' ')
1012 .ok_or_else(|| Error::CorruptObject("tree entry missing space".to_owned()))?;
1013 let mode_bytes = &data[*pos..*pos + sp];
1014 let mode = std::str::from_utf8(mode_bytes)
1015 .ok()
1016 .and_then(|s| u32::from_str_radix(s, 8).ok())
1017 .ok_or_else(|| {
1018 Error::CorruptObject(format!(
1019 "invalid tree mode: {}",
1020 String::from_utf8_lossy(mode_bytes)
1021 ))
1022 })?;
1023 *pos += sp + 1;
1024
1025 let nul = data[*pos..]
1026 .iter()
1027 .position(|&b| b == 0)
1028 .ok_or_else(|| Error::CorruptObject("tree entry missing NUL".to_owned()))?;
1029 let name = &data[*pos..*pos + nul];
1030 *pos += nul + 1;
1031
1032 if *pos + oid_len > data.len() {
1033 return Err(Error::CorruptObject("tree entry truncated SHA".to_owned()));
1034 }
1035 let oid = ObjectId::from_bytes(&data[*pos..*pos + oid_len])?;
1036 *pos += oid_len;
1037
1038 Ok(Some(TreeEntryRef { mode, name, oid }))
1039}
1040
1041impl<'a> Iterator for TreeRefIter<'a> {
1042 type Item = Result<TreeEntryRef<'a>>;
1043
1044 fn next(&mut self) -> Option<Self::Item> {
1045 if self.failed || self.pos >= self.data.len() {
1046 return None;
1047 }
1048 match self.next_entry() {
1049 Ok(Some(entry)) => Some(Ok(entry)),
1050 Ok(None) => None,
1051 Err(err) => {
1052 self.failed = true;
1053 self.pos = self.data.len();
1054 Some(Err(err))
1055 }
1056 }
1057 }
1058}
1059
1060fn try_parse_tree_oid_len(data: &[u8], oid_len: usize) -> Result<()> {
1062 let mut pos = 0usize;
1063 while parse_tree_entry_at(data, &mut pos, oid_len)?.is_some() {}
1064 Ok(())
1065}
1066
1067fn collect_tree_with_oid_len(data: &[u8], oid_len: usize) -> Result<Vec<TreeEntry>> {
1068 let mut pos = 0usize;
1069 let mut entries = Vec::with_capacity(data.len() / 32);
1070 while let Some(entry) = parse_tree_entry_at(data, &mut pos, oid_len)? {
1071 entries.push(TreeEntry {
1072 mode: entry.mode,
1073 name: entry.name.to_vec(),
1074 oid: entry.oid,
1075 });
1076 }
1077 Ok(entries)
1078}
1079
1080pub fn parse_tree(data: &[u8]) -> Result<Vec<TreeEntry>> {
1091 match collect_tree_with_oid_len(data, HashAlgo::Sha1.len()) {
1092 Ok(entries) => Ok(entries),
1093 Err(sha1_err) => {
1094 collect_tree_with_oid_len(data, HashAlgo::Sha256.len()).map_err(|_| sha1_err)
1095 }
1096 }
1097}
1098
1099pub fn parse_tree_with_oid_len(data: &[u8], oid_len: usize) -> Result<Vec<TreeEntry>> {
1104 collect_tree_with_oid_len(data, oid_len)
1105}
1106
1107#[must_use]
1114pub fn serialize_tree(entries: &[TreeEntry]) -> Vec<u8> {
1115 let len = entries
1116 .iter()
1117 .map(|e| 8 + e.name.len() + e.oid.as_bytes().len())
1118 .sum();
1119 let mut out = Vec::with_capacity(len);
1120 for e in entries {
1121 push_tree_mode(&mut out, e.mode);
1122 out.push(b' ');
1123 out.extend_from_slice(&e.name);
1124 out.push(0);
1125 out.extend_from_slice(e.oid.as_bytes());
1126 }
1127 out
1128}
1129
1130fn push_tree_mode(out: &mut Vec<u8>, mode: u32) {
1132 let mut digits = [0u8; 11];
1133 let mut start = digits.len();
1134 let mut rest = mode;
1135 loop {
1136 start -= 1;
1137 digits[start] = b'0' + (rest & 7) as u8;
1138 rest >>= 3;
1139 if rest == 0 {
1140 break;
1141 }
1142 }
1143 out.extend_from_slice(&digits[start..]);
1144}
1145
1146#[must_use]
1159pub fn tree_entry_cmp(
1160 a_name: &[u8],
1161 a_is_tree: bool,
1162 b_name: &[u8],
1163 b_is_tree: bool,
1164) -> std::cmp::Ordering {
1165 let a_trailer = if a_is_tree { b'/' } else { 0u8 };
1166 let b_trailer = if b_is_tree { b'/' } else { 0u8 };
1167
1168 let min_len = a_name.len().min(b_name.len());
1169 let cmp = a_name[..min_len].cmp(&b_name[..min_len]);
1170 if cmp != std::cmp::Ordering::Equal {
1171 return cmp;
1172 }
1173 let ac = a_name.get(min_len).copied().unwrap_or(a_trailer);
1175 let bc = b_name.get(min_len).copied().unwrap_or(b_trailer);
1176 ac.cmp(&bc)
1177}
1178
1179fn parse_tree_mode_octal(bytes: &[u8]) -> Option<u32> {
1181 if bytes.is_empty() || bytes.len() > 7 {
1182 return None;
1183 }
1184 bytes.iter().try_fold(0u32, |acc, &b| match b {
1185 b'0'..=b'7' => Some(acc * 8 + u32::from(b - b'0')),
1186 _ => None,
1187 })
1188}
1189
1190pub(crate) fn find_tree_entry_by_name_in_data(
1202 data: &[u8],
1203 oid_len: usize,
1204 name: &[u8],
1205 name_is_tree: bool,
1206) -> Result<Option<TreeEntry>> {
1207 let mut pos = 0usize;
1208 while pos < data.len() {
1209 let rest = &data[pos..];
1210 let sp = memchr::memchr(b' ', rest)
1211 .ok_or_else(|| Error::CorruptObject("tree entry missing space".to_owned()))?;
1212 let mode = parse_tree_mode_octal(&rest[..sp])
1213 .ok_or_else(|| Error::CorruptObject("invalid tree mode".to_owned()))?;
1214 let name_start = sp + 1;
1215 let nul = memchr::memchr(0, &rest[name_start..])
1216 .ok_or_else(|| Error::CorruptObject("tree entry missing NUL".to_owned()))?;
1217 let entry_name = &rest[name_start..name_start + nul];
1218 let oid_start = name_start + nul + 1;
1219 let oid_end = oid_start + oid_len;
1220 if oid_end > rest.len() {
1221 return Err(Error::CorruptObject("tree entry truncated SHA".to_owned()));
1222 }
1223 if entry_name == name {
1224 return Ok(Some(TreeEntry {
1225 mode,
1226 name: entry_name.to_vec(),
1227 oid: ObjectId::from_bytes(&rest[oid_start..oid_end])?,
1228 }));
1229 }
1230 if tree_entry_cmp(entry_name, mode == 0o040000, name, name_is_tree)
1231 == std::cmp::Ordering::Greater
1232 {
1233 return Ok(None);
1234 }
1235 pos += oid_end;
1236 }
1237 Ok(None)
1238}
1239
1240pub(crate) fn find_tree_entry_by_name_dir_or_file_in_data(
1242 data: &[u8],
1243 oid_len: usize,
1244 name: &[u8],
1245) -> Result<Option<TreeEntry>> {
1246 if let Some(entry) = find_tree_entry_by_name_in_data(data, oid_len, name, true)? {
1247 if entry.mode == 0o040000 {
1248 return Ok(Some(entry));
1249 }
1250 }
1251 find_tree_entry_by_name_in_data(data, oid_len, name, false)
1252}
1253
1254#[derive(Debug, Clone, PartialEq, Eq)]
1260pub struct CommitData {
1261 pub tree: ObjectId,
1263 pub parents: Vec<ObjectId>,
1265 pub author: String,
1267 pub committer: String,
1269 pub author_raw: Vec<u8>,
1273 pub committer_raw: Vec<u8>,
1275 pub encoding: Option<String>,
1277 pub message: String,
1279 #[doc = "Optional raw message bytes for non-UTF-8 messages."]
1282 pub raw_message: Option<Vec<u8>>,
1283 pub extra_headers: Vec<u8>,
1289}
1290
1291#[must_use]
1298pub fn commit_subject_pretty(message: &str) -> String {
1299 let mut parts = Vec::new();
1300 for line in message.lines() {
1301 if line.trim().is_empty() {
1302 break;
1303 }
1304 parts.push(line.trim_end());
1305 }
1306 parts.join(" ")
1307}
1308
1309pub fn read_commit_subject_pretty(data: &[u8]) -> Result<String> {
1315 let message_start = data.windows(2).position(|w| w == b"\n\n").map(|p| p + 2);
1316 let Some(message_start) = message_start else {
1317 return Ok(String::new());
1318 };
1319 let body = std::str::from_utf8(data.get(message_start..).unwrap_or_default()).unwrap_or("");
1320 Ok(commit_subject_pretty(body))
1321}
1322
1323pub fn read_commit_subject_line(data: &[u8]) -> Result<String> {
1331 let mut pos = 0usize;
1332 let mut encoding: Option<String> = None;
1333 while pos < data.len() {
1334 let line_start = pos;
1335 let mut line_end = pos;
1336 while line_end < data.len() && data[line_end] != b'\n' {
1337 line_end += 1;
1338 }
1339 let line = &data[line_start..line_end];
1340 let after_nl = line_end.saturating_add(1);
1341 if line.is_empty() {
1342 let body = data.get(after_nl..).unwrap_or_default();
1343 let message = commit_encoding::decode_bytes(encoding.as_deref(), body);
1344 return Ok(message
1345 .lines()
1346 .find(|l| !l.trim().is_empty())
1347 .map(str::trim)
1348 .unwrap_or("(no subject)")
1349 .to_owned());
1350 }
1351 if line.first() == Some(&b' ') {
1352 pos = after_nl;
1353 continue;
1354 }
1355 if let Some(rest) = line.strip_prefix(b"encoding ") {
1356 encoding = Some(
1357 std::str::from_utf8(rest)
1358 .map_err(|_| Error::CorruptObject("encoding is not UTF-8".to_owned()))?
1359 .trim()
1360 .to_owned(),
1361 );
1362 }
1363 pos = after_nl;
1364 }
1365 Ok("(no subject)".to_owned())
1366}
1367
1368pub(crate) fn parse_oid_field(value: &[u8]) -> Result<ObjectId> {
1369 let s = std::str::from_utf8(value)
1370 .map_err(|_| Error::CorruptObject("commit header object id is not UTF-8".to_owned()))?;
1371 s.trim()
1372 .parse::<ObjectId>()
1373 .map_err(|_| Error::CorruptObject("invalid object id in commit header".to_owned()))
1374}
1375
1376pub(crate) fn parse_commit_tree_and_parents_prefix(
1386 prefix: &[u8],
1387 hex_len: usize,
1388) -> Option<(ObjectId, Vec<ObjectId>)> {
1389 canonical_commit_tree_and_parents(prefix, hex_len, false)
1390}
1391
1392pub(crate) fn parse_commit_tree_and_parents_lenient(
1399 data: &[u8],
1400 hex_len: usize,
1401) -> Option<(ObjectId, Vec<ObjectId>)> {
1402 canonical_commit_tree_and_parents(data, hex_len, true)
1403}
1404
1405fn canonical_commit_tree_and_parents(
1406 data: &[u8],
1407 hex_len: usize,
1408 complete: bool,
1409) -> Option<(ObjectId, Vec<ObjectId>)> {
1410 fn header_oid<'a>(rest: &'a [u8], key: &[u8], hex_len: usize) -> Option<(ObjectId, &'a [u8])> {
1411 let line = rest.strip_prefix(key)?;
1412 if line.len() <= hex_len || line[hex_len] != b'\n' {
1413 return None;
1414 }
1415 let oid = ObjectId::from_hex(std::str::from_utf8(&line[..hex_len]).ok()?).ok()?;
1416 Some((oid, &line[hex_len + 1..]))
1417 }
1418 const PARENT: &[u8] = b"parent ";
1419 let (tree, mut rest) = header_oid(data, b"tree ", hex_len)?;
1420 let mut parents = Vec::new();
1421 loop {
1422 if !complete && rest.len() < PARENT.len() && PARENT.starts_with(rest) {
1423 return None;
1424 }
1425 if !rest.starts_with(PARENT) {
1426 return Some((tree, parents));
1427 }
1428 let (parent, next) = header_oid(rest, PARENT, hex_len)?;
1429 parents.push(parent);
1430 rest = next;
1431 }
1432}
1433
1434pub fn parse_commit(data: &[u8]) -> Result<CommitData> {
1440 Ok(super::CommitRef::parse(data)?.to_owned())
1441}
1442
1443#[must_use]
1446pub fn tag_header_field(data: &[u8], prefix: &[u8]) -> Option<String> {
1447 let mut pos = 0usize;
1448 while pos < data.len() {
1449 let rest = &data[pos..];
1450 let nl = rest.iter().position(|&b| b == b'\n');
1451 let line = if let Some(i) = nl { &rest[..i] } else { rest };
1452 if line.is_empty() {
1453 break;
1454 }
1455 if let Some(after) = line.strip_prefix(prefix) {
1456 return Some(String::from_utf8_lossy(after).trim().to_owned());
1457 }
1458 pos += line.len().saturating_add(nl.map(|_| 1).unwrap_or(0));
1459 if nl.is_none() {
1460 break;
1461 }
1462 }
1463 None
1464}
1465
1466#[must_use]
1468pub fn tag_object_line_oid(data: &[u8]) -> Option<ObjectId> {
1469 let s = tag_header_field(data, b"object ")?;
1470 s.parse().ok()
1471}
1472
1473#[derive(Debug, Clone)]
1475pub struct TagData {
1476 pub object: ObjectId,
1478 pub object_type: String,
1480 pub tag: String,
1482 pub tagger: Option<String>,
1484 pub extra_headers: Vec<u8>,
1486 pub message: String,
1488}
1489
1490pub fn parse_tag(data: &[u8]) -> Result<TagData> {
1498 use super::extra_headers::next_header_block;
1499
1500 #[derive(Clone, Copy)]
1501 enum Continuation {
1502 None,
1503 Extra,
1504 }
1505
1506 let mut pos = 0usize;
1507 let mut object = None;
1508 let mut object_type = None;
1509 let mut tag_name = None;
1510 let mut tagger = None;
1511 let mut extra_headers = Vec::new();
1512 let mut cont = Continuation::None;
1513 let mut past_tagger = false;
1514
1515 while pos < data.len() {
1516 let line_start = pos;
1517 let mut line_end = pos;
1518 while line_end < data.len() && data[line_end] != b'\n' {
1519 line_end += 1;
1520 }
1521 let line = &data[line_start..line_end];
1522 let after_nl = (line_end + 1).min(data.len());
1523 if line.is_empty() {
1524 let body = data.get(after_nl..).unwrap_or_default();
1525 let message = std::str::from_utf8(body)
1526 .map_err(|_| Error::CorruptObject("tag message is not valid UTF-8".to_owned()))?
1527 .to_owned();
1528 return Ok(TagData {
1529 object: object
1530 .ok_or_else(|| Error::CorruptObject("tag missing object header".to_owned()))?,
1531 object_type: object_type
1532 .ok_or_else(|| Error::CorruptObject("tag missing type header".to_owned()))?,
1533 tag: tag_name
1534 .ok_or_else(|| Error::CorruptObject("tag missing tag header".to_owned()))?,
1535 tagger,
1536 extra_headers,
1537 message,
1538 });
1539 }
1540
1541 if past_tagger {
1542 let (_, block_end, _) = next_header_block(data, line_start);
1543 extra_headers.extend_from_slice(&data[line_start..block_end]);
1544 pos = block_end;
1545 continue;
1546 }
1547
1548 if line.first() == Some(&b' ') {
1549 match cont {
1550 Continuation::Extra => {
1551 extra_headers.extend_from_slice(&data[line_start..after_nl]);
1552 }
1553 Continuation::None => {}
1554 }
1555 pos = after_nl;
1556 continue;
1557 }
1558
1559 let key_end = line
1560 .iter()
1561 .position(|&b| b == b' ')
1562 .ok_or_else(|| Error::CorruptObject("malformed tag header line".to_owned()))?;
1563 let key = &line[..key_end];
1564 let rest = line.get(key_end + 1..).unwrap_or_default();
1565
1566 match key {
1567 b"object" => {
1568 let line_str = std::str::from_utf8(rest).map_err(|_| {
1569 Error::CorruptObject("tag object line is not valid UTF-8".to_owned())
1570 })?;
1571 object = Some(line_str.trim().parse::<ObjectId>()?);
1572 cont = Continuation::None;
1573 }
1574 b"type" => {
1575 let typ = std::str::from_utf8(rest).map_err(|_| {
1576 Error::CorruptObject("tag type line is not valid UTF-8".to_owned())
1577 })?;
1578 let typ = typ.trim();
1579 if ObjectKind::from_tag_type_field(typ.as_bytes()).is_none() {
1580 return Err(Error::CorruptObject(format!(
1581 "invalid 'type' value in tag: {typ}"
1582 )));
1583 }
1584 object_type = Some(typ.to_owned());
1585 cont = Continuation::None;
1586 }
1587 b"tag" => {
1588 let line_str = std::str::from_utf8(rest).map_err(|_| {
1589 Error::CorruptObject("tag name line is not valid UTF-8".to_owned())
1590 })?;
1591 tag_name = Some(line_str.trim().to_owned());
1592 cont = Continuation::None;
1593 }
1594 b"tagger" => {
1595 tagger = Some(
1596 std::str::from_utf8(rest)
1597 .map_err(|_| {
1598 Error::CorruptObject("tag tagger line is not valid UTF-8".to_owned())
1599 })?
1600 .to_owned(),
1601 );
1602 past_tagger = true;
1603 cont = Continuation::None;
1604 }
1605 _ => {
1606 cont = Continuation::Extra;
1607 extra_headers.extend_from_slice(&data[line_start..after_nl]);
1608 }
1609 }
1610 pos = after_nl;
1611 }
1612
1613 Ok(TagData {
1614 object: object
1615 .ok_or_else(|| Error::CorruptObject("tag missing object header".to_owned()))?,
1616 object_type: object_type
1617 .ok_or_else(|| Error::CorruptObject("tag missing type header".to_owned()))?,
1618 tag: tag_name.ok_or_else(|| Error::CorruptObject("tag missing tag header".to_owned()))?,
1619 tagger,
1620 extra_headers,
1621 message: String::new(),
1622 })
1623}
1624
1625#[must_use]
1630pub fn serialize_tag(t: &TagData) -> Vec<u8> {
1631 let mut out = Vec::new();
1632 out.extend_from_slice(format!("object {}\n", t.object).as_bytes());
1633 out.extend_from_slice(format!("type {}\n", t.object_type).as_bytes());
1634 out.extend_from_slice(format!("tag {}\n", t.tag).as_bytes());
1635 if let Some(ref tagger) = t.tagger {
1636 out.extend_from_slice(format!("tagger {tagger}\n").as_bytes());
1637 }
1638 out.extend_from_slice(&t.extra_headers);
1639 out.push(b'\n');
1640 let msg = t.message.trim_end_matches('\n');
1641 if !msg.is_empty() {
1642 out.extend_from_slice(msg.as_bytes());
1643 out.push(b'\n');
1644 }
1645 out
1646}
1647
1648#[must_use]
1658pub fn serialize_commit(c: &CommitData) -> Vec<u8> {
1659 let mut out = Vec::new();
1660 out.extend_from_slice(format!("tree {}\n", c.tree).as_bytes());
1661 for p in &c.parents {
1662 out.extend_from_slice(format!("parent {p}\n").as_bytes());
1663 }
1664 out.extend_from_slice(b"author ");
1665 if c.author_raw.is_empty() {
1666 out.extend_from_slice(c.author.as_bytes());
1667 } else {
1668 out.extend_from_slice(&c.author_raw);
1669 }
1670 out.push(b'\n');
1671 out.extend_from_slice(b"committer ");
1672 if c.committer_raw.is_empty() {
1673 out.extend_from_slice(c.committer.as_bytes());
1674 } else {
1675 out.extend_from_slice(&c.committer_raw);
1676 }
1677 out.push(b'\n');
1678 if let Some(enc) = &c.encoding {
1679 out.extend_from_slice(format!("encoding {enc}\n").as_bytes());
1680 }
1681 out.extend_from_slice(&c.extra_headers);
1682 out.push(b'\n');
1683 if let Some(raw) = &c.raw_message {
1684 out.extend_from_slice(raw);
1685 } else if !c.message.is_empty() {
1686 out.extend_from_slice(c.message.as_bytes());
1687 }
1688 out
1689}
1690
1691#[cfg(test)]
1692mod object_id_prefix_tests {
1693 use super::*;
1694
1695 const ID: &str = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
1696
1697 fn id() -> ObjectId {
1698 ObjectId::from_hex(ID).unwrap()
1699 }
1700
1701 fn prefix(hex: &str) -> ObjectIdPrefix {
1702 ObjectIdPrefix::from_hex(hex, HashAlgo::Sha1).unwrap()
1703 }
1704
1705 #[test]
1706 fn matches_every_length_of_the_id_and_rejects_a_changed_last_digit() {
1707 let id = id();
1708 for n in 0..=ID.len() {
1709 let p = prefix(&ID[..n]);
1710 assert_eq!(p.nibbles(), n);
1711 assert_eq!(p.is_empty(), n == 0);
1712 assert!(p.matches(&id), "{n} digits");
1713 assert_eq!(p.to_string(), ID[..n]);
1714 if n > 0 {
1715 let mut changed = ID[..n].to_owned();
1716 let last = changed.pop().unwrap();
1717 changed.push(if last == '0' { '1' } else { '0' });
1718 assert!(!prefix(&changed).matches(&id), "{changed}");
1719 }
1720 }
1721 }
1722
1723 #[test]
1724 fn odd_prefix_ignores_the_low_nibble_of_its_last_byte() {
1725 let p = prefix("4b8");
1726 assert!(p.matches(&ObjectId::from_hex("4b80000000000000000000000000000000000000").unwrap()));
1727 assert!(p.matches(&ObjectId::from_hex("4b8fffffffffffffffffffffffffffffffffffff").unwrap()));
1728 assert!(
1729 !p.matches(&ObjectId::from_hex("4b90000000000000000000000000000000000000").unwrap())
1730 );
1731 }
1732
1733 #[test]
1734 fn parsing_is_case_insensitive_and_validates() {
1735 assert_eq!(prefix("4B825DC"), prefix("4b825dc"));
1736 assert!(matches!(
1737 ObjectIdPrefix::from_hex("4b8g", HashAlgo::Sha1),
1738 Err(Error::InvalidObjectId(_))
1739 ));
1740 assert!(matches!(
1741 ObjectIdPrefix::from_hex(&format!("{ID}0"), HashAlgo::Sha1),
1742 Err(Error::InvalidObjectId(_))
1743 ));
1744 let sha256 = "a".repeat(64);
1745 assert_eq!(
1746 ObjectIdPrefix::from_hex(&sha256, HashAlgo::Sha256)
1747 .unwrap()
1748 .nibbles(),
1749 64
1750 );
1751 assert_eq!(prefix("").algo(), HashAlgo::Sha1);
1752 }
1753
1754 #[test]
1755 fn longer_prefix_never_matches_a_shorter_digest() {
1756 let long = ObjectIdPrefix::from_hex(&format!("{ID}0000"), HashAlgo::Sha256).unwrap();
1757 assert!(!long.matches(&id()));
1758 assert!(ObjectIdPrefix::from_hex(ID, HashAlgo::Sha256)
1759 .unwrap()
1760 .matches(&id()));
1761 }
1762
1763 #[test]
1764 fn bounds_are_the_first_and_last_matching_ids() {
1765 let p = prefix("4b8");
1766 assert_eq!(
1767 p.lowest().to_hex(),
1768 "4b80000000000000000000000000000000000000"
1769 );
1770 assert_eq!(
1771 p.highest().to_hex(),
1772 "4b8fffffffffffffffffffffffffffffffffffff"
1773 );
1774 let even = prefix("4b82");
1775 assert_eq!(
1776 even.lowest().to_hex(),
1777 "4b82000000000000000000000000000000000000"
1778 );
1779 assert_eq!(
1780 even.highest().to_hex(),
1781 "4b82ffffffffffffffffffffffffffffffffffff"
1782 );
1783 assert_eq!(prefix("").lowest(), ObjectId::null(HashAlgo::Sha1));
1784 assert_eq!(prefix("").highest().to_hex(), "f".repeat(40));
1785 assert_eq!(prefix(ID).lowest(), id());
1786 assert_eq!(prefix(ID).highest(), id());
1787 let sha256 = ObjectIdPrefix::from_hex("abc", HashAlgo::Sha256).unwrap();
1788 assert_eq!(sha256.highest().to_hex(), format!("abc{}", "f".repeat(61)));
1789 assert_eq!(format!("{sha256:?}"), "ObjectIdPrefix(abc)");
1790 }
1791
1792 #[test]
1793 fn common_prefix_nibbles_counts_shared_leading_digits() {
1794 let a = id();
1795 let hex = |s: &str| ObjectId::from_hex(s).unwrap();
1796 assert_eq!(a.common_prefix_nibbles(&a), 40);
1797 assert_eq!(
1798 a.common_prefix_nibbles(&hex("4b825dc642cb6eb9a060e54bf8d69288fbee4905")),
1799 39
1800 );
1801 assert_eq!(
1802 a.common_prefix_nibbles(&hex("4b815dc642cb6eb9a060e54bf8d69288fbee4904")),
1803 3
1804 );
1805 assert_eq!(
1806 a.common_prefix_nibbles(&hex("4b925dc642cb6eb9a060e54bf8d69288fbee4904")),
1807 2
1808 );
1809 assert_eq!(
1810 a.common_prefix_nibbles(&hex("5b825dc642cb6eb9a060e54bf8d69288fbee4904")),
1811 0
1812 );
1813 assert_eq!(
1814 a.common_prefix_nibbles(&hex("4c825dc642cb6eb9a060e54bf8d69288fbee4904")),
1815 1
1816 );
1817 }
1818}
1819
1820#[cfg(test)]
1821mod commit_parse_tests {
1822 use super::*;
1823
1824 #[test]
1825 fn empty_tree_constant_is_canonical_sha1_digest() {
1826 assert_eq!(
1827 ObjectId::empty_tree().to_hex(),
1828 "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
1829 );
1830 }
1831
1832 #[test]
1833 fn is_empty_tree_for_matches_hashed_empty_tree() {
1834 for algo in [HashAlgo::Sha1, HashAlgo::Sha256] {
1835 let hashed =
1836 crate::hash::hash_object(algo, ObjectKind::Tree, b"").expect("hash empty tree");
1837 assert!(hashed.is_empty_tree_for(algo), "{algo:?}");
1838 let other = match algo {
1839 HashAlgo::Sha1 => HashAlgo::Sha256,
1840 HashAlgo::Sha256 => HashAlgo::Sha1,
1841 };
1842 assert!(!hashed.is_empty_tree_for(other), "{algo:?}");
1843 let empty_blob =
1844 crate::hash::hash_object(algo, ObjectKind::Blob, b"").expect("hash empty blob");
1845 assert!(!empty_blob.is_empty_tree_for(algo), "{algo:?}");
1846 }
1847 let legacy = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf899d69f7c6948d4").expect("hex");
1848 assert!(legacy.is_empty_tree_for(HashAlgo::Sha1));
1849 assert!(!legacy.is_empty_tree_for(HashAlgo::Sha256));
1850 }
1851
1852 #[test]
1853 fn serialize_tree_mode_matches_mode_str() {
1854 for mode in [
1855 0,
1856 0o7,
1857 0o100644,
1858 0o100755,
1859 0o120000,
1860 0o040000,
1861 0o160000,
1862 u32::MAX,
1863 ] {
1864 let mut out = Vec::new();
1865 push_tree_mode(&mut out, mode);
1866 let entry = TreeEntry {
1867 mode,
1868 name: Vec::new(),
1869 oid: ObjectId::empty_tree(),
1870 };
1871 assert_eq!(out, entry.mode_str().into_bytes(), "mode {mode:o}");
1872 }
1873 }
1874
1875 #[test]
1876 fn is_empty_blob_matches_both_hash_algorithms() {
1877 for algo in [HashAlgo::Sha1, HashAlgo::Sha256] {
1878 let oid = algo.hash_object(ObjectKind::Blob, b"").unwrap();
1879 assert!(oid.is_empty_blob(), "{algo:?} empty blob {}", oid.to_hex());
1880 let other = algo.hash_object(ObjectKind::Blob, b"x").unwrap();
1881 assert!(!other.is_empty_blob());
1882 }
1883 assert!(!ObjectId::empty_tree().is_empty_blob());
1884 assert!(!ObjectId::zero().is_empty_blob());
1885 }
1886
1887 #[test]
1888 fn commit_subject_pretty_folds_first_paragraph() {
1889 assert_eq!(commit_subject_pretty("hello\nworld\n\nbody"), "hello world");
1890 assert_eq!(commit_subject_pretty(""), "");
1891 assert_eq!(
1892 commit_subject_pretty(" hello world\tthere \n next\t line \n\nbody\n"),
1893 " hello world\tthere next\t line"
1894 );
1895 }
1896
1897 #[test]
1898 fn commit_ref_matches_parse_commit_headers() {
1899 let raw = concat!(
1900 "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
1901 "parent 1111111111111111111111111111111111111111\n",
1902 "author A <a@example.com> 1 +0000\n",
1903 "committer C <c@example.com> 2 +0000\n",
1904 "\n",
1905 "ignored body\n",
1906 );
1907 let full = parse_commit(raw.as_bytes()).expect("parse");
1908 let link = crate::objects::CommitRef::parse(raw.as_bytes()).expect("CommitRef");
1909 assert_eq!(link.tree(), full.tree);
1910 assert_eq!(link.parents(), full.parents.as_slice());
1911 assert_eq!(link.author(), full.author);
1912 assert_eq!(link.committer(), full.committer);
1913 }
1914
1915 #[test]
1916 fn parse_commit_skips_multiline_gpgsig_continuation() {
1917 let raw = concat!(
1918 "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
1919 "author A U Thor <author@example.com> 1 +0000\n",
1920 "committer C O Mitter <committer@example.com> 1 +0000\n",
1921 "gpgsig -----BEGIN PGP SIGNATURE-----\n",
1922 " abcdef\n",
1923 " -----END PGP SIGNATURE-----\n",
1924 "\n",
1925 "msg\n",
1926 );
1927 let c = parse_commit(raw.as_bytes()).expect("parse signed commit");
1928 assert_eq!(c.tree.to_hex(), "4b825dc642cb6eb9a060e54bf8d69288fbee4904");
1929 assert_eq!(c.message, "msg\n");
1930 assert_eq!(serialize_commit(&c), raw.as_bytes());
1931 }
1932
1933 #[test]
1934 fn hex_parsing_accepts_both_cases_and_lengths_and_rejects_the_rest() {
1935 let sha1 = "0123456789abcdefABCDEF0123456789abcdef01";
1936 let oid = ObjectId::from_hex(sha1).unwrap();
1937 assert_eq!(oid.algo(), HashAlgo::Sha1);
1938 assert_eq!(oid.to_hex(), sha1.to_ascii_lowercase());
1939 assert_eq!(ObjectId::from_hex_ascii(sha1.as_bytes()).unwrap(), oid);
1940 assert_eq!(sha1.parse::<ObjectId>().unwrap(), oid);
1941
1942 let sha256 = "f".repeat(64);
1943 let oid = sha256.parse::<ObjectId>().unwrap();
1944 assert_eq!(oid.algo(), HashAlgo::Sha256);
1945 assert_eq!(oid.as_bytes(), [0xff; 32]);
1946 assert!(ObjectId::from_hex_ascii(sha256.as_bytes()).is_err());
1947
1948 for bad in [
1949 "",
1950 &sha1[..39],
1951 &format!("{sha1}0"),
1952 "g123456789abcdef0123456789abcdef01234567",
1953 "0123456789abcdef0123456789abcdef0123456 ",
1954 "0123456789abcdef0123456789abcdef012345/7",
1955 "0123456789abcdef0123456789abcdef012345:7",
1956 "0123456789abcdef0123456789abcdef012345`7",
1957 "0123456789abcdef0123456789abcdef012345G7",
1958 ] {
1959 assert!(
1960 matches!(ObjectId::from_hex(bad), Err(Error::InvalidObjectId(s)) if s == bad),
1961 "{bad:?}"
1962 );
1963 assert!(bad.parse::<ObjectId>().is_err(), "{bad:?}");
1964 assert!(ObjectId::from_hex_ascii(bad.as_bytes()).is_err(), "{bad:?}");
1965 }
1966 let mut non_ascii = sha1.as_bytes().to_vec();
1967 non_ascii[3] = 0xb0;
1968 assert!(ObjectId::from_hex_ascii(&non_ascii).is_err());
1969 }
1970
1971 #[test]
1972 fn hex_and_display_cover_every_byte_for_both_lengths() {
1973 let all: Vec<u8> = (0..=255).collect();
1974 for width in [20, 32] {
1975 for chunk in all.chunks(width).filter(|c| c.len() == width) {
1976 let oid = ObjectId::from_bytes(chunk).unwrap();
1977 let expected: String = chunk.iter().map(|b| format!("{b:02x}")).collect();
1978 assert_eq!(oid.to_hex(), expected);
1979 assert_eq!(oid.to_string(), expected);
1980 assert_eq!(format!("{oid:?}"), format!("ObjectId({expected})"));
1981 assert_eq!(ObjectId::from_hex(&expected).unwrap(), oid);
1982 }
1983 }
1984 }
1985
1986 #[test]
1987 fn serialize_commit_applies_public_field_edits_after_parse() {
1988 let raw = concat!(
1989 "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
1990 "author A <a@example.com> 1 +0000\n",
1991 "committer C <c@example.com> 1 +0000\n",
1992 "\n",
1993 "msg\n",
1994 );
1995 let mut commit = parse_commit(raw.as_bytes()).expect("parse");
1996 let zero = ObjectId::from_hex("0000000000000000000000000000000000000000").unwrap();
1997 commit.tree = zero;
1998 let serialized = serialize_commit(&commit);
1999 let tree_line = format!("tree {}\n", zero.to_hex());
2000 assert!(
2001 serialized.starts_with(tree_line.as_bytes()),
2002 "serialize_commit ignored the updated public tree field"
2003 );
2004 }
2005
2006 #[test]
2007 fn parse_commit_preserves_gpgsig_mergetag_bytes_exactly() {
2008 let raw = concat!(
2009 "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
2010 "author A <a@example.com> 1 +0000\n",
2011 "committer C <c@example.com> 1 +0000\n",
2012 "encoding ISO-8859-1\n",
2013 "gpgsig -----BEGIN PGP SIGNATURE-----\n",
2014 " line2\n",
2015 " -----END PGP SIGNATURE-----\n",
2016 "mergetag object 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
2017 " type commit\n",
2018 " tag merge-tag\n",
2019 " tagger T <t@example.com> 1 +0000\n",
2020 " -----BEGIN PGP SIGNATURE-----\n",
2021 " inner\n",
2022 " -----END PGP SIGNATURE-----\n",
2023 "\n",
2024 "msg-only\n",
2025 );
2026 let parsed = parse_commit(raw.as_bytes()).expect("parse");
2027 assert_eq!(serialize_commit(&parsed), raw.as_bytes());
2028 }
2029
2030 #[test]
2031 fn tree_entry_mode_str_and_tag_object_line_oid() {
2032 let oid = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap();
2033 let entry = TreeEntry {
2034 mode: 0o040000,
2035 name: b"dir".to_vec(),
2036 oid,
2037 };
2038 assert_eq!(entry.mode_str(), "40000");
2039 let tag_body = format!("object {}\ntype commit\ntag t\n\n", oid.to_hex());
2040 assert_eq!(tag_object_line_oid(tag_body.as_bytes()), Some(oid));
2041 }
2042
2043 #[test]
2044 fn parse_commit_non_commit_bytes_does_not_panic() {
2045 let tree_bytes = b"100644 blob 0123456789012345678901234567890123456789\tf";
2046 let err = parse_commit(tree_bytes).expect_err("tree bytes are not a commit");
2047 assert!(matches!(err, Error::CorruptObject(_)));
2048 }
2049
2050 #[test]
2051 fn parse_tree_with_sha256_oid_width() {
2052 let oid_bytes = [0x6eu8; 32];
2053 let oid = ObjectId::from_bytes(&oid_bytes).expect("oid");
2054 let mut raw = b"100644 file\0".to_vec();
2055 raw.extend_from_slice(oid.as_bytes());
2056 let entries = parse_tree_with_oid_len(&raw, HashAlgo::Sha256.len()).expect("tree");
2057 assert_eq!(entries.len(), 1);
2058 assert_eq!(entries[0].oid, oid);
2059 }
2060
2061 #[test]
2062 fn parse_commit_tree_and_parents_prefix_matches_full_parse_or_declines() -> Result<()> {
2063 let tree = "1111111111111111111111111111111111111111";
2064 let p1 = "2222222222222222222222222222222222222222";
2065 let p2 = "3333333333333333333333333333333333333333";
2066 let raw = format!(
2067 "tree {tree}\nparent {p1}\nparent {p2}\nauthor A <a@x> 1 +0000\n\
2068 committer C <c@x> 2 +0000\n\nmessage\n"
2069 );
2070 let commit = parse_commit(raw.as_bytes())?;
2071 let full = (commit.tree, commit.parents);
2072 assert_eq!(full.1.len(), 2);
2073 assert_eq!(
2074 parse_commit_tree_and_parents_lenient(raw.as_bytes(), HashAlgo::Sha1.hex_len()),
2075 Some(full.clone())
2076 );
2077 let hex_len = HashAlgo::Sha1.hex_len();
2078 let header_end = raw.find("author").expect("author line");
2079 for cut in 0..=raw.len() {
2080 let got = parse_commit_tree_and_parents_prefix(&raw.as_bytes()[..cut], hex_len);
2081 if cut > header_end {
2082 assert_eq!(got.as_ref(), Some(&full), "cut at {cut}");
2083 } else {
2084 assert_eq!(
2085 got, None,
2086 "cut at {cut} must not claim a complete parent list"
2087 );
2088 }
2089 }
2090 Ok(())
2091 }
2092
2093 #[test]
2094 fn commit_tree_and_parents_prefix_never_disagrees_with_whole_object() {
2095 let tree = "tree 1111111111111111111111111111111111111111\n";
2096 let parent = "parent 2222222222222222222222222222222222222222\n";
2097 let parent2 = "parent 3333333333333333333333333333333333333333\n";
2098 let bodies = [
2099 format!("{tree}{parent}author A <a@x> 1 +0000\n\nmessage\n"),
2100 format!("{tree}{parent}{parent2}committer C <c@x> 2 +0000\n\nno author\n"),
2101 format!("{tree}{parent}"),
2102 format!("{tree}{parent}{parent2}"),
2103 format!("{tree}author A <a@x> 1 +0000\n{parent}\nlate parent is ignored\n"),
2104 format!("{tree}{parent}not a header\n"),
2105 format!("{tree}parent zz\n\n"),
2106 format!("{tree}{parent}parent 2222"),
2107 format!("author A <a@x> 1 +0000\n{tree}\n"),
2108 ];
2109 let hex_len = HashAlgo::Sha1.hex_len();
2110 for body in &bodies {
2111 let whole = parse_commit_tree_and_parents_lenient(body.as_bytes(), hex_len);
2112 for cut in 0..=body.len() {
2113 if let Some(got) =
2114 parse_commit_tree_and_parents_prefix(&body.as_bytes()[..cut], hex_len)
2115 {
2116 assert_eq!(Some(got), whole, "cut {cut} of {body:?}");
2117 }
2118 }
2119 }
2120 let parents_of = |body: &str| {
2121 parse_commit_tree_and_parents_lenient(body.as_bytes(), hex_len).map(|(_, p)| p.len())
2122 };
2123 assert_eq!(parents_of(&bodies[1]), Some(2), "missing author is fine");
2124 assert_eq!(
2125 parents_of(&bodies[3]),
2126 Some(2),
2127 "object may end after parents"
2128 );
2129 assert_eq!(
2130 parents_of(&bodies[4]),
2131 Some(0),
2132 "only leading parents count"
2133 );
2134 assert_eq!(parents_of(&bodies[6]), None, "malformed parent line");
2135 assert_eq!(parents_of(&bodies[7]), None, "truncated parent line");
2136 assert_eq!(parents_of(&bodies[8]), None, "tree must come first");
2137 }
2138
2139 #[test]
2140 fn parse_commit_tree_and_parents_prefix_handles_root_and_sha256() -> Result<()> {
2141 let root = b"tree 1111111111111111111111111111111111111111\nauthor A <a@x> 1 +0000\n";
2142 let (tree, parents) =
2143 parse_commit_tree_and_parents_prefix(root, HashAlgo::Sha1.hex_len()).expect("root");
2144 assert_eq!(tree.to_hex(), "1111111111111111111111111111111111111111");
2145 assert!(parents.is_empty());
2146
2147 let t = "a".repeat(64);
2148 let p = "b".repeat(64);
2149 let raw = format!("tree {t}\nparent {p}\ncommitter C <c@x> 2 +0000\n");
2150 let (tree, parents) =
2151 parse_commit_tree_and_parents_prefix(raw.as_bytes(), HashAlgo::Sha256.hex_len())
2152 .expect("sha256");
2153 assert_eq!(tree.to_hex(), t);
2154 assert_eq!(parents.len(), 1);
2155 assert_eq!(parents[0].to_hex(), p);
2156
2157 assert!(parse_commit_tree_and_parents_prefix(raw.as_bytes(), 40).is_none());
2158 assert!(parse_commit_tree_and_parents_prefix(b"parent x\n", 40).is_none());
2159 assert!(parse_commit_tree_and_parents_prefix(
2160 b"tree zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz\nauthor\n",
2161 40
2162 )
2163 .is_none());
2164 Ok(())
2165 }
2166
2167 #[test]
2168 fn object_id_loose_path_helpers() {
2169 let oid = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap();
2170 assert_eq!(oid.loose_prefix(), "4b");
2171 assert_eq!(oid.loose_suffix().len(), 38);
2172 assert!(ObjectId::is_hex_len(40));
2173 assert!(ObjectId::is_loose_suffix_len(38));
2174 }
2175
2176 #[test]
2177 fn tree_ref_iter_yields_at_most_one_error() {
2178 let bad = b"100644 incomplete";
2179 let mut iter = TreeRefIter::with_oid_len(bad, HashAlgo::Sha1.len());
2180 assert!(iter.next().unwrap().is_err());
2181 assert!(iter.next().is_none());
2182 }
2183
2184 #[test]
2185 fn tree_ref_iter_public_api_smoke() {
2186 let data = serialize_tree(&[TreeEntry {
2187 mode: 0o100644,
2188 name: b"a".to_vec(),
2189 oid: ObjectId::from_hex("0123456789012345678901234567890123456789").unwrap(),
2190 }]);
2191 let mut iter = TreeRefIter::infer_oid_len(&data).expect("infer");
2192 let entry = iter.next().unwrap().expect("entry");
2193 assert_eq!(entry.name, b"a");
2194 let _owned = entry.to_owned();
2195 let _vec = parse_tree(&data).expect("parse");
2196 let _ = TreeRefIter::with_oid_len(&data, HashAlgo::Sha1.len());
2197 }
2198
2199 #[test]
2200 fn find_tree_entry_by_name_in_data_locates_directory_in_large_tree() -> Result<()> {
2201 let dummy = ObjectId::from_hex("0123456789012345678901234567890123456789")?;
2202 let mut entries: Vec<TreeEntry> = (1..=60)
2203 .map(|i| TreeEntry {
2204 mode: 0o100644,
2205 name: format!("f{i}.txt").into_bytes(),
2206 oid: dummy,
2207 })
2208 .collect();
2209 entries.push(TreeEntry {
2210 mode: 0o040000,
2211 name: b"sub".to_vec(),
2212 oid: dummy,
2213 });
2214 entries.sort_by(|a, b| {
2215 tree_entry_cmp(&a.name, a.mode == 0o040000, &b.name, b.mode == 0o040000)
2216 });
2217 let data = serialize_tree(&entries);
2218 assert!(entries.len() >= 48);
2219 let found = find_tree_entry_by_name_in_data(&data, HashAlgo::Sha1.len(), b"sub", true)?
2220 .expect("sub directory in wide tree");
2221 assert_eq!(found.mode, 0o040000);
2222 assert_eq!(found.name, b"sub");
2223 Ok(())
2224 }
2225
2226 fn sorted_tree_data(entries: &mut [TreeEntry]) -> Vec<u8> {
2227 entries.sort_by(|a, b| {
2228 tree_entry_cmp(&a.name, a.mode == 0o040000, &b.name, b.mode == 0o040000)
2229 });
2230 serialize_tree(entries)
2231 }
2232
2233 #[test]
2234 fn find_tree_entry_by_name_in_data_matches_parse_tree_for_every_name() -> Result<()> {
2235 let oid_a = ObjectId::from_hex("1111111111111111111111111111111111111111")?;
2236 let oid_b = ObjectId::from_hex("2222222222222222222222222222222222222222")?;
2237 let mut entries = vec![
2238 TreeEntry {
2239 mode: 0o040000,
2240 name: b"foo".to_vec(),
2241 oid: oid_a,
2242 },
2243 TreeEntry {
2244 mode: 0o100644,
2245 name: b"foo.c".to_vec(),
2246 oid: oid_b,
2247 },
2248 TreeEntry {
2249 mode: 0o100755,
2250 name: b"foo-bar".to_vec(),
2251 oid: oid_b,
2252 },
2253 TreeEntry {
2254 mode: 0o120000,
2255 name: b"link".to_vec(),
2256 oid: oid_a,
2257 },
2258 TreeEntry {
2259 mode: 0o160000,
2260 name: b"sub".to_vec(),
2261 oid: oid_b,
2262 },
2263 TreeEntry {
2264 mode: 0o100644,
2265 name: b"README".to_vec(),
2266 oid: oid_a,
2267 },
2268 ];
2269 let data = sorted_tree_data(&mut entries);
2270 for expected in parse_tree(&data)? {
2271 let name_is_tree = expected.mode == 0o040000;
2272 let found = find_tree_entry_by_name_in_data(
2273 &data,
2274 HashAlgo::Sha1.len(),
2275 &expected.name,
2276 name_is_tree,
2277 )?
2278 .expect("entry present");
2279 assert_eq!(found, expected);
2280 }
2281 for missing in [&b"fo"[..], b"foo/", b"foo.d", b"zzz", b"", b"AAA", b"lin"] {
2282 assert!(
2283 find_tree_entry_by_name_in_data(&data, HashAlgo::Sha1.len(), missing, false)?
2284 .is_none(),
2285 "unexpected match for {:?}",
2286 String::from_utf8_lossy(missing)
2287 );
2288 }
2289 Ok(())
2290 }
2291
2292 #[test]
2293 fn find_tree_entry_by_name_in_data_stops_after_sorted_position() -> Result<()> {
2294 let oid = ObjectId::from_hex("3333333333333333333333333333333333333333")?;
2295 let mut entries = vec![
2296 TreeEntry {
2297 mode: 0o100644,
2298 name: b"a".to_vec(),
2299 oid,
2300 },
2301 TreeEntry {
2302 mode: 0o100644,
2303 name: b"c".to_vec(),
2304 oid,
2305 },
2306 ];
2307 let mut data = sorted_tree_data(&mut entries);
2308 data.extend_from_slice(b"garbage without separators");
2309 let found = find_tree_entry_by_name_in_data(&data, HashAlgo::Sha1.len(), b"a", false)?;
2310 assert_eq!(found.map(|e| e.name), Some(b"a".to_vec()));
2311 assert!(
2312 find_tree_entry_by_name_in_data(&data, HashAlgo::Sha1.len(), b"b", false)?.is_none()
2313 );
2314 assert!(matches!(
2315 find_tree_entry_by_name_in_data(&data, HashAlgo::Sha1.len(), b"d", false),
2316 Err(Error::CorruptObject(_))
2317 ));
2318 Ok(())
2319 }
2320
2321 #[test]
2322 fn find_tree_entry_by_name_in_data_rejects_bad_mode_and_truncated_oid() {
2323 let bad_mode = b"10x644 a\0aaaaaaaaaaaaaaaaaaaa";
2324 assert!(matches!(
2325 find_tree_entry_by_name_in_data(bad_mode, HashAlgo::Sha1.len(), b"a", false),
2326 Err(Error::CorruptObject(_))
2327 ));
2328 let truncated = b"100644 a\0short";
2329 assert!(matches!(
2330 find_tree_entry_by_name_in_data(truncated, HashAlgo::Sha1.len(), b"a", false),
2331 Err(Error::CorruptObject(_))
2332 ));
2333 }
2334
2335 #[test]
2336 fn find_tree_entry_by_name_in_data_reads_sha256_width() -> Result<()> {
2337 let oid = ObjectId::from_bytes(&[0x5au8; 32])?;
2338 let mut raw = b"100644 file\0".to_vec();
2339 raw.extend_from_slice(oid.as_bytes());
2340 let found = find_tree_entry_by_name_in_data(&raw, HashAlgo::Sha256.len(), b"file", false)?
2341 .expect("file");
2342 assert_eq!(found.oid, oid);
2343 assert_eq!(found.mode, 0o100644);
2344 Ok(())
2345 }
2346}