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 hex::encode(self.as_bytes())
326 }
327
328 #[must_use]
332 pub fn loose_prefix(&self) -> String {
333 hex::encode(&self.bytes[..1])
334 }
335
336 pub fn from_hex(s: &str) -> Result<Self> {
343 Self::decode_hex(s.as_bytes()).ok_or_else(|| Error::InvalidObjectId(s.to_owned()))
344 }
345
346 pub fn from_hex_ascii(bytes: &[u8]) -> Result<Self> {
352 if bytes.len() != HashAlgo::Sha1.hex_len() {
353 return Err(Error::InvalidObjectId(
354 String::from_utf8_lossy(bytes).into_owned(),
355 ));
356 }
357 Self::decode_hex(bytes)
358 .ok_or_else(|| Error::InvalidObjectId(String::from_utf8_lossy(bytes).into_owned()))
359 }
360
361 fn decode_hex(hex: &[u8]) -> Option<Self> {
366 let len = hex.len() / 2;
367 if !Self::is_hex_len(hex.len()) {
368 return None;
369 }
370 let mut bytes = [0u8; MAX_OID_LEN];
371 let mut invalid = 0u8;
372 for (out, pair) in bytes.iter_mut().zip(hex.as_chunks::<2>().0) {
373 let hi = HEX_DECODE[usize::from(pair[0])];
374 let lo = HEX_DECODE[usize::from(pair[1])];
375 invalid |= hi | lo;
376 *out = (hi << 4) | lo;
377 }
378 (invalid & HEX_INVALID == 0).then_some(Self {
379 bytes,
380 len: len as u8,
381 })
382 }
383
384 #[must_use]
387 pub fn loose_suffix(&self) -> String {
388 hex::encode(&self.bytes[1..self.len as usize])
389 }
390
391 #[must_use]
394 pub fn is_full_hex(s: &str) -> bool {
395 (s.len() == HashAlgo::Sha1.hex_len() || s.len() == HashAlgo::Sha256.hex_len())
396 && s.bytes().all(|b| b.is_ascii_hexdigit())
397 }
398
399 #[must_use]
401 pub const fn is_hex_len(len: usize) -> bool {
402 len == HashAlgo::Sha1.hex_len() || len == HashAlgo::Sha256.hex_len()
403 }
404
405 #[must_use]
408 pub const fn is_loose_suffix_len(len: usize) -> bool {
409 len == HashAlgo::Sha1.hex_len() - 2 || len == HashAlgo::Sha256.hex_len() - 2
410 }
411}
412
413impl std::hash::Hash for ObjectId {
414 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
415 state.write(self.as_bytes());
416 }
417}
418
419impl fmt::Display for ObjectId {
420 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
421 f.write_str(&self.to_hex())
422 }
423}
424
425impl fmt::Debug for ObjectId {
426 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
427 write!(f, "ObjectId({})", self.to_hex())
428 }
429}
430
431#[cfg(feature = "serde-models")]
432impl serde::Serialize for ObjectId {
433 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
434 where
435 S: serde::Serializer,
436 {
437 serializer.serialize_str(&self.to_hex())
438 }
439}
440
441const HEX_INVALID: u8 = 0x10;
443
444const HEX_DECODE: [u8; 256] = {
446 let mut table = [HEX_INVALID; 256];
447 let mut i = 0u8;
448 while i < 10 {
449 table[(b'0' + i) as usize] = i;
450 i += 1;
451 }
452 let mut i = 0u8;
453 while i < 6 {
454 table[(b'a' + i) as usize] = 10 + i;
455 table[(b'A' + i) as usize] = 10 + i;
456 i += 1;
457 }
458 table
459};
460
461impl FromStr for ObjectId {
462 type Err = Error;
463
464 fn from_str(s: &str) -> Result<Self> {
465 Self::from_hex(s)
466 }
467}
468
469#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
486pub struct FileMode(u32);
487
488impl FileMode {
489 pub const ABSENT: Self = Self(0);
491 pub const REGULAR: Self = Self(0o100644);
493 pub const EXECUTABLE: Self = Self(0o100755);
495 pub const SYMLINK: Self = Self(0o120000);
497 pub const GITLINK: Self = Self(0o160000);
499 pub const TREE: Self = Self(0o040000);
501
502 #[must_use]
504 pub const fn new(bits: u32) -> Self {
505 Self(bits)
506 }
507
508 #[must_use]
510 pub const fn bits(self) -> u32 {
511 self.0
512 }
513
514 #[must_use]
516 pub const fn is_absent(self) -> bool {
517 self.0 == 0
518 }
519
520 #[must_use]
522 pub fn octal_if_present(self) -> Option<String> {
523 (!self.is_absent()).then(|| self.to_string())
524 }
525
526 #[must_use]
528 pub const fn is_symlink(self) -> bool {
529 self.0 & 0o170000 == 0o120000
530 }
531
532 #[must_use]
534 pub const fn is_gitlink(self) -> bool {
535 self.0 & 0o170000 == 0o160000
536 }
537}
538
539impl From<u32> for FileMode {
540 fn from(bits: u32) -> Self {
541 Self(bits)
542 }
543}
544
545impl From<FileMode> for u32 {
546 fn from(mode: FileMode) -> Self {
547 mode.0
548 }
549}
550
551impl fmt::Display for FileMode {
552 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
553 write!(f, "{:06o}", self.0)
554 }
555}
556
557impl fmt::Debug for FileMode {
558 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
559 write!(f, "FileMode({:06o})", self.0)
560 }
561}
562
563#[cfg(feature = "serde-models")]
564impl serde::Serialize for FileMode {
565 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
566 where
567 S: serde::Serializer,
568 {
569 serializer.collect_str(self)
570 }
571}
572
573#[derive(Clone, Copy, PartialEq, Eq)]
597pub struct ObjectIdPrefix {
598 bytes: [u8; MAX_OID_LEN],
601 nibbles: u8,
602 algo: HashAlgo,
603}
604
605impl ObjectIdPrefix {
606 pub fn from_hex(hex: &str, algo: HashAlgo) -> Result<Self> {
615 let digits = hex.as_bytes();
616 if digits.len() > algo.hex_len() {
617 return Err(Error::InvalidObjectId(hex.to_owned()));
618 }
619 let mut bytes = [0u8; MAX_OID_LEN];
620 let mut invalid = 0u8;
621 for (i, &digit) in digits.iter().enumerate() {
622 let nibble = HEX_DECODE[usize::from(digit)];
623 invalid |= nibble;
624 let shift = if i.is_multiple_of(2) { 4 } else { 0 };
625 bytes[i / 2] |= (nibble & 0x0f) << shift;
626 }
627 if invalid & HEX_INVALID != 0 {
628 return Err(Error::InvalidObjectId(hex.to_owned()));
629 }
630 Ok(Self {
631 bytes,
632 nibbles: digits.len() as u8,
633 algo,
634 })
635 }
636
637 #[must_use]
639 pub fn nibbles(&self) -> usize {
640 usize::from(self.nibbles)
641 }
642
643 #[must_use]
645 pub fn is_empty(&self) -> bool {
646 self.nibbles == 0
647 }
648
649 #[must_use]
651 pub fn algo(&self) -> HashAlgo {
652 self.algo
653 }
654
655 #[must_use]
660 pub fn matches(&self, oid: &ObjectId) -> bool {
661 self.matches_digest(oid.as_bytes())
662 }
663
664 #[must_use]
666 pub(crate) fn matches_digest(&self, digest: &[u8]) -> bool {
667 let whole = self.nibbles() / 2;
668 let Some(head) = digest.get(..whole) else {
669 return false;
670 };
671 if head != &self.bytes[..whole] {
672 return false;
673 }
674 if self.nibbles.is_multiple_of(2) {
675 return true;
676 }
677 digest
678 .get(whole)
679 .is_some_and(|&b| b & 0xf0 == self.bytes[whole])
680 }
681
682 #[must_use]
684 pub fn lowest(&self) -> ObjectId {
685 ObjectId {
686 bytes: self.bytes,
687 len: self.algo.len() as u8,
688 }
689 }
690
691 #[must_use]
693 pub fn highest(&self) -> ObjectId {
694 let mut bytes = self.bytes;
695 let whole = self.nibbles() / 2;
696 if !self.nibbles.is_multiple_of(2) {
697 bytes[whole] |= 0x0f;
698 }
699 let first_free = whole + usize::from(self.nibbles % 2);
700 bytes[first_free..self.algo.len()].fill(0xff);
701 ObjectId {
702 bytes,
703 len: self.algo.len() as u8,
704 }
705 }
706}
707
708impl fmt::Display for ObjectIdPrefix {
709 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
710 let hex = hex::encode(&self.bytes[..self.nibbles().div_ceil(2)]);
711 f.write_str(&hex[..self.nibbles()])
712 }
713}
714
715impl fmt::Debug for ObjectIdPrefix {
716 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
717 write!(f, "ObjectIdPrefix({self})")
718 }
719}
720
721#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
723pub enum ObjectKind {
724 Blob,
726 Tree,
728 Commit,
730 Tag,
732}
733
734impl ObjectKind {
735 pub fn from_bytes(b: &[u8]) -> Result<Self> {
741 match b {
742 b"blob" => Ok(Self::Blob),
743 b"tree" => Ok(Self::Tree),
744 b"commit" => Ok(Self::Commit),
745 b"tag" => Ok(Self::Tag),
746 other => Err(Error::UnknownObjectType(
747 String::from_utf8_lossy(other).into_owned(),
748 )),
749 }
750 }
751
752 #[must_use]
757 pub fn from_tag_type_field(line: &[u8]) -> Option<Self> {
758 fn keyword_matches(canonical: &[u8], field: &[u8]) -> bool {
759 if field.is_empty() {
760 return false;
761 }
762 for (i, &bc) in field.iter().enumerate() {
763 let sc = canonical.get(i).copied().unwrap_or(0);
764 if sc != bc {
765 return false;
766 }
767 }
768 canonical.get(field.len()).copied().unwrap_or(0) == 0
769 }
770
771 const NAMES: &[(ObjectKind, &[u8])] = &[
772 (ObjectKind::Blob, b"blob"),
773 (ObjectKind::Tree, b"tree"),
774 (ObjectKind::Commit, b"commit"),
775 (ObjectKind::Tag, b"tag"),
776 ];
777 for &(kind, name) in NAMES {
778 if keyword_matches(name, line) {
779 return Some(kind);
780 }
781 }
782 None
783 }
784
785 #[must_use]
787 pub fn as_str(&self) -> &'static str {
788 match self {
789 Self::Blob => "blob",
790 Self::Tree => "tree",
791 Self::Commit => "commit",
792 Self::Tag => "tag",
793 }
794 }
795}
796
797impl fmt::Display for ObjectKind {
798 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
799 f.write_str(self.as_str())
800 }
801}
802
803impl FromStr for ObjectKind {
804 type Err = Error;
805
806 fn from_str(s: &str) -> Result<Self> {
807 Self::from_bytes(s.as_bytes())
808 }
809}
810
811#[derive(Debug, Clone, Copy, PartialEq, Eq)]
813pub struct ObjectInfo {
814 pub kind: ObjectKind,
816 pub size: u64,
818}
819
820#[derive(Debug, Clone)]
822pub struct Object {
823 pub kind: ObjectKind,
825 pub data: Vec<u8>,
827}
828
829#[derive(Debug, Clone, Copy, PartialEq, Eq)]
835pub struct ObjectRef<'a> {
836 pub kind: ObjectKind,
838 pub data: &'a [u8],
840}
841
842impl<'a> ObjectRef<'a> {
843 #[must_use]
845 pub fn from_buf(kind: ObjectKind, buf: &'a [u8]) -> Self {
846 Self { kind, data: buf }
847 }
848}
849
850impl Object {
851 #[must_use]
853 pub fn new(kind: ObjectKind, data: Vec<u8>) -> Self {
854 Self { kind, data }
855 }
856
857 #[must_use]
859 pub fn as_ref(&self) -> ObjectRef<'_> {
860 ObjectRef {
861 kind: self.kind,
862 data: self.data.as_slice(),
863 }
864 }
865
866 #[must_use]
868 pub fn to_store_bytes(&self) -> Vec<u8> {
869 let header = format!("{} {}\0", self.kind, self.data.len());
870 let mut out = Vec::with_capacity(header.len() + self.data.len());
871 out.extend_from_slice(header.as_bytes());
872 out.extend_from_slice(&self.data);
873 out
874 }
875}
876
877#[derive(Debug, Clone, PartialEq, Eq)]
879pub struct TreeEntry {
880 pub mode: u32,
882 pub name: Vec<u8>,
884 pub oid: ObjectId,
886}
887
888impl TreeEntry {
889 #[must_use]
893 pub fn mode_str(&self) -> String {
894 if self.mode == 0o040000 {
896 "40000".to_owned()
897 } else {
898 format!("{:o}", self.mode)
899 }
900 }
901}
902
903#[derive(Debug, Clone, Copy, PartialEq, Eq)]
905pub struct TreeEntryRef<'a> {
906 pub mode: u32,
908 pub name: &'a [u8],
910 pub oid: ObjectId,
912}
913
914impl TreeEntryRef<'_> {
915 #[must_use]
917 pub fn to_owned(&self) -> TreeEntry {
918 TreeEntry {
919 mode: self.mode,
920 name: self.name.to_vec(),
921 oid: self.oid,
922 }
923 }
924}
925
926#[derive(Debug, Clone)]
931pub struct TreeRefIter<'a> {
932 data: &'a [u8],
933 pos: usize,
934 oid_len: usize,
935 failed: bool,
937}
938
939impl<'a> TreeRefIter<'a> {
940 #[must_use]
942 pub fn with_oid_len(data: &'a [u8], oid_len: usize) -> Self {
943 Self {
944 data,
945 pos: 0,
946 oid_len,
947 failed: false,
948 }
949 }
950
951 pub fn infer_oid_len(data: &'a [u8]) -> Result<Self> {
957 match try_parse_tree_oid_len(data, HashAlgo::Sha1.len()) {
958 Ok(()) => Ok(Self::with_oid_len(data, HashAlgo::Sha1.len())),
959 Err(sha1_err) => match try_parse_tree_oid_len(data, HashAlgo::Sha256.len()) {
960 Ok(()) => Ok(Self::with_oid_len(data, HashAlgo::Sha256.len())),
961 Err(_) => Err(sha1_err),
962 },
963 }
964 }
965
966 fn next_entry(&mut self) -> Result<Option<TreeEntryRef<'a>>> {
967 parse_tree_entry_at(self.data, &mut self.pos, self.oid_len)
968 }
969}
970
971fn parse_tree_entry_at<'a>(
973 data: &'a [u8],
974 pos: &mut usize,
975 oid_len: usize,
976) -> Result<Option<TreeEntryRef<'a>>> {
977 if *pos >= data.len() {
978 return Ok(None);
979 }
980 let sp = data[*pos..]
981 .iter()
982 .position(|&b| b == b' ')
983 .ok_or_else(|| Error::CorruptObject("tree entry missing space".to_owned()))?;
984 let mode_bytes = &data[*pos..*pos + sp];
985 let mode = std::str::from_utf8(mode_bytes)
986 .ok()
987 .and_then(|s| u32::from_str_radix(s, 8).ok())
988 .ok_or_else(|| {
989 Error::CorruptObject(format!(
990 "invalid tree mode: {}",
991 String::from_utf8_lossy(mode_bytes)
992 ))
993 })?;
994 *pos += sp + 1;
995
996 let nul = data[*pos..]
997 .iter()
998 .position(|&b| b == 0)
999 .ok_or_else(|| Error::CorruptObject("tree entry missing NUL".to_owned()))?;
1000 let name = &data[*pos..*pos + nul];
1001 *pos += nul + 1;
1002
1003 if *pos + oid_len > data.len() {
1004 return Err(Error::CorruptObject("tree entry truncated SHA".to_owned()));
1005 }
1006 let oid = ObjectId::from_bytes(&data[*pos..*pos + oid_len])?;
1007 *pos += oid_len;
1008
1009 Ok(Some(TreeEntryRef { mode, name, oid }))
1010}
1011
1012impl<'a> Iterator for TreeRefIter<'a> {
1013 type Item = Result<TreeEntryRef<'a>>;
1014
1015 fn next(&mut self) -> Option<Self::Item> {
1016 if self.failed || self.pos >= self.data.len() {
1017 return None;
1018 }
1019 match self.next_entry() {
1020 Ok(Some(entry)) => Some(Ok(entry)),
1021 Ok(None) => None,
1022 Err(err) => {
1023 self.failed = true;
1024 self.pos = self.data.len();
1025 Some(Err(err))
1026 }
1027 }
1028 }
1029}
1030
1031fn try_parse_tree_oid_len(data: &[u8], oid_len: usize) -> Result<()> {
1033 let mut pos = 0usize;
1034 while parse_tree_entry_at(data, &mut pos, oid_len)?.is_some() {}
1035 Ok(())
1036}
1037
1038fn collect_tree_with_oid_len(data: &[u8], oid_len: usize) -> Result<Vec<TreeEntry>> {
1039 let mut pos = 0usize;
1040 let mut entries = Vec::with_capacity(data.len() / 32);
1041 while let Some(entry) = parse_tree_entry_at(data, &mut pos, oid_len)? {
1042 entries.push(TreeEntry {
1043 mode: entry.mode,
1044 name: entry.name.to_vec(),
1045 oid: entry.oid,
1046 });
1047 }
1048 Ok(entries)
1049}
1050
1051pub fn parse_tree(data: &[u8]) -> Result<Vec<TreeEntry>> {
1062 match collect_tree_with_oid_len(data, HashAlgo::Sha1.len()) {
1063 Ok(entries) => Ok(entries),
1064 Err(sha1_err) => {
1065 collect_tree_with_oid_len(data, HashAlgo::Sha256.len()).map_err(|_| sha1_err)
1066 }
1067 }
1068}
1069
1070pub fn parse_tree_with_oid_len(data: &[u8], oid_len: usize) -> Result<Vec<TreeEntry>> {
1075 collect_tree_with_oid_len(data, oid_len)
1076}
1077
1078#[must_use]
1085pub fn serialize_tree(entries: &[TreeEntry]) -> Vec<u8> {
1086 let len = entries
1087 .iter()
1088 .map(|e| 8 + e.name.len() + e.oid.as_bytes().len())
1089 .sum();
1090 let mut out = Vec::with_capacity(len);
1091 for e in entries {
1092 push_tree_mode(&mut out, e.mode);
1093 out.push(b' ');
1094 out.extend_from_slice(&e.name);
1095 out.push(0);
1096 out.extend_from_slice(e.oid.as_bytes());
1097 }
1098 out
1099}
1100
1101fn push_tree_mode(out: &mut Vec<u8>, mode: u32) {
1103 let mut digits = [0u8; 11];
1104 let mut start = digits.len();
1105 let mut rest = mode;
1106 loop {
1107 start -= 1;
1108 digits[start] = b'0' + (rest & 7) as u8;
1109 rest >>= 3;
1110 if rest == 0 {
1111 break;
1112 }
1113 }
1114 out.extend_from_slice(&digits[start..]);
1115}
1116
1117#[must_use]
1130pub fn tree_entry_cmp(
1131 a_name: &[u8],
1132 a_is_tree: bool,
1133 b_name: &[u8],
1134 b_is_tree: bool,
1135) -> std::cmp::Ordering {
1136 let a_trailer = if a_is_tree { b'/' } else { 0u8 };
1137 let b_trailer = if b_is_tree { b'/' } else { 0u8 };
1138
1139 let min_len = a_name.len().min(b_name.len());
1140 let cmp = a_name[..min_len].cmp(&b_name[..min_len]);
1141 if cmp != std::cmp::Ordering::Equal {
1142 return cmp;
1143 }
1144 let ac = a_name.get(min_len).copied().unwrap_or(a_trailer);
1146 let bc = b_name.get(min_len).copied().unwrap_or(b_trailer);
1147 ac.cmp(&bc)
1148}
1149
1150fn parse_tree_mode_octal(bytes: &[u8]) -> Option<u32> {
1152 if bytes.is_empty() || bytes.len() > 7 {
1153 return None;
1154 }
1155 bytes.iter().try_fold(0u32, |acc, &b| match b {
1156 b'0'..=b'7' => Some(acc * 8 + u32::from(b - b'0')),
1157 _ => None,
1158 })
1159}
1160
1161pub(crate) fn find_tree_entry_by_name_in_data(
1173 data: &[u8],
1174 oid_len: usize,
1175 name: &[u8],
1176 name_is_tree: bool,
1177) -> Result<Option<TreeEntry>> {
1178 let mut pos = 0usize;
1179 while pos < data.len() {
1180 let rest = &data[pos..];
1181 let sp = memchr::memchr(b' ', rest)
1182 .ok_or_else(|| Error::CorruptObject("tree entry missing space".to_owned()))?;
1183 let mode = parse_tree_mode_octal(&rest[..sp])
1184 .ok_or_else(|| Error::CorruptObject("invalid tree mode".to_owned()))?;
1185 let name_start = sp + 1;
1186 let nul = memchr::memchr(0, &rest[name_start..])
1187 .ok_or_else(|| Error::CorruptObject("tree entry missing NUL".to_owned()))?;
1188 let entry_name = &rest[name_start..name_start + nul];
1189 let oid_start = name_start + nul + 1;
1190 let oid_end = oid_start + oid_len;
1191 if oid_end > rest.len() {
1192 return Err(Error::CorruptObject("tree entry truncated SHA".to_owned()));
1193 }
1194 if entry_name == name {
1195 return Ok(Some(TreeEntry {
1196 mode,
1197 name: entry_name.to_vec(),
1198 oid: ObjectId::from_bytes(&rest[oid_start..oid_end])?,
1199 }));
1200 }
1201 if tree_entry_cmp(entry_name, mode == 0o040000, name, name_is_tree)
1202 == std::cmp::Ordering::Greater
1203 {
1204 return Ok(None);
1205 }
1206 pos += oid_end;
1207 }
1208 Ok(None)
1209}
1210
1211pub(crate) fn find_tree_entry_by_name_dir_or_file_in_data(
1213 data: &[u8],
1214 oid_len: usize,
1215 name: &[u8],
1216) -> Result<Option<TreeEntry>> {
1217 if let Some(entry) = find_tree_entry_by_name_in_data(data, oid_len, name, true)? {
1218 if entry.mode == 0o040000 {
1219 return Ok(Some(entry));
1220 }
1221 }
1222 find_tree_entry_by_name_in_data(data, oid_len, name, false)
1223}
1224
1225#[derive(Debug, Clone, PartialEq, Eq)]
1231pub struct CommitData {
1232 pub tree: ObjectId,
1234 pub parents: Vec<ObjectId>,
1236 pub author: String,
1238 pub committer: String,
1240 pub author_raw: Vec<u8>,
1244 pub committer_raw: Vec<u8>,
1246 pub encoding: Option<String>,
1248 pub message: String,
1250 #[doc = "Optional raw message bytes for non-UTF-8 messages."]
1253 pub raw_message: Option<Vec<u8>>,
1254 pub extra_headers: Vec<u8>,
1260}
1261
1262#[must_use]
1269pub fn commit_subject_pretty(message: &str) -> String {
1270 let mut parts = Vec::new();
1271 for line in message.lines() {
1272 if line.trim().is_empty() {
1273 break;
1274 }
1275 parts.push(line.trim_end());
1276 }
1277 parts.join(" ")
1278}
1279
1280pub fn read_commit_subject_pretty(data: &[u8]) -> Result<String> {
1286 let message_start = data
1287 .windows(2)
1288 .position(|w| w == b"\n\n")
1289 .map(|p| p + 2)
1290 .ok_or_else(|| {
1291 Error::CorruptObject("commit missing blank line before message".to_owned())
1292 })?;
1293 let body = std::str::from_utf8(data.get(message_start..).unwrap_or_default()).unwrap_or("");
1294 Ok(commit_subject_pretty(body))
1295}
1296
1297pub fn read_commit_subject_line(data: &[u8]) -> Result<String> {
1305 let mut pos = 0usize;
1306 let mut encoding: Option<String> = None;
1307 while pos < data.len() {
1308 let line_start = pos;
1309 let mut line_end = pos;
1310 while line_end < data.len() && data[line_end] != b'\n' {
1311 line_end += 1;
1312 }
1313 let line = &data[line_start..line_end];
1314 let after_nl = line_end.saturating_add(1);
1315 if line.is_empty() {
1316 let body = data.get(after_nl..).unwrap_or_default();
1317 let message = commit_encoding::decode_bytes(encoding.as_deref(), body);
1318 return Ok(message
1319 .lines()
1320 .find(|l| !l.trim().is_empty())
1321 .map(str::trim)
1322 .unwrap_or("(no subject)")
1323 .to_owned());
1324 }
1325 if line.first() == Some(&b' ') {
1326 pos = after_nl;
1327 continue;
1328 }
1329 if let Some(rest) = line.strip_prefix(b"encoding ") {
1330 encoding = Some(
1331 std::str::from_utf8(rest)
1332 .map_err(|_| Error::CorruptObject("encoding is not UTF-8".to_owned()))?
1333 .trim()
1334 .to_owned(),
1335 );
1336 }
1337 pos = after_nl;
1338 }
1339 Err(Error::CorruptObject(
1340 "commit missing blank line before message".to_owned(),
1341 ))
1342}
1343
1344pub(crate) fn parse_oid_field(value: &[u8]) -> Result<ObjectId> {
1345 let s = std::str::from_utf8(value)
1346 .map_err(|_| Error::CorruptObject("commit header object id is not UTF-8".to_owned()))?;
1347 s.trim()
1348 .parse::<ObjectId>()
1349 .map_err(|_| Error::CorruptObject("invalid object id in commit header".to_owned()))
1350}
1351
1352pub(crate) fn parse_commit_tree_and_parents_prefix(
1362 prefix: &[u8],
1363 hex_len: usize,
1364) -> Option<(ObjectId, Vec<ObjectId>)> {
1365 canonical_commit_tree_and_parents(prefix, hex_len, false)
1366}
1367
1368pub(crate) fn parse_commit_tree_and_parents_lenient(
1375 data: &[u8],
1376 hex_len: usize,
1377) -> Option<(ObjectId, Vec<ObjectId>)> {
1378 canonical_commit_tree_and_parents(data, hex_len, true)
1379}
1380
1381fn canonical_commit_tree_and_parents(
1382 data: &[u8],
1383 hex_len: usize,
1384 complete: bool,
1385) -> Option<(ObjectId, Vec<ObjectId>)> {
1386 fn header_oid<'a>(rest: &'a [u8], key: &[u8], hex_len: usize) -> Option<(ObjectId, &'a [u8])> {
1387 let line = rest.strip_prefix(key)?;
1388 if line.len() <= hex_len || line[hex_len] != b'\n' {
1389 return None;
1390 }
1391 let oid = ObjectId::from_hex(std::str::from_utf8(&line[..hex_len]).ok()?).ok()?;
1392 Some((oid, &line[hex_len + 1..]))
1393 }
1394 const PARENT: &[u8] = b"parent ";
1395 let (tree, mut rest) = header_oid(data, b"tree ", hex_len)?;
1396 let mut parents = Vec::new();
1397 loop {
1398 if !complete && rest.len() < PARENT.len() && PARENT.starts_with(rest) {
1399 return None;
1400 }
1401 if !rest.starts_with(PARENT) {
1402 return Some((tree, parents));
1403 }
1404 let (parent, next) = header_oid(rest, PARENT, hex_len)?;
1405 parents.push(parent);
1406 rest = next;
1407 }
1408}
1409
1410pub fn parse_commit(data: &[u8]) -> Result<CommitData> {
1416 Ok(super::CommitRef::parse(data)?.to_owned())
1417}
1418
1419#[must_use]
1422pub fn tag_header_field(data: &[u8], prefix: &[u8]) -> Option<String> {
1423 let mut pos = 0usize;
1424 while pos < data.len() {
1425 let rest = &data[pos..];
1426 let nl = rest.iter().position(|&b| b == b'\n');
1427 let line = if let Some(i) = nl { &rest[..i] } else { rest };
1428 if line.is_empty() {
1429 break;
1430 }
1431 if let Some(after) = line.strip_prefix(prefix) {
1432 return Some(String::from_utf8_lossy(after).trim().to_owned());
1433 }
1434 pos += line.len().saturating_add(nl.map(|_| 1).unwrap_or(0));
1435 if nl.is_none() {
1436 break;
1437 }
1438 }
1439 None
1440}
1441
1442#[must_use]
1444pub fn tag_object_line_oid(data: &[u8]) -> Option<ObjectId> {
1445 let s = tag_header_field(data, b"object ")?;
1446 s.parse().ok()
1447}
1448
1449#[derive(Debug, Clone)]
1451pub struct TagData {
1452 pub object: ObjectId,
1454 pub object_type: String,
1456 pub tag: String,
1458 pub tagger: Option<String>,
1460 pub extra_headers: Vec<u8>,
1462 pub message: String,
1464}
1465
1466pub fn parse_tag(data: &[u8]) -> Result<TagData> {
1472 use super::extra_headers::next_header_block;
1473
1474 #[derive(Clone, Copy)]
1475 enum Continuation {
1476 None,
1477 Extra,
1478 }
1479
1480 let mut pos = 0usize;
1481 let mut object = None;
1482 let mut object_type = None;
1483 let mut tag_name = None;
1484 let mut tagger = None;
1485 let mut extra_headers = Vec::new();
1486 let mut cont = Continuation::None;
1487 let mut past_tagger = false;
1488
1489 while pos < data.len() {
1490 let line_start = pos;
1491 let mut line_end = pos;
1492 while line_end < data.len() && data[line_end] != b'\n' {
1493 line_end += 1;
1494 }
1495 let line = &data[line_start..line_end];
1496 let after_nl = (line_end + 1).min(data.len());
1497 if line.is_empty() {
1498 let body = data.get(after_nl..).unwrap_or_default();
1499 let message = std::str::from_utf8(body)
1500 .map_err(|_| Error::CorruptObject("tag message is not valid UTF-8".to_owned()))?
1501 .to_owned();
1502 return Ok(TagData {
1503 object: object
1504 .ok_or_else(|| Error::CorruptObject("tag missing object header".to_owned()))?,
1505 object_type: object_type
1506 .ok_or_else(|| Error::CorruptObject("tag missing type header".to_owned()))?,
1507 tag: tag_name
1508 .ok_or_else(|| Error::CorruptObject("tag missing tag header".to_owned()))?,
1509 tagger,
1510 extra_headers,
1511 message,
1512 });
1513 }
1514
1515 if past_tagger {
1516 let (_, block_end, _) = next_header_block(data, line_start);
1517 extra_headers.extend_from_slice(&data[line_start..block_end]);
1518 pos = block_end;
1519 continue;
1520 }
1521
1522 if line.first() == Some(&b' ') {
1523 match cont {
1524 Continuation::Extra => {
1525 extra_headers.extend_from_slice(&data[line_start..after_nl]);
1526 }
1527 Continuation::None => {}
1528 }
1529 pos = after_nl;
1530 continue;
1531 }
1532
1533 let key_end = line
1534 .iter()
1535 .position(|&b| b == b' ')
1536 .ok_or_else(|| Error::CorruptObject("malformed tag header line".to_owned()))?;
1537 let key = &line[..key_end];
1538 let rest = line.get(key_end + 1..).unwrap_or_default();
1539
1540 match key {
1541 b"object" => {
1542 let line_str = std::str::from_utf8(rest).map_err(|_| {
1543 Error::CorruptObject("tag object line is not valid UTF-8".to_owned())
1544 })?;
1545 object = Some(line_str.trim().parse::<ObjectId>()?);
1546 cont = Continuation::None;
1547 }
1548 b"type" => {
1549 let typ = std::str::from_utf8(rest).map_err(|_| {
1550 Error::CorruptObject("tag type line is not valid UTF-8".to_owned())
1551 })?;
1552 let typ = typ.trim();
1553 if ObjectKind::from_tag_type_field(typ.as_bytes()).is_none() {
1554 return Err(Error::CorruptObject(format!(
1555 "invalid 'type' value in tag: {typ}"
1556 )));
1557 }
1558 object_type = Some(typ.to_owned());
1559 cont = Continuation::None;
1560 }
1561 b"tag" => {
1562 let line_str = std::str::from_utf8(rest).map_err(|_| {
1563 Error::CorruptObject("tag name line is not valid UTF-8".to_owned())
1564 })?;
1565 tag_name = Some(line_str.trim().to_owned());
1566 cont = Continuation::None;
1567 }
1568 b"tagger" => {
1569 tagger = Some(
1570 std::str::from_utf8(rest)
1571 .map_err(|_| {
1572 Error::CorruptObject("tag tagger line is not valid UTF-8".to_owned())
1573 })?
1574 .to_owned(),
1575 );
1576 past_tagger = true;
1577 cont = Continuation::None;
1578 }
1579 _ => {
1580 cont = Continuation::Extra;
1581 extra_headers.extend_from_slice(&data[line_start..after_nl]);
1582 }
1583 }
1584 pos = after_nl;
1585 }
1586
1587 Err(Error::CorruptObject(
1588 "tag missing blank line before message".to_owned(),
1589 ))
1590}
1591
1592#[must_use]
1597pub fn serialize_tag(t: &TagData) -> Vec<u8> {
1598 let mut out = Vec::new();
1599 out.extend_from_slice(format!("object {}\n", t.object).as_bytes());
1600 out.extend_from_slice(format!("type {}\n", t.object_type).as_bytes());
1601 out.extend_from_slice(format!("tag {}\n", t.tag).as_bytes());
1602 if let Some(ref tagger) = t.tagger {
1603 out.extend_from_slice(format!("tagger {tagger}\n").as_bytes());
1604 }
1605 out.extend_from_slice(&t.extra_headers);
1606 out.push(b'\n');
1607 let msg = t.message.trim_end_matches('\n');
1608 if !msg.is_empty() {
1609 out.extend_from_slice(msg.as_bytes());
1610 out.push(b'\n');
1611 }
1612 out
1613}
1614
1615#[must_use]
1625pub fn serialize_commit(c: &CommitData) -> Vec<u8> {
1626 let mut out = Vec::new();
1627 out.extend_from_slice(format!("tree {}\n", c.tree).as_bytes());
1628 for p in &c.parents {
1629 out.extend_from_slice(format!("parent {p}\n").as_bytes());
1630 }
1631 out.extend_from_slice(b"author ");
1632 if c.author_raw.is_empty() {
1633 out.extend_from_slice(c.author.as_bytes());
1634 } else {
1635 out.extend_from_slice(&c.author_raw);
1636 }
1637 out.push(b'\n');
1638 out.extend_from_slice(b"committer ");
1639 if c.committer_raw.is_empty() {
1640 out.extend_from_slice(c.committer.as_bytes());
1641 } else {
1642 out.extend_from_slice(&c.committer_raw);
1643 }
1644 out.push(b'\n');
1645 if let Some(enc) = &c.encoding {
1646 out.extend_from_slice(format!("encoding {enc}\n").as_bytes());
1647 }
1648 out.extend_from_slice(&c.extra_headers);
1649 out.push(b'\n');
1650 if let Some(raw) = &c.raw_message {
1651 out.extend_from_slice(raw);
1652 } else if !c.message.is_empty() {
1653 out.extend_from_slice(c.message.as_bytes());
1654 }
1655 out
1656}
1657
1658#[cfg(test)]
1659mod object_id_prefix_tests {
1660 use super::*;
1661
1662 const ID: &str = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
1663
1664 fn id() -> ObjectId {
1665 ObjectId::from_hex(ID).unwrap()
1666 }
1667
1668 fn prefix(hex: &str) -> ObjectIdPrefix {
1669 ObjectIdPrefix::from_hex(hex, HashAlgo::Sha1).unwrap()
1670 }
1671
1672 #[test]
1673 fn matches_every_length_of_the_id_and_rejects_a_changed_last_digit() {
1674 let id = id();
1675 for n in 0..=ID.len() {
1676 let p = prefix(&ID[..n]);
1677 assert_eq!(p.nibbles(), n);
1678 assert_eq!(p.is_empty(), n == 0);
1679 assert!(p.matches(&id), "{n} digits");
1680 assert_eq!(p.to_string(), ID[..n]);
1681 if n > 0 {
1682 let mut changed = ID[..n].to_owned();
1683 let last = changed.pop().unwrap();
1684 changed.push(if last == '0' { '1' } else { '0' });
1685 assert!(!prefix(&changed).matches(&id), "{changed}");
1686 }
1687 }
1688 }
1689
1690 #[test]
1691 fn odd_prefix_ignores_the_low_nibble_of_its_last_byte() {
1692 let p = prefix("4b8");
1693 assert!(p.matches(&ObjectId::from_hex("4b80000000000000000000000000000000000000").unwrap()));
1694 assert!(p.matches(&ObjectId::from_hex("4b8fffffffffffffffffffffffffffffffffffff").unwrap()));
1695 assert!(
1696 !p.matches(&ObjectId::from_hex("4b90000000000000000000000000000000000000").unwrap())
1697 );
1698 }
1699
1700 #[test]
1701 fn parsing_is_case_insensitive_and_validates() {
1702 assert_eq!(prefix("4B825DC"), prefix("4b825dc"));
1703 assert!(matches!(
1704 ObjectIdPrefix::from_hex("4b8g", HashAlgo::Sha1),
1705 Err(Error::InvalidObjectId(_))
1706 ));
1707 assert!(matches!(
1708 ObjectIdPrefix::from_hex(&format!("{ID}0"), HashAlgo::Sha1),
1709 Err(Error::InvalidObjectId(_))
1710 ));
1711 let sha256 = "a".repeat(64);
1712 assert_eq!(
1713 ObjectIdPrefix::from_hex(&sha256, HashAlgo::Sha256)
1714 .unwrap()
1715 .nibbles(),
1716 64
1717 );
1718 assert_eq!(prefix("").algo(), HashAlgo::Sha1);
1719 }
1720
1721 #[test]
1722 fn longer_prefix_never_matches_a_shorter_digest() {
1723 let long = ObjectIdPrefix::from_hex(&format!("{ID}0000"), HashAlgo::Sha256).unwrap();
1724 assert!(!long.matches(&id()));
1725 assert!(ObjectIdPrefix::from_hex(ID, HashAlgo::Sha256)
1726 .unwrap()
1727 .matches(&id()));
1728 }
1729
1730 #[test]
1731 fn bounds_are_the_first_and_last_matching_ids() {
1732 let p = prefix("4b8");
1733 assert_eq!(
1734 p.lowest().to_hex(),
1735 "4b80000000000000000000000000000000000000"
1736 );
1737 assert_eq!(
1738 p.highest().to_hex(),
1739 "4b8fffffffffffffffffffffffffffffffffffff"
1740 );
1741 let even = prefix("4b82");
1742 assert_eq!(
1743 even.lowest().to_hex(),
1744 "4b82000000000000000000000000000000000000"
1745 );
1746 assert_eq!(
1747 even.highest().to_hex(),
1748 "4b82ffffffffffffffffffffffffffffffffffff"
1749 );
1750 assert_eq!(prefix("").lowest(), ObjectId::null(HashAlgo::Sha1));
1751 assert_eq!(prefix("").highest().to_hex(), "f".repeat(40));
1752 assert_eq!(prefix(ID).lowest(), id());
1753 assert_eq!(prefix(ID).highest(), id());
1754 let sha256 = ObjectIdPrefix::from_hex("abc", HashAlgo::Sha256).unwrap();
1755 assert_eq!(sha256.highest().to_hex(), format!("abc{}", "f".repeat(61)));
1756 assert_eq!(format!("{sha256:?}"), "ObjectIdPrefix(abc)");
1757 }
1758
1759 #[test]
1760 fn common_prefix_nibbles_counts_shared_leading_digits() {
1761 let a = id();
1762 let hex = |s: &str| ObjectId::from_hex(s).unwrap();
1763 assert_eq!(a.common_prefix_nibbles(&a), 40);
1764 assert_eq!(
1765 a.common_prefix_nibbles(&hex("4b825dc642cb6eb9a060e54bf8d69288fbee4905")),
1766 39
1767 );
1768 assert_eq!(
1769 a.common_prefix_nibbles(&hex("4b815dc642cb6eb9a060e54bf8d69288fbee4904")),
1770 3
1771 );
1772 assert_eq!(
1773 a.common_prefix_nibbles(&hex("4b925dc642cb6eb9a060e54bf8d69288fbee4904")),
1774 2
1775 );
1776 assert_eq!(
1777 a.common_prefix_nibbles(&hex("5b825dc642cb6eb9a060e54bf8d69288fbee4904")),
1778 0
1779 );
1780 assert_eq!(
1781 a.common_prefix_nibbles(&hex("4c825dc642cb6eb9a060e54bf8d69288fbee4904")),
1782 1
1783 );
1784 }
1785}
1786
1787#[cfg(test)]
1788mod commit_parse_tests {
1789 use super::*;
1790
1791 #[test]
1792 fn empty_tree_constant_is_canonical_sha1_digest() {
1793 assert_eq!(
1794 ObjectId::empty_tree().to_hex(),
1795 "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
1796 );
1797 }
1798
1799 #[test]
1800 fn is_empty_tree_for_matches_hashed_empty_tree() {
1801 for algo in [HashAlgo::Sha1, HashAlgo::Sha256] {
1802 let hashed =
1803 crate::hash::hash_object(algo, ObjectKind::Tree, b"").expect("hash empty tree");
1804 assert!(hashed.is_empty_tree_for(algo), "{algo:?}");
1805 let other = match algo {
1806 HashAlgo::Sha1 => HashAlgo::Sha256,
1807 HashAlgo::Sha256 => HashAlgo::Sha1,
1808 };
1809 assert!(!hashed.is_empty_tree_for(other), "{algo:?}");
1810 let empty_blob =
1811 crate::hash::hash_object(algo, ObjectKind::Blob, b"").expect("hash empty blob");
1812 assert!(!empty_blob.is_empty_tree_for(algo), "{algo:?}");
1813 }
1814 let legacy = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf899d69f7c6948d4").expect("hex");
1815 assert!(legacy.is_empty_tree_for(HashAlgo::Sha1));
1816 assert!(!legacy.is_empty_tree_for(HashAlgo::Sha256));
1817 }
1818
1819 #[test]
1820 fn serialize_tree_mode_matches_mode_str() {
1821 for mode in [
1822 0,
1823 0o7,
1824 0o100644,
1825 0o100755,
1826 0o120000,
1827 0o040000,
1828 0o160000,
1829 u32::MAX,
1830 ] {
1831 let mut out = Vec::new();
1832 push_tree_mode(&mut out, mode);
1833 let entry = TreeEntry {
1834 mode,
1835 name: Vec::new(),
1836 oid: ObjectId::empty_tree(),
1837 };
1838 assert_eq!(out, entry.mode_str().into_bytes(), "mode {mode:o}");
1839 }
1840 }
1841
1842 #[test]
1843 fn is_empty_blob_matches_both_hash_algorithms() {
1844 for algo in [HashAlgo::Sha1, HashAlgo::Sha256] {
1845 let oid = algo.hash_object(ObjectKind::Blob, b"").unwrap();
1846 assert!(oid.is_empty_blob(), "{algo:?} empty blob {}", oid.to_hex());
1847 let other = algo.hash_object(ObjectKind::Blob, b"x").unwrap();
1848 assert!(!other.is_empty_blob());
1849 }
1850 assert!(!ObjectId::empty_tree().is_empty_blob());
1851 assert!(!ObjectId::zero().is_empty_blob());
1852 }
1853
1854 #[test]
1855 fn commit_subject_pretty_folds_first_paragraph() {
1856 assert_eq!(commit_subject_pretty("hello\nworld\n\nbody"), "hello world");
1857 assert_eq!(commit_subject_pretty(""), "");
1858 assert_eq!(
1859 commit_subject_pretty(" hello world\tthere \n next\t line \n\nbody\n"),
1860 " hello world\tthere next\t line"
1861 );
1862 }
1863
1864 #[test]
1865 fn commit_ref_matches_parse_commit_headers() {
1866 let raw = concat!(
1867 "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
1868 "parent 1111111111111111111111111111111111111111\n",
1869 "author A <a@example.com> 1 +0000\n",
1870 "committer C <c@example.com> 2 +0000\n",
1871 "\n",
1872 "ignored body\n",
1873 );
1874 let full = parse_commit(raw.as_bytes()).expect("parse");
1875 let link = crate::objects::CommitRef::parse(raw.as_bytes()).expect("CommitRef");
1876 assert_eq!(link.tree(), full.tree);
1877 assert_eq!(link.parents(), full.parents.as_slice());
1878 assert_eq!(link.author(), full.author);
1879 assert_eq!(link.committer(), full.committer);
1880 }
1881
1882 #[test]
1883 fn parse_commit_skips_multiline_gpgsig_continuation() {
1884 let raw = concat!(
1885 "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
1886 "author A U Thor <author@example.com> 1 +0000\n",
1887 "committer C O Mitter <committer@example.com> 1 +0000\n",
1888 "gpgsig -----BEGIN PGP SIGNATURE-----\n",
1889 " abcdef\n",
1890 " -----END PGP SIGNATURE-----\n",
1891 "\n",
1892 "msg\n",
1893 );
1894 let c = parse_commit(raw.as_bytes()).expect("parse signed commit");
1895 assert_eq!(c.tree.to_hex(), "4b825dc642cb6eb9a060e54bf8d69288fbee4904");
1896 assert_eq!(c.message, "msg\n");
1897 assert_eq!(serialize_commit(&c), raw.as_bytes());
1898 }
1899
1900 #[test]
1901 fn hex_parsing_accepts_both_cases_and_lengths_and_rejects_the_rest() {
1902 let sha1 = "0123456789abcdefABCDEF0123456789abcdef01";
1903 let oid = ObjectId::from_hex(sha1).unwrap();
1904 assert_eq!(oid.algo(), HashAlgo::Sha1);
1905 assert_eq!(oid.to_hex(), sha1.to_ascii_lowercase());
1906 assert_eq!(ObjectId::from_hex_ascii(sha1.as_bytes()).unwrap(), oid);
1907 assert_eq!(sha1.parse::<ObjectId>().unwrap(), oid);
1908
1909 let sha256 = "f".repeat(64);
1910 let oid = sha256.parse::<ObjectId>().unwrap();
1911 assert_eq!(oid.algo(), HashAlgo::Sha256);
1912 assert_eq!(oid.as_bytes(), [0xff; 32]);
1913 assert!(ObjectId::from_hex_ascii(sha256.as_bytes()).is_err());
1914
1915 for bad in [
1916 "",
1917 &sha1[..39],
1918 &format!("{sha1}0"),
1919 "g123456789abcdef0123456789abcdef01234567",
1920 "0123456789abcdef0123456789abcdef0123456 ",
1921 "0123456789abcdef0123456789abcdef012345/7",
1922 "0123456789abcdef0123456789abcdef012345:7",
1923 "0123456789abcdef0123456789abcdef012345`7",
1924 "0123456789abcdef0123456789abcdef012345G7",
1925 ] {
1926 assert!(
1927 matches!(ObjectId::from_hex(bad), Err(Error::InvalidObjectId(s)) if s == bad),
1928 "{bad:?}"
1929 );
1930 assert!(bad.parse::<ObjectId>().is_err(), "{bad:?}");
1931 assert!(ObjectId::from_hex_ascii(bad.as_bytes()).is_err(), "{bad:?}");
1932 }
1933 let mut non_ascii = sha1.as_bytes().to_vec();
1934 non_ascii[3] = 0xb0;
1935 assert!(ObjectId::from_hex_ascii(&non_ascii).is_err());
1936 }
1937
1938 #[test]
1939 fn serialize_commit_applies_public_field_edits_after_parse() {
1940 let raw = concat!(
1941 "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
1942 "author A <a@example.com> 1 +0000\n",
1943 "committer C <c@example.com> 1 +0000\n",
1944 "\n",
1945 "msg\n",
1946 );
1947 let mut commit = parse_commit(raw.as_bytes()).expect("parse");
1948 let zero = ObjectId::from_hex("0000000000000000000000000000000000000000").unwrap();
1949 commit.tree = zero;
1950 let serialized = serialize_commit(&commit);
1951 let tree_line = format!("tree {}\n", zero.to_hex());
1952 assert!(
1953 serialized.starts_with(tree_line.as_bytes()),
1954 "serialize_commit ignored the updated public tree field"
1955 );
1956 }
1957
1958 #[test]
1959 fn parse_commit_preserves_gpgsig_mergetag_bytes_exactly() {
1960 let raw = concat!(
1961 "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
1962 "author A <a@example.com> 1 +0000\n",
1963 "committer C <c@example.com> 1 +0000\n",
1964 "encoding ISO-8859-1\n",
1965 "gpgsig -----BEGIN PGP SIGNATURE-----\n",
1966 " line2\n",
1967 " -----END PGP SIGNATURE-----\n",
1968 "mergetag object 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
1969 " type commit\n",
1970 " tag merge-tag\n",
1971 " tagger T <t@example.com> 1 +0000\n",
1972 " -----BEGIN PGP SIGNATURE-----\n",
1973 " inner\n",
1974 " -----END PGP SIGNATURE-----\n",
1975 "\n",
1976 "msg-only\n",
1977 );
1978 let parsed = parse_commit(raw.as_bytes()).expect("parse");
1979 assert_eq!(serialize_commit(&parsed), raw.as_bytes());
1980 }
1981
1982 #[test]
1983 fn tree_entry_mode_str_and_tag_object_line_oid() {
1984 let oid = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap();
1985 let entry = TreeEntry {
1986 mode: 0o040000,
1987 name: b"dir".to_vec(),
1988 oid,
1989 };
1990 assert_eq!(entry.mode_str(), "40000");
1991 let tag_body = format!("object {}\ntype commit\ntag t\n\n", oid.to_hex());
1992 assert_eq!(tag_object_line_oid(tag_body.as_bytes()), Some(oid));
1993 }
1994
1995 #[test]
1996 fn parse_commit_non_commit_bytes_does_not_panic() {
1997 let tree_bytes = b"100644 blob 0123456789012345678901234567890123456789\tf";
1998 let err = parse_commit(tree_bytes).expect_err("tree bytes are not a commit");
1999 assert!(matches!(err, Error::CorruptObject(_)));
2000 }
2001
2002 #[test]
2003 fn parse_tree_with_sha256_oid_width() {
2004 let oid_bytes = [0x6eu8; 32];
2005 let oid = ObjectId::from_bytes(&oid_bytes).expect("oid");
2006 let mut raw = b"100644 file\0".to_vec();
2007 raw.extend_from_slice(oid.as_bytes());
2008 let entries = parse_tree_with_oid_len(&raw, HashAlgo::Sha256.len()).expect("tree");
2009 assert_eq!(entries.len(), 1);
2010 assert_eq!(entries[0].oid, oid);
2011 }
2012
2013 #[test]
2014 fn parse_commit_tree_and_parents_prefix_matches_full_parse_or_declines() -> Result<()> {
2015 let tree = "1111111111111111111111111111111111111111";
2016 let p1 = "2222222222222222222222222222222222222222";
2017 let p2 = "3333333333333333333333333333333333333333";
2018 let raw = format!(
2019 "tree {tree}\nparent {p1}\nparent {p2}\nauthor A <a@x> 1 +0000\n\
2020 committer C <c@x> 2 +0000\n\nmessage\n"
2021 );
2022 let commit = parse_commit(raw.as_bytes())?;
2023 let full = (commit.tree, commit.parents);
2024 assert_eq!(full.1.len(), 2);
2025 assert_eq!(
2026 parse_commit_tree_and_parents_lenient(raw.as_bytes(), HashAlgo::Sha1.hex_len()),
2027 Some(full.clone())
2028 );
2029 let hex_len = HashAlgo::Sha1.hex_len();
2030 let header_end = raw.find("author").expect("author line");
2031 for cut in 0..=raw.len() {
2032 let got = parse_commit_tree_and_parents_prefix(&raw.as_bytes()[..cut], hex_len);
2033 if cut > header_end {
2034 assert_eq!(got.as_ref(), Some(&full), "cut at {cut}");
2035 } else {
2036 assert_eq!(
2037 got, None,
2038 "cut at {cut} must not claim a complete parent list"
2039 );
2040 }
2041 }
2042 Ok(())
2043 }
2044
2045 #[test]
2046 fn commit_tree_and_parents_prefix_never_disagrees_with_whole_object() {
2047 let tree = "tree 1111111111111111111111111111111111111111\n";
2048 let parent = "parent 2222222222222222222222222222222222222222\n";
2049 let parent2 = "parent 3333333333333333333333333333333333333333\n";
2050 let bodies = [
2051 format!("{tree}{parent}author A <a@x> 1 +0000\n\nmessage\n"),
2052 format!("{tree}{parent}{parent2}committer C <c@x> 2 +0000\n\nno author\n"),
2053 format!("{tree}{parent}"),
2054 format!("{tree}{parent}{parent2}"),
2055 format!("{tree}author A <a@x> 1 +0000\n{parent}\nlate parent is ignored\n"),
2056 format!("{tree}{parent}not a header\n"),
2057 format!("{tree}parent zz\n\n"),
2058 format!("{tree}{parent}parent 2222"),
2059 format!("author A <a@x> 1 +0000\n{tree}\n"),
2060 ];
2061 let hex_len = HashAlgo::Sha1.hex_len();
2062 for body in &bodies {
2063 let whole = parse_commit_tree_and_parents_lenient(body.as_bytes(), hex_len);
2064 for cut in 0..=body.len() {
2065 if let Some(got) =
2066 parse_commit_tree_and_parents_prefix(&body.as_bytes()[..cut], hex_len)
2067 {
2068 assert_eq!(Some(got), whole, "cut {cut} of {body:?}");
2069 }
2070 }
2071 }
2072 let parents_of = |body: &str| {
2073 parse_commit_tree_and_parents_lenient(body.as_bytes(), hex_len).map(|(_, p)| p.len())
2074 };
2075 assert_eq!(parents_of(&bodies[1]), Some(2), "missing author is fine");
2076 assert_eq!(
2077 parents_of(&bodies[3]),
2078 Some(2),
2079 "object may end after parents"
2080 );
2081 assert_eq!(
2082 parents_of(&bodies[4]),
2083 Some(0),
2084 "only leading parents count"
2085 );
2086 assert_eq!(parents_of(&bodies[6]), None, "malformed parent line");
2087 assert_eq!(parents_of(&bodies[7]), None, "truncated parent line");
2088 assert_eq!(parents_of(&bodies[8]), None, "tree must come first");
2089 }
2090
2091 #[test]
2092 fn parse_commit_tree_and_parents_prefix_handles_root_and_sha256() -> Result<()> {
2093 let root = b"tree 1111111111111111111111111111111111111111\nauthor A <a@x> 1 +0000\n";
2094 let (tree, parents) =
2095 parse_commit_tree_and_parents_prefix(root, HashAlgo::Sha1.hex_len()).expect("root");
2096 assert_eq!(tree.to_hex(), "1111111111111111111111111111111111111111");
2097 assert!(parents.is_empty());
2098
2099 let t = "a".repeat(64);
2100 let p = "b".repeat(64);
2101 let raw = format!("tree {t}\nparent {p}\ncommitter C <c@x> 2 +0000\n");
2102 let (tree, parents) =
2103 parse_commit_tree_and_parents_prefix(raw.as_bytes(), HashAlgo::Sha256.hex_len())
2104 .expect("sha256");
2105 assert_eq!(tree.to_hex(), t);
2106 assert_eq!(parents.len(), 1);
2107 assert_eq!(parents[0].to_hex(), p);
2108
2109 assert!(parse_commit_tree_and_parents_prefix(raw.as_bytes(), 40).is_none());
2110 assert!(parse_commit_tree_and_parents_prefix(b"parent x\n", 40).is_none());
2111 assert!(parse_commit_tree_and_parents_prefix(
2112 b"tree zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz\nauthor\n",
2113 40
2114 )
2115 .is_none());
2116 Ok(())
2117 }
2118
2119 #[test]
2120 fn object_id_loose_path_helpers() {
2121 let oid = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap();
2122 assert_eq!(oid.loose_prefix(), "4b");
2123 assert_eq!(oid.loose_suffix().len(), 38);
2124 assert!(ObjectId::is_hex_len(40));
2125 assert!(ObjectId::is_loose_suffix_len(38));
2126 }
2127
2128 #[test]
2129 fn tree_ref_iter_yields_at_most_one_error() {
2130 let bad = b"100644 incomplete";
2131 let mut iter = TreeRefIter::with_oid_len(bad, HashAlgo::Sha1.len());
2132 assert!(iter.next().unwrap().is_err());
2133 assert!(iter.next().is_none());
2134 }
2135
2136 #[test]
2137 fn tree_ref_iter_public_api_smoke() {
2138 let data = serialize_tree(&[TreeEntry {
2139 mode: 0o100644,
2140 name: b"a".to_vec(),
2141 oid: ObjectId::from_hex("0123456789012345678901234567890123456789").unwrap(),
2142 }]);
2143 let mut iter = TreeRefIter::infer_oid_len(&data).expect("infer");
2144 let entry = iter.next().unwrap().expect("entry");
2145 assert_eq!(entry.name, b"a");
2146 let _owned = entry.to_owned();
2147 let _vec = parse_tree(&data).expect("parse");
2148 let _ = TreeRefIter::with_oid_len(&data, HashAlgo::Sha1.len());
2149 }
2150
2151 #[test]
2152 fn find_tree_entry_by_name_in_data_locates_directory_in_large_tree() -> Result<()> {
2153 let dummy = ObjectId::from_hex("0123456789012345678901234567890123456789")?;
2154 let mut entries: Vec<TreeEntry> = (1..=60)
2155 .map(|i| TreeEntry {
2156 mode: 0o100644,
2157 name: format!("f{i}.txt").into_bytes(),
2158 oid: dummy,
2159 })
2160 .collect();
2161 entries.push(TreeEntry {
2162 mode: 0o040000,
2163 name: b"sub".to_vec(),
2164 oid: dummy,
2165 });
2166 entries.sort_by(|a, b| {
2167 tree_entry_cmp(&a.name, a.mode == 0o040000, &b.name, b.mode == 0o040000)
2168 });
2169 let data = serialize_tree(&entries);
2170 assert!(entries.len() >= 48);
2171 let found = find_tree_entry_by_name_in_data(&data, HashAlgo::Sha1.len(), b"sub", true)?
2172 .expect("sub directory in wide tree");
2173 assert_eq!(found.mode, 0o040000);
2174 assert_eq!(found.name, b"sub");
2175 Ok(())
2176 }
2177
2178 fn sorted_tree_data(entries: &mut [TreeEntry]) -> Vec<u8> {
2179 entries.sort_by(|a, b| {
2180 tree_entry_cmp(&a.name, a.mode == 0o040000, &b.name, b.mode == 0o040000)
2181 });
2182 serialize_tree(entries)
2183 }
2184
2185 #[test]
2186 fn find_tree_entry_by_name_in_data_matches_parse_tree_for_every_name() -> Result<()> {
2187 let oid_a = ObjectId::from_hex("1111111111111111111111111111111111111111")?;
2188 let oid_b = ObjectId::from_hex("2222222222222222222222222222222222222222")?;
2189 let mut entries = vec![
2190 TreeEntry {
2191 mode: 0o040000,
2192 name: b"foo".to_vec(),
2193 oid: oid_a,
2194 },
2195 TreeEntry {
2196 mode: 0o100644,
2197 name: b"foo.c".to_vec(),
2198 oid: oid_b,
2199 },
2200 TreeEntry {
2201 mode: 0o100755,
2202 name: b"foo-bar".to_vec(),
2203 oid: oid_b,
2204 },
2205 TreeEntry {
2206 mode: 0o120000,
2207 name: b"link".to_vec(),
2208 oid: oid_a,
2209 },
2210 TreeEntry {
2211 mode: 0o160000,
2212 name: b"sub".to_vec(),
2213 oid: oid_b,
2214 },
2215 TreeEntry {
2216 mode: 0o100644,
2217 name: b"README".to_vec(),
2218 oid: oid_a,
2219 },
2220 ];
2221 let data = sorted_tree_data(&mut entries);
2222 for expected in parse_tree(&data)? {
2223 let name_is_tree = expected.mode == 0o040000;
2224 let found = find_tree_entry_by_name_in_data(
2225 &data,
2226 HashAlgo::Sha1.len(),
2227 &expected.name,
2228 name_is_tree,
2229 )?
2230 .expect("entry present");
2231 assert_eq!(found, expected);
2232 }
2233 for missing in [&b"fo"[..], b"foo/", b"foo.d", b"zzz", b"", b"AAA", b"lin"] {
2234 assert!(
2235 find_tree_entry_by_name_in_data(&data, HashAlgo::Sha1.len(), missing, false)?
2236 .is_none(),
2237 "unexpected match for {:?}",
2238 String::from_utf8_lossy(missing)
2239 );
2240 }
2241 Ok(())
2242 }
2243
2244 #[test]
2245 fn find_tree_entry_by_name_in_data_stops_after_sorted_position() -> Result<()> {
2246 let oid = ObjectId::from_hex("3333333333333333333333333333333333333333")?;
2247 let mut entries = vec![
2248 TreeEntry {
2249 mode: 0o100644,
2250 name: b"a".to_vec(),
2251 oid,
2252 },
2253 TreeEntry {
2254 mode: 0o100644,
2255 name: b"c".to_vec(),
2256 oid,
2257 },
2258 ];
2259 let mut data = sorted_tree_data(&mut entries);
2260 data.extend_from_slice(b"garbage without separators");
2261 let found = find_tree_entry_by_name_in_data(&data, HashAlgo::Sha1.len(), b"a", false)?;
2262 assert_eq!(found.map(|e| e.name), Some(b"a".to_vec()));
2263 assert!(
2264 find_tree_entry_by_name_in_data(&data, HashAlgo::Sha1.len(), b"b", false)?.is_none()
2265 );
2266 assert!(matches!(
2267 find_tree_entry_by_name_in_data(&data, HashAlgo::Sha1.len(), b"d", false),
2268 Err(Error::CorruptObject(_))
2269 ));
2270 Ok(())
2271 }
2272
2273 #[test]
2274 fn find_tree_entry_by_name_in_data_rejects_bad_mode_and_truncated_oid() {
2275 let bad_mode = b"10x644 a\0aaaaaaaaaaaaaaaaaaaa";
2276 assert!(matches!(
2277 find_tree_entry_by_name_in_data(bad_mode, HashAlgo::Sha1.len(), b"a", false),
2278 Err(Error::CorruptObject(_))
2279 ));
2280 let truncated = b"100644 a\0short";
2281 assert!(matches!(
2282 find_tree_entry_by_name_in_data(truncated, HashAlgo::Sha1.len(), b"a", false),
2283 Err(Error::CorruptObject(_))
2284 ));
2285 }
2286
2287 #[test]
2288 fn find_tree_entry_by_name_in_data_reads_sha256_width() -> Result<()> {
2289 let oid = ObjectId::from_bytes(&[0x5au8; 32])?;
2290 let mut raw = b"100644 file\0".to_vec();
2291 raw.extend_from_slice(oid.as_bytes());
2292 let found = find_tree_entry_by_name_in_data(&raw, HashAlgo::Sha256.len(), b"file", false)?
2293 .expect("file");
2294 assert_eq!(found.oid, oid);
2295 assert_eq!(found.mode, 0o100644);
2296 Ok(())
2297 }
2298}