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
469const HEX_INVALID: u8 = 0x10;
471
472const HEX_DECODE: [u8; 256] = {
474 let mut table = [HEX_INVALID; 256];
475 let mut i = 0u8;
476 while i < 10 {
477 table[(b'0' + i) as usize] = i;
478 i += 1;
479 }
480 let mut i = 0u8;
481 while i < 6 {
482 table[(b'a' + i) as usize] = 10 + i;
483 table[(b'A' + i) as usize] = 10 + i;
484 i += 1;
485 }
486 table
487};
488
489impl FromStr for ObjectId {
490 type Err = Error;
491
492 fn from_str(s: &str) -> Result<Self> {
493 Self::from_hex(s)
494 }
495}
496
497#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
514pub struct FileMode(u32);
515
516impl FileMode {
517 pub const ABSENT: Self = Self(0);
519 pub const REGULAR: Self = Self(0o100644);
521 pub const EXECUTABLE: Self = Self(0o100755);
523 pub const SYMLINK: Self = Self(0o120000);
525 pub const GITLINK: Self = Self(0o160000);
527 pub const TREE: Self = Self(0o040000);
529
530 #[must_use]
532 pub const fn new(bits: u32) -> Self {
533 Self(bits)
534 }
535
536 #[must_use]
538 pub const fn bits(self) -> u32 {
539 self.0
540 }
541
542 #[must_use]
544 pub const fn is_absent(self) -> bool {
545 self.0 == 0
546 }
547
548 #[must_use]
550 pub fn octal_if_present(self) -> Option<String> {
551 (!self.is_absent()).then(|| self.to_string())
552 }
553
554 #[must_use]
556 pub const fn is_symlink(self) -> bool {
557 self.0 & 0o170000 == 0o120000
558 }
559
560 #[must_use]
562 pub const fn is_gitlink(self) -> bool {
563 self.0 & 0o170000 == 0o160000
564 }
565}
566
567impl From<u32> for FileMode {
568 fn from(bits: u32) -> Self {
569 Self(bits)
570 }
571}
572
573impl From<FileMode> for u32 {
574 fn from(mode: FileMode) -> Self {
575 mode.0
576 }
577}
578
579impl fmt::Display for FileMode {
580 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
581 write!(f, "{:06o}", self.0)
582 }
583}
584
585impl fmt::Debug for FileMode {
586 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
587 write!(f, "FileMode({:06o})", self.0)
588 }
589}
590
591#[cfg(feature = "serde-models")]
592impl serde::Serialize for FileMode {
593 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
594 where
595 S: serde::Serializer,
596 {
597 serializer.collect_str(self)
598 }
599}
600
601#[derive(Clone, Copy, PartialEq, Eq)]
625pub struct ObjectIdPrefix {
626 bytes: [u8; MAX_OID_LEN],
629 nibbles: u8,
630 algo: HashAlgo,
631}
632
633impl ObjectIdPrefix {
634 pub fn from_hex(hex: &str, algo: HashAlgo) -> Result<Self> {
643 let digits = hex.as_bytes();
644 if digits.len() > algo.hex_len() {
645 return Err(Error::InvalidObjectId(hex.to_owned()));
646 }
647 let mut bytes = [0u8; MAX_OID_LEN];
648 let mut invalid = 0u8;
649 for (i, &digit) in digits.iter().enumerate() {
650 let nibble = HEX_DECODE[usize::from(digit)];
651 invalid |= nibble;
652 let shift = if i.is_multiple_of(2) { 4 } else { 0 };
653 bytes[i / 2] |= (nibble & 0x0f) << shift;
654 }
655 if invalid & HEX_INVALID != 0 {
656 return Err(Error::InvalidObjectId(hex.to_owned()));
657 }
658 Ok(Self {
659 bytes,
660 nibbles: digits.len() as u8,
661 algo,
662 })
663 }
664
665 #[must_use]
667 pub fn nibbles(&self) -> usize {
668 usize::from(self.nibbles)
669 }
670
671 #[must_use]
673 pub fn is_empty(&self) -> bool {
674 self.nibbles == 0
675 }
676
677 #[must_use]
679 pub fn algo(&self) -> HashAlgo {
680 self.algo
681 }
682
683 #[must_use]
688 pub fn matches(&self, oid: &ObjectId) -> bool {
689 self.matches_digest(oid.as_bytes())
690 }
691
692 #[must_use]
694 pub(crate) fn matches_digest(&self, digest: &[u8]) -> bool {
695 let whole = self.nibbles() / 2;
696 let Some(head) = digest.get(..whole) else {
697 return false;
698 };
699 if head != &self.bytes[..whole] {
700 return false;
701 }
702 if self.nibbles.is_multiple_of(2) {
703 return true;
704 }
705 digest
706 .get(whole)
707 .is_some_and(|&b| b & 0xf0 == self.bytes[whole])
708 }
709
710 #[must_use]
712 pub fn lowest(&self) -> ObjectId {
713 ObjectId {
714 bytes: self.bytes,
715 len: self.algo.len() as u8,
716 }
717 }
718
719 #[must_use]
721 pub fn highest(&self) -> ObjectId {
722 let mut bytes = self.bytes;
723 let whole = self.nibbles() / 2;
724 if !self.nibbles.is_multiple_of(2) {
725 bytes[whole] |= 0x0f;
726 }
727 let first_free = whole + usize::from(self.nibbles % 2);
728 bytes[first_free..self.algo.len()].fill(0xff);
729 ObjectId {
730 bytes,
731 len: self.algo.len() as u8,
732 }
733 }
734}
735
736impl fmt::Display for ObjectIdPrefix {
737 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
738 let hex = hex::encode(&self.bytes[..self.nibbles().div_ceil(2)]);
739 f.write_str(&hex[..self.nibbles()])
740 }
741}
742
743impl fmt::Debug for ObjectIdPrefix {
744 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
745 write!(f, "ObjectIdPrefix({self})")
746 }
747}
748
749#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
751pub enum ObjectKind {
752 Blob,
754 Tree,
756 Commit,
758 Tag,
760}
761
762impl ObjectKind {
763 pub fn from_bytes(b: &[u8]) -> Result<Self> {
769 match b {
770 b"blob" => Ok(Self::Blob),
771 b"tree" => Ok(Self::Tree),
772 b"commit" => Ok(Self::Commit),
773 b"tag" => Ok(Self::Tag),
774 other => Err(Error::UnknownObjectType(
775 String::from_utf8_lossy(other).into_owned(),
776 )),
777 }
778 }
779
780 #[must_use]
785 pub fn from_tag_type_field(line: &[u8]) -> Option<Self> {
786 fn keyword_matches(canonical: &[u8], field: &[u8]) -> bool {
787 if field.is_empty() {
788 return false;
789 }
790 for (i, &bc) in field.iter().enumerate() {
791 let sc = canonical.get(i).copied().unwrap_or(0);
792 if sc != bc {
793 return false;
794 }
795 }
796 canonical.get(field.len()).copied().unwrap_or(0) == 0
797 }
798
799 const NAMES: &[(ObjectKind, &[u8])] = &[
800 (ObjectKind::Blob, b"blob"),
801 (ObjectKind::Tree, b"tree"),
802 (ObjectKind::Commit, b"commit"),
803 (ObjectKind::Tag, b"tag"),
804 ];
805 for &(kind, name) in NAMES {
806 if keyword_matches(name, line) {
807 return Some(kind);
808 }
809 }
810 None
811 }
812
813 #[must_use]
815 pub fn as_str(&self) -> &'static str {
816 match self {
817 Self::Blob => "blob",
818 Self::Tree => "tree",
819 Self::Commit => "commit",
820 Self::Tag => "tag",
821 }
822 }
823}
824
825impl fmt::Display for ObjectKind {
826 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
827 f.write_str(self.as_str())
828 }
829}
830
831impl FromStr for ObjectKind {
832 type Err = Error;
833
834 fn from_str(s: &str) -> Result<Self> {
835 Self::from_bytes(s.as_bytes())
836 }
837}
838
839#[derive(Debug, Clone, Copy, PartialEq, Eq)]
841pub struct ObjectInfo {
842 pub kind: ObjectKind,
844 pub size: u64,
846}
847
848#[derive(Debug, Clone)]
850pub struct Object {
851 pub kind: ObjectKind,
853 pub data: Vec<u8>,
855}
856
857#[derive(Debug, Clone, Copy, PartialEq, Eq)]
863pub struct ObjectRef<'a> {
864 pub kind: ObjectKind,
866 pub data: &'a [u8],
868}
869
870impl<'a> ObjectRef<'a> {
871 #[must_use]
873 pub fn from_buf(kind: ObjectKind, buf: &'a [u8]) -> Self {
874 Self { kind, data: buf }
875 }
876}
877
878impl Object {
879 #[must_use]
881 pub fn new(kind: ObjectKind, data: Vec<u8>) -> Self {
882 Self { kind, data }
883 }
884
885 #[must_use]
887 pub fn as_ref(&self) -> ObjectRef<'_> {
888 ObjectRef {
889 kind: self.kind,
890 data: self.data.as_slice(),
891 }
892 }
893}
894
895#[derive(Debug, Clone, PartialEq, Eq)]
897pub struct TreeEntry {
898 pub mode: u32,
900 pub name: Vec<u8>,
902 pub oid: ObjectId,
904}
905
906impl TreeEntry {
907 #[must_use]
911 pub fn mode_str(&self) -> String {
912 if self.mode == 0o040000 {
914 "40000".to_owned()
915 } else {
916 format!("{:o}", self.mode)
917 }
918 }
919}
920
921#[derive(Debug, Clone, Copy, PartialEq, Eq)]
923pub struct TreeEntryRef<'a> {
924 pub mode: u32,
926 pub name: &'a [u8],
928 pub oid: ObjectId,
930}
931
932impl TreeEntryRef<'_> {
933 #[must_use]
935 pub fn to_owned(&self) -> TreeEntry {
936 TreeEntry {
937 mode: self.mode,
938 name: self.name.to_vec(),
939 oid: self.oid,
940 }
941 }
942}
943
944#[derive(Debug, Clone)]
949pub struct TreeRefIter<'a> {
950 data: &'a [u8],
951 pos: usize,
952 oid_len: usize,
953 failed: bool,
955}
956
957impl<'a> TreeRefIter<'a> {
958 #[must_use]
960 pub fn with_oid_len(data: &'a [u8], oid_len: usize) -> Self {
961 Self {
962 data,
963 pos: 0,
964 oid_len,
965 failed: false,
966 }
967 }
968
969 pub fn infer_oid_len(data: &'a [u8]) -> Result<Self> {
975 match try_parse_tree_oid_len(data, HashAlgo::Sha1.len()) {
976 Ok(()) => Ok(Self::with_oid_len(data, HashAlgo::Sha1.len())),
977 Err(sha1_err) => match try_parse_tree_oid_len(data, HashAlgo::Sha256.len()) {
978 Ok(()) => Ok(Self::with_oid_len(data, HashAlgo::Sha256.len())),
979 Err(_) => Err(sha1_err),
980 },
981 }
982 }
983
984 fn next_entry(&mut self) -> Result<Option<TreeEntryRef<'a>>> {
985 parse_tree_entry_at(self.data, &mut self.pos, self.oid_len)
986 }
987}
988
989fn parse_tree_entry_at<'a>(
991 data: &'a [u8],
992 pos: &mut usize,
993 oid_len: usize,
994) -> Result<Option<TreeEntryRef<'a>>> {
995 if *pos >= data.len() {
996 return Ok(None);
997 }
998 let sp = data[*pos..]
999 .iter()
1000 .position(|&b| b == b' ')
1001 .ok_or_else(|| Error::CorruptObject("tree entry missing space".to_owned()))?;
1002 let mode_bytes = &data[*pos..*pos + sp];
1003 let mode = std::str::from_utf8(mode_bytes)
1004 .ok()
1005 .and_then(|s| u32::from_str_radix(s, 8).ok())
1006 .ok_or_else(|| {
1007 Error::CorruptObject(format!(
1008 "invalid tree mode: {}",
1009 String::from_utf8_lossy(mode_bytes)
1010 ))
1011 })?;
1012 *pos += sp + 1;
1013
1014 let nul = data[*pos..]
1015 .iter()
1016 .position(|&b| b == 0)
1017 .ok_or_else(|| Error::CorruptObject("tree entry missing NUL".to_owned()))?;
1018 let name = &data[*pos..*pos + nul];
1019 *pos += nul + 1;
1020
1021 if *pos + oid_len > data.len() {
1022 return Err(Error::CorruptObject("tree entry truncated SHA".to_owned()));
1023 }
1024 let oid = ObjectId::from_bytes(&data[*pos..*pos + oid_len])?;
1025 *pos += oid_len;
1026
1027 Ok(Some(TreeEntryRef { mode, name, oid }))
1028}
1029
1030impl<'a> Iterator for TreeRefIter<'a> {
1031 type Item = Result<TreeEntryRef<'a>>;
1032
1033 fn next(&mut self) -> Option<Self::Item> {
1034 if self.failed || self.pos >= self.data.len() {
1035 return None;
1036 }
1037 match self.next_entry() {
1038 Ok(Some(entry)) => Some(Ok(entry)),
1039 Ok(None) => None,
1040 Err(err) => {
1041 self.failed = true;
1042 self.pos = self.data.len();
1043 Some(Err(err))
1044 }
1045 }
1046 }
1047}
1048
1049fn try_parse_tree_oid_len(data: &[u8], oid_len: usize) -> Result<()> {
1051 let mut pos = 0usize;
1052 while parse_tree_entry_at(data, &mut pos, oid_len)?.is_some() {}
1053 Ok(())
1054}
1055
1056fn collect_tree_with_oid_len(data: &[u8], oid_len: usize) -> Result<Vec<TreeEntry>> {
1057 let mut pos = 0usize;
1058 let mut entries = Vec::with_capacity(data.len() / 32);
1059 while let Some(entry) = parse_tree_entry_at(data, &mut pos, oid_len)? {
1060 entries.push(TreeEntry {
1061 mode: entry.mode,
1062 name: entry.name.to_vec(),
1063 oid: entry.oid,
1064 });
1065 }
1066 Ok(entries)
1067}
1068
1069pub fn parse_tree(data: &[u8]) -> Result<Vec<TreeEntry>> {
1080 match collect_tree_with_oid_len(data, HashAlgo::Sha1.len()) {
1081 Ok(entries) => Ok(entries),
1082 Err(sha1_err) => {
1083 collect_tree_with_oid_len(data, HashAlgo::Sha256.len()).map_err(|_| sha1_err)
1084 }
1085 }
1086}
1087
1088pub fn parse_tree_with_oid_len(data: &[u8], oid_len: usize) -> Result<Vec<TreeEntry>> {
1093 collect_tree_with_oid_len(data, oid_len)
1094}
1095
1096#[must_use]
1103pub fn serialize_tree(entries: &[TreeEntry]) -> Vec<u8> {
1104 let len = entries
1105 .iter()
1106 .map(|e| 8 + e.name.len() + e.oid.as_bytes().len())
1107 .sum();
1108 let mut out = Vec::with_capacity(len);
1109 for e in entries {
1110 push_tree_mode(&mut out, e.mode);
1111 out.push(b' ');
1112 out.extend_from_slice(&e.name);
1113 out.push(0);
1114 out.extend_from_slice(e.oid.as_bytes());
1115 }
1116 out
1117}
1118
1119fn push_tree_mode(out: &mut Vec<u8>, mode: u32) {
1121 let mut digits = [0u8; 11];
1122 let mut start = digits.len();
1123 let mut rest = mode;
1124 loop {
1125 start -= 1;
1126 digits[start] = b'0' + (rest & 7) as u8;
1127 rest >>= 3;
1128 if rest == 0 {
1129 break;
1130 }
1131 }
1132 out.extend_from_slice(&digits[start..]);
1133}
1134
1135#[must_use]
1148pub fn tree_entry_cmp(
1149 a_name: &[u8],
1150 a_is_tree: bool,
1151 b_name: &[u8],
1152 b_is_tree: bool,
1153) -> std::cmp::Ordering {
1154 let a_trailer = if a_is_tree { b'/' } else { 0u8 };
1155 let b_trailer = if b_is_tree { b'/' } else { 0u8 };
1156
1157 let min_len = a_name.len().min(b_name.len());
1158 let cmp = a_name[..min_len].cmp(&b_name[..min_len]);
1159 if cmp != std::cmp::Ordering::Equal {
1160 return cmp;
1161 }
1162 let ac = a_name.get(min_len).copied().unwrap_or(a_trailer);
1164 let bc = b_name.get(min_len).copied().unwrap_or(b_trailer);
1165 ac.cmp(&bc)
1166}
1167
1168fn parse_tree_mode_octal(bytes: &[u8]) -> Option<u32> {
1170 if bytes.is_empty() || bytes.len() > 7 {
1171 return None;
1172 }
1173 bytes.iter().try_fold(0u32, |acc, &b| match b {
1174 b'0'..=b'7' => Some(acc * 8 + u32::from(b - b'0')),
1175 _ => None,
1176 })
1177}
1178
1179pub(crate) fn find_tree_entry_by_name_in_data(
1191 data: &[u8],
1192 oid_len: usize,
1193 name: &[u8],
1194 name_is_tree: bool,
1195) -> Result<Option<TreeEntry>> {
1196 let mut pos = 0usize;
1197 while pos < data.len() {
1198 let rest = &data[pos..];
1199 let sp = memchr::memchr(b' ', rest)
1200 .ok_or_else(|| Error::CorruptObject("tree entry missing space".to_owned()))?;
1201 let mode = parse_tree_mode_octal(&rest[..sp])
1202 .ok_or_else(|| Error::CorruptObject("invalid tree mode".to_owned()))?;
1203 let name_start = sp + 1;
1204 let nul = memchr::memchr(0, &rest[name_start..])
1205 .ok_or_else(|| Error::CorruptObject("tree entry missing NUL".to_owned()))?;
1206 let entry_name = &rest[name_start..name_start + nul];
1207 let oid_start = name_start + nul + 1;
1208 let oid_end = oid_start + oid_len;
1209 if oid_end > rest.len() {
1210 return Err(Error::CorruptObject("tree entry truncated SHA".to_owned()));
1211 }
1212 if entry_name == name {
1213 return Ok(Some(TreeEntry {
1214 mode,
1215 name: entry_name.to_vec(),
1216 oid: ObjectId::from_bytes(&rest[oid_start..oid_end])?,
1217 }));
1218 }
1219 if tree_entry_cmp(entry_name, mode == 0o040000, name, name_is_tree)
1220 == std::cmp::Ordering::Greater
1221 {
1222 return Ok(None);
1223 }
1224 pos += oid_end;
1225 }
1226 Ok(None)
1227}
1228
1229pub(crate) fn find_tree_entry_by_name_dir_or_file_in_data(
1231 data: &[u8],
1232 oid_len: usize,
1233 name: &[u8],
1234) -> Result<Option<TreeEntry>> {
1235 if let Some(entry) = find_tree_entry_by_name_in_data(data, oid_len, name, true)? {
1236 if entry.mode == 0o040000 {
1237 return Ok(Some(entry));
1238 }
1239 }
1240 find_tree_entry_by_name_in_data(data, oid_len, name, false)
1241}
1242
1243#[derive(Debug, Clone, PartialEq, Eq)]
1249pub struct CommitData {
1250 pub tree: ObjectId,
1252 pub parents: Vec<ObjectId>,
1254 pub author: String,
1256 pub committer: String,
1258 pub author_raw: Vec<u8>,
1262 pub committer_raw: Vec<u8>,
1264 pub encoding: Option<String>,
1266 pub message: String,
1268 #[doc = "Optional raw message bytes for non-UTF-8 messages."]
1271 pub raw_message: Option<Vec<u8>>,
1272 pub extra_headers: Vec<u8>,
1278}
1279
1280#[must_use]
1287pub fn commit_subject_pretty(message: &str) -> String {
1288 let mut parts = Vec::new();
1289 for line in message.lines() {
1290 if line.trim().is_empty() {
1291 break;
1292 }
1293 parts.push(line.trim_end());
1294 }
1295 parts.join(" ")
1296}
1297
1298pub fn read_commit_subject_pretty(data: &[u8]) -> Result<String> {
1304 let message_start = data.windows(2).position(|w| w == b"\n\n").map(|p| p + 2);
1305 let Some(message_start) = message_start else {
1306 return Ok(String::new());
1307 };
1308 let body = std::str::from_utf8(data.get(message_start..).unwrap_or_default()).unwrap_or("");
1309 Ok(commit_subject_pretty(body))
1310}
1311
1312pub fn read_commit_subject_line(data: &[u8]) -> Result<String> {
1320 let mut pos = 0usize;
1321 let mut encoding: Option<String> = None;
1322 while pos < data.len() {
1323 let line_start = pos;
1324 let mut line_end = pos;
1325 while line_end < data.len() && data[line_end] != b'\n' {
1326 line_end += 1;
1327 }
1328 let line = &data[line_start..line_end];
1329 let after_nl = line_end.saturating_add(1);
1330 if line.is_empty() {
1331 let body = data.get(after_nl..).unwrap_or_default();
1332 let message = commit_encoding::decode_bytes(encoding.as_deref(), body);
1333 return Ok(message
1334 .lines()
1335 .find(|l| !l.trim().is_empty())
1336 .map(str::trim)
1337 .unwrap_or("(no subject)")
1338 .to_owned());
1339 }
1340 if line.first() == Some(&b' ') {
1341 pos = after_nl;
1342 continue;
1343 }
1344 if let Some(rest) = line.strip_prefix(b"encoding ") {
1345 encoding = Some(
1346 std::str::from_utf8(rest)
1347 .map_err(|_| Error::CorruptObject("encoding is not UTF-8".to_owned()))?
1348 .trim()
1349 .to_owned(),
1350 );
1351 }
1352 pos = after_nl;
1353 }
1354 Ok("(no subject)".to_owned())
1355}
1356
1357pub(crate) fn parse_oid_field(value: &[u8]) -> Result<ObjectId> {
1358 let s = std::str::from_utf8(value)
1359 .map_err(|_| Error::CorruptObject("commit header object id is not UTF-8".to_owned()))?;
1360 s.trim()
1361 .parse::<ObjectId>()
1362 .map_err(|_| Error::CorruptObject("invalid object id in commit header".to_owned()))
1363}
1364
1365pub(crate) fn parse_commit_tree_and_parents_prefix(
1375 prefix: &[u8],
1376 hex_len: usize,
1377) -> Option<(ObjectId, Vec<ObjectId>)> {
1378 canonical_commit_tree_and_parents(prefix, hex_len, false)
1379}
1380
1381pub(crate) fn parse_commit_tree_and_parents_lenient(
1388 data: &[u8],
1389 hex_len: usize,
1390) -> Option<(ObjectId, Vec<ObjectId>)> {
1391 canonical_commit_tree_and_parents(data, hex_len, true)
1392}
1393
1394fn canonical_commit_tree_and_parents(
1395 data: &[u8],
1396 hex_len: usize,
1397 complete: bool,
1398) -> Option<(ObjectId, Vec<ObjectId>)> {
1399 fn header_oid<'a>(rest: &'a [u8], key: &[u8], hex_len: usize) -> Option<(ObjectId, &'a [u8])> {
1400 let line = rest.strip_prefix(key)?;
1401 if line.len() <= hex_len || line[hex_len] != b'\n' {
1402 return None;
1403 }
1404 let oid = ObjectId::from_hex(std::str::from_utf8(&line[..hex_len]).ok()?).ok()?;
1405 Some((oid, &line[hex_len + 1..]))
1406 }
1407 const PARENT: &[u8] = b"parent ";
1408 let (tree, mut rest) = header_oid(data, b"tree ", hex_len)?;
1409 let mut parents = Vec::new();
1410 loop {
1411 if !complete && rest.len() < PARENT.len() && PARENT.starts_with(rest) {
1412 return None;
1413 }
1414 if !rest.starts_with(PARENT) {
1415 return Some((tree, parents));
1416 }
1417 let (parent, next) = header_oid(rest, PARENT, hex_len)?;
1418 parents.push(parent);
1419 rest = next;
1420 }
1421}
1422
1423pub fn parse_commit(data: &[u8]) -> Result<CommitData> {
1429 Ok(super::CommitRef::parse(data)?.to_owned())
1430}
1431
1432#[must_use]
1435pub fn tag_header_field(data: &[u8], prefix: &[u8]) -> Option<String> {
1436 let mut pos = 0usize;
1437 while pos < data.len() {
1438 let rest = &data[pos..];
1439 let nl = rest.iter().position(|&b| b == b'\n');
1440 let line = if let Some(i) = nl { &rest[..i] } else { rest };
1441 if line.is_empty() {
1442 break;
1443 }
1444 if let Some(after) = line.strip_prefix(prefix) {
1445 return Some(String::from_utf8_lossy(after).trim().to_owned());
1446 }
1447 pos += line.len().saturating_add(nl.map(|_| 1).unwrap_or(0));
1448 if nl.is_none() {
1449 break;
1450 }
1451 }
1452 None
1453}
1454
1455#[must_use]
1457pub fn tag_object_line_oid(data: &[u8]) -> Option<ObjectId> {
1458 let s = tag_header_field(data, b"object ")?;
1459 s.parse().ok()
1460}
1461
1462#[derive(Debug, Clone)]
1464pub struct TagData {
1465 pub object: ObjectId,
1467 pub object_type: String,
1469 pub tag: String,
1471 pub tagger: Option<String>,
1473 pub extra_headers: Vec<u8>,
1475 pub message: String,
1477}
1478
1479pub fn parse_tag(data: &[u8]) -> Result<TagData> {
1487 use super::extra_headers::next_header_block;
1488
1489 #[derive(Clone, Copy)]
1490 enum Continuation {
1491 None,
1492 Extra,
1493 }
1494
1495 let mut pos = 0usize;
1496 let mut object = None;
1497 let mut object_type = None;
1498 let mut tag_name = None;
1499 let mut tagger = None;
1500 let mut extra_headers = Vec::new();
1501 let mut cont = Continuation::None;
1502 let mut past_tagger = false;
1503
1504 while pos < data.len() {
1505 let line_start = pos;
1506 let mut line_end = pos;
1507 while line_end < data.len() && data[line_end] != b'\n' {
1508 line_end += 1;
1509 }
1510 let line = &data[line_start..line_end];
1511 let after_nl = (line_end + 1).min(data.len());
1512 if line.is_empty() {
1513 let body = data.get(after_nl..).unwrap_or_default();
1514 let message = std::str::from_utf8(body)
1515 .map_err(|_| Error::CorruptObject("tag message is not valid UTF-8".to_owned()))?
1516 .to_owned();
1517 return Ok(TagData {
1518 object: object
1519 .ok_or_else(|| Error::CorruptObject("tag missing object header".to_owned()))?,
1520 object_type: object_type
1521 .ok_or_else(|| Error::CorruptObject("tag missing type header".to_owned()))?,
1522 tag: tag_name
1523 .ok_or_else(|| Error::CorruptObject("tag missing tag header".to_owned()))?,
1524 tagger,
1525 extra_headers,
1526 message,
1527 });
1528 }
1529
1530 if past_tagger {
1531 let (_, block_end, _) = next_header_block(data, line_start);
1532 extra_headers.extend_from_slice(&data[line_start..block_end]);
1533 pos = block_end;
1534 continue;
1535 }
1536
1537 if line.first() == Some(&b' ') {
1538 match cont {
1539 Continuation::Extra => {
1540 extra_headers.extend_from_slice(&data[line_start..after_nl]);
1541 }
1542 Continuation::None => {}
1543 }
1544 pos = after_nl;
1545 continue;
1546 }
1547
1548 let key_end = line
1549 .iter()
1550 .position(|&b| b == b' ')
1551 .ok_or_else(|| Error::CorruptObject("malformed tag header line".to_owned()))?;
1552 let key = &line[..key_end];
1553 let rest = line.get(key_end + 1..).unwrap_or_default();
1554
1555 match key {
1556 b"object" => {
1557 let line_str = std::str::from_utf8(rest).map_err(|_| {
1558 Error::CorruptObject("tag object line is not valid UTF-8".to_owned())
1559 })?;
1560 object = Some(line_str.trim().parse::<ObjectId>()?);
1561 cont = Continuation::None;
1562 }
1563 b"type" => {
1564 let typ = std::str::from_utf8(rest).map_err(|_| {
1565 Error::CorruptObject("tag type line is not valid UTF-8".to_owned())
1566 })?;
1567 let typ = typ.trim();
1568 if ObjectKind::from_tag_type_field(typ.as_bytes()).is_none() {
1569 return Err(Error::CorruptObject(format!(
1570 "invalid 'type' value in tag: {typ}"
1571 )));
1572 }
1573 object_type = Some(typ.to_owned());
1574 cont = Continuation::None;
1575 }
1576 b"tag" => {
1577 let line_str = std::str::from_utf8(rest).map_err(|_| {
1578 Error::CorruptObject("tag name line is not valid UTF-8".to_owned())
1579 })?;
1580 tag_name = Some(line_str.trim().to_owned());
1581 cont = Continuation::None;
1582 }
1583 b"tagger" => {
1584 tagger = Some(
1585 std::str::from_utf8(rest)
1586 .map_err(|_| {
1587 Error::CorruptObject("tag tagger line is not valid UTF-8".to_owned())
1588 })?
1589 .to_owned(),
1590 );
1591 past_tagger = true;
1592 cont = Continuation::None;
1593 }
1594 _ => {
1595 cont = Continuation::Extra;
1596 extra_headers.extend_from_slice(&data[line_start..after_nl]);
1597 }
1598 }
1599 pos = after_nl;
1600 }
1601
1602 Ok(TagData {
1603 object: object
1604 .ok_or_else(|| Error::CorruptObject("tag missing object header".to_owned()))?,
1605 object_type: object_type
1606 .ok_or_else(|| Error::CorruptObject("tag missing type header".to_owned()))?,
1607 tag: tag_name.ok_or_else(|| Error::CorruptObject("tag missing tag header".to_owned()))?,
1608 tagger,
1609 extra_headers,
1610 message: String::new(),
1611 })
1612}
1613
1614#[must_use]
1619pub fn serialize_tag(t: &TagData) -> Vec<u8> {
1620 let mut out = Vec::new();
1621 out.extend_from_slice(format!("object {}\n", t.object).as_bytes());
1622 out.extend_from_slice(format!("type {}\n", t.object_type).as_bytes());
1623 out.extend_from_slice(format!("tag {}\n", t.tag).as_bytes());
1624 if let Some(ref tagger) = t.tagger {
1625 out.extend_from_slice(format!("tagger {tagger}\n").as_bytes());
1626 }
1627 out.extend_from_slice(&t.extra_headers);
1628 out.push(b'\n');
1629 let msg = t.message.trim_end_matches('\n');
1630 if !msg.is_empty() {
1631 out.extend_from_slice(msg.as_bytes());
1632 out.push(b'\n');
1633 }
1634 out
1635}
1636
1637#[must_use]
1647pub fn serialize_commit(c: &CommitData) -> Vec<u8> {
1648 let mut out = Vec::new();
1649 out.extend_from_slice(format!("tree {}\n", c.tree).as_bytes());
1650 for p in &c.parents {
1651 out.extend_from_slice(format!("parent {p}\n").as_bytes());
1652 }
1653 out.extend_from_slice(b"author ");
1654 if c.author_raw.is_empty() {
1655 out.extend_from_slice(c.author.as_bytes());
1656 } else {
1657 out.extend_from_slice(&c.author_raw);
1658 }
1659 out.push(b'\n');
1660 out.extend_from_slice(b"committer ");
1661 if c.committer_raw.is_empty() {
1662 out.extend_from_slice(c.committer.as_bytes());
1663 } else {
1664 out.extend_from_slice(&c.committer_raw);
1665 }
1666 out.push(b'\n');
1667 if let Some(enc) = &c.encoding {
1668 out.extend_from_slice(format!("encoding {enc}\n").as_bytes());
1669 }
1670 out.extend_from_slice(&c.extra_headers);
1671 out.push(b'\n');
1672 if let Some(raw) = &c.raw_message {
1673 out.extend_from_slice(raw);
1674 } else if !c.message.is_empty() {
1675 out.extend_from_slice(c.message.as_bytes());
1676 }
1677 out
1678}
1679
1680#[cfg(test)]
1681mod object_id_prefix_tests {
1682 use super::*;
1683
1684 const ID: &str = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
1685
1686 fn id() -> ObjectId {
1687 ObjectId::from_hex(ID).unwrap()
1688 }
1689
1690 fn prefix(hex: &str) -> ObjectIdPrefix {
1691 ObjectIdPrefix::from_hex(hex, HashAlgo::Sha1).unwrap()
1692 }
1693
1694 #[test]
1695 fn matches_every_length_of_the_id_and_rejects_a_changed_last_digit() {
1696 let id = id();
1697 for n in 0..=ID.len() {
1698 let p = prefix(&ID[..n]);
1699 assert_eq!(p.nibbles(), n);
1700 assert_eq!(p.is_empty(), n == 0);
1701 assert!(p.matches(&id), "{n} digits");
1702 assert_eq!(p.to_string(), ID[..n]);
1703 if n > 0 {
1704 let mut changed = ID[..n].to_owned();
1705 let last = changed.pop().unwrap();
1706 changed.push(if last == '0' { '1' } else { '0' });
1707 assert!(!prefix(&changed).matches(&id), "{changed}");
1708 }
1709 }
1710 }
1711
1712 #[test]
1713 fn odd_prefix_ignores_the_low_nibble_of_its_last_byte() {
1714 let p = prefix("4b8");
1715 assert!(p.matches(&ObjectId::from_hex("4b80000000000000000000000000000000000000").unwrap()));
1716 assert!(p.matches(&ObjectId::from_hex("4b8fffffffffffffffffffffffffffffffffffff").unwrap()));
1717 assert!(
1718 !p.matches(&ObjectId::from_hex("4b90000000000000000000000000000000000000").unwrap())
1719 );
1720 }
1721
1722 #[test]
1723 fn parsing_is_case_insensitive_and_validates() {
1724 assert_eq!(prefix("4B825DC"), prefix("4b825dc"));
1725 assert!(matches!(
1726 ObjectIdPrefix::from_hex("4b8g", HashAlgo::Sha1),
1727 Err(Error::InvalidObjectId(_))
1728 ));
1729 assert!(matches!(
1730 ObjectIdPrefix::from_hex(&format!("{ID}0"), HashAlgo::Sha1),
1731 Err(Error::InvalidObjectId(_))
1732 ));
1733 let sha256 = "a".repeat(64);
1734 assert_eq!(
1735 ObjectIdPrefix::from_hex(&sha256, HashAlgo::Sha256)
1736 .unwrap()
1737 .nibbles(),
1738 64
1739 );
1740 assert_eq!(prefix("").algo(), HashAlgo::Sha1);
1741 }
1742
1743 #[test]
1744 fn longer_prefix_never_matches_a_shorter_digest() {
1745 let long = ObjectIdPrefix::from_hex(&format!("{ID}0000"), HashAlgo::Sha256).unwrap();
1746 assert!(!long.matches(&id()));
1747 assert!(ObjectIdPrefix::from_hex(ID, HashAlgo::Sha256)
1748 .unwrap()
1749 .matches(&id()));
1750 }
1751
1752 #[test]
1753 fn bounds_are_the_first_and_last_matching_ids() {
1754 let p = prefix("4b8");
1755 assert_eq!(
1756 p.lowest().to_hex(),
1757 "4b80000000000000000000000000000000000000"
1758 );
1759 assert_eq!(
1760 p.highest().to_hex(),
1761 "4b8fffffffffffffffffffffffffffffffffffff"
1762 );
1763 let even = prefix("4b82");
1764 assert_eq!(
1765 even.lowest().to_hex(),
1766 "4b82000000000000000000000000000000000000"
1767 );
1768 assert_eq!(
1769 even.highest().to_hex(),
1770 "4b82ffffffffffffffffffffffffffffffffffff"
1771 );
1772 assert_eq!(prefix("").lowest(), ObjectId::null(HashAlgo::Sha1));
1773 assert_eq!(prefix("").highest().to_hex(), "f".repeat(40));
1774 assert_eq!(prefix(ID).lowest(), id());
1775 assert_eq!(prefix(ID).highest(), id());
1776 let sha256 = ObjectIdPrefix::from_hex("abc", HashAlgo::Sha256).unwrap();
1777 assert_eq!(sha256.highest().to_hex(), format!("abc{}", "f".repeat(61)));
1778 assert_eq!(format!("{sha256:?}"), "ObjectIdPrefix(abc)");
1779 }
1780
1781 #[test]
1782 fn common_prefix_nibbles_counts_shared_leading_digits() {
1783 let a = id();
1784 let hex = |s: &str| ObjectId::from_hex(s).unwrap();
1785 assert_eq!(a.common_prefix_nibbles(&a), 40);
1786 assert_eq!(
1787 a.common_prefix_nibbles(&hex("4b825dc642cb6eb9a060e54bf8d69288fbee4905")),
1788 39
1789 );
1790 assert_eq!(
1791 a.common_prefix_nibbles(&hex("4b815dc642cb6eb9a060e54bf8d69288fbee4904")),
1792 3
1793 );
1794 assert_eq!(
1795 a.common_prefix_nibbles(&hex("4b925dc642cb6eb9a060e54bf8d69288fbee4904")),
1796 2
1797 );
1798 assert_eq!(
1799 a.common_prefix_nibbles(&hex("5b825dc642cb6eb9a060e54bf8d69288fbee4904")),
1800 0
1801 );
1802 assert_eq!(
1803 a.common_prefix_nibbles(&hex("4c825dc642cb6eb9a060e54bf8d69288fbee4904")),
1804 1
1805 );
1806 }
1807}
1808
1809#[cfg(test)]
1810mod commit_parse_tests {
1811 use super::*;
1812
1813 #[test]
1814 fn empty_tree_constant_is_canonical_sha1_digest() {
1815 assert_eq!(
1816 ObjectId::empty_tree().to_hex(),
1817 "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
1818 );
1819 }
1820
1821 #[test]
1822 fn is_empty_tree_for_matches_hashed_empty_tree() {
1823 for algo in [HashAlgo::Sha1, HashAlgo::Sha256] {
1824 let hashed =
1825 crate::hash::hash_object(algo, ObjectKind::Tree, b"").expect("hash empty tree");
1826 assert!(hashed.is_empty_tree_for(algo), "{algo:?}");
1827 let other = match algo {
1828 HashAlgo::Sha1 => HashAlgo::Sha256,
1829 HashAlgo::Sha256 => HashAlgo::Sha1,
1830 };
1831 assert!(!hashed.is_empty_tree_for(other), "{algo:?}");
1832 let empty_blob =
1833 crate::hash::hash_object(algo, ObjectKind::Blob, b"").expect("hash empty blob");
1834 assert!(!empty_blob.is_empty_tree_for(algo), "{algo:?}");
1835 }
1836 let legacy = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf899d69f7c6948d4").expect("hex");
1837 assert!(legacy.is_empty_tree_for(HashAlgo::Sha1));
1838 assert!(!legacy.is_empty_tree_for(HashAlgo::Sha256));
1839 }
1840
1841 #[test]
1842 fn serialize_tree_mode_matches_mode_str() {
1843 for mode in [
1844 0,
1845 0o7,
1846 0o100644,
1847 0o100755,
1848 0o120000,
1849 0o040000,
1850 0o160000,
1851 u32::MAX,
1852 ] {
1853 let mut out = Vec::new();
1854 push_tree_mode(&mut out, mode);
1855 let entry = TreeEntry {
1856 mode,
1857 name: Vec::new(),
1858 oid: ObjectId::empty_tree(),
1859 };
1860 assert_eq!(out, entry.mode_str().into_bytes(), "mode {mode:o}");
1861 }
1862 }
1863
1864 #[test]
1865 fn is_empty_blob_matches_both_hash_algorithms() {
1866 for algo in [HashAlgo::Sha1, HashAlgo::Sha256] {
1867 let oid = algo.hash_object(ObjectKind::Blob, b"").unwrap();
1868 assert!(oid.is_empty_blob(), "{algo:?} empty blob {}", oid.to_hex());
1869 let other = algo.hash_object(ObjectKind::Blob, b"x").unwrap();
1870 assert!(!other.is_empty_blob());
1871 }
1872 assert!(!ObjectId::empty_tree().is_empty_blob());
1873 assert!(!ObjectId::zero().is_empty_blob());
1874 }
1875
1876 #[test]
1877 fn commit_subject_pretty_folds_first_paragraph() {
1878 assert_eq!(commit_subject_pretty("hello\nworld\n\nbody"), "hello world");
1879 assert_eq!(commit_subject_pretty(""), "");
1880 assert_eq!(
1881 commit_subject_pretty(" hello world\tthere \n next\t line \n\nbody\n"),
1882 " hello world\tthere next\t line"
1883 );
1884 }
1885
1886 #[test]
1887 fn commit_ref_matches_parse_commit_headers() {
1888 let raw = concat!(
1889 "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
1890 "parent 1111111111111111111111111111111111111111\n",
1891 "author A <a@example.com> 1 +0000\n",
1892 "committer C <c@example.com> 2 +0000\n",
1893 "\n",
1894 "ignored body\n",
1895 );
1896 let full = parse_commit(raw.as_bytes()).expect("parse");
1897 let link = crate::objects::CommitRef::parse(raw.as_bytes()).expect("CommitRef");
1898 assert_eq!(link.tree(), full.tree);
1899 assert_eq!(link.parents(), full.parents.as_slice());
1900 assert_eq!(link.author(), full.author);
1901 assert_eq!(link.committer(), full.committer);
1902 }
1903
1904 #[test]
1905 fn parse_commit_skips_multiline_gpgsig_continuation() {
1906 let raw = concat!(
1907 "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
1908 "author A U Thor <author@example.com> 1 +0000\n",
1909 "committer C O Mitter <committer@example.com> 1 +0000\n",
1910 "gpgsig -----BEGIN PGP SIGNATURE-----\n",
1911 " abcdef\n",
1912 " -----END PGP SIGNATURE-----\n",
1913 "\n",
1914 "msg\n",
1915 );
1916 let c = parse_commit(raw.as_bytes()).expect("parse signed commit");
1917 assert_eq!(c.tree.to_hex(), "4b825dc642cb6eb9a060e54bf8d69288fbee4904");
1918 assert_eq!(c.message, "msg\n");
1919 assert_eq!(serialize_commit(&c), raw.as_bytes());
1920 }
1921
1922 #[test]
1923 fn hex_parsing_accepts_both_cases_and_lengths_and_rejects_the_rest() {
1924 let sha1 = "0123456789abcdefABCDEF0123456789abcdef01";
1925 let oid = ObjectId::from_hex(sha1).unwrap();
1926 assert_eq!(oid.algo(), HashAlgo::Sha1);
1927 assert_eq!(oid.to_hex(), sha1.to_ascii_lowercase());
1928 assert_eq!(ObjectId::from_hex_ascii(sha1.as_bytes()).unwrap(), oid);
1929 assert_eq!(sha1.parse::<ObjectId>().unwrap(), oid);
1930
1931 let sha256 = "f".repeat(64);
1932 let oid = sha256.parse::<ObjectId>().unwrap();
1933 assert_eq!(oid.algo(), HashAlgo::Sha256);
1934 assert_eq!(oid.as_bytes(), [0xff; 32]);
1935 assert!(ObjectId::from_hex_ascii(sha256.as_bytes()).is_err());
1936
1937 for bad in [
1938 "",
1939 &sha1[..39],
1940 &format!("{sha1}0"),
1941 "g123456789abcdef0123456789abcdef01234567",
1942 "0123456789abcdef0123456789abcdef0123456 ",
1943 "0123456789abcdef0123456789abcdef012345/7",
1944 "0123456789abcdef0123456789abcdef012345:7",
1945 "0123456789abcdef0123456789abcdef012345`7",
1946 "0123456789abcdef0123456789abcdef012345G7",
1947 ] {
1948 assert!(
1949 matches!(ObjectId::from_hex(bad), Err(Error::InvalidObjectId(s)) if s == bad),
1950 "{bad:?}"
1951 );
1952 assert!(bad.parse::<ObjectId>().is_err(), "{bad:?}");
1953 assert!(ObjectId::from_hex_ascii(bad.as_bytes()).is_err(), "{bad:?}");
1954 }
1955 let mut non_ascii = sha1.as_bytes().to_vec();
1956 non_ascii[3] = 0xb0;
1957 assert!(ObjectId::from_hex_ascii(&non_ascii).is_err());
1958 }
1959
1960 #[test]
1961 fn hex_and_display_cover_every_byte_for_both_lengths() {
1962 let all: Vec<u8> = (0..=255).collect();
1963 for width in [20, 32] {
1964 for chunk in all.chunks(width).filter(|c| c.len() == width) {
1965 let oid = ObjectId::from_bytes(chunk).unwrap();
1966 let expected: String = chunk.iter().map(|b| format!("{b:02x}")).collect();
1967 assert_eq!(oid.to_hex(), expected);
1968 assert_eq!(oid.to_string(), expected);
1969 assert_eq!(format!("{oid:?}"), format!("ObjectId({expected})"));
1970 assert_eq!(ObjectId::from_hex(&expected).unwrap(), oid);
1971 }
1972 }
1973 }
1974
1975 #[test]
1976 fn serialize_commit_applies_public_field_edits_after_parse() {
1977 let raw = concat!(
1978 "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
1979 "author A <a@example.com> 1 +0000\n",
1980 "committer C <c@example.com> 1 +0000\n",
1981 "\n",
1982 "msg\n",
1983 );
1984 let mut commit = parse_commit(raw.as_bytes()).expect("parse");
1985 let zero = ObjectId::from_hex("0000000000000000000000000000000000000000").unwrap();
1986 commit.tree = zero;
1987 let serialized = serialize_commit(&commit);
1988 let tree_line = format!("tree {}\n", zero.to_hex());
1989 assert!(
1990 serialized.starts_with(tree_line.as_bytes()),
1991 "serialize_commit ignored the updated public tree field"
1992 );
1993 }
1994
1995 #[test]
1996 fn parse_commit_preserves_gpgsig_mergetag_bytes_exactly() {
1997 let raw = concat!(
1998 "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
1999 "author A <a@example.com> 1 +0000\n",
2000 "committer C <c@example.com> 1 +0000\n",
2001 "encoding ISO-8859-1\n",
2002 "gpgsig -----BEGIN PGP SIGNATURE-----\n",
2003 " line2\n",
2004 " -----END PGP SIGNATURE-----\n",
2005 "mergetag object 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
2006 " type commit\n",
2007 " tag merge-tag\n",
2008 " tagger T <t@example.com> 1 +0000\n",
2009 " -----BEGIN PGP SIGNATURE-----\n",
2010 " inner\n",
2011 " -----END PGP SIGNATURE-----\n",
2012 "\n",
2013 "msg-only\n",
2014 );
2015 let parsed = parse_commit(raw.as_bytes()).expect("parse");
2016 assert_eq!(serialize_commit(&parsed), raw.as_bytes());
2017 }
2018
2019 #[test]
2020 fn tree_entry_mode_str_and_tag_object_line_oid() {
2021 let oid = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap();
2022 let entry = TreeEntry {
2023 mode: 0o040000,
2024 name: b"dir".to_vec(),
2025 oid,
2026 };
2027 assert_eq!(entry.mode_str(), "40000");
2028 let tag_body = format!("object {}\ntype commit\ntag t\n\n", oid.to_hex());
2029 assert_eq!(tag_object_line_oid(tag_body.as_bytes()), Some(oid));
2030 }
2031
2032 #[test]
2033 fn parse_commit_non_commit_bytes_does_not_panic() {
2034 let tree_bytes = b"100644 blob 0123456789012345678901234567890123456789\tf";
2035 let err = parse_commit(tree_bytes).expect_err("tree bytes are not a commit");
2036 assert!(matches!(err, Error::CorruptObject(_)));
2037 }
2038
2039 #[test]
2040 fn parse_tree_with_sha256_oid_width() {
2041 let oid_bytes = [0x6eu8; 32];
2042 let oid = ObjectId::from_bytes(&oid_bytes).expect("oid");
2043 let mut raw = b"100644 file\0".to_vec();
2044 raw.extend_from_slice(oid.as_bytes());
2045 let entries = parse_tree_with_oid_len(&raw, HashAlgo::Sha256.len()).expect("tree");
2046 assert_eq!(entries.len(), 1);
2047 assert_eq!(entries[0].oid, oid);
2048 }
2049
2050 #[test]
2051 fn parse_commit_tree_and_parents_prefix_matches_full_parse_or_declines() -> Result<()> {
2052 let tree = "1111111111111111111111111111111111111111";
2053 let p1 = "2222222222222222222222222222222222222222";
2054 let p2 = "3333333333333333333333333333333333333333";
2055 let raw = format!(
2056 "tree {tree}\nparent {p1}\nparent {p2}\nauthor A <a@x> 1 +0000\n\
2057 committer C <c@x> 2 +0000\n\nmessage\n"
2058 );
2059 let commit = parse_commit(raw.as_bytes())?;
2060 let full = (commit.tree, commit.parents);
2061 assert_eq!(full.1.len(), 2);
2062 assert_eq!(
2063 parse_commit_tree_and_parents_lenient(raw.as_bytes(), HashAlgo::Sha1.hex_len()),
2064 Some(full.clone())
2065 );
2066 let hex_len = HashAlgo::Sha1.hex_len();
2067 let header_end = raw.find("author").expect("author line");
2068 for cut in 0..=raw.len() {
2069 let got = parse_commit_tree_and_parents_prefix(&raw.as_bytes()[..cut], hex_len);
2070 if cut > header_end {
2071 assert_eq!(got.as_ref(), Some(&full), "cut at {cut}");
2072 } else {
2073 assert_eq!(
2074 got, None,
2075 "cut at {cut} must not claim a complete parent list"
2076 );
2077 }
2078 }
2079 Ok(())
2080 }
2081
2082 #[test]
2083 fn commit_tree_and_parents_prefix_never_disagrees_with_whole_object() {
2084 let tree = "tree 1111111111111111111111111111111111111111\n";
2085 let parent = "parent 2222222222222222222222222222222222222222\n";
2086 let parent2 = "parent 3333333333333333333333333333333333333333\n";
2087 let bodies = [
2088 format!("{tree}{parent}author A <a@x> 1 +0000\n\nmessage\n"),
2089 format!("{tree}{parent}{parent2}committer C <c@x> 2 +0000\n\nno author\n"),
2090 format!("{tree}{parent}"),
2091 format!("{tree}{parent}{parent2}"),
2092 format!("{tree}author A <a@x> 1 +0000\n{parent}\nlate parent is ignored\n"),
2093 format!("{tree}{parent}not a header\n"),
2094 format!("{tree}parent zz\n\n"),
2095 format!("{tree}{parent}parent 2222"),
2096 format!("author A <a@x> 1 +0000\n{tree}\n"),
2097 ];
2098 let hex_len = HashAlgo::Sha1.hex_len();
2099 for body in &bodies {
2100 let whole = parse_commit_tree_and_parents_lenient(body.as_bytes(), hex_len);
2101 for cut in 0..=body.len() {
2102 if let Some(got) =
2103 parse_commit_tree_and_parents_prefix(&body.as_bytes()[..cut], hex_len)
2104 {
2105 assert_eq!(Some(got), whole, "cut {cut} of {body:?}");
2106 }
2107 }
2108 }
2109 let parents_of = |body: &str| {
2110 parse_commit_tree_and_parents_lenient(body.as_bytes(), hex_len).map(|(_, p)| p.len())
2111 };
2112 assert_eq!(parents_of(&bodies[1]), Some(2), "missing author is fine");
2113 assert_eq!(
2114 parents_of(&bodies[3]),
2115 Some(2),
2116 "object may end after parents"
2117 );
2118 assert_eq!(
2119 parents_of(&bodies[4]),
2120 Some(0),
2121 "only leading parents count"
2122 );
2123 assert_eq!(parents_of(&bodies[6]), None, "malformed parent line");
2124 assert_eq!(parents_of(&bodies[7]), None, "truncated parent line");
2125 assert_eq!(parents_of(&bodies[8]), None, "tree must come first");
2126 }
2127
2128 #[test]
2129 fn parse_commit_tree_and_parents_prefix_handles_root_and_sha256() -> Result<()> {
2130 let root = b"tree 1111111111111111111111111111111111111111\nauthor A <a@x> 1 +0000\n";
2131 let (tree, parents) =
2132 parse_commit_tree_and_parents_prefix(root, HashAlgo::Sha1.hex_len()).expect("root");
2133 assert_eq!(tree.to_hex(), "1111111111111111111111111111111111111111");
2134 assert!(parents.is_empty());
2135
2136 let t = "a".repeat(64);
2137 let p = "b".repeat(64);
2138 let raw = format!("tree {t}\nparent {p}\ncommitter C <c@x> 2 +0000\n");
2139 let (tree, parents) =
2140 parse_commit_tree_and_parents_prefix(raw.as_bytes(), HashAlgo::Sha256.hex_len())
2141 .expect("sha256");
2142 assert_eq!(tree.to_hex(), t);
2143 assert_eq!(parents.len(), 1);
2144 assert_eq!(parents[0].to_hex(), p);
2145
2146 assert!(parse_commit_tree_and_parents_prefix(raw.as_bytes(), 40).is_none());
2147 assert!(parse_commit_tree_and_parents_prefix(b"parent x\n", 40).is_none());
2148 assert!(parse_commit_tree_and_parents_prefix(
2149 b"tree zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz\nauthor\n",
2150 40
2151 )
2152 .is_none());
2153 Ok(())
2154 }
2155
2156 #[test]
2157 fn object_id_loose_path_helpers() {
2158 let oid = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap();
2159 assert_eq!(oid.loose_prefix(), "4b");
2160 assert_eq!(oid.loose_suffix().len(), 38);
2161 assert!(ObjectId::is_hex_len(40));
2162 assert!(ObjectId::is_loose_suffix_len(38));
2163 }
2164
2165 #[test]
2166 fn tree_ref_iter_yields_at_most_one_error() {
2167 let bad = b"100644 incomplete";
2168 let mut iter = TreeRefIter::with_oid_len(bad, HashAlgo::Sha1.len());
2169 assert!(iter.next().unwrap().is_err());
2170 assert!(iter.next().is_none());
2171 }
2172
2173 #[test]
2174 fn tree_ref_iter_public_api_smoke() {
2175 let data = serialize_tree(&[TreeEntry {
2176 mode: 0o100644,
2177 name: b"a".to_vec(),
2178 oid: ObjectId::from_hex("0123456789012345678901234567890123456789").unwrap(),
2179 }]);
2180 let mut iter = TreeRefIter::infer_oid_len(&data).expect("infer");
2181 let entry = iter.next().unwrap().expect("entry");
2182 assert_eq!(entry.name, b"a");
2183 let _owned = entry.to_owned();
2184 let _vec = parse_tree(&data).expect("parse");
2185 let _ = TreeRefIter::with_oid_len(&data, HashAlgo::Sha1.len());
2186 }
2187
2188 #[test]
2189 fn find_tree_entry_by_name_in_data_locates_directory_in_large_tree() -> Result<()> {
2190 let dummy = ObjectId::from_hex("0123456789012345678901234567890123456789")?;
2191 let mut entries: Vec<TreeEntry> = (1..=60)
2192 .map(|i| TreeEntry {
2193 mode: 0o100644,
2194 name: format!("f{i}.txt").into_bytes(),
2195 oid: dummy,
2196 })
2197 .collect();
2198 entries.push(TreeEntry {
2199 mode: 0o040000,
2200 name: b"sub".to_vec(),
2201 oid: dummy,
2202 });
2203 entries.sort_by(|a, b| {
2204 tree_entry_cmp(&a.name, a.mode == 0o040000, &b.name, b.mode == 0o040000)
2205 });
2206 let data = serialize_tree(&entries);
2207 assert!(entries.len() >= 48);
2208 let found = find_tree_entry_by_name_in_data(&data, HashAlgo::Sha1.len(), b"sub", true)?
2209 .expect("sub directory in wide tree");
2210 assert_eq!(found.mode, 0o040000);
2211 assert_eq!(found.name, b"sub");
2212 Ok(())
2213 }
2214
2215 fn sorted_tree_data(entries: &mut [TreeEntry]) -> Vec<u8> {
2216 entries.sort_by(|a, b| {
2217 tree_entry_cmp(&a.name, a.mode == 0o040000, &b.name, b.mode == 0o040000)
2218 });
2219 serialize_tree(entries)
2220 }
2221
2222 #[test]
2223 fn find_tree_entry_by_name_in_data_matches_parse_tree_for_every_name() -> Result<()> {
2224 let oid_a = ObjectId::from_hex("1111111111111111111111111111111111111111")?;
2225 let oid_b = ObjectId::from_hex("2222222222222222222222222222222222222222")?;
2226 let mut entries = vec![
2227 TreeEntry {
2228 mode: 0o040000,
2229 name: b"foo".to_vec(),
2230 oid: oid_a,
2231 },
2232 TreeEntry {
2233 mode: 0o100644,
2234 name: b"foo.c".to_vec(),
2235 oid: oid_b,
2236 },
2237 TreeEntry {
2238 mode: 0o100755,
2239 name: b"foo-bar".to_vec(),
2240 oid: oid_b,
2241 },
2242 TreeEntry {
2243 mode: 0o120000,
2244 name: b"link".to_vec(),
2245 oid: oid_a,
2246 },
2247 TreeEntry {
2248 mode: 0o160000,
2249 name: b"sub".to_vec(),
2250 oid: oid_b,
2251 },
2252 TreeEntry {
2253 mode: 0o100644,
2254 name: b"README".to_vec(),
2255 oid: oid_a,
2256 },
2257 ];
2258 let data = sorted_tree_data(&mut entries);
2259 for expected in parse_tree(&data)? {
2260 let name_is_tree = expected.mode == 0o040000;
2261 let found = find_tree_entry_by_name_in_data(
2262 &data,
2263 HashAlgo::Sha1.len(),
2264 &expected.name,
2265 name_is_tree,
2266 )?
2267 .expect("entry present");
2268 assert_eq!(found, expected);
2269 }
2270 for missing in [&b"fo"[..], b"foo/", b"foo.d", b"zzz", b"", b"AAA", b"lin"] {
2271 assert!(
2272 find_tree_entry_by_name_in_data(&data, HashAlgo::Sha1.len(), missing, false)?
2273 .is_none(),
2274 "unexpected match for {:?}",
2275 String::from_utf8_lossy(missing)
2276 );
2277 }
2278 Ok(())
2279 }
2280
2281 #[test]
2282 fn find_tree_entry_by_name_in_data_stops_after_sorted_position() -> Result<()> {
2283 let oid = ObjectId::from_hex("3333333333333333333333333333333333333333")?;
2284 let mut entries = vec![
2285 TreeEntry {
2286 mode: 0o100644,
2287 name: b"a".to_vec(),
2288 oid,
2289 },
2290 TreeEntry {
2291 mode: 0o100644,
2292 name: b"c".to_vec(),
2293 oid,
2294 },
2295 ];
2296 let mut data = sorted_tree_data(&mut entries);
2297 data.extend_from_slice(b"garbage without separators");
2298 let found = find_tree_entry_by_name_in_data(&data, HashAlgo::Sha1.len(), b"a", false)?;
2299 assert_eq!(found.map(|e| e.name), Some(b"a".to_vec()));
2300 assert!(
2301 find_tree_entry_by_name_in_data(&data, HashAlgo::Sha1.len(), b"b", false)?.is_none()
2302 );
2303 assert!(matches!(
2304 find_tree_entry_by_name_in_data(&data, HashAlgo::Sha1.len(), b"d", false),
2305 Err(Error::CorruptObject(_))
2306 ));
2307 Ok(())
2308 }
2309
2310 #[test]
2311 fn find_tree_entry_by_name_in_data_rejects_bad_mode_and_truncated_oid() {
2312 let bad_mode = b"10x644 a\0aaaaaaaaaaaaaaaaaaaa";
2313 assert!(matches!(
2314 find_tree_entry_by_name_in_data(bad_mode, HashAlgo::Sha1.len(), b"a", false),
2315 Err(Error::CorruptObject(_))
2316 ));
2317 let truncated = b"100644 a\0short";
2318 assert!(matches!(
2319 find_tree_entry_by_name_in_data(truncated, HashAlgo::Sha1.len(), b"a", false),
2320 Err(Error::CorruptObject(_))
2321 ));
2322 }
2323
2324 #[test]
2325 fn find_tree_entry_by_name_in_data_reads_sha256_width() -> Result<()> {
2326 let oid = ObjectId::from_bytes(&[0x5au8; 32])?;
2327 let mut raw = b"100644 file\0".to_vec();
2328 raw.extend_from_slice(oid.as_bytes());
2329 let found = find_tree_entry_by_name_in_data(&raw, HashAlgo::Sha256.len(), b"file", false)?
2330 .expect("file");
2331 assert_eq!(found.oid, oid);
2332 assert_eq!(found.mode, 0o100644);
2333 Ok(())
2334 }
2335}