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
129pub const SHORT_HEX_LEN: usize = 7;
131
132#[must_use]
147pub fn short_hex(hex: &str) -> &str {
148 hex.get(..SHORT_HEX_LEN).unwrap_or(hex)
149}
150
151const WELL_KNOWN_EMPTY_TREE_CANON: [u8; 20] = [
153 0x4b, 0x82, 0x5d, 0xc6, 0x42, 0xcb, 0x6e, 0xb9, 0xa0, 0x60, 0xe5, 0x4b, 0xf8, 0xd6, 0x92, 0x88,
154 0xfb, 0xee, 0x49, 0x04,
155];
156const WELL_KNOWN_EMPTY_TREE_LEGACY: [u8; 20] = [
158 0x4b, 0x82, 0x5d, 0xc6, 0x42, 0xcb, 0x6e, 0xb9, 0xa0, 0x60, 0xe5, 0x4b, 0xf8, 0x99, 0xd6, 0x9f,
159 0x7c, 0x69, 0x48, 0xd4,
160];
161const EMPTY_TREE_SHA256: [u8; 32] = [
163 0x6e, 0xf1, 0x9b, 0x41, 0x22, 0x5c, 0x53, 0x69, 0xf1, 0xc1, 0x04, 0xd4, 0x5d, 0x8d, 0x85, 0xef,
164 0xa9, 0xb0, 0x57, 0xb5, 0x3b, 0x14, 0xb4, 0xb9, 0xb9, 0x39, 0xdd, 0x74, 0xde, 0xcc, 0x53, 0x21,
165];
166const EMPTY_BLOB_SHA1: [u8; 20] = [
168 0xe6, 0x9d, 0xe2, 0x9b, 0xb2, 0xd1, 0xd6, 0x43, 0x4b, 0x8b, 0x29, 0xae, 0x77, 0x5a, 0xd8, 0xc2,
169 0xe4, 0x8c, 0x53, 0x91,
170];
171const EMPTY_BLOB_SHA256: [u8; 32] = [
173 0x47, 0x3a, 0x0f, 0x4c, 0x3b, 0xe8, 0xa9, 0x36, 0x81, 0xa2, 0x67, 0xe3, 0xb1, 0xe9, 0xa7, 0xdc,
174 0xda, 0x11, 0x85, 0x43, 0x6f, 0xe1, 0x41, 0xf7, 0x74, 0x91, 0x20, 0xa3, 0x03, 0x72, 0x18, 0x13,
175];
176
177#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
195pub struct ObjectId {
196 bytes: [u8; MAX_OID_LEN],
197 len: u8,
198}
199
200impl ObjectId {
201 #[must_use]
207 pub const fn zero() -> Self {
208 Self {
209 bytes: [0u8; MAX_OID_LEN],
210 len: 20,
211 }
212 }
213
214 #[must_use]
216 pub const fn null(algo: HashAlgo) -> Self {
217 Self {
218 bytes: [0u8; MAX_OID_LEN],
219 len: algo.len() as u8,
220 }
221 }
222
223 pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
230 if HashAlgo::from_len(bytes.len()).is_none() {
231 return Err(Error::InvalidObjectId(hex::encode(bytes)));
232 }
233 let mut buf = [0u8; MAX_OID_LEN];
234 buf[..bytes.len()].copy_from_slice(bytes);
235 Ok(Self {
236 bytes: buf,
237 len: bytes.len() as u8,
238 })
239 }
240
241 #[must_use]
243 pub fn as_bytes(&self) -> &[u8] {
244 &self.bytes[..self.len as usize]
245 }
246
247 #[must_use]
249 pub fn algo(&self) -> HashAlgo {
250 HashAlgo::from_len(self.len as usize).unwrap_or(HashAlgo::Sha1)
251 }
252
253 #[must_use]
255 pub fn is_zero(&self) -> bool {
256 self.as_bytes().iter().all(|&b| b == 0)
257 }
258
259 #[must_use]
261 pub(crate) fn common_prefix_nibbles(&self, other: &Self) -> usize {
262 let (a, b) = (self.as_bytes(), other.as_bytes());
263 match a.iter().zip(b).position(|(x, y)| x != y) {
264 Some(i) => i * 2 + usize::from((a[i] ^ b[i]) & 0xf0 == 0),
265 None => a.len().min(b.len()) * 2,
266 }
267 }
268
269 #[must_use]
271 pub(crate) fn is_empty_blob(&self) -> bool {
272 let bytes = self.as_bytes();
273 bytes == EMPTY_BLOB_SHA1 || bytes == EMPTY_BLOB_SHA256
274 }
275
276 #[must_use]
278 pub(crate) fn is_well_known_empty_tree(&self) -> bool {
279 self.is_canonical_empty_tree()
280 || (self.as_bytes().len() == HashAlgo::Sha1.len()
281 && self.as_bytes() == WELL_KNOWN_EMPTY_TREE_LEGACY)
282 }
283
284 #[must_use]
287 pub(crate) fn is_empty_tree_for(&self, algo: HashAlgo) -> bool {
288 match algo {
289 HashAlgo::Sha1 => self.is_well_known_empty_tree(),
290 HashAlgo::Sha256 => self.as_bytes() == EMPTY_TREE_SHA256,
291 }
292 }
293
294 #[must_use]
296 pub(crate) fn is_canonical_empty_tree(&self) -> bool {
297 self.as_bytes().len() == HashAlgo::Sha1.len()
298 && self.as_bytes() == WELL_KNOWN_EMPTY_TREE_CANON
299 }
300
301 #[must_use]
307 pub const fn empty_tree() -> Self {
308 let mut bytes = [0u8; MAX_OID_LEN];
309 let mut i = 0;
310 while i < WELL_KNOWN_EMPTY_TREE_CANON.len() {
311 bytes[i] = WELL_KNOWN_EMPTY_TREE_CANON[i];
312 i += 1;
313 }
314 Self { bytes, len: 20 }
315 }
316
317 pub(crate) fn push_loose_relative(&self, path: &mut std::path::PathBuf) {
319 const HEX: &[u8; 16] = b"0123456789abcdef";
320 let bytes = self.as_bytes();
321 let prefix = [
322 HEX[(bytes[0] >> 4) as usize],
323 HEX[(bytes[0] & 0xf) as usize],
324 ];
325 path.push(unsafe { std::str::from_utf8_unchecked(&prefix) });
327 let suffix_len = (bytes.len() - 1) * 2;
328 let mut suffix = [0u8; 62];
329 for (i, &b) in bytes[1..].iter().enumerate() {
330 suffix[i * 2] = HEX[(b >> 4) as usize];
331 suffix[i * 2 + 1] = HEX[(b & 0xf) as usize];
332 }
333 path.push(unsafe { std::str::from_utf8_unchecked(&suffix[..suffix_len]) });
334 }
335
336 #[must_use]
338 pub(crate) fn loose_path_in(&self, objects_dir: &std::path::Path) -> std::path::PathBuf {
339 let mut path = objects_dir.to_path_buf();
340 self.push_loose_relative(&mut path);
341 path
342 }
343
344 #[must_use]
346 pub fn to_hex(&self) -> String {
347 self.hex_digits().as_str().to_owned()
348 }
349
350 pub(crate) fn push_hex_to(&self, out: &mut String) {
352 out.push_str(self.hex_digits().as_str());
353 }
354
355 #[must_use]
361 pub fn to_short_hex(&self) -> String {
362 short_hex(self.hex_digits().as_str()).to_owned()
363 }
364
365 fn hex_digits(&self) -> HexDigits {
367 const DIGITS: &[u8; 16] = b"0123456789abcdef";
368 let mut buf = [0u8; MAX_OID_LEN * 2];
369 for (pair, &byte) in buf.as_chunks_mut::<2>().0.iter_mut().zip(self.as_bytes()) {
370 *pair = [
371 DIGITS[usize::from(byte >> 4)],
372 DIGITS[usize::from(byte & 0x0f)],
373 ];
374 }
375 HexDigits {
376 buf,
377 len: usize::from(self.len) * 2,
378 }
379 }
380
381 #[must_use]
385 pub fn loose_prefix(&self) -> String {
386 hex::encode(&self.bytes[..1])
387 }
388
389 pub fn from_hex(s: &str) -> Result<Self> {
396 Self::decode_hex(s.as_bytes()).ok_or_else(|| Error::InvalidObjectId(s.to_owned()))
397 }
398
399 pub fn from_hex_ascii(bytes: &[u8]) -> Result<Self> {
405 if bytes.len() != HashAlgo::Sha1.hex_len() {
406 return Err(Error::InvalidObjectId(
407 String::from_utf8_lossy(bytes).into_owned(),
408 ));
409 }
410 Self::decode_hex(bytes)
411 .ok_or_else(|| Error::InvalidObjectId(String::from_utf8_lossy(bytes).into_owned()))
412 }
413
414 fn decode_hex(hex: &[u8]) -> Option<Self> {
419 let len = hex.len() / 2;
420 if !Self::is_hex_len(hex.len()) {
421 return None;
422 }
423 let mut bytes = [0u8; MAX_OID_LEN];
424 let mut invalid = 0u8;
425 for (out, pair) in bytes.iter_mut().zip(hex.as_chunks::<2>().0) {
426 let hi = HEX_DECODE[usize::from(pair[0])];
427 let lo = HEX_DECODE[usize::from(pair[1])];
428 invalid |= hi | lo;
429 *out = (hi << 4) | lo;
430 }
431 (invalid & HEX_INVALID == 0).then_some(Self {
432 bytes,
433 len: len as u8,
434 })
435 }
436
437 #[must_use]
440 pub fn loose_suffix(&self) -> String {
441 hex::encode(&self.bytes[1..self.len as usize])
442 }
443
444 #[must_use]
447 pub fn is_full_hex(s: &str) -> bool {
448 (s.len() == HashAlgo::Sha1.hex_len() || s.len() == HashAlgo::Sha256.hex_len())
449 && s.bytes().all(|b| b.is_ascii_hexdigit())
450 }
451
452 #[must_use]
454 pub const fn is_hex_len(len: usize) -> bool {
455 len == HashAlgo::Sha1.hex_len() || len == HashAlgo::Sha256.hex_len()
456 }
457
458 #[must_use]
461 pub const fn is_loose_suffix_len(len: usize) -> bool {
462 len == HashAlgo::Sha1.hex_len() - 2 || len == HashAlgo::Sha256.hex_len() - 2
463 }
464}
465
466impl std::hash::Hash for ObjectId {
467 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
468 state.write(self.as_bytes());
469 }
470}
471
472impl fmt::Display for ObjectId {
473 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
474 f.write_str(self.hex_digits().as_str())
475 }
476}
477
478struct HexDigits {
480 buf: [u8; MAX_OID_LEN * 2],
481 len: usize,
482}
483
484impl HexDigits {
485 fn as_str(&self) -> &str {
486 std::str::from_utf8(&self.buf[..self.len]).unwrap_or_default()
487 }
488}
489
490impl fmt::Debug for ObjectId {
491 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
492 write!(f, "ObjectId({})", self.to_hex())
493 }
494}
495
496#[cfg(feature = "serde-models")]
497impl serde::Serialize for ObjectId {
498 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
499 where
500 S: serde::Serializer,
501 {
502 serializer.serialize_str(&self.to_hex())
503 }
504}
505
506#[cfg(feature = "serde-models")]
507impl<'de> serde::Deserialize<'de> for ObjectId {
508 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
509 where
510 D: serde::Deserializer<'de>,
511 {
512 let hex = String::deserialize(deserializer)?;
513 Self::from_hex(&hex).map_err(serde::de::Error::custom)
514 }
515}
516
517const HEX_INVALID: u8 = 0x10;
519
520const HEX_DECODE: [u8; 256] = {
522 let mut table = [HEX_INVALID; 256];
523 let mut i = 0u8;
524 while i < 10 {
525 table[(b'0' + i) as usize] = i;
526 i += 1;
527 }
528 let mut i = 0u8;
529 while i < 6 {
530 table[(b'a' + i) as usize] = 10 + i;
531 table[(b'A' + i) as usize] = 10 + i;
532 i += 1;
533 }
534 table
535};
536
537impl FromStr for ObjectId {
538 type Err = Error;
539
540 fn from_str(s: &str) -> Result<Self> {
541 Self::from_hex(s)
542 }
543}
544
545#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
562pub struct FileMode(u32);
563
564impl FileMode {
565 pub const ABSENT: Self = Self(0);
567 pub const REGULAR: Self = Self(0o100644);
569 pub const EXECUTABLE: Self = Self(0o100755);
571 pub const SYMLINK: Self = Self(0o120000);
573 pub const GITLINK: Self = Self(0o160000);
575 pub const TREE: Self = Self(0o040000);
577
578 #[must_use]
580 pub const fn new(bits: u32) -> Self {
581 Self(bits)
582 }
583
584 #[must_use]
586 pub const fn bits(self) -> u32 {
587 self.0
588 }
589
590 #[must_use]
592 pub const fn is_absent(self) -> bool {
593 self.0 == 0
594 }
595
596 #[must_use]
598 pub fn octal_if_present(self) -> Option<String> {
599 (!self.is_absent()).then(|| self.to_string())
600 }
601
602 #[must_use]
604 pub const fn is_symlink(self) -> bool {
605 self.0 & 0o170000 == 0o120000
606 }
607
608 #[must_use]
610 pub const fn is_gitlink(self) -> bool {
611 self.0 & 0o170000 == 0o160000
612 }
613}
614
615impl From<u32> for FileMode {
616 fn from(bits: u32) -> Self {
617 Self(bits)
618 }
619}
620
621impl From<FileMode> for u32 {
622 fn from(mode: FileMode) -> Self {
623 mode.0
624 }
625}
626
627impl fmt::Display for FileMode {
628 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
629 write!(f, "{:06o}", self.0)
630 }
631}
632
633impl fmt::Debug for FileMode {
634 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
635 write!(f, "FileMode({:06o})", self.0)
636 }
637}
638
639#[cfg(feature = "serde-models")]
640impl serde::Serialize for FileMode {
641 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
642 where
643 S: serde::Serializer,
644 {
645 serializer.collect_str(self)
646 }
647}
648
649#[derive(Clone, Copy, PartialEq, Eq)]
673pub struct ObjectIdPrefix {
674 bytes: [u8; MAX_OID_LEN],
677 nibbles: u8,
678 algo: HashAlgo,
679}
680
681impl ObjectIdPrefix {
682 pub fn from_hex(hex: &str, algo: HashAlgo) -> Result<Self> {
691 let digits = hex.as_bytes();
692 if digits.len() > algo.hex_len() {
693 return Err(Error::InvalidObjectId(hex.to_owned()));
694 }
695 let mut bytes = [0u8; MAX_OID_LEN];
696 let mut invalid = 0u8;
697 for (i, &digit) in digits.iter().enumerate() {
698 let nibble = HEX_DECODE[usize::from(digit)];
699 invalid |= nibble;
700 let shift = if i.is_multiple_of(2) { 4 } else { 0 };
701 bytes[i / 2] |= (nibble & 0x0f) << shift;
702 }
703 if invalid & HEX_INVALID != 0 {
704 return Err(Error::InvalidObjectId(hex.to_owned()));
705 }
706 Ok(Self {
707 bytes,
708 nibbles: digits.len() as u8,
709 algo,
710 })
711 }
712
713 #[must_use]
715 pub fn nibbles(&self) -> usize {
716 usize::from(self.nibbles)
717 }
718
719 #[must_use]
721 pub fn is_empty(&self) -> bool {
722 self.nibbles == 0
723 }
724
725 #[must_use]
727 pub fn algo(&self) -> HashAlgo {
728 self.algo
729 }
730
731 #[must_use]
736 pub fn matches(&self, oid: &ObjectId) -> bool {
737 self.matches_digest(oid.as_bytes())
738 }
739
740 #[must_use]
742 pub(crate) fn matches_digest(&self, digest: &[u8]) -> bool {
743 let whole = self.nibbles() / 2;
744 let Some(head) = digest.get(..whole) else {
745 return false;
746 };
747 if head != &self.bytes[..whole] {
748 return false;
749 }
750 if self.nibbles.is_multiple_of(2) {
751 return true;
752 }
753 digest
754 .get(whole)
755 .is_some_and(|&b| b & 0xf0 == self.bytes[whole])
756 }
757
758 #[must_use]
760 pub fn lowest(&self) -> ObjectId {
761 ObjectId {
762 bytes: self.bytes,
763 len: self.algo.len() as u8,
764 }
765 }
766
767 #[must_use]
769 pub fn highest(&self) -> ObjectId {
770 let mut bytes = self.bytes;
771 let whole = self.nibbles() / 2;
772 if !self.nibbles.is_multiple_of(2) {
773 bytes[whole] |= 0x0f;
774 }
775 let first_free = whole + usize::from(self.nibbles % 2);
776 bytes[first_free..self.algo.len()].fill(0xff);
777 ObjectId {
778 bytes,
779 len: self.algo.len() as u8,
780 }
781 }
782}
783
784impl fmt::Display for ObjectIdPrefix {
785 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
786 let hex = hex::encode(&self.bytes[..self.nibbles().div_ceil(2)]);
787 f.write_str(&hex[..self.nibbles()])
788 }
789}
790
791impl fmt::Debug for ObjectIdPrefix {
792 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
793 write!(f, "ObjectIdPrefix({self})")
794 }
795}
796
797#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
799pub enum ObjectKind {
800 Blob,
802 Tree,
804 Commit,
806 Tag,
808}
809
810impl ObjectKind {
811 pub fn from_bytes(b: &[u8]) -> Result<Self> {
817 match b {
818 b"blob" => Ok(Self::Blob),
819 b"tree" => Ok(Self::Tree),
820 b"commit" => Ok(Self::Commit),
821 b"tag" => Ok(Self::Tag),
822 other => Err(Error::UnknownObjectType(
823 String::from_utf8_lossy(other).into_owned(),
824 )),
825 }
826 }
827
828 #[must_use]
833 pub fn from_tag_type_field(line: &[u8]) -> Option<Self> {
834 fn keyword_matches(canonical: &[u8], field: &[u8]) -> bool {
835 if field.is_empty() {
836 return false;
837 }
838 for (i, &bc) in field.iter().enumerate() {
839 let sc = canonical.get(i).copied().unwrap_or(0);
840 if sc != bc {
841 return false;
842 }
843 }
844 canonical.get(field.len()).copied().unwrap_or(0) == 0
845 }
846
847 const NAMES: &[(ObjectKind, &[u8])] = &[
848 (ObjectKind::Blob, b"blob"),
849 (ObjectKind::Tree, b"tree"),
850 (ObjectKind::Commit, b"commit"),
851 (ObjectKind::Tag, b"tag"),
852 ];
853 for &(kind, name) in NAMES {
854 if keyword_matches(name, line) {
855 return Some(kind);
856 }
857 }
858 None
859 }
860
861 #[must_use]
863 pub fn as_str(&self) -> &'static str {
864 match self {
865 Self::Blob => "blob",
866 Self::Tree => "tree",
867 Self::Commit => "commit",
868 Self::Tag => "tag",
869 }
870 }
871}
872
873impl fmt::Display for ObjectKind {
874 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
875 f.write_str(self.as_str())
876 }
877}
878
879impl FromStr for ObjectKind {
880 type Err = Error;
881
882 fn from_str(s: &str) -> Result<Self> {
883 Self::from_bytes(s.as_bytes())
884 }
885}
886
887#[derive(Debug, Clone, Copy, PartialEq, Eq)]
889pub struct ObjectInfo {
890 pub kind: ObjectKind,
892 pub size: u64,
894}
895
896#[derive(Debug, Clone)]
898pub struct Object {
899 pub kind: ObjectKind,
901 pub data: Vec<u8>,
903}
904
905#[derive(Debug, Clone, Copy, PartialEq, Eq)]
911pub struct ObjectRef<'a> {
912 pub kind: ObjectKind,
914 pub data: &'a [u8],
916}
917
918impl<'a> ObjectRef<'a> {
919 #[must_use]
921 pub fn from_buf(kind: ObjectKind, buf: &'a [u8]) -> Self {
922 Self { kind, data: buf }
923 }
924}
925
926impl Object {
927 #[must_use]
929 pub fn new(kind: ObjectKind, data: Vec<u8>) -> Self {
930 Self { kind, data }
931 }
932
933 #[must_use]
935 pub fn as_ref(&self) -> ObjectRef<'_> {
936 ObjectRef {
937 kind: self.kind,
938 data: self.data.as_slice(),
939 }
940 }
941}
942
943#[derive(Debug, Clone, PartialEq, Eq)]
945pub struct TreeEntry {
946 pub mode: u32,
948 pub name: Vec<u8>,
950 pub oid: ObjectId,
952}
953
954impl TreeEntry {
955 #[must_use]
959 pub fn mode_str(&self) -> String {
960 if self.mode == 0o040000 {
962 "40000".to_owned()
963 } else {
964 format!("{:o}", self.mode)
965 }
966 }
967}
968
969#[derive(Debug, Clone, Copy, PartialEq, Eq)]
971pub struct TreeEntryRef<'a> {
972 pub mode: u32,
974 pub name: &'a [u8],
976 pub oid: ObjectId,
978}
979
980impl TreeEntryRef<'_> {
981 #[must_use]
983 pub fn to_owned(&self) -> TreeEntry {
984 TreeEntry {
985 mode: self.mode,
986 name: self.name.to_vec(),
987 oid: self.oid,
988 }
989 }
990}
991
992#[derive(Debug, Clone)]
997pub struct TreeRefIter<'a> {
998 data: &'a [u8],
999 pos: usize,
1000 oid_len: usize,
1001 failed: bool,
1003}
1004
1005impl<'a> TreeRefIter<'a> {
1006 #[must_use]
1008 pub fn with_oid_len(data: &'a [u8], oid_len: usize) -> Self {
1009 Self {
1010 data,
1011 pos: 0,
1012 oid_len,
1013 failed: false,
1014 }
1015 }
1016
1017 pub fn infer_oid_len(data: &'a [u8]) -> Result<Self> {
1023 match try_parse_tree_oid_len(data, HashAlgo::Sha1.len()) {
1024 Ok(()) => Ok(Self::with_oid_len(data, HashAlgo::Sha1.len())),
1025 Err(sha1_err) => match try_parse_tree_oid_len(data, HashAlgo::Sha256.len()) {
1026 Ok(()) => Ok(Self::with_oid_len(data, HashAlgo::Sha256.len())),
1027 Err(_) => Err(sha1_err),
1028 },
1029 }
1030 }
1031
1032 fn next_entry(&mut self) -> Result<Option<TreeEntryRef<'a>>> {
1033 parse_tree_entry_at(self.data, &mut self.pos, self.oid_len)
1034 }
1035}
1036
1037fn parse_tree_entry_at<'a>(
1039 data: &'a [u8],
1040 pos: &mut usize,
1041 oid_len: usize,
1042) -> Result<Option<TreeEntryRef<'a>>> {
1043 if *pos >= data.len() {
1044 return Ok(None);
1045 }
1046 let sp = data[*pos..]
1047 .iter()
1048 .position(|&b| b == b' ')
1049 .ok_or_else(|| Error::CorruptObject("tree entry missing space".to_owned()))?;
1050 let mode_bytes = &data[*pos..*pos + sp];
1051 let mode = std::str::from_utf8(mode_bytes)
1052 .ok()
1053 .and_then(|s| u32::from_str_radix(s, 8).ok())
1054 .ok_or_else(|| {
1055 Error::CorruptObject(format!(
1056 "invalid tree mode: {}",
1057 String::from_utf8_lossy(mode_bytes)
1058 ))
1059 })?;
1060 *pos += sp + 1;
1061
1062 let nul = data[*pos..]
1063 .iter()
1064 .position(|&b| b == 0)
1065 .ok_or_else(|| Error::CorruptObject("tree entry missing NUL".to_owned()))?;
1066 let name = &data[*pos..*pos + nul];
1067 *pos += nul + 1;
1068
1069 if *pos + oid_len > data.len() {
1070 return Err(Error::CorruptObject("tree entry truncated SHA".to_owned()));
1071 }
1072 let oid = ObjectId::from_bytes(&data[*pos..*pos + oid_len])?;
1073 *pos += oid_len;
1074
1075 Ok(Some(TreeEntryRef { mode, name, oid }))
1076}
1077
1078impl<'a> Iterator for TreeRefIter<'a> {
1079 type Item = Result<TreeEntryRef<'a>>;
1080
1081 fn next(&mut self) -> Option<Self::Item> {
1082 if self.failed || self.pos >= self.data.len() {
1083 return None;
1084 }
1085 match self.next_entry() {
1086 Ok(Some(entry)) => Some(Ok(entry)),
1087 Ok(None) => None,
1088 Err(err) => {
1089 self.failed = true;
1090 self.pos = self.data.len();
1091 Some(Err(err))
1092 }
1093 }
1094 }
1095}
1096
1097fn try_parse_tree_oid_len(data: &[u8], oid_len: usize) -> Result<()> {
1099 let mut pos = 0usize;
1100 while parse_tree_entry_at(data, &mut pos, oid_len)?.is_some() {}
1101 Ok(())
1102}
1103
1104fn collect_tree_with_oid_len(data: &[u8], oid_len: usize) -> Result<Vec<TreeEntry>> {
1105 let mut pos = 0usize;
1106 let mut entries = Vec::with_capacity(data.len() / 32);
1107 while let Some(entry) = parse_tree_entry_at(data, &mut pos, oid_len)? {
1108 entries.push(TreeEntry {
1109 mode: entry.mode,
1110 name: entry.name.to_vec(),
1111 oid: entry.oid,
1112 });
1113 }
1114 Ok(entries)
1115}
1116
1117pub fn parse_tree(data: &[u8]) -> Result<Vec<TreeEntry>> {
1128 match collect_tree_with_oid_len(data, HashAlgo::Sha1.len()) {
1129 Ok(entries) => Ok(entries),
1130 Err(sha1_err) => {
1131 collect_tree_with_oid_len(data, HashAlgo::Sha256.len()).map_err(|_| sha1_err)
1132 }
1133 }
1134}
1135
1136pub fn parse_tree_with_oid_len(data: &[u8], oid_len: usize) -> Result<Vec<TreeEntry>> {
1141 collect_tree_with_oid_len(data, oid_len)
1142}
1143
1144#[must_use]
1151pub fn serialize_tree(entries: &[TreeEntry]) -> Vec<u8> {
1152 let len = entries
1153 .iter()
1154 .map(|e| 8 + e.name.len() + e.oid.as_bytes().len())
1155 .sum();
1156 let mut out = Vec::with_capacity(len);
1157 for e in entries {
1158 push_tree_mode(&mut out, e.mode);
1159 out.push(b' ');
1160 out.extend_from_slice(&e.name);
1161 out.push(0);
1162 out.extend_from_slice(e.oid.as_bytes());
1163 }
1164 out
1165}
1166
1167fn push_tree_mode(out: &mut Vec<u8>, mode: u32) {
1169 let mut digits = [0u8; 11];
1170 let mut start = digits.len();
1171 let mut rest = mode;
1172 loop {
1173 start -= 1;
1174 digits[start] = b'0' + (rest & 7) as u8;
1175 rest >>= 3;
1176 if rest == 0 {
1177 break;
1178 }
1179 }
1180 out.extend_from_slice(&digits[start..]);
1181}
1182
1183#[must_use]
1196pub fn tree_entry_cmp(
1197 a_name: &[u8],
1198 a_is_tree: bool,
1199 b_name: &[u8],
1200 b_is_tree: bool,
1201) -> std::cmp::Ordering {
1202 let a_trailer = if a_is_tree { b'/' } else { 0u8 };
1203 let b_trailer = if b_is_tree { b'/' } else { 0u8 };
1204
1205 let min_len = a_name.len().min(b_name.len());
1206 let cmp = a_name[..min_len].cmp(&b_name[..min_len]);
1207 if cmp != std::cmp::Ordering::Equal {
1208 return cmp;
1209 }
1210 let ac = a_name.get(min_len).copied().unwrap_or(a_trailer);
1212 let bc = b_name.get(min_len).copied().unwrap_or(b_trailer);
1213 ac.cmp(&bc)
1214}
1215
1216fn parse_tree_mode_octal(bytes: &[u8]) -> Option<u32> {
1218 if bytes.is_empty() || bytes.len() > 7 {
1219 return None;
1220 }
1221 bytes.iter().try_fold(0u32, |acc, &b| match b {
1222 b'0'..=b'7' => Some(acc * 8 + u32::from(b - b'0')),
1223 _ => None,
1224 })
1225}
1226
1227pub(crate) fn find_tree_entry_by_name_in_data(
1239 data: &[u8],
1240 oid_len: usize,
1241 name: &[u8],
1242 name_is_tree: bool,
1243) -> Result<Option<TreeEntry>> {
1244 let mut pos = 0usize;
1245 while pos < data.len() {
1246 let rest = &data[pos..];
1247 let sp = memchr::memchr(b' ', rest)
1248 .ok_or_else(|| Error::CorruptObject("tree entry missing space".to_owned()))?;
1249 let mode = parse_tree_mode_octal(&rest[..sp])
1250 .ok_or_else(|| Error::CorruptObject("invalid tree mode".to_owned()))?;
1251 let name_start = sp + 1;
1252 let nul = memchr::memchr(0, &rest[name_start..])
1253 .ok_or_else(|| Error::CorruptObject("tree entry missing NUL".to_owned()))?;
1254 let entry_name = &rest[name_start..name_start + nul];
1255 let oid_start = name_start + nul + 1;
1256 let oid_end = oid_start + oid_len;
1257 if oid_end > rest.len() {
1258 return Err(Error::CorruptObject("tree entry truncated SHA".to_owned()));
1259 }
1260 if entry_name == name {
1261 return Ok(Some(TreeEntry {
1262 mode,
1263 name: entry_name.to_vec(),
1264 oid: ObjectId::from_bytes(&rest[oid_start..oid_end])?,
1265 }));
1266 }
1267 if tree_entry_cmp(entry_name, mode == 0o040000, name, name_is_tree)
1268 == std::cmp::Ordering::Greater
1269 {
1270 return Ok(None);
1271 }
1272 pos += oid_end;
1273 }
1274 Ok(None)
1275}
1276
1277pub(crate) fn find_tree_entry_by_name_dir_or_file_in_data(
1279 data: &[u8],
1280 oid_len: usize,
1281 name: &[u8],
1282) -> Result<Option<TreeEntry>> {
1283 if let Some(entry) = find_tree_entry_by_name_in_data(data, oid_len, name, true)? {
1284 if entry.mode == 0o040000 {
1285 return Ok(Some(entry));
1286 }
1287 }
1288 find_tree_entry_by_name_in_data(data, oid_len, name, false)
1289}
1290
1291#[derive(Debug, Clone, PartialEq, Eq)]
1297pub struct CommitData {
1298 pub tree: ObjectId,
1300 pub parents: Vec<ObjectId>,
1302 pub author: String,
1304 pub committer: String,
1306 pub author_raw: Vec<u8>,
1310 pub committer_raw: Vec<u8>,
1312 pub encoding: Option<String>,
1314 pub message: String,
1316 #[doc = "Optional raw message bytes for non-UTF-8 messages."]
1319 pub raw_message: Option<Vec<u8>>,
1320 pub extra_headers: Vec<u8>,
1326}
1327
1328#[must_use]
1335pub fn commit_subject_pretty(message: &str) -> String {
1336 let mut parts = Vec::new();
1337 for line in message.lines() {
1338 if line.trim().is_empty() {
1339 break;
1340 }
1341 parts.push(line.trim_end());
1342 }
1343 parts.join(" ")
1344}
1345
1346pub fn read_commit_subject_pretty(data: &[u8]) -> Result<String> {
1352 let message_start = data.windows(2).position(|w| w == b"\n\n").map(|p| p + 2);
1353 let Some(message_start) = message_start else {
1354 return Ok(String::new());
1355 };
1356 let body = std::str::from_utf8(data.get(message_start..).unwrap_or_default()).unwrap_or("");
1357 Ok(commit_subject_pretty(body))
1358}
1359
1360pub fn read_commit_subject_line(data: &[u8]) -> Result<String> {
1368 let mut pos = 0usize;
1369 let mut encoding: Option<String> = None;
1370 while pos < data.len() {
1371 let line_start = pos;
1372 let mut line_end = pos;
1373 while line_end < data.len() && data[line_end] != b'\n' {
1374 line_end += 1;
1375 }
1376 let line = &data[line_start..line_end];
1377 let after_nl = line_end.saturating_add(1);
1378 if line.is_empty() {
1379 let body = data.get(after_nl..).unwrap_or_default();
1380 let message = commit_encoding::decode_bytes(encoding.as_deref(), body);
1381 return Ok(message
1382 .lines()
1383 .find(|l| !l.trim().is_empty())
1384 .map(str::trim)
1385 .unwrap_or("(no subject)")
1386 .to_owned());
1387 }
1388 if line.first() == Some(&b' ') {
1389 pos = after_nl;
1390 continue;
1391 }
1392 if let Some(rest) = line.strip_prefix(b"encoding ") {
1393 encoding = Some(
1394 std::str::from_utf8(rest)
1395 .map_err(|_| Error::CorruptObject("encoding is not UTF-8".to_owned()))?
1396 .trim()
1397 .to_owned(),
1398 );
1399 }
1400 pos = after_nl;
1401 }
1402 Ok("(no subject)".to_owned())
1403}
1404
1405pub(crate) fn commit_through_subject_len(data: &[u8]) -> Option<usize> {
1415 let header_end = memchr::memmem::find(data, b"\n\n")? + 2;
1416 let declares_other_encoding = data[..header_end]
1417 .split(|&b| b == b'\n')
1418 .filter_map(|line| line.strip_prefix(b"encoding "))
1419 .any(|label| {
1420 let label = label.trim_ascii();
1421 !label.eq_ignore_ascii_case(b"utf-8") && !label.eq_ignore_ascii_case(b"utf8")
1422 });
1423 if declares_other_encoding {
1424 return None;
1425 }
1426 let mut pos = header_end;
1427 loop {
1428 let line_end = pos + memchr::memchr(b'\n', data.get(pos..)?)?;
1429 if data[pos..line_end].iter().any(u8::is_ascii_graphic) {
1430 return Some(line_end + 1);
1431 }
1432 pos = line_end + 1;
1433 }
1434}
1435
1436pub(crate) fn parse_oid_field(value: &[u8]) -> Result<ObjectId> {
1437 let s = std::str::from_utf8(value)
1438 .map_err(|_| Error::CorruptObject("commit header object id is not UTF-8".to_owned()))?;
1439 s.trim()
1440 .parse::<ObjectId>()
1441 .map_err(|_| Error::CorruptObject("invalid object id in commit header".to_owned()))
1442}
1443
1444pub(crate) fn parse_commit_tree_and_parents_prefix(
1454 prefix: &[u8],
1455 hex_len: usize,
1456) -> Option<(ObjectId, Vec<ObjectId>)> {
1457 canonical_commit_tree_and_parents(prefix, hex_len, false)
1458}
1459
1460pub(crate) fn parse_commit_tree_and_parents_lenient(
1467 data: &[u8],
1468 hex_len: usize,
1469) -> Option<(ObjectId, Vec<ObjectId>)> {
1470 canonical_commit_tree_and_parents(data, hex_len, true)
1471}
1472
1473fn canonical_commit_tree_and_parents(
1474 data: &[u8],
1475 hex_len: usize,
1476 complete: bool,
1477) -> Option<(ObjectId, Vec<ObjectId>)> {
1478 fn header_oid<'a>(rest: &'a [u8], key: &[u8], hex_len: usize) -> Option<(ObjectId, &'a [u8])> {
1479 let line = rest.strip_prefix(key)?;
1480 if line.len() <= hex_len || line[hex_len] != b'\n' {
1481 return None;
1482 }
1483 let oid = ObjectId::from_hex(std::str::from_utf8(&line[..hex_len]).ok()?).ok()?;
1484 Some((oid, &line[hex_len + 1..]))
1485 }
1486 const PARENT: &[u8] = b"parent ";
1487 let (tree, mut rest) = header_oid(data, b"tree ", hex_len)?;
1488 let mut parents = Vec::new();
1489 loop {
1490 if !complete && rest.len() < PARENT.len() && PARENT.starts_with(rest) {
1491 return None;
1492 }
1493 if !rest.starts_with(PARENT) {
1494 return Some((tree, parents));
1495 }
1496 let (parent, next) = header_oid(rest, PARENT, hex_len)?;
1497 parents.push(parent);
1498 rest = next;
1499 }
1500}
1501
1502pub fn parse_commit(data: &[u8]) -> Result<CommitData> {
1508 Ok(super::CommitRef::parse(data)?.to_owned())
1509}
1510
1511#[must_use]
1514pub fn tag_header_field(data: &[u8], prefix: &[u8]) -> Option<String> {
1515 let mut pos = 0usize;
1516 while pos < data.len() {
1517 let rest = &data[pos..];
1518 let nl = rest.iter().position(|&b| b == b'\n');
1519 let line = if let Some(i) = nl { &rest[..i] } else { rest };
1520 if line.is_empty() {
1521 break;
1522 }
1523 if let Some(after) = line.strip_prefix(prefix) {
1524 return Some(String::from_utf8_lossy(after).trim().to_owned());
1525 }
1526 pos += line.len().saturating_add(nl.map(|_| 1).unwrap_or(0));
1527 if nl.is_none() {
1528 break;
1529 }
1530 }
1531 None
1532}
1533
1534#[must_use]
1536pub fn tag_object_line_oid(data: &[u8]) -> Option<ObjectId> {
1537 let s = tag_header_field(data, b"object ")?;
1538 s.parse().ok()
1539}
1540
1541#[derive(Debug, Clone)]
1543pub struct TagData {
1544 pub object: ObjectId,
1546 pub object_type: String,
1548 pub tag: String,
1550 pub tagger: Option<String>,
1552 pub extra_headers: Vec<u8>,
1554 pub message: String,
1556}
1557
1558pub fn parse_tag(data: &[u8]) -> Result<TagData> {
1566 use super::extra_headers::next_header_block;
1567
1568 #[derive(Clone, Copy)]
1569 enum Continuation {
1570 None,
1571 Extra,
1572 }
1573
1574 let mut pos = 0usize;
1575 let mut object = None;
1576 let mut object_type = None;
1577 let mut tag_name = None;
1578 let mut tagger = None;
1579 let mut extra_headers = Vec::new();
1580 let mut cont = Continuation::None;
1581 let mut past_tagger = false;
1582
1583 while pos < data.len() {
1584 let line_start = pos;
1585 let mut line_end = pos;
1586 while line_end < data.len() && data[line_end] != b'\n' {
1587 line_end += 1;
1588 }
1589 let line = &data[line_start..line_end];
1590 let after_nl = (line_end + 1).min(data.len());
1591 if line.is_empty() {
1592 let body = data.get(after_nl..).unwrap_or_default();
1593 let message = std::str::from_utf8(body)
1594 .map_err(|_| Error::CorruptObject("tag message is not valid UTF-8".to_owned()))?
1595 .to_owned();
1596 return Ok(TagData {
1597 object: object
1598 .ok_or_else(|| Error::CorruptObject("tag missing object header".to_owned()))?,
1599 object_type: object_type
1600 .ok_or_else(|| Error::CorruptObject("tag missing type header".to_owned()))?,
1601 tag: tag_name
1602 .ok_or_else(|| Error::CorruptObject("tag missing tag header".to_owned()))?,
1603 tagger,
1604 extra_headers,
1605 message,
1606 });
1607 }
1608
1609 if past_tagger {
1610 let (_, block_end, _) = next_header_block(data, line_start);
1611 extra_headers.extend_from_slice(&data[line_start..block_end]);
1612 pos = block_end;
1613 continue;
1614 }
1615
1616 if line.first() == Some(&b' ') {
1617 match cont {
1618 Continuation::Extra => {
1619 extra_headers.extend_from_slice(&data[line_start..after_nl]);
1620 }
1621 Continuation::None => {}
1622 }
1623 pos = after_nl;
1624 continue;
1625 }
1626
1627 let key_end = line
1628 .iter()
1629 .position(|&b| b == b' ')
1630 .ok_or_else(|| Error::CorruptObject("malformed tag header line".to_owned()))?;
1631 let key = &line[..key_end];
1632 let rest = line.get(key_end + 1..).unwrap_or_default();
1633
1634 match key {
1635 b"object" => {
1636 let line_str = std::str::from_utf8(rest).map_err(|_| {
1637 Error::CorruptObject("tag object line is not valid UTF-8".to_owned())
1638 })?;
1639 object = Some(line_str.trim().parse::<ObjectId>()?);
1640 cont = Continuation::None;
1641 }
1642 b"type" => {
1643 let typ = std::str::from_utf8(rest).map_err(|_| {
1644 Error::CorruptObject("tag type line is not valid UTF-8".to_owned())
1645 })?;
1646 let typ = typ.trim();
1647 if ObjectKind::from_tag_type_field(typ.as_bytes()).is_none() {
1648 return Err(Error::CorruptObject(format!(
1649 "invalid 'type' value in tag: {typ}"
1650 )));
1651 }
1652 object_type = Some(typ.to_owned());
1653 cont = Continuation::None;
1654 }
1655 b"tag" => {
1656 let line_str = std::str::from_utf8(rest).map_err(|_| {
1657 Error::CorruptObject("tag name line is not valid UTF-8".to_owned())
1658 })?;
1659 tag_name = Some(line_str.trim().to_owned());
1660 cont = Continuation::None;
1661 }
1662 b"tagger" => {
1663 tagger = Some(
1664 std::str::from_utf8(rest)
1665 .map_err(|_| {
1666 Error::CorruptObject("tag tagger line is not valid UTF-8".to_owned())
1667 })?
1668 .to_owned(),
1669 );
1670 past_tagger = true;
1671 cont = Continuation::None;
1672 }
1673 _ => {
1674 cont = Continuation::Extra;
1675 extra_headers.extend_from_slice(&data[line_start..after_nl]);
1676 }
1677 }
1678 pos = after_nl;
1679 }
1680
1681 Ok(TagData {
1682 object: object
1683 .ok_or_else(|| Error::CorruptObject("tag missing object header".to_owned()))?,
1684 object_type: object_type
1685 .ok_or_else(|| Error::CorruptObject("tag missing type header".to_owned()))?,
1686 tag: tag_name.ok_or_else(|| Error::CorruptObject("tag missing tag header".to_owned()))?,
1687 tagger,
1688 extra_headers,
1689 message: String::new(),
1690 })
1691}
1692
1693#[must_use]
1698pub fn serialize_tag(t: &TagData) -> Vec<u8> {
1699 let mut out = Vec::new();
1700 out.extend_from_slice(format!("object {}\n", t.object).as_bytes());
1701 out.extend_from_slice(format!("type {}\n", t.object_type).as_bytes());
1702 out.extend_from_slice(format!("tag {}\n", t.tag).as_bytes());
1703 if let Some(ref tagger) = t.tagger {
1704 out.extend_from_slice(format!("tagger {tagger}\n").as_bytes());
1705 }
1706 out.extend_from_slice(&t.extra_headers);
1707 out.push(b'\n');
1708 let msg = t.message.trim_end_matches('\n');
1709 if !msg.is_empty() {
1710 out.extend_from_slice(msg.as_bytes());
1711 out.push(b'\n');
1712 }
1713 out
1714}
1715
1716#[must_use]
1726pub fn serialize_commit(c: &CommitData) -> Vec<u8> {
1727 let mut out = Vec::new();
1728 out.extend_from_slice(format!("tree {}\n", c.tree).as_bytes());
1729 for p in &c.parents {
1730 out.extend_from_slice(format!("parent {p}\n").as_bytes());
1731 }
1732 out.extend_from_slice(b"author ");
1733 if c.author_raw.is_empty() {
1734 out.extend_from_slice(c.author.as_bytes());
1735 } else {
1736 out.extend_from_slice(&c.author_raw);
1737 }
1738 out.push(b'\n');
1739 out.extend_from_slice(b"committer ");
1740 if c.committer_raw.is_empty() {
1741 out.extend_from_slice(c.committer.as_bytes());
1742 } else {
1743 out.extend_from_slice(&c.committer_raw);
1744 }
1745 out.push(b'\n');
1746 if let Some(enc) = &c.encoding {
1747 out.extend_from_slice(format!("encoding {enc}\n").as_bytes());
1748 }
1749 out.extend_from_slice(&c.extra_headers);
1750 out.push(b'\n');
1751 if let Some(raw) = &c.raw_message {
1752 out.extend_from_slice(raw);
1753 } else if !c.message.is_empty() {
1754 out.extend_from_slice(c.message.as_bytes());
1755 }
1756 out
1757}
1758
1759#[cfg(test)]
1760mod short_hex_tests {
1761 use super::*;
1762
1763 #[test]
1764 fn short_hex_takes_seven_digits_or_the_whole_shorter_input() {
1765 assert_eq!(
1766 short_hex("4b825dc642cb6eb9a060e54bf8d69288fbee4904"),
1767 "4b825dc"
1768 );
1769 assert_eq!(short_hex("4b825dc"), "4b825dc");
1770 assert_eq!(short_hex("4b8"), "4b8");
1771 assert_eq!(short_hex(""), "");
1772 assert_eq!(short_hex("ééééé"), "ééééé");
1773 }
1774
1775 #[test]
1776 fn object_id_short_hex_matches_the_hex_prefix_for_both_algorithms() {
1777 let digest: Vec<u8> = (0x9a..=0xff).take(32).collect();
1778 for len in [HashAlgo::Sha1.len(), HashAlgo::Sha256.len()] {
1779 let id = ObjectId::from_bytes(&digest[..len]).unwrap();
1780 let short = id.to_short_hex();
1781 assert_eq!(short.len(), SHORT_HEX_LEN);
1782 assert_eq!(short, id.to_hex()[..SHORT_HEX_LEN]);
1783 assert_eq!(short, short_hex(&id.to_hex()));
1784 }
1785 }
1786}
1787
1788#[cfg(test)]
1789mod object_id_prefix_tests {
1790 use super::*;
1791
1792 const ID: &str = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
1793
1794 fn id() -> ObjectId {
1795 ObjectId::from_hex(ID).unwrap()
1796 }
1797
1798 fn prefix(hex: &str) -> ObjectIdPrefix {
1799 ObjectIdPrefix::from_hex(hex, HashAlgo::Sha1).unwrap()
1800 }
1801
1802 #[test]
1803 fn matches_every_length_of_the_id_and_rejects_a_changed_last_digit() {
1804 let id = id();
1805 for n in 0..=ID.len() {
1806 let p = prefix(&ID[..n]);
1807 assert_eq!(p.nibbles(), n);
1808 assert_eq!(p.is_empty(), n == 0);
1809 assert!(p.matches(&id), "{n} digits");
1810 assert_eq!(p.to_string(), ID[..n]);
1811 if n > 0 {
1812 let mut changed = ID[..n].to_owned();
1813 let last = changed.pop().unwrap();
1814 changed.push(if last == '0' { '1' } else { '0' });
1815 assert!(!prefix(&changed).matches(&id), "{changed}");
1816 }
1817 }
1818 }
1819
1820 #[test]
1821 fn odd_prefix_ignores_the_low_nibble_of_its_last_byte() {
1822 let p = prefix("4b8");
1823 assert!(p.matches(&ObjectId::from_hex("4b80000000000000000000000000000000000000").unwrap()));
1824 assert!(p.matches(&ObjectId::from_hex("4b8fffffffffffffffffffffffffffffffffffff").unwrap()));
1825 assert!(
1826 !p.matches(&ObjectId::from_hex("4b90000000000000000000000000000000000000").unwrap())
1827 );
1828 }
1829
1830 #[test]
1831 fn parsing_is_case_insensitive_and_validates() {
1832 assert_eq!(prefix("4B825DC"), prefix("4b825dc"));
1833 assert!(matches!(
1834 ObjectIdPrefix::from_hex("4b8g", HashAlgo::Sha1),
1835 Err(Error::InvalidObjectId(_))
1836 ));
1837 assert!(matches!(
1838 ObjectIdPrefix::from_hex(&format!("{ID}0"), HashAlgo::Sha1),
1839 Err(Error::InvalidObjectId(_))
1840 ));
1841 let sha256 = "a".repeat(64);
1842 assert_eq!(
1843 ObjectIdPrefix::from_hex(&sha256, HashAlgo::Sha256)
1844 .unwrap()
1845 .nibbles(),
1846 64
1847 );
1848 assert_eq!(prefix("").algo(), HashAlgo::Sha1);
1849 }
1850
1851 #[test]
1852 fn longer_prefix_never_matches_a_shorter_digest() {
1853 let long = ObjectIdPrefix::from_hex(&format!("{ID}0000"), HashAlgo::Sha256).unwrap();
1854 assert!(!long.matches(&id()));
1855 assert!(ObjectIdPrefix::from_hex(ID, HashAlgo::Sha256)
1856 .unwrap()
1857 .matches(&id()));
1858 }
1859
1860 #[test]
1861 fn bounds_are_the_first_and_last_matching_ids() {
1862 let p = prefix("4b8");
1863 assert_eq!(
1864 p.lowest().to_hex(),
1865 "4b80000000000000000000000000000000000000"
1866 );
1867 assert_eq!(
1868 p.highest().to_hex(),
1869 "4b8fffffffffffffffffffffffffffffffffffff"
1870 );
1871 let even = prefix("4b82");
1872 assert_eq!(
1873 even.lowest().to_hex(),
1874 "4b82000000000000000000000000000000000000"
1875 );
1876 assert_eq!(
1877 even.highest().to_hex(),
1878 "4b82ffffffffffffffffffffffffffffffffffff"
1879 );
1880 assert_eq!(prefix("").lowest(), ObjectId::null(HashAlgo::Sha1));
1881 assert_eq!(prefix("").highest().to_hex(), "f".repeat(40));
1882 assert_eq!(prefix(ID).lowest(), id());
1883 assert_eq!(prefix(ID).highest(), id());
1884 let sha256 = ObjectIdPrefix::from_hex("abc", HashAlgo::Sha256).unwrap();
1885 assert_eq!(sha256.highest().to_hex(), format!("abc{}", "f".repeat(61)));
1886 assert_eq!(format!("{sha256:?}"), "ObjectIdPrefix(abc)");
1887 }
1888
1889 #[test]
1890 fn common_prefix_nibbles_counts_shared_leading_digits() {
1891 let a = id();
1892 let hex = |s: &str| ObjectId::from_hex(s).unwrap();
1893 assert_eq!(a.common_prefix_nibbles(&a), 40);
1894 assert_eq!(
1895 a.common_prefix_nibbles(&hex("4b825dc642cb6eb9a060e54bf8d69288fbee4905")),
1896 39
1897 );
1898 assert_eq!(
1899 a.common_prefix_nibbles(&hex("4b815dc642cb6eb9a060e54bf8d69288fbee4904")),
1900 3
1901 );
1902 assert_eq!(
1903 a.common_prefix_nibbles(&hex("4b925dc642cb6eb9a060e54bf8d69288fbee4904")),
1904 2
1905 );
1906 assert_eq!(
1907 a.common_prefix_nibbles(&hex("5b825dc642cb6eb9a060e54bf8d69288fbee4904")),
1908 0
1909 );
1910 assert_eq!(
1911 a.common_prefix_nibbles(&hex("4c825dc642cb6eb9a060e54bf8d69288fbee4904")),
1912 1
1913 );
1914 }
1915}
1916
1917#[cfg(test)]
1918mod commit_parse_tests {
1919 use super::*;
1920
1921 #[test]
1922 fn empty_tree_constant_is_canonical_sha1_digest() {
1923 assert_eq!(
1924 ObjectId::empty_tree().to_hex(),
1925 "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
1926 );
1927 }
1928
1929 #[test]
1930 fn is_empty_tree_for_matches_hashed_empty_tree() {
1931 for algo in [HashAlgo::Sha1, HashAlgo::Sha256] {
1932 let hashed =
1933 crate::hash::hash_object(algo, ObjectKind::Tree, b"").expect("hash empty tree");
1934 assert!(hashed.is_empty_tree_for(algo), "{algo:?}");
1935 let other = match algo {
1936 HashAlgo::Sha1 => HashAlgo::Sha256,
1937 HashAlgo::Sha256 => HashAlgo::Sha1,
1938 };
1939 assert!(!hashed.is_empty_tree_for(other), "{algo:?}");
1940 let empty_blob =
1941 crate::hash::hash_object(algo, ObjectKind::Blob, b"").expect("hash empty blob");
1942 assert!(!empty_blob.is_empty_tree_for(algo), "{algo:?}");
1943 }
1944 let legacy = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf899d69f7c6948d4").expect("hex");
1945 assert!(legacy.is_empty_tree_for(HashAlgo::Sha1));
1946 assert!(!legacy.is_empty_tree_for(HashAlgo::Sha256));
1947 }
1948
1949 #[test]
1950 fn serialize_tree_mode_matches_mode_str() {
1951 for mode in [
1952 0,
1953 0o7,
1954 0o100644,
1955 0o100755,
1956 0o120000,
1957 0o040000,
1958 0o160000,
1959 u32::MAX,
1960 ] {
1961 let mut out = Vec::new();
1962 push_tree_mode(&mut out, mode);
1963 let entry = TreeEntry {
1964 mode,
1965 name: Vec::new(),
1966 oid: ObjectId::empty_tree(),
1967 };
1968 assert_eq!(out, entry.mode_str().into_bytes(), "mode {mode:o}");
1969 }
1970 }
1971
1972 #[test]
1973 fn is_empty_blob_matches_both_hash_algorithms() {
1974 for algo in [HashAlgo::Sha1, HashAlgo::Sha256] {
1975 let oid = algo.hash_object(ObjectKind::Blob, b"").unwrap();
1976 assert!(oid.is_empty_blob(), "{algo:?} empty blob {}", oid.to_hex());
1977 let other = algo.hash_object(ObjectKind::Blob, b"x").unwrap();
1978 assert!(!other.is_empty_blob());
1979 }
1980 assert!(!ObjectId::empty_tree().is_empty_blob());
1981 assert!(!ObjectId::zero().is_empty_blob());
1982 }
1983
1984 #[test]
1985 fn commit_subject_pretty_folds_first_paragraph() {
1986 assert_eq!(commit_subject_pretty("hello\nworld\n\nbody"), "hello world");
1987 assert_eq!(commit_subject_pretty(""), "");
1988 assert_eq!(
1989 commit_subject_pretty(" hello world\tthere \n next\t line \n\nbody\n"),
1990 " hello world\tthere next\t line"
1991 );
1992 }
1993
1994 #[test]
1995 fn commit_ref_matches_parse_commit_headers() {
1996 let raw = concat!(
1997 "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
1998 "parent 1111111111111111111111111111111111111111\n",
1999 "author A <a@example.com> 1 +0000\n",
2000 "committer C <c@example.com> 2 +0000\n",
2001 "\n",
2002 "ignored body\n",
2003 );
2004 let full = parse_commit(raw.as_bytes()).expect("parse");
2005 let link = crate::objects::CommitRef::parse(raw.as_bytes()).expect("CommitRef");
2006 assert_eq!(link.tree(), full.tree);
2007 assert_eq!(link.parents(), full.parents.as_slice());
2008 assert_eq!(link.author(), full.author);
2009 assert_eq!(link.committer(), full.committer);
2010 }
2011
2012 #[test]
2013 fn parse_commit_skips_multiline_gpgsig_continuation() {
2014 let raw = concat!(
2015 "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
2016 "author A U Thor <author@example.com> 1 +0000\n",
2017 "committer C O Mitter <committer@example.com> 1 +0000\n",
2018 "gpgsig -----BEGIN PGP SIGNATURE-----\n",
2019 " abcdef\n",
2020 " -----END PGP SIGNATURE-----\n",
2021 "\n",
2022 "msg\n",
2023 );
2024 let c = parse_commit(raw.as_bytes()).expect("parse signed commit");
2025 assert_eq!(c.tree.to_hex(), "4b825dc642cb6eb9a060e54bf8d69288fbee4904");
2026 assert_eq!(c.message, "msg\n");
2027 assert_eq!(serialize_commit(&c), raw.as_bytes());
2028 }
2029
2030 #[test]
2031 fn hex_parsing_accepts_both_cases_and_lengths_and_rejects_the_rest() {
2032 let sha1 = "0123456789abcdefABCDEF0123456789abcdef01";
2033 let oid = ObjectId::from_hex(sha1).unwrap();
2034 assert_eq!(oid.algo(), HashAlgo::Sha1);
2035 assert_eq!(oid.to_hex(), sha1.to_ascii_lowercase());
2036 assert_eq!(ObjectId::from_hex_ascii(sha1.as_bytes()).unwrap(), oid);
2037 assert_eq!(sha1.parse::<ObjectId>().unwrap(), oid);
2038
2039 let sha256 = "f".repeat(64);
2040 let oid = sha256.parse::<ObjectId>().unwrap();
2041 assert_eq!(oid.algo(), HashAlgo::Sha256);
2042 assert_eq!(oid.as_bytes(), [0xff; 32]);
2043 assert!(ObjectId::from_hex_ascii(sha256.as_bytes()).is_err());
2044
2045 for bad in [
2046 "",
2047 &sha1[..39],
2048 &format!("{sha1}0"),
2049 "g123456789abcdef0123456789abcdef01234567",
2050 "0123456789abcdef0123456789abcdef0123456 ",
2051 "0123456789abcdef0123456789abcdef012345/7",
2052 "0123456789abcdef0123456789abcdef012345:7",
2053 "0123456789abcdef0123456789abcdef012345`7",
2054 "0123456789abcdef0123456789abcdef012345G7",
2055 ] {
2056 assert!(
2057 matches!(ObjectId::from_hex(bad), Err(Error::InvalidObjectId(s)) if s == bad),
2058 "{bad:?}"
2059 );
2060 assert!(bad.parse::<ObjectId>().is_err(), "{bad:?}");
2061 assert!(ObjectId::from_hex_ascii(bad.as_bytes()).is_err(), "{bad:?}");
2062 }
2063 let mut non_ascii = sha1.as_bytes().to_vec();
2064 non_ascii[3] = 0xb0;
2065 assert!(ObjectId::from_hex_ascii(&non_ascii).is_err());
2066 }
2067
2068 #[test]
2069 fn hex_and_display_cover_every_byte_for_both_lengths() {
2070 let all: Vec<u8> = (0..=255).collect();
2071 for width in [20, 32] {
2072 for chunk in all.chunks(width).filter(|c| c.len() == width) {
2073 let oid = ObjectId::from_bytes(chunk).unwrap();
2074 let expected: String = chunk.iter().map(|b| format!("{b:02x}")).collect();
2075 assert_eq!(oid.to_hex(), expected);
2076 assert_eq!(oid.to_string(), expected);
2077 assert_eq!(format!("{oid:?}"), format!("ObjectId({expected})"));
2078 assert_eq!(ObjectId::from_hex(&expected).unwrap(), oid);
2079 }
2080 }
2081 }
2082
2083 #[test]
2084 fn serialize_commit_applies_public_field_edits_after_parse() {
2085 let raw = concat!(
2086 "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
2087 "author A <a@example.com> 1 +0000\n",
2088 "committer C <c@example.com> 1 +0000\n",
2089 "\n",
2090 "msg\n",
2091 );
2092 let mut commit = parse_commit(raw.as_bytes()).expect("parse");
2093 let zero = ObjectId::from_hex("0000000000000000000000000000000000000000").unwrap();
2094 commit.tree = zero;
2095 let serialized = serialize_commit(&commit);
2096 let tree_line = format!("tree {}\n", zero.to_hex());
2097 assert!(
2098 serialized.starts_with(tree_line.as_bytes()),
2099 "serialize_commit ignored the updated public tree field"
2100 );
2101 }
2102
2103 #[test]
2104 fn parse_commit_preserves_gpgsig_mergetag_bytes_exactly() {
2105 let raw = concat!(
2106 "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
2107 "author A <a@example.com> 1 +0000\n",
2108 "committer C <c@example.com> 1 +0000\n",
2109 "encoding ISO-8859-1\n",
2110 "gpgsig -----BEGIN PGP SIGNATURE-----\n",
2111 " line2\n",
2112 " -----END PGP SIGNATURE-----\n",
2113 "mergetag object 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
2114 " type commit\n",
2115 " tag merge-tag\n",
2116 " tagger T <t@example.com> 1 +0000\n",
2117 " -----BEGIN PGP SIGNATURE-----\n",
2118 " inner\n",
2119 " -----END PGP SIGNATURE-----\n",
2120 "\n",
2121 "msg-only\n",
2122 );
2123 let parsed = parse_commit(raw.as_bytes()).expect("parse");
2124 assert_eq!(serialize_commit(&parsed), raw.as_bytes());
2125 }
2126
2127 #[test]
2128 fn tree_entry_mode_str_and_tag_object_line_oid() {
2129 let oid = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap();
2130 let entry = TreeEntry {
2131 mode: 0o040000,
2132 name: b"dir".to_vec(),
2133 oid,
2134 };
2135 assert_eq!(entry.mode_str(), "40000");
2136 let tag_body = format!("object {}\ntype commit\ntag t\n\n", oid.to_hex());
2137 assert_eq!(tag_object_line_oid(tag_body.as_bytes()), Some(oid));
2138 }
2139
2140 #[test]
2141 fn parse_commit_non_commit_bytes_does_not_panic() {
2142 let tree_bytes = b"100644 blob 0123456789012345678901234567890123456789\tf";
2143 let err = parse_commit(tree_bytes).expect_err("tree bytes are not a commit");
2144 assert!(matches!(err, Error::CorruptObject(_)));
2145 }
2146
2147 #[test]
2148 fn parse_tree_with_sha256_oid_width() {
2149 let oid_bytes = [0x6eu8; 32];
2150 let oid = ObjectId::from_bytes(&oid_bytes).expect("oid");
2151 let mut raw = b"100644 file\0".to_vec();
2152 raw.extend_from_slice(oid.as_bytes());
2153 let entries = parse_tree_with_oid_len(&raw, HashAlgo::Sha256.len()).expect("tree");
2154 assert_eq!(entries.len(), 1);
2155 assert_eq!(entries[0].oid, oid);
2156 }
2157
2158 #[test]
2159 fn parse_commit_tree_and_parents_prefix_matches_full_parse_or_declines() -> Result<()> {
2160 let tree = "1111111111111111111111111111111111111111";
2161 let p1 = "2222222222222222222222222222222222222222";
2162 let p2 = "3333333333333333333333333333333333333333";
2163 let raw = format!(
2164 "tree {tree}\nparent {p1}\nparent {p2}\nauthor A <a@x> 1 +0000\n\
2165 committer C <c@x> 2 +0000\n\nmessage\n"
2166 );
2167 let commit = parse_commit(raw.as_bytes())?;
2168 let full = (commit.tree, commit.parents);
2169 assert_eq!(full.1.len(), 2);
2170 assert_eq!(
2171 parse_commit_tree_and_parents_lenient(raw.as_bytes(), HashAlgo::Sha1.hex_len()),
2172 Some(full.clone())
2173 );
2174 let hex_len = HashAlgo::Sha1.hex_len();
2175 let header_end = raw.find("author").expect("author line");
2176 for cut in 0..=raw.len() {
2177 let got = parse_commit_tree_and_parents_prefix(&raw.as_bytes()[..cut], hex_len);
2178 if cut > header_end {
2179 assert_eq!(got.as_ref(), Some(&full), "cut at {cut}");
2180 } else {
2181 assert_eq!(
2182 got, None,
2183 "cut at {cut} must not claim a complete parent list"
2184 );
2185 }
2186 }
2187 Ok(())
2188 }
2189
2190 #[test]
2191 fn commit_tree_and_parents_prefix_never_disagrees_with_whole_object() {
2192 let tree = "tree 1111111111111111111111111111111111111111\n";
2193 let parent = "parent 2222222222222222222222222222222222222222\n";
2194 let parent2 = "parent 3333333333333333333333333333333333333333\n";
2195 let bodies = [
2196 format!("{tree}{parent}author A <a@x> 1 +0000\n\nmessage\n"),
2197 format!("{tree}{parent}{parent2}committer C <c@x> 2 +0000\n\nno author\n"),
2198 format!("{tree}{parent}"),
2199 format!("{tree}{parent}{parent2}"),
2200 format!("{tree}author A <a@x> 1 +0000\n{parent}\nlate parent is ignored\n"),
2201 format!("{tree}{parent}not a header\n"),
2202 format!("{tree}parent zz\n\n"),
2203 format!("{tree}{parent}parent 2222"),
2204 format!("author A <a@x> 1 +0000\n{tree}\n"),
2205 ];
2206 let hex_len = HashAlgo::Sha1.hex_len();
2207 for body in &bodies {
2208 let whole = parse_commit_tree_and_parents_lenient(body.as_bytes(), hex_len);
2209 for cut in 0..=body.len() {
2210 if let Some(got) =
2211 parse_commit_tree_and_parents_prefix(&body.as_bytes()[..cut], hex_len)
2212 {
2213 assert_eq!(Some(got), whole, "cut {cut} of {body:?}");
2214 }
2215 }
2216 }
2217 let parents_of = |body: &str| {
2218 parse_commit_tree_and_parents_lenient(body.as_bytes(), hex_len).map(|(_, p)| p.len())
2219 };
2220 assert_eq!(parents_of(&bodies[1]), Some(2), "missing author is fine");
2221 assert_eq!(
2222 parents_of(&bodies[3]),
2223 Some(2),
2224 "object may end after parents"
2225 );
2226 assert_eq!(
2227 parents_of(&bodies[4]),
2228 Some(0),
2229 "only leading parents count"
2230 );
2231 assert_eq!(parents_of(&bodies[6]), None, "malformed parent line");
2232 assert_eq!(parents_of(&bodies[7]), None, "truncated parent line");
2233 assert_eq!(parents_of(&bodies[8]), None, "tree must come first");
2234 }
2235
2236 #[test]
2237 fn parse_commit_tree_and_parents_prefix_handles_root_and_sha256() -> Result<()> {
2238 let root = b"tree 1111111111111111111111111111111111111111\nauthor A <a@x> 1 +0000\n";
2239 let (tree, parents) =
2240 parse_commit_tree_and_parents_prefix(root, HashAlgo::Sha1.hex_len()).expect("root");
2241 assert_eq!(tree.to_hex(), "1111111111111111111111111111111111111111");
2242 assert!(parents.is_empty());
2243
2244 let t = "a".repeat(64);
2245 let p = "b".repeat(64);
2246 let raw = format!("tree {t}\nparent {p}\ncommitter C <c@x> 2 +0000\n");
2247 let (tree, parents) =
2248 parse_commit_tree_and_parents_prefix(raw.as_bytes(), HashAlgo::Sha256.hex_len())
2249 .expect("sha256");
2250 assert_eq!(tree.to_hex(), t);
2251 assert_eq!(parents.len(), 1);
2252 assert_eq!(parents[0].to_hex(), p);
2253
2254 assert!(parse_commit_tree_and_parents_prefix(raw.as_bytes(), 40).is_none());
2255 assert!(parse_commit_tree_and_parents_prefix(b"parent x\n", 40).is_none());
2256 assert!(parse_commit_tree_and_parents_prefix(
2257 b"tree zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz\nauthor\n",
2258 40
2259 )
2260 .is_none());
2261 Ok(())
2262 }
2263
2264 #[test]
2265 fn object_id_loose_path_helpers() {
2266 let oid = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap();
2267 assert_eq!(oid.loose_prefix(), "4b");
2268 assert_eq!(oid.loose_suffix().len(), 38);
2269 assert!(ObjectId::is_hex_len(40));
2270 assert!(ObjectId::is_loose_suffix_len(38));
2271 }
2272
2273 #[test]
2274 fn tree_ref_iter_yields_at_most_one_error() {
2275 let bad = b"100644 incomplete";
2276 let mut iter = TreeRefIter::with_oid_len(bad, HashAlgo::Sha1.len());
2277 assert!(iter.next().unwrap().is_err());
2278 assert!(iter.next().is_none());
2279 }
2280
2281 #[test]
2282 fn tree_ref_iter_public_api_smoke() {
2283 let data = serialize_tree(&[TreeEntry {
2284 mode: 0o100644,
2285 name: b"a".to_vec(),
2286 oid: ObjectId::from_hex("0123456789012345678901234567890123456789").unwrap(),
2287 }]);
2288 let mut iter = TreeRefIter::infer_oid_len(&data).expect("infer");
2289 let entry = iter.next().unwrap().expect("entry");
2290 assert_eq!(entry.name, b"a");
2291 let _owned = entry.to_owned();
2292 let _vec = parse_tree(&data).expect("parse");
2293 let _ = TreeRefIter::with_oid_len(&data, HashAlgo::Sha1.len());
2294 }
2295
2296 #[test]
2297 fn find_tree_entry_by_name_in_data_locates_directory_in_large_tree() -> Result<()> {
2298 let dummy = ObjectId::from_hex("0123456789012345678901234567890123456789")?;
2299 let mut entries: Vec<TreeEntry> = (1..=60)
2300 .map(|i| TreeEntry {
2301 mode: 0o100644,
2302 name: format!("f{i}.txt").into_bytes(),
2303 oid: dummy,
2304 })
2305 .collect();
2306 entries.push(TreeEntry {
2307 mode: 0o040000,
2308 name: b"sub".to_vec(),
2309 oid: dummy,
2310 });
2311 entries.sort_by(|a, b| {
2312 tree_entry_cmp(&a.name, a.mode == 0o040000, &b.name, b.mode == 0o040000)
2313 });
2314 let data = serialize_tree(&entries);
2315 assert!(entries.len() >= 48);
2316 let found = find_tree_entry_by_name_in_data(&data, HashAlgo::Sha1.len(), b"sub", true)?
2317 .expect("sub directory in wide tree");
2318 assert_eq!(found.mode, 0o040000);
2319 assert_eq!(found.name, b"sub");
2320 Ok(())
2321 }
2322
2323 fn sorted_tree_data(entries: &mut [TreeEntry]) -> Vec<u8> {
2324 entries.sort_by(|a, b| {
2325 tree_entry_cmp(&a.name, a.mode == 0o040000, &b.name, b.mode == 0o040000)
2326 });
2327 serialize_tree(entries)
2328 }
2329
2330 #[test]
2331 fn find_tree_entry_by_name_in_data_matches_parse_tree_for_every_name() -> Result<()> {
2332 let oid_a = ObjectId::from_hex("1111111111111111111111111111111111111111")?;
2333 let oid_b = ObjectId::from_hex("2222222222222222222222222222222222222222")?;
2334 let mut entries = vec![
2335 TreeEntry {
2336 mode: 0o040000,
2337 name: b"foo".to_vec(),
2338 oid: oid_a,
2339 },
2340 TreeEntry {
2341 mode: 0o100644,
2342 name: b"foo.c".to_vec(),
2343 oid: oid_b,
2344 },
2345 TreeEntry {
2346 mode: 0o100755,
2347 name: b"foo-bar".to_vec(),
2348 oid: oid_b,
2349 },
2350 TreeEntry {
2351 mode: 0o120000,
2352 name: b"link".to_vec(),
2353 oid: oid_a,
2354 },
2355 TreeEntry {
2356 mode: 0o160000,
2357 name: b"sub".to_vec(),
2358 oid: oid_b,
2359 },
2360 TreeEntry {
2361 mode: 0o100644,
2362 name: b"README".to_vec(),
2363 oid: oid_a,
2364 },
2365 ];
2366 let data = sorted_tree_data(&mut entries);
2367 for expected in parse_tree(&data)? {
2368 let name_is_tree = expected.mode == 0o040000;
2369 let found = find_tree_entry_by_name_in_data(
2370 &data,
2371 HashAlgo::Sha1.len(),
2372 &expected.name,
2373 name_is_tree,
2374 )?
2375 .expect("entry present");
2376 assert_eq!(found, expected);
2377 }
2378 for missing in [&b"fo"[..], b"foo/", b"foo.d", b"zzz", b"", b"AAA", b"lin"] {
2379 assert!(
2380 find_tree_entry_by_name_in_data(&data, HashAlgo::Sha1.len(), missing, false)?
2381 .is_none(),
2382 "unexpected match for {:?}",
2383 String::from_utf8_lossy(missing)
2384 );
2385 }
2386 Ok(())
2387 }
2388
2389 #[test]
2390 fn find_tree_entry_by_name_in_data_stops_after_sorted_position() -> Result<()> {
2391 let oid = ObjectId::from_hex("3333333333333333333333333333333333333333")?;
2392 let mut entries = vec![
2393 TreeEntry {
2394 mode: 0o100644,
2395 name: b"a".to_vec(),
2396 oid,
2397 },
2398 TreeEntry {
2399 mode: 0o100644,
2400 name: b"c".to_vec(),
2401 oid,
2402 },
2403 ];
2404 let mut data = sorted_tree_data(&mut entries);
2405 data.extend_from_slice(b"garbage without separators");
2406 let found = find_tree_entry_by_name_in_data(&data, HashAlgo::Sha1.len(), b"a", false)?;
2407 assert_eq!(found.map(|e| e.name), Some(b"a".to_vec()));
2408 assert!(
2409 find_tree_entry_by_name_in_data(&data, HashAlgo::Sha1.len(), b"b", false)?.is_none()
2410 );
2411 assert!(matches!(
2412 find_tree_entry_by_name_in_data(&data, HashAlgo::Sha1.len(), b"d", false),
2413 Err(Error::CorruptObject(_))
2414 ));
2415 Ok(())
2416 }
2417
2418 #[test]
2419 fn find_tree_entry_by_name_in_data_rejects_bad_mode_and_truncated_oid() {
2420 let bad_mode = b"10x644 a\0aaaaaaaaaaaaaaaaaaaa";
2421 assert!(matches!(
2422 find_tree_entry_by_name_in_data(bad_mode, HashAlgo::Sha1.len(), b"a", false),
2423 Err(Error::CorruptObject(_))
2424 ));
2425 let truncated = b"100644 a\0short";
2426 assert!(matches!(
2427 find_tree_entry_by_name_in_data(truncated, HashAlgo::Sha1.len(), b"a", false),
2428 Err(Error::CorruptObject(_))
2429 ));
2430 }
2431
2432 #[test]
2433 fn find_tree_entry_by_name_in_data_reads_sha256_width() -> Result<()> {
2434 let oid = ObjectId::from_bytes(&[0x5au8; 32])?;
2435 let mut raw = b"100644 file\0".to_vec();
2436 raw.extend_from_slice(oid.as_bytes());
2437 let found = find_tree_entry_by_name_in_data(&raw, HashAlgo::Sha256.len(), b"file", false)?
2438 .expect("file");
2439 assert_eq!(found.oid, oid);
2440 assert_eq!(found.mode, 0o100644);
2441 Ok(())
2442 }
2443}
2444
2445#[cfg(test)]
2446mod tests {
2447 use super::*;
2448
2449 const HEAD: &str = "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\nauthor A <a@example.com> 1 +0000\ncommitter C <c@example.com> 2 +0000\n";
2450
2451 fn len_of(message: &[u8]) -> Option<usize> {
2452 let mut data = HEAD.as_bytes().to_vec();
2453 data.extend_from_slice(message);
2454 commit_through_subject_len(&data).map(|len| len - HEAD.len())
2455 }
2456
2457 #[test]
2458 fn commit_through_subject_len_ends_after_the_first_non_blank_line() {
2459 assert_eq!(len_of(b"\nsubject\n\nbody\n"), Some(9));
2460 assert_eq!(len_of(b"\n\n \t\r\n subject\r\nmore\n"), Some(17));
2461 assert_eq!(len_of(b"\nx\n"), Some(3));
2462 let signed = b"gpgsig -----BEGIN-----\n \n abc\n -----END-----\n\nsigned\nbody\n";
2463 assert_eq!(len_of(signed), Some(signed.len() - 5));
2464 }
2465
2466 #[test]
2467 fn commit_through_subject_len_skips_lines_that_may_decode_blank() {
2468 let message = "\n\u{a0}\u{3000}\n\u{65e5}\u{672c}\nascii\nbody\n";
2469 assert_eq!(
2470 len_of(message.as_bytes()),
2471 Some(message.len() - "body\n".len())
2472 );
2473 }
2474
2475 #[test]
2476 fn commit_through_subject_len_needs_a_complete_prefix() {
2477 assert_eq!(len_of(b""), None);
2478 assert_eq!(len_of(b"\n"), None);
2479 assert_eq!(len_of(b"\n\n \n"), None);
2480 assert_eq!(len_of(b"\nsubject without newline"), None);
2481 assert_eq!(commit_through_subject_len(b"tree 4b82"), None);
2482 assert_eq!(commit_through_subject_len(HEAD.as_bytes()), None);
2483 }
2484
2485 #[test]
2486 fn commit_through_subject_len_gives_up_on_other_encodings() {
2487 assert_eq!(len_of(b"encoding ISO-8859-1\n\nS\xe9ance\n\nbody\n"), None);
2488 assert_eq!(len_of(b"encoding ISO-2022-JP\n\nsubject\n"), None);
2489 assert_eq!(len_of(b"encoding UTF-8\n\nsubject\nbody\n"), Some(24));
2490 assert_eq!(len_of(b"encoding utf8 \n\nsubject\nbody\n"), Some(24));
2491 }
2492
2493 #[test]
2494 fn commit_through_subject_prefix_parses_like_the_whole_commit() {
2495 let whole = format!("{HEAD}change-id zzzz\n\n\n Subject here \n\nlong body\n");
2496 let len = commit_through_subject_len(whole.as_bytes()).unwrap();
2497 let prefix = &whole.as_bytes()[..len];
2498 assert_eq!(
2499 read_commit_subject_line(prefix).unwrap(),
2500 read_commit_subject_line(whole.as_bytes()).unwrap()
2501 );
2502 let (short, full) = (
2503 parse_commit(prefix).unwrap(),
2504 parse_commit(whole.as_bytes()).unwrap(),
2505 );
2506 assert_eq!(short.extra_headers, full.extra_headers);
2507 assert_eq!(short.author, full.author);
2508 assert_eq!(short.message, "\n Subject here \n");
2509 }
2510}