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
129#[cold]
130fn invalid_oid_bytes(bytes: &[u8]) -> Error {
131 Error::InvalidObjectId(hex::encode(bytes))
132}
133
134pub const SHORT_HEX_LEN: usize = 7;
136
137#[must_use]
152pub fn short_hex(hex: &str) -> &str {
153 hex.get(..SHORT_HEX_LEN).unwrap_or(hex)
154}
155
156const WELL_KNOWN_EMPTY_TREE_CANON: [u8; 20] = [
158 0x4b, 0x82, 0x5d, 0xc6, 0x42, 0xcb, 0x6e, 0xb9, 0xa0, 0x60, 0xe5, 0x4b, 0xf8, 0xd6, 0x92, 0x88,
159 0xfb, 0xee, 0x49, 0x04,
160];
161const WELL_KNOWN_EMPTY_TREE_LEGACY: [u8; 20] = [
163 0x4b, 0x82, 0x5d, 0xc6, 0x42, 0xcb, 0x6e, 0xb9, 0xa0, 0x60, 0xe5, 0x4b, 0xf8, 0x99, 0xd6, 0x9f,
164 0x7c, 0x69, 0x48, 0xd4,
165];
166const EMPTY_TREE_SHA256: [u8; 32] = [
168 0x6e, 0xf1, 0x9b, 0x41, 0x22, 0x5c, 0x53, 0x69, 0xf1, 0xc1, 0x04, 0xd4, 0x5d, 0x8d, 0x85, 0xef,
169 0xa9, 0xb0, 0x57, 0xb5, 0x3b, 0x14, 0xb4, 0xb9, 0xb9, 0x39, 0xdd, 0x74, 0xde, 0xcc, 0x53, 0x21,
170];
171const EMPTY_BLOB_SHA1: [u8; 20] = [
173 0xe6, 0x9d, 0xe2, 0x9b, 0xb2, 0xd1, 0xd6, 0x43, 0x4b, 0x8b, 0x29, 0xae, 0x77, 0x5a, 0xd8, 0xc2,
174 0xe4, 0x8c, 0x53, 0x91,
175];
176const EMPTY_BLOB_SHA256: [u8; 32] = [
178 0x47, 0x3a, 0x0f, 0x4c, 0x3b, 0xe8, 0xa9, 0x36, 0x81, 0xa2, 0x67, 0xe3, 0xb1, 0xe9, 0xa7, 0xdc,
179 0xda, 0x11, 0x85, 0x43, 0x6f, 0xe1, 0x41, 0xf7, 0x74, 0x91, 0x20, 0xa3, 0x03, 0x72, 0x18, 0x13,
180];
181
182#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
200pub struct ObjectId {
201 bytes: [u8; MAX_OID_LEN],
202 len: u8,
203}
204
205impl ObjectId {
206 #[must_use]
212 pub const fn zero() -> Self {
213 Self {
214 bytes: [0u8; MAX_OID_LEN],
215 len: 20,
216 }
217 }
218
219 #[must_use]
221 pub const fn null(algo: HashAlgo) -> Self {
222 Self {
223 bytes: [0u8; MAX_OID_LEN],
224 len: algo.len() as u8,
225 }
226 }
227
228 #[inline]
235 pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
236 let mut buf = [0u8; MAX_OID_LEN];
237 match HashAlgo::from_len(bytes.len()) {
239 Some(HashAlgo::Sha1) => buf[..20].copy_from_slice(bytes),
240 Some(HashAlgo::Sha256) => buf.copy_from_slice(bytes),
241 None => return Err(invalid_oid_bytes(bytes)),
242 }
243 Ok(Self {
244 bytes: buf,
245 len: bytes.len() as u8,
246 })
247 }
248
249 #[must_use]
251 pub fn as_bytes(&self) -> &[u8] {
252 &self.bytes[..self.len as usize]
253 }
254
255 #[must_use]
257 pub fn algo(&self) -> HashAlgo {
258 HashAlgo::from_len(self.len as usize).unwrap_or(HashAlgo::Sha1)
259 }
260
261 #[must_use]
263 pub fn is_zero(&self) -> bool {
264 self.as_bytes().iter().all(|&b| b == 0)
265 }
266
267 #[must_use]
269 pub(crate) fn common_prefix_nibbles(&self, other: &Self) -> usize {
270 let (a, b) = (self.as_bytes(), other.as_bytes());
271 match a.iter().zip(b).position(|(x, y)| x != y) {
272 Some(i) => i * 2 + usize::from((a[i] ^ b[i]) & 0xf0 == 0),
273 None => a.len().min(b.len()) * 2,
274 }
275 }
276
277 #[must_use]
279 pub(crate) fn is_empty_blob(&self) -> bool {
280 let bytes = self.as_bytes();
281 bytes == EMPTY_BLOB_SHA1 || bytes == EMPTY_BLOB_SHA256
282 }
283
284 #[must_use]
286 pub(crate) fn is_well_known_empty_tree(&self) -> bool {
287 self.is_canonical_empty_tree()
288 || (self.as_bytes().len() == HashAlgo::Sha1.len()
289 && self.as_bytes() == WELL_KNOWN_EMPTY_TREE_LEGACY)
290 }
291
292 #[must_use]
295 pub(crate) fn is_empty_tree_for(&self, algo: HashAlgo) -> bool {
296 match algo {
297 HashAlgo::Sha1 => self.is_well_known_empty_tree(),
298 HashAlgo::Sha256 => self.as_bytes() == EMPTY_TREE_SHA256,
299 }
300 }
301
302 #[must_use]
304 pub(crate) fn is_canonical_empty_tree(&self) -> bool {
305 self.as_bytes().len() == HashAlgo::Sha1.len()
306 && self.as_bytes() == WELL_KNOWN_EMPTY_TREE_CANON
307 }
308
309 #[must_use]
315 pub const fn empty_tree() -> Self {
316 let mut bytes = [0u8; MAX_OID_LEN];
317 let mut i = 0;
318 while i < WELL_KNOWN_EMPTY_TREE_CANON.len() {
319 bytes[i] = WELL_KNOWN_EMPTY_TREE_CANON[i];
320 i += 1;
321 }
322 Self { bytes, len: 20 }
323 }
324
325 pub(crate) fn push_loose_relative(&self, path: &mut std::path::PathBuf) {
327 const HEX: &[u8; 16] = b"0123456789abcdef";
328 let bytes = self.as_bytes();
329 let prefix = [
330 HEX[(bytes[0] >> 4) as usize],
331 HEX[(bytes[0] & 0xf) as usize],
332 ];
333 path.push(unsafe { std::str::from_utf8_unchecked(&prefix) });
335 let suffix_len = (bytes.len() - 1) * 2;
336 let mut suffix = [0u8; 62];
337 for (i, &b) in bytes[1..].iter().enumerate() {
338 suffix[i * 2] = HEX[(b >> 4) as usize];
339 suffix[i * 2 + 1] = HEX[(b & 0xf) as usize];
340 }
341 path.push(unsafe { std::str::from_utf8_unchecked(&suffix[..suffix_len]) });
342 }
343
344 #[must_use]
346 pub(crate) fn loose_path_in(&self, objects_dir: &std::path::Path) -> std::path::PathBuf {
347 let mut path = objects_dir.to_path_buf();
348 self.push_loose_relative(&mut path);
349 path
350 }
351
352 #[must_use]
354 pub fn to_hex(&self) -> String {
355 self.hex_digits().as_str().to_owned()
356 }
357
358 pub(crate) fn push_hex_to(&self, out: &mut String) {
360 out.push_str(self.hex_digits().as_str());
361 }
362
363 #[must_use]
369 pub fn to_short_hex(&self) -> String {
370 short_hex(self.hex_digits().as_str()).to_owned()
371 }
372
373 fn hex_digits(&self) -> HexDigits {
375 const DIGITS: &[u8; 16] = b"0123456789abcdef";
376 let mut buf = [0u8; MAX_OID_LEN * 2];
377 for (pair, &byte) in buf.as_chunks_mut::<2>().0.iter_mut().zip(self.as_bytes()) {
378 *pair = [
379 DIGITS[usize::from(byte >> 4)],
380 DIGITS[usize::from(byte & 0x0f)],
381 ];
382 }
383 HexDigits {
384 buf,
385 len: usize::from(self.len) * 2,
386 }
387 }
388
389 #[must_use]
393 pub fn loose_prefix(&self) -> String {
394 hex::encode(&self.bytes[..1])
395 }
396
397 pub fn from_hex(s: &str) -> Result<Self> {
404 Self::decode_hex(s.as_bytes()).ok_or_else(|| Error::InvalidObjectId(s.to_owned()))
405 }
406
407 pub fn from_hex_ascii(bytes: &[u8]) -> Result<Self> {
413 if bytes.len() != HashAlgo::Sha1.hex_len() {
414 return Err(Error::InvalidObjectId(
415 String::from_utf8_lossy(bytes).into_owned(),
416 ));
417 }
418 Self::decode_hex(bytes)
419 .ok_or_else(|| Error::InvalidObjectId(String::from_utf8_lossy(bytes).into_owned()))
420 }
421
422 fn decode_hex(hex: &[u8]) -> Option<Self> {
427 let len = hex.len() / 2;
428 if !Self::is_hex_len(hex.len()) {
429 return None;
430 }
431 let mut bytes = [0u8; MAX_OID_LEN];
432 let mut invalid = 0u8;
433 for (out, pair) in bytes.iter_mut().zip(hex.as_chunks::<2>().0) {
434 let hi = HEX_DECODE[usize::from(pair[0])];
435 let lo = HEX_DECODE[usize::from(pair[1])];
436 invalid |= hi | lo;
437 *out = (hi << 4) | lo;
438 }
439 (invalid & HEX_INVALID == 0).then_some(Self {
440 bytes,
441 len: len as u8,
442 })
443 }
444
445 #[must_use]
448 pub fn loose_suffix(&self) -> String {
449 hex::encode(&self.bytes[1..self.len as usize])
450 }
451
452 #[must_use]
455 pub fn is_full_hex(s: &str) -> bool {
456 (s.len() == HashAlgo::Sha1.hex_len() || s.len() == HashAlgo::Sha256.hex_len())
457 && s.bytes().all(|b| b.is_ascii_hexdigit())
458 }
459
460 #[must_use]
462 pub const fn is_hex_len(len: usize) -> bool {
463 len == HashAlgo::Sha1.hex_len() || len == HashAlgo::Sha256.hex_len()
464 }
465
466 #[must_use]
469 pub const fn is_loose_suffix_len(len: usize) -> bool {
470 len == HashAlgo::Sha1.hex_len() - 2 || len == HashAlgo::Sha256.hex_len() - 2
471 }
472}
473
474impl std::hash::Hash for ObjectId {
475 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
476 state.write(self.as_bytes());
477 }
478}
479
480impl fmt::Display for ObjectId {
481 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
482 f.write_str(self.hex_digits().as_str())
483 }
484}
485
486struct HexDigits {
488 buf: [u8; MAX_OID_LEN * 2],
489 len: usize,
490}
491
492impl HexDigits {
493 fn as_str(&self) -> &str {
494 std::str::from_utf8(&self.buf[..self.len]).unwrap_or_default()
495 }
496}
497
498impl fmt::Debug for ObjectId {
499 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
500 write!(f, "ObjectId({})", self.to_hex())
501 }
502}
503
504#[cfg(feature = "serde-models")]
505impl serde::Serialize for ObjectId {
506 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
507 where
508 S: serde::Serializer,
509 {
510 serializer.serialize_str(&self.to_hex())
511 }
512}
513
514#[cfg(feature = "serde-models")]
515impl<'de> serde::Deserialize<'de> for ObjectId {
516 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
517 where
518 D: serde::Deserializer<'de>,
519 {
520 let hex = String::deserialize(deserializer)?;
521 Self::from_hex(&hex).map_err(serde::de::Error::custom)
522 }
523}
524
525const HEX_INVALID: u8 = 0x10;
527
528const HEX_DECODE: [u8; 256] = {
530 let mut table = [HEX_INVALID; 256];
531 let mut i = 0u8;
532 while i < 10 {
533 table[(b'0' + i) as usize] = i;
534 i += 1;
535 }
536 let mut i = 0u8;
537 while i < 6 {
538 table[(b'a' + i) as usize] = 10 + i;
539 table[(b'A' + i) as usize] = 10 + i;
540 i += 1;
541 }
542 table
543};
544
545impl FromStr for ObjectId {
546 type Err = Error;
547
548 fn from_str(s: &str) -> Result<Self> {
549 Self::from_hex(s)
550 }
551}
552
553#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
570pub struct FileMode(u32);
571
572impl FileMode {
573 pub const ABSENT: Self = Self(0);
575 pub const REGULAR: Self = Self(0o100644);
577 pub const EXECUTABLE: Self = Self(0o100755);
579 pub const SYMLINK: Self = Self(0o120000);
581 pub const GITLINK: Self = Self(0o160000);
583 pub const TREE: Self = Self(0o040000);
585
586 #[must_use]
588 pub const fn new(bits: u32) -> Self {
589 Self(bits)
590 }
591
592 #[must_use]
594 pub const fn bits(self) -> u32 {
595 self.0
596 }
597
598 #[must_use]
600 pub const fn is_absent(self) -> bool {
601 self.0 == 0
602 }
603
604 #[must_use]
606 pub fn octal_if_present(self) -> Option<String> {
607 (!self.is_absent()).then(|| self.to_string())
608 }
609
610 #[must_use]
612 pub const fn is_symlink(self) -> bool {
613 self.0 & 0o170000 == 0o120000
614 }
615
616 #[must_use]
618 pub const fn is_gitlink(self) -> bool {
619 self.0 & 0o170000 == 0o160000
620 }
621}
622
623impl From<u32> for FileMode {
624 fn from(bits: u32) -> Self {
625 Self(bits)
626 }
627}
628
629impl From<FileMode> for u32 {
630 fn from(mode: FileMode) -> Self {
631 mode.0
632 }
633}
634
635impl fmt::Display for FileMode {
636 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
637 write!(f, "{:06o}", self.0)
638 }
639}
640
641impl fmt::Debug for FileMode {
642 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
643 write!(f, "FileMode({:06o})", self.0)
644 }
645}
646
647#[cfg(feature = "serde-models")]
648impl serde::Serialize for FileMode {
649 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
650 where
651 S: serde::Serializer,
652 {
653 serializer.collect_str(self)
654 }
655}
656
657#[derive(Clone, Copy, PartialEq, Eq)]
681pub struct ObjectIdPrefix {
682 bytes: [u8; MAX_OID_LEN],
685 nibbles: u8,
686 algo: HashAlgo,
687}
688
689impl ObjectIdPrefix {
690 pub fn from_hex(hex: &str, algo: HashAlgo) -> Result<Self> {
699 let digits = hex.as_bytes();
700 if digits.len() > algo.hex_len() {
701 return Err(Error::InvalidObjectId(hex.to_owned()));
702 }
703 let mut bytes = [0u8; MAX_OID_LEN];
704 let mut invalid = 0u8;
705 for (i, &digit) in digits.iter().enumerate() {
706 let nibble = HEX_DECODE[usize::from(digit)];
707 invalid |= nibble;
708 let shift = if i.is_multiple_of(2) { 4 } else { 0 };
709 bytes[i / 2] |= (nibble & 0x0f) << shift;
710 }
711 if invalid & HEX_INVALID != 0 {
712 return Err(Error::InvalidObjectId(hex.to_owned()));
713 }
714 Ok(Self {
715 bytes,
716 nibbles: digits.len() as u8,
717 algo,
718 })
719 }
720
721 #[must_use]
723 pub fn nibbles(&self) -> usize {
724 usize::from(self.nibbles)
725 }
726
727 #[must_use]
729 pub fn is_empty(&self) -> bool {
730 self.nibbles == 0
731 }
732
733 #[must_use]
735 pub fn algo(&self) -> HashAlgo {
736 self.algo
737 }
738
739 #[must_use]
744 pub fn matches(&self, oid: &ObjectId) -> bool {
745 self.matches_digest(oid.as_bytes())
746 }
747
748 #[must_use]
750 pub(crate) fn matches_digest(&self, digest: &[u8]) -> bool {
751 let whole = self.nibbles() / 2;
752 let Some(head) = digest.get(..whole) else {
753 return false;
754 };
755 if head != &self.bytes[..whole] {
756 return false;
757 }
758 if self.nibbles.is_multiple_of(2) {
759 return true;
760 }
761 digest
762 .get(whole)
763 .is_some_and(|&b| b & 0xf0 == self.bytes[whole])
764 }
765
766 #[must_use]
768 pub fn lowest(&self) -> ObjectId {
769 ObjectId {
770 bytes: self.bytes,
771 len: self.algo.len() as u8,
772 }
773 }
774
775 #[must_use]
777 pub fn highest(&self) -> ObjectId {
778 let mut bytes = self.bytes;
779 let whole = self.nibbles() / 2;
780 if !self.nibbles.is_multiple_of(2) {
781 bytes[whole] |= 0x0f;
782 }
783 let first_free = whole + usize::from(self.nibbles % 2);
784 bytes[first_free..self.algo.len()].fill(0xff);
785 ObjectId {
786 bytes,
787 len: self.algo.len() as u8,
788 }
789 }
790}
791
792impl fmt::Display for ObjectIdPrefix {
793 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
794 let hex = hex::encode(&self.bytes[..self.nibbles().div_ceil(2)]);
795 f.write_str(&hex[..self.nibbles()])
796 }
797}
798
799impl fmt::Debug for ObjectIdPrefix {
800 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
801 write!(f, "ObjectIdPrefix({self})")
802 }
803}
804
805#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
807#[cfg_attr(feature = "serde-models", derive(serde::Serialize))]
808#[cfg_attr(feature = "serde-models", serde(rename_all = "snake_case"))]
809pub enum ObjectKind {
810 Blob,
812 Tree,
814 Commit,
816 Tag,
818}
819
820impl ObjectKind {
821 pub fn from_bytes(b: &[u8]) -> Result<Self> {
827 match b {
828 b"blob" => Ok(Self::Blob),
829 b"tree" => Ok(Self::Tree),
830 b"commit" => Ok(Self::Commit),
831 b"tag" => Ok(Self::Tag),
832 other => Err(Error::UnknownObjectType(
833 String::from_utf8_lossy(other).into_owned(),
834 )),
835 }
836 }
837
838 #[must_use]
843 pub fn from_tag_type_field(line: &[u8]) -> Option<Self> {
844 fn keyword_matches(canonical: &[u8], field: &[u8]) -> bool {
845 if field.is_empty() {
846 return false;
847 }
848 for (i, &bc) in field.iter().enumerate() {
849 let sc = canonical.get(i).copied().unwrap_or(0);
850 if sc != bc {
851 return false;
852 }
853 }
854 canonical.get(field.len()).copied().unwrap_or(0) == 0
855 }
856
857 const NAMES: &[(ObjectKind, &[u8])] = &[
858 (ObjectKind::Blob, b"blob"),
859 (ObjectKind::Tree, b"tree"),
860 (ObjectKind::Commit, b"commit"),
861 (ObjectKind::Tag, b"tag"),
862 ];
863 for &(kind, name) in NAMES {
864 if keyword_matches(name, line) {
865 return Some(kind);
866 }
867 }
868 None
869 }
870
871 #[must_use]
873 pub fn as_str(&self) -> &'static str {
874 match self {
875 Self::Blob => "blob",
876 Self::Tree => "tree",
877 Self::Commit => "commit",
878 Self::Tag => "tag",
879 }
880 }
881}
882
883impl fmt::Display for ObjectKind {
884 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
885 f.write_str(self.as_str())
886 }
887}
888
889impl FromStr for ObjectKind {
890 type Err = Error;
891
892 fn from_str(s: &str) -> Result<Self> {
893 Self::from_bytes(s.as_bytes())
894 }
895}
896
897#[derive(Debug, Clone, Copy, PartialEq, Eq)]
899pub struct ObjectInfo {
900 pub kind: ObjectKind,
902 pub size: u64,
904}
905
906#[derive(Debug, Clone)]
908pub struct Object {
909 pub kind: ObjectKind,
911 pub data: Vec<u8>,
913}
914
915#[derive(Debug, Clone, Copy, PartialEq, Eq)]
921pub struct ObjectRef<'a> {
922 pub kind: ObjectKind,
924 pub data: &'a [u8],
926}
927
928impl<'a> ObjectRef<'a> {
929 #[must_use]
931 pub fn from_buf(kind: ObjectKind, buf: &'a [u8]) -> Self {
932 Self { kind, data: buf }
933 }
934}
935
936impl Object {
937 #[must_use]
939 pub fn new(kind: ObjectKind, data: Vec<u8>) -> Self {
940 Self { kind, data }
941 }
942
943 #[must_use]
945 pub fn as_ref(&self) -> ObjectRef<'_> {
946 ObjectRef {
947 kind: self.kind,
948 data: self.data.as_slice(),
949 }
950 }
951}
952
953#[derive(Debug, Clone, PartialEq, Eq)]
955pub struct TreeEntry {
956 pub mode: u32,
958 pub name: Vec<u8>,
960 pub oid: ObjectId,
962}
963
964impl TreeEntry {
965 #[must_use]
969 pub fn mode_str(&self) -> String {
970 if self.mode == 0o040000 {
972 "40000".to_owned()
973 } else {
974 format!("{:o}", self.mode)
975 }
976 }
977}
978
979#[derive(Debug, Clone, Copy, PartialEq, Eq)]
981pub struct TreeEntryRef<'a> {
982 pub mode: u32,
984 pub name: &'a [u8],
986 pub oid: ObjectId,
988}
989
990impl TreeEntryRef<'_> {
991 #[must_use]
993 pub fn to_owned(&self) -> TreeEntry {
994 TreeEntry {
995 mode: self.mode,
996 name: self.name.to_vec(),
997 oid: self.oid,
998 }
999 }
1000}
1001
1002#[derive(Debug, Clone)]
1007pub struct TreeRefIter<'a> {
1008 data: &'a [u8],
1009 pos: usize,
1010 oid_len: usize,
1011 failed: bool,
1013}
1014
1015impl<'a> TreeRefIter<'a> {
1016 #[must_use]
1018 pub fn with_oid_len(data: &'a [u8], oid_len: usize) -> Self {
1019 Self {
1020 data,
1021 pos: 0,
1022 oid_len,
1023 failed: false,
1024 }
1025 }
1026
1027 pub fn infer_oid_len(data: &'a [u8]) -> Result<Self> {
1033 match try_parse_tree_oid_len(data, HashAlgo::Sha1.len()) {
1034 Ok(()) => Ok(Self::with_oid_len(data, HashAlgo::Sha1.len())),
1035 Err(sha1_err) => match try_parse_tree_oid_len(data, HashAlgo::Sha256.len()) {
1036 Ok(()) => Ok(Self::with_oid_len(data, HashAlgo::Sha256.len())),
1037 Err(_) => Err(sha1_err),
1038 },
1039 }
1040 }
1041
1042 pub(crate) fn with_oid_len_at(data: &'a [u8], oid_len: usize, pos: usize) -> Self {
1044 Self {
1045 data,
1046 pos,
1047 oid_len,
1048 failed: false,
1049 }
1050 }
1051
1052 pub(crate) fn position(&self) -> usize {
1054 self.pos
1055 }
1056
1057 fn next_entry(&mut self) -> Result<Option<TreeEntryRef<'a>>> {
1058 parse_tree_entry_at(self.data, &mut self.pos, self.oid_len)
1059 }
1060}
1061
1062pub(crate) struct TreeEntrySpan {
1064 pub(crate) mode: u32,
1066 pub(crate) name: std::ops::Range<usize>,
1068 pub(crate) end: usize,
1070}
1071
1072#[inline]
1079pub(crate) fn tree_entry_span(data: &[u8], pos: usize, oid_len: usize) -> Option<TreeEntrySpan> {
1080 HashAlgo::from_len(oid_len)?;
1081 let rest = data.get(pos..)?;
1082 let space = memchr::memchr(b' ', rest)?;
1083 let mode = parse_tree_mode(&rest[..space])?;
1084 let name_start = pos + space + 1;
1085 let name_end = name_start + memchr::memchr(0, &data[name_start..])?;
1086 let end = name_end + 1 + oid_len;
1087 (end <= data.len()).then_some(TreeEntrySpan {
1088 mode,
1089 name: name_start..name_end,
1090 end,
1091 })
1092}
1093
1094#[inline]
1096fn parse_tree_mode(bytes: &[u8]) -> Option<u32> {
1097 if (1..=10).contains(&bytes.len()) {
1099 let mut mode = 0u32;
1100 let mut all_octal = true;
1101 for &b in bytes {
1102 let digit = b.wrapping_sub(b'0');
1103 all_octal &= digit < 8;
1104 mode = (mode << 3) | u32::from(digit & 7);
1105 }
1106 if all_octal {
1107 return Some(mode);
1108 }
1109 }
1110 std::str::from_utf8(bytes)
1111 .ok()
1112 .and_then(|s| u32::from_str_radix(s, 8).ok())
1113}
1114
1115fn parse_tree_entry_at<'a>(
1117 data: &'a [u8],
1118 pos: &mut usize,
1119 oid_len: usize,
1120) -> Result<Option<TreeEntryRef<'a>>> {
1121 if *pos >= data.len() {
1122 return Ok(None);
1123 }
1124 let sp = memchr::memchr(b' ', &data[*pos..])
1125 .ok_or_else(|| Error::CorruptObject("tree entry missing space".to_owned()))?;
1126 let mode_bytes = &data[*pos..*pos + sp];
1127 let mode = parse_tree_mode(mode_bytes).ok_or_else(|| {
1128 Error::CorruptObject(format!(
1129 "invalid tree mode: {}",
1130 String::from_utf8_lossy(mode_bytes)
1131 ))
1132 })?;
1133 *pos += sp + 1;
1134
1135 let nul = memchr::memchr(0, &data[*pos..])
1136 .ok_or_else(|| Error::CorruptObject("tree entry missing NUL".to_owned()))?;
1137 let name = &data[*pos..*pos + nul];
1138 *pos += nul + 1;
1139
1140 if *pos + oid_len > data.len() {
1141 return Err(Error::CorruptObject("tree entry truncated SHA".to_owned()));
1142 }
1143 let oid = ObjectId::from_bytes(&data[*pos..*pos + oid_len])?;
1144 *pos += oid_len;
1145
1146 Ok(Some(TreeEntryRef { mode, name, oid }))
1147}
1148
1149impl<'a> Iterator for TreeRefIter<'a> {
1150 type Item = Result<TreeEntryRef<'a>>;
1151
1152 fn next(&mut self) -> Option<Self::Item> {
1153 if self.failed || self.pos >= self.data.len() {
1154 return None;
1155 }
1156 match self.next_entry() {
1157 Ok(Some(entry)) => Some(Ok(entry)),
1158 Ok(None) => None,
1159 Err(err) => {
1160 self.failed = true;
1161 self.pos = self.data.len();
1162 Some(Err(err))
1163 }
1164 }
1165 }
1166}
1167
1168fn try_parse_tree_oid_len(data: &[u8], oid_len: usize) -> Result<()> {
1170 let mut pos = 0usize;
1171 while parse_tree_entry_at(data, &mut pos, oid_len)?.is_some() {}
1172 Ok(())
1173}
1174
1175fn collect_tree_with_oid_len(data: &[u8], oid_len: usize) -> Result<Vec<TreeEntry>> {
1176 let mut pos = 0usize;
1177 let mut entries = Vec::with_capacity(data.len() / 32);
1178 while let Some(entry) = parse_tree_entry_at(data, &mut pos, oid_len)? {
1179 entries.push(TreeEntry {
1180 mode: entry.mode,
1181 name: entry.name.to_vec(),
1182 oid: entry.oid,
1183 });
1184 }
1185 Ok(entries)
1186}
1187
1188pub fn parse_tree(data: &[u8]) -> Result<Vec<TreeEntry>> {
1199 match collect_tree_with_oid_len(data, HashAlgo::Sha1.len()) {
1200 Ok(entries) => Ok(entries),
1201 Err(sha1_err) => {
1202 collect_tree_with_oid_len(data, HashAlgo::Sha256.len()).map_err(|_| sha1_err)
1203 }
1204 }
1205}
1206
1207pub fn parse_tree_with_oid_len(data: &[u8], oid_len: usize) -> Result<Vec<TreeEntry>> {
1212 collect_tree_with_oid_len(data, oid_len)
1213}
1214
1215#[must_use]
1222pub fn serialize_tree(entries: &[TreeEntry]) -> Vec<u8> {
1223 let len = entries
1224 .iter()
1225 .map(|e| 8 + e.name.len() + e.oid.as_bytes().len())
1226 .sum();
1227 let mut out = Vec::with_capacity(len);
1228 for e in entries {
1229 push_tree_mode(&mut out, e.mode);
1230 out.push(b' ');
1231 out.extend_from_slice(&e.name);
1232 out.push(0);
1233 out.extend_from_slice(e.oid.as_bytes());
1234 }
1235 out
1236}
1237
1238fn push_tree_mode(out: &mut Vec<u8>, mode: u32) {
1240 let mut digits = [0u8; 11];
1241 let mut start = digits.len();
1242 let mut rest = mode;
1243 loop {
1244 start -= 1;
1245 digits[start] = b'0' + (rest & 7) as u8;
1246 rest >>= 3;
1247 if rest == 0 {
1248 break;
1249 }
1250 }
1251 out.extend_from_slice(&digits[start..]);
1252}
1253
1254#[must_use]
1267pub fn tree_entry_cmp(
1268 a_name: &[u8],
1269 a_is_tree: bool,
1270 b_name: &[u8],
1271 b_is_tree: bool,
1272) -> std::cmp::Ordering {
1273 let a_trailer = if a_is_tree { b'/' } else { 0u8 };
1274 let b_trailer = if b_is_tree { b'/' } else { 0u8 };
1275
1276 let min_len = a_name.len().min(b_name.len());
1277 let cmp = a_name[..min_len].cmp(&b_name[..min_len]);
1278 if cmp != std::cmp::Ordering::Equal {
1279 return cmp;
1280 }
1281 let ac = a_name.get(min_len).copied().unwrap_or(a_trailer);
1283 let bc = b_name.get(min_len).copied().unwrap_or(b_trailer);
1284 ac.cmp(&bc)
1285}
1286
1287fn parse_tree_mode_octal(bytes: &[u8]) -> Option<u32> {
1289 if bytes.is_empty() || bytes.len() > 7 {
1290 return None;
1291 }
1292 bytes.iter().try_fold(0u32, |acc, &b| match b {
1293 b'0'..=b'7' => Some(acc * 8 + u32::from(b - b'0')),
1294 _ => None,
1295 })
1296}
1297
1298pub(crate) fn find_tree_entry_by_name_in_data(
1310 data: &[u8],
1311 oid_len: usize,
1312 name: &[u8],
1313 name_is_tree: bool,
1314) -> Result<Option<TreeEntry>> {
1315 let mut pos = 0usize;
1316 while pos < data.len() {
1317 let rest = &data[pos..];
1318 let sp = memchr::memchr(b' ', rest)
1319 .ok_or_else(|| Error::CorruptObject("tree entry missing space".to_owned()))?;
1320 let mode = parse_tree_mode_octal(&rest[..sp])
1321 .ok_or_else(|| Error::CorruptObject("invalid tree mode".to_owned()))?;
1322 let name_start = sp + 1;
1323 let nul = memchr::memchr(0, &rest[name_start..])
1324 .ok_or_else(|| Error::CorruptObject("tree entry missing NUL".to_owned()))?;
1325 let entry_name = &rest[name_start..name_start + nul];
1326 let oid_start = name_start + nul + 1;
1327 let oid_end = oid_start + oid_len;
1328 if oid_end > rest.len() {
1329 return Err(Error::CorruptObject("tree entry truncated SHA".to_owned()));
1330 }
1331 if entry_name == name {
1332 return Ok(Some(TreeEntry {
1333 mode,
1334 name: entry_name.to_vec(),
1335 oid: ObjectId::from_bytes(&rest[oid_start..oid_end])?,
1336 }));
1337 }
1338 if tree_entry_cmp(entry_name, mode == 0o040000, name, name_is_tree)
1339 == std::cmp::Ordering::Greater
1340 {
1341 return Ok(None);
1342 }
1343 pos += oid_end;
1344 }
1345 Ok(None)
1346}
1347
1348pub(crate) fn find_tree_entry_by_name_dir_or_file_in_data(
1350 data: &[u8],
1351 oid_len: usize,
1352 name: &[u8],
1353) -> Result<Option<TreeEntry>> {
1354 if let Some(entry) = find_tree_entry_by_name_in_data(data, oid_len, name, true)? {
1355 if entry.mode == 0o040000 {
1356 return Ok(Some(entry));
1357 }
1358 }
1359 find_tree_entry_by_name_in_data(data, oid_len, name, false)
1360}
1361
1362#[derive(Debug, Clone, PartialEq, Eq)]
1368pub struct CommitData {
1369 pub tree: ObjectId,
1371 pub parents: Vec<ObjectId>,
1373 pub author: String,
1375 pub committer: String,
1377 pub author_raw: Vec<u8>,
1381 pub committer_raw: Vec<u8>,
1383 pub encoding: Option<String>,
1385 pub message: String,
1387 #[doc = "Optional raw message bytes for non-UTF-8 messages."]
1390 pub raw_message: Option<Vec<u8>>,
1391 pub extra_headers: Vec<u8>,
1397}
1398
1399#[must_use]
1406pub fn commit_subject_pretty(message: &str) -> String {
1407 let mut parts = Vec::new();
1408 for line in message.lines() {
1409 if line.trim().is_empty() {
1410 break;
1411 }
1412 parts.push(line.trim_end());
1413 }
1414 parts.join(" ")
1415}
1416
1417pub fn read_commit_subject_pretty(data: &[u8]) -> Result<String> {
1423 let message_start = data.windows(2).position(|w| w == b"\n\n").map(|p| p + 2);
1424 let Some(message_start) = message_start else {
1425 return Ok(String::new());
1426 };
1427 let body = std::str::from_utf8(data.get(message_start..).unwrap_or_default()).unwrap_or("");
1428 Ok(commit_subject_pretty(body))
1429}
1430
1431pub fn read_commit_subject_line(data: &[u8]) -> Result<String> {
1439 let mut pos = 0usize;
1440 let mut encoding: Option<String> = None;
1441 while pos < data.len() {
1442 let line_start = pos;
1443 let mut line_end = pos;
1444 while line_end < data.len() && data[line_end] != b'\n' {
1445 line_end += 1;
1446 }
1447 let line = &data[line_start..line_end];
1448 let after_nl = line_end.saturating_add(1);
1449 if line.is_empty() {
1450 let body = data.get(after_nl..).unwrap_or_default();
1451 let message = commit_encoding::decode_bytes(encoding.as_deref(), body);
1452 return Ok(message
1453 .lines()
1454 .find(|l| !l.trim().is_empty())
1455 .map(str::trim)
1456 .unwrap_or("(no subject)")
1457 .to_owned());
1458 }
1459 if line.first() == Some(&b' ') {
1460 pos = after_nl;
1461 continue;
1462 }
1463 if let Some(rest) = line.strip_prefix(b"encoding ") {
1464 encoding = Some(
1465 std::str::from_utf8(rest)
1466 .map_err(|_| Error::CorruptObject("encoding is not UTF-8".to_owned()))?
1467 .trim()
1468 .to_owned(),
1469 );
1470 }
1471 pos = after_nl;
1472 }
1473 Ok("(no subject)".to_owned())
1474}
1475
1476pub(crate) fn commit_through_subject_len(data: &[u8]) -> Option<usize> {
1486 let header_end = memchr::memmem::find(data, b"\n\n")? + 2;
1487 let declares_other_encoding = data[..header_end]
1488 .split(|&b| b == b'\n')
1489 .filter_map(|line| line.strip_prefix(b"encoding "))
1490 .any(|label| {
1491 let label = label.trim_ascii();
1492 !label.eq_ignore_ascii_case(b"utf-8") && !label.eq_ignore_ascii_case(b"utf8")
1493 });
1494 if declares_other_encoding {
1495 return None;
1496 }
1497 let mut pos = header_end;
1498 loop {
1499 let line_end = pos + memchr::memchr(b'\n', data.get(pos..)?)?;
1500 if data[pos..line_end].iter().any(u8::is_ascii_graphic) {
1501 return Some(line_end + 1);
1502 }
1503 pos = line_end + 1;
1504 }
1505}
1506
1507pub(crate) fn parse_oid_field(value: &[u8]) -> Result<ObjectId> {
1508 let s = std::str::from_utf8(value)
1509 .map_err(|_| Error::CorruptObject("commit header object id is not UTF-8".to_owned()))?;
1510 s.trim()
1511 .parse::<ObjectId>()
1512 .map_err(|_| Error::CorruptObject("invalid object id in commit header".to_owned()))
1513}
1514
1515pub(crate) fn parse_commit_tree_and_parents_prefix(
1525 prefix: &[u8],
1526 hex_len: usize,
1527) -> Option<(ObjectId, Vec<ObjectId>)> {
1528 canonical_commit_tree_and_parents(prefix, hex_len, false)
1529}
1530
1531pub(crate) fn parse_commit_tree_and_parents_lenient(
1538 data: &[u8],
1539 hex_len: usize,
1540) -> Option<(ObjectId, Vec<ObjectId>)> {
1541 canonical_commit_tree_and_parents(data, hex_len, true)
1542}
1543
1544fn canonical_commit_tree_and_parents(
1545 data: &[u8],
1546 hex_len: usize,
1547 complete: bool,
1548) -> Option<(ObjectId, Vec<ObjectId>)> {
1549 fn header_oid<'a>(rest: &'a [u8], key: &[u8], hex_len: usize) -> Option<(ObjectId, &'a [u8])> {
1550 let line = rest.strip_prefix(key)?;
1551 if line.len() <= hex_len || line[hex_len] != b'\n' {
1552 return None;
1553 }
1554 let oid = ObjectId::from_hex(std::str::from_utf8(&line[..hex_len]).ok()?).ok()?;
1555 Some((oid, &line[hex_len + 1..]))
1556 }
1557 const PARENT: &[u8] = b"parent ";
1558 let (tree, mut rest) = header_oid(data, b"tree ", hex_len)?;
1559 let mut parents = Vec::new();
1560 loop {
1561 if !complete && rest.len() < PARENT.len() && PARENT.starts_with(rest) {
1562 return None;
1563 }
1564 if !rest.starts_with(PARENT) {
1565 return Some((tree, parents));
1566 }
1567 let (parent, next) = header_oid(rest, PARENT, hex_len)?;
1568 parents.push(parent);
1569 rest = next;
1570 }
1571}
1572
1573pub fn parse_commit(data: &[u8]) -> Result<CommitData> {
1579 Ok(super::CommitRef::parse(data)?.to_owned())
1580}
1581
1582#[must_use]
1585pub fn tag_header_field(data: &[u8], prefix: &[u8]) -> Option<String> {
1586 let mut pos = 0usize;
1587 while pos < data.len() {
1588 let rest = &data[pos..];
1589 let nl = rest.iter().position(|&b| b == b'\n');
1590 let line = if let Some(i) = nl { &rest[..i] } else { rest };
1591 if line.is_empty() {
1592 break;
1593 }
1594 if let Some(after) = line.strip_prefix(prefix) {
1595 return Some(String::from_utf8_lossy(after).trim().to_owned());
1596 }
1597 pos += line.len().saturating_add(nl.map(|_| 1).unwrap_or(0));
1598 if nl.is_none() {
1599 break;
1600 }
1601 }
1602 None
1603}
1604
1605#[must_use]
1607pub fn tag_object_line_oid(data: &[u8]) -> Option<ObjectId> {
1608 let s = tag_header_field(data, b"object ")?;
1609 s.parse().ok()
1610}
1611
1612#[derive(Debug, Clone)]
1614pub struct TagData {
1615 pub object: ObjectId,
1617 pub object_type: String,
1619 pub tag: String,
1621 pub tagger: Option<String>,
1623 pub extra_headers: Vec<u8>,
1625 pub message: String,
1627 pub raw_message: Option<Vec<u8>>,
1631}
1632
1633pub fn parse_tag(data: &[u8]) -> Result<TagData> {
1641 use super::extra_headers::next_header_block;
1642
1643 #[derive(Clone, Copy)]
1644 enum Continuation {
1645 None,
1646 Extra,
1647 }
1648
1649 let mut pos = 0usize;
1650 let mut object = None;
1651 let mut object_type = None;
1652 let mut tag_name = None;
1653 let mut tagger = None;
1654 let mut extra_headers = Vec::new();
1655 let mut cont = Continuation::None;
1656 let mut past_tagger = false;
1657
1658 while pos < data.len() {
1659 let line_start = pos;
1660 let mut line_end = pos;
1661 while line_end < data.len() && data[line_end] != b'\n' {
1662 line_end += 1;
1663 }
1664 let line = &data[line_start..line_end];
1665 let after_nl = (line_end + 1).min(data.len());
1666 if line.is_empty() {
1667 let body = data.get(after_nl..).unwrap_or_default();
1668 let (message, raw_message) = match std::str::from_utf8(body) {
1669 Ok(text) => (text.to_owned(), None),
1670 Err(_) => (
1671 String::from_utf8_lossy(body).into_owned(),
1672 Some(body.to_vec()),
1673 ),
1674 };
1675 return Ok(TagData {
1676 object: object
1677 .ok_or_else(|| Error::CorruptObject("tag missing object header".to_owned()))?,
1678 object_type: object_type
1679 .ok_or_else(|| Error::CorruptObject("tag missing type header".to_owned()))?,
1680 tag: tag_name
1681 .ok_or_else(|| Error::CorruptObject("tag missing tag header".to_owned()))?,
1682 tagger,
1683 extra_headers,
1684 message,
1685 raw_message,
1686 });
1687 }
1688
1689 if past_tagger {
1690 let (_, block_end, _) = next_header_block(data, line_start);
1691 extra_headers.extend_from_slice(&data[line_start..block_end]);
1692 pos = block_end;
1693 continue;
1694 }
1695
1696 if line.first() == Some(&b' ') {
1697 match cont {
1698 Continuation::Extra => {
1699 extra_headers.extend_from_slice(&data[line_start..after_nl]);
1700 }
1701 Continuation::None => {}
1702 }
1703 pos = after_nl;
1704 continue;
1705 }
1706
1707 let key_end = line
1708 .iter()
1709 .position(|&b| b == b' ')
1710 .ok_or_else(|| Error::CorruptObject("malformed tag header line".to_owned()))?;
1711 let key = &line[..key_end];
1712 let rest = line.get(key_end + 1..).unwrap_or_default();
1713
1714 match key {
1715 b"object" => {
1716 let line_str = std::str::from_utf8(rest).map_err(|_| {
1717 Error::CorruptObject("tag object line is not valid UTF-8".to_owned())
1718 })?;
1719 object = Some(line_str.trim().parse::<ObjectId>()?);
1720 cont = Continuation::None;
1721 }
1722 b"type" => {
1723 let typ = std::str::from_utf8(rest).map_err(|_| {
1724 Error::CorruptObject("tag type line is not valid UTF-8".to_owned())
1725 })?;
1726 let typ = typ.trim();
1727 if ObjectKind::from_tag_type_field(typ.as_bytes()).is_none() {
1728 return Err(Error::CorruptObject(format!(
1729 "invalid 'type' value in tag: {typ}"
1730 )));
1731 }
1732 object_type = Some(typ.to_owned());
1733 cont = Continuation::None;
1734 }
1735 b"tag" => {
1736 let line_str = std::str::from_utf8(rest).map_err(|_| {
1737 Error::CorruptObject("tag name line is not valid UTF-8".to_owned())
1738 })?;
1739 tag_name = Some(line_str.trim().to_owned());
1740 cont = Continuation::None;
1741 }
1742 b"tagger" => {
1743 tagger = Some(
1744 std::str::from_utf8(rest)
1745 .map_err(|_| {
1746 Error::CorruptObject("tag tagger line is not valid UTF-8".to_owned())
1747 })?
1748 .to_owned(),
1749 );
1750 past_tagger = true;
1751 cont = Continuation::None;
1752 }
1753 _ => {
1754 cont = Continuation::Extra;
1755 extra_headers.extend_from_slice(&data[line_start..after_nl]);
1756 }
1757 }
1758 pos = after_nl;
1759 }
1760
1761 Ok(TagData {
1762 object: object
1763 .ok_or_else(|| Error::CorruptObject("tag missing object header".to_owned()))?,
1764 object_type: object_type
1765 .ok_or_else(|| Error::CorruptObject("tag missing type header".to_owned()))?,
1766 tag: tag_name.ok_or_else(|| Error::CorruptObject("tag missing tag header".to_owned()))?,
1767 tagger,
1768 extra_headers,
1769 message: String::new(),
1770 raw_message: None,
1771 })
1772}
1773
1774#[must_use]
1779pub fn serialize_tag(t: &TagData) -> Vec<u8> {
1780 let mut out = Vec::new();
1781 out.extend_from_slice(format!("object {}\n", t.object).as_bytes());
1782 out.extend_from_slice(format!("type {}\n", t.object_type).as_bytes());
1783 out.extend_from_slice(format!("tag {}\n", t.tag).as_bytes());
1784 if let Some(ref tagger) = t.tagger {
1785 out.extend_from_slice(format!("tagger {tagger}\n").as_bytes());
1786 }
1787 out.extend_from_slice(&t.extra_headers);
1788 out.push(b'\n');
1789 if let Some(raw) = &t.raw_message {
1790 out.extend_from_slice(raw);
1791 } else {
1792 let msg = t.message.trim_end_matches('\n');
1793 if !msg.is_empty() {
1794 out.extend_from_slice(msg.as_bytes());
1795 out.push(b'\n');
1796 }
1797 }
1798 out
1799}
1800
1801#[must_use]
1811pub fn serialize_commit(c: &CommitData) -> Vec<u8> {
1812 let mut out = Vec::new();
1813 out.extend_from_slice(format!("tree {}\n", c.tree).as_bytes());
1814 for p in &c.parents {
1815 out.extend_from_slice(format!("parent {p}\n").as_bytes());
1816 }
1817 out.extend_from_slice(b"author ");
1818 if c.author_raw.is_empty() {
1819 out.extend_from_slice(c.author.as_bytes());
1820 } else {
1821 out.extend_from_slice(&c.author_raw);
1822 }
1823 out.push(b'\n');
1824 out.extend_from_slice(b"committer ");
1825 if c.committer_raw.is_empty() {
1826 out.extend_from_slice(c.committer.as_bytes());
1827 } else {
1828 out.extend_from_slice(&c.committer_raw);
1829 }
1830 out.push(b'\n');
1831 if let Some(enc) = &c.encoding {
1832 out.extend_from_slice(format!("encoding {enc}\n").as_bytes());
1833 }
1834 out.extend_from_slice(&c.extra_headers);
1835 out.push(b'\n');
1836 if let Some(raw) = &c.raw_message {
1837 out.extend_from_slice(raw);
1838 } else if !c.message.is_empty() {
1839 out.extend_from_slice(c.message.as_bytes());
1840 }
1841 out
1842}
1843
1844#[cfg(test)]
1845mod short_hex_tests {
1846 use super::*;
1847
1848 #[test]
1849 fn short_hex_takes_seven_digits_or_the_whole_shorter_input() {
1850 assert_eq!(
1851 short_hex("4b825dc642cb6eb9a060e54bf8d69288fbee4904"),
1852 "4b825dc"
1853 );
1854 assert_eq!(short_hex("4b825dc"), "4b825dc");
1855 assert_eq!(short_hex("4b8"), "4b8");
1856 assert_eq!(short_hex(""), "");
1857 assert_eq!(short_hex("ééééé"), "ééééé");
1858 }
1859
1860 #[test]
1861 fn object_id_short_hex_matches_the_hex_prefix_for_both_algorithms() {
1862 let digest: Vec<u8> = (0x9a..=0xff).take(32).collect();
1863 for len in [HashAlgo::Sha1.len(), HashAlgo::Sha256.len()] {
1864 let id = ObjectId::from_bytes(&digest[..len]).unwrap();
1865 let short = id.to_short_hex();
1866 assert_eq!(short.len(), SHORT_HEX_LEN);
1867 assert_eq!(short, id.to_hex()[..SHORT_HEX_LEN]);
1868 assert_eq!(short, short_hex(&id.to_hex()));
1869 }
1870 }
1871}
1872
1873#[cfg(test)]
1874mod object_id_prefix_tests {
1875 use super::*;
1876
1877 const ID: &str = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
1878
1879 fn id() -> ObjectId {
1880 ObjectId::from_hex(ID).unwrap()
1881 }
1882
1883 fn prefix(hex: &str) -> ObjectIdPrefix {
1884 ObjectIdPrefix::from_hex(hex, HashAlgo::Sha1).unwrap()
1885 }
1886
1887 #[test]
1888 fn matches_every_length_of_the_id_and_rejects_a_changed_last_digit() {
1889 let id = id();
1890 for n in 0..=ID.len() {
1891 let p = prefix(&ID[..n]);
1892 assert_eq!(p.nibbles(), n);
1893 assert_eq!(p.is_empty(), n == 0);
1894 assert!(p.matches(&id), "{n} digits");
1895 assert_eq!(p.to_string(), ID[..n]);
1896 if n > 0 {
1897 let mut changed = ID[..n].to_owned();
1898 let last = changed.pop().unwrap();
1899 changed.push(if last == '0' { '1' } else { '0' });
1900 assert!(!prefix(&changed).matches(&id), "{changed}");
1901 }
1902 }
1903 }
1904
1905 #[test]
1906 fn odd_prefix_ignores_the_low_nibble_of_its_last_byte() {
1907 let p = prefix("4b8");
1908 assert!(p.matches(&ObjectId::from_hex("4b80000000000000000000000000000000000000").unwrap()));
1909 assert!(p.matches(&ObjectId::from_hex("4b8fffffffffffffffffffffffffffffffffffff").unwrap()));
1910 assert!(
1911 !p.matches(&ObjectId::from_hex("4b90000000000000000000000000000000000000").unwrap())
1912 );
1913 }
1914
1915 #[test]
1916 fn parsing_is_case_insensitive_and_validates() {
1917 assert_eq!(prefix("4B825DC"), prefix("4b825dc"));
1918 assert!(matches!(
1919 ObjectIdPrefix::from_hex("4b8g", HashAlgo::Sha1),
1920 Err(Error::InvalidObjectId(_))
1921 ));
1922 assert!(matches!(
1923 ObjectIdPrefix::from_hex(&format!("{ID}0"), HashAlgo::Sha1),
1924 Err(Error::InvalidObjectId(_))
1925 ));
1926 let sha256 = "a".repeat(64);
1927 assert_eq!(
1928 ObjectIdPrefix::from_hex(&sha256, HashAlgo::Sha256)
1929 .unwrap()
1930 .nibbles(),
1931 64
1932 );
1933 assert_eq!(prefix("").algo(), HashAlgo::Sha1);
1934 }
1935
1936 #[test]
1937 fn longer_prefix_never_matches_a_shorter_digest() {
1938 let long = ObjectIdPrefix::from_hex(&format!("{ID}0000"), HashAlgo::Sha256).unwrap();
1939 assert!(!long.matches(&id()));
1940 assert!(ObjectIdPrefix::from_hex(ID, HashAlgo::Sha256)
1941 .unwrap()
1942 .matches(&id()));
1943 }
1944
1945 #[test]
1946 fn bounds_are_the_first_and_last_matching_ids() {
1947 let p = prefix("4b8");
1948 assert_eq!(
1949 p.lowest().to_hex(),
1950 "4b80000000000000000000000000000000000000"
1951 );
1952 assert_eq!(
1953 p.highest().to_hex(),
1954 "4b8fffffffffffffffffffffffffffffffffffff"
1955 );
1956 let even = prefix("4b82");
1957 assert_eq!(
1958 even.lowest().to_hex(),
1959 "4b82000000000000000000000000000000000000"
1960 );
1961 assert_eq!(
1962 even.highest().to_hex(),
1963 "4b82ffffffffffffffffffffffffffffffffffff"
1964 );
1965 assert_eq!(prefix("").lowest(), ObjectId::null(HashAlgo::Sha1));
1966 assert_eq!(prefix("").highest().to_hex(), "f".repeat(40));
1967 assert_eq!(prefix(ID).lowest(), id());
1968 assert_eq!(prefix(ID).highest(), id());
1969 let sha256 = ObjectIdPrefix::from_hex("abc", HashAlgo::Sha256).unwrap();
1970 assert_eq!(sha256.highest().to_hex(), format!("abc{}", "f".repeat(61)));
1971 assert_eq!(format!("{sha256:?}"), "ObjectIdPrefix(abc)");
1972 }
1973
1974 #[test]
1975 fn common_prefix_nibbles_counts_shared_leading_digits() {
1976 let a = id();
1977 let hex = |s: &str| ObjectId::from_hex(s).unwrap();
1978 assert_eq!(a.common_prefix_nibbles(&a), 40);
1979 assert_eq!(
1980 a.common_prefix_nibbles(&hex("4b825dc642cb6eb9a060e54bf8d69288fbee4905")),
1981 39
1982 );
1983 assert_eq!(
1984 a.common_prefix_nibbles(&hex("4b815dc642cb6eb9a060e54bf8d69288fbee4904")),
1985 3
1986 );
1987 assert_eq!(
1988 a.common_prefix_nibbles(&hex("4b925dc642cb6eb9a060e54bf8d69288fbee4904")),
1989 2
1990 );
1991 assert_eq!(
1992 a.common_prefix_nibbles(&hex("5b825dc642cb6eb9a060e54bf8d69288fbee4904")),
1993 0
1994 );
1995 assert_eq!(
1996 a.common_prefix_nibbles(&hex("4c825dc642cb6eb9a060e54bf8d69288fbee4904")),
1997 1
1998 );
1999 }
2000}
2001
2002#[cfg(test)]
2003mod commit_parse_tests {
2004 use super::*;
2005
2006 #[test]
2007 fn empty_tree_constant_is_canonical_sha1_digest() {
2008 assert_eq!(
2009 ObjectId::empty_tree().to_hex(),
2010 "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
2011 );
2012 }
2013
2014 #[test]
2015 fn is_empty_tree_for_matches_hashed_empty_tree() {
2016 for algo in [HashAlgo::Sha1, HashAlgo::Sha256] {
2017 let hashed =
2018 crate::hash::hash_object(algo, ObjectKind::Tree, b"").expect("hash empty tree");
2019 assert!(hashed.is_empty_tree_for(algo), "{algo:?}");
2020 let other = match algo {
2021 HashAlgo::Sha1 => HashAlgo::Sha256,
2022 HashAlgo::Sha256 => HashAlgo::Sha1,
2023 };
2024 assert!(!hashed.is_empty_tree_for(other), "{algo:?}");
2025 let empty_blob =
2026 crate::hash::hash_object(algo, ObjectKind::Blob, b"").expect("hash empty blob");
2027 assert!(!empty_blob.is_empty_tree_for(algo), "{algo:?}");
2028 }
2029 let legacy = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf899d69f7c6948d4").expect("hex");
2030 assert!(legacy.is_empty_tree_for(HashAlgo::Sha1));
2031 assert!(!legacy.is_empty_tree_for(HashAlgo::Sha256));
2032 }
2033
2034 #[test]
2035 fn serialize_tree_mode_matches_mode_str() {
2036 for mode in [
2037 0,
2038 0o7,
2039 0o100644,
2040 0o100755,
2041 0o120000,
2042 0o040000,
2043 0o160000,
2044 u32::MAX,
2045 ] {
2046 let mut out = Vec::new();
2047 push_tree_mode(&mut out, mode);
2048 let entry = TreeEntry {
2049 mode,
2050 name: Vec::new(),
2051 oid: ObjectId::empty_tree(),
2052 };
2053 assert_eq!(out, entry.mode_str().into_bytes(), "mode {mode:o}");
2054 }
2055 }
2056
2057 #[test]
2058 fn is_empty_blob_matches_both_hash_algorithms() {
2059 for algo in [HashAlgo::Sha1, HashAlgo::Sha256] {
2060 let oid = algo.hash_object(ObjectKind::Blob, b"").unwrap();
2061 assert!(oid.is_empty_blob(), "{algo:?} empty blob {}", oid.to_hex());
2062 let other = algo.hash_object(ObjectKind::Blob, b"x").unwrap();
2063 assert!(!other.is_empty_blob());
2064 }
2065 assert!(!ObjectId::empty_tree().is_empty_blob());
2066 assert!(!ObjectId::zero().is_empty_blob());
2067 }
2068
2069 #[test]
2070 fn commit_subject_pretty_folds_first_paragraph() {
2071 assert_eq!(commit_subject_pretty("hello\nworld\n\nbody"), "hello world");
2072 assert_eq!(commit_subject_pretty(""), "");
2073 assert_eq!(
2074 commit_subject_pretty(" hello world\tthere \n next\t line \n\nbody\n"),
2075 " hello world\tthere next\t line"
2076 );
2077 }
2078
2079 #[test]
2080 fn commit_ref_matches_parse_commit_headers() {
2081 let raw = concat!(
2082 "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
2083 "parent 1111111111111111111111111111111111111111\n",
2084 "author A <a@example.com> 1 +0000\n",
2085 "committer C <c@example.com> 2 +0000\n",
2086 "\n",
2087 "ignored body\n",
2088 );
2089 let full = parse_commit(raw.as_bytes()).expect("parse");
2090 let link = crate::objects::CommitRef::parse(raw.as_bytes()).expect("CommitRef");
2091 assert_eq!(link.tree(), full.tree);
2092 assert_eq!(link.parents(), full.parents.as_slice());
2093 assert_eq!(link.author(), full.author);
2094 assert_eq!(link.committer(), full.committer);
2095 }
2096
2097 #[test]
2098 fn parse_commit_skips_multiline_gpgsig_continuation() {
2099 let raw = concat!(
2100 "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
2101 "author A U Thor <author@example.com> 1 +0000\n",
2102 "committer C O Mitter <committer@example.com> 1 +0000\n",
2103 "gpgsig -----BEGIN PGP SIGNATURE-----\n",
2104 " abcdef\n",
2105 " -----END PGP SIGNATURE-----\n",
2106 "\n",
2107 "msg\n",
2108 );
2109 let c = parse_commit(raw.as_bytes()).expect("parse signed commit");
2110 assert_eq!(c.tree.to_hex(), "4b825dc642cb6eb9a060e54bf8d69288fbee4904");
2111 assert_eq!(c.message, "msg\n");
2112 assert_eq!(serialize_commit(&c), raw.as_bytes());
2113 }
2114
2115 #[test]
2116 fn hex_parsing_accepts_both_cases_and_lengths_and_rejects_the_rest() {
2117 let sha1 = "0123456789abcdefABCDEF0123456789abcdef01";
2118 let oid = ObjectId::from_hex(sha1).unwrap();
2119 assert_eq!(oid.algo(), HashAlgo::Sha1);
2120 assert_eq!(oid.to_hex(), sha1.to_ascii_lowercase());
2121 assert_eq!(ObjectId::from_hex_ascii(sha1.as_bytes()).unwrap(), oid);
2122 assert_eq!(sha1.parse::<ObjectId>().unwrap(), oid);
2123
2124 let sha256 = "f".repeat(64);
2125 let oid = sha256.parse::<ObjectId>().unwrap();
2126 assert_eq!(oid.algo(), HashAlgo::Sha256);
2127 assert_eq!(oid.as_bytes(), [0xff; 32]);
2128 assert!(ObjectId::from_hex_ascii(sha256.as_bytes()).is_err());
2129
2130 for bad in [
2131 "",
2132 &sha1[..39],
2133 &format!("{sha1}0"),
2134 "g123456789abcdef0123456789abcdef01234567",
2135 "0123456789abcdef0123456789abcdef0123456 ",
2136 "0123456789abcdef0123456789abcdef012345/7",
2137 "0123456789abcdef0123456789abcdef012345:7",
2138 "0123456789abcdef0123456789abcdef012345`7",
2139 "0123456789abcdef0123456789abcdef012345G7",
2140 ] {
2141 assert!(
2142 matches!(ObjectId::from_hex(bad), Err(Error::InvalidObjectId(s)) if s == bad),
2143 "{bad:?}"
2144 );
2145 assert!(bad.parse::<ObjectId>().is_err(), "{bad:?}");
2146 assert!(ObjectId::from_hex_ascii(bad.as_bytes()).is_err(), "{bad:?}");
2147 }
2148 let mut non_ascii = sha1.as_bytes().to_vec();
2149 non_ascii[3] = 0xb0;
2150 assert!(ObjectId::from_hex_ascii(&non_ascii).is_err());
2151 }
2152
2153 #[test]
2154 fn hex_and_display_cover_every_byte_for_both_lengths() {
2155 let all: Vec<u8> = (0..=255).collect();
2156 for width in [20, 32] {
2157 for chunk in all.chunks(width).filter(|c| c.len() == width) {
2158 let oid = ObjectId::from_bytes(chunk).unwrap();
2159 let expected: String = chunk.iter().map(|b| format!("{b:02x}")).collect();
2160 assert_eq!(oid.to_hex(), expected);
2161 assert_eq!(oid.to_string(), expected);
2162 assert_eq!(format!("{oid:?}"), format!("ObjectId({expected})"));
2163 assert_eq!(ObjectId::from_hex(&expected).unwrap(), oid);
2164 }
2165 }
2166 }
2167
2168 #[test]
2169 fn serialize_commit_applies_public_field_edits_after_parse() {
2170 let raw = concat!(
2171 "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
2172 "author A <a@example.com> 1 +0000\n",
2173 "committer C <c@example.com> 1 +0000\n",
2174 "\n",
2175 "msg\n",
2176 );
2177 let mut commit = parse_commit(raw.as_bytes()).expect("parse");
2178 let zero = ObjectId::from_hex("0000000000000000000000000000000000000000").unwrap();
2179 commit.tree = zero;
2180 let serialized = serialize_commit(&commit);
2181 let tree_line = format!("tree {}\n", zero.to_hex());
2182 assert!(
2183 serialized.starts_with(tree_line.as_bytes()),
2184 "serialize_commit ignored the updated public tree field"
2185 );
2186 }
2187
2188 #[test]
2189 fn parse_commit_preserves_gpgsig_mergetag_bytes_exactly() {
2190 let raw = concat!(
2191 "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
2192 "author A <a@example.com> 1 +0000\n",
2193 "committer C <c@example.com> 1 +0000\n",
2194 "encoding ISO-8859-1\n",
2195 "gpgsig -----BEGIN PGP SIGNATURE-----\n",
2196 " line2\n",
2197 " -----END PGP SIGNATURE-----\n",
2198 "mergetag object 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
2199 " type commit\n",
2200 " tag merge-tag\n",
2201 " tagger T <t@example.com> 1 +0000\n",
2202 " -----BEGIN PGP SIGNATURE-----\n",
2203 " inner\n",
2204 " -----END PGP SIGNATURE-----\n",
2205 "\n",
2206 "msg-only\n",
2207 );
2208 let parsed = parse_commit(raw.as_bytes()).expect("parse");
2209 assert_eq!(serialize_commit(&parsed), raw.as_bytes());
2210 }
2211
2212 #[test]
2213 fn tree_entry_mode_str_and_tag_object_line_oid() {
2214 let oid = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap();
2215 let entry = TreeEntry {
2216 mode: 0o040000,
2217 name: b"dir".to_vec(),
2218 oid,
2219 };
2220 assert_eq!(entry.mode_str(), "40000");
2221 let tag_body = format!("object {}\ntype commit\ntag t\n\n", oid.to_hex());
2222 assert_eq!(tag_object_line_oid(tag_body.as_bytes()), Some(oid));
2223 }
2224
2225 #[test]
2226 fn parse_commit_non_commit_bytes_does_not_panic() {
2227 let tree_bytes = b"100644 blob 0123456789012345678901234567890123456789\tf";
2228 let err = parse_commit(tree_bytes).expect_err("tree bytes are not a commit");
2229 assert!(matches!(err, Error::CorruptObject(_)));
2230 }
2231
2232 #[test]
2233 fn parse_tree_with_sha256_oid_width() {
2234 let oid_bytes = [0x6eu8; 32];
2235 let oid = ObjectId::from_bytes(&oid_bytes).expect("oid");
2236 let mut raw = b"100644 file\0".to_vec();
2237 raw.extend_from_slice(oid.as_bytes());
2238 let entries = parse_tree_with_oid_len(&raw, HashAlgo::Sha256.len()).expect("tree");
2239 assert_eq!(entries.len(), 1);
2240 assert_eq!(entries[0].oid, oid);
2241 }
2242
2243 #[test]
2244 fn parse_commit_tree_and_parents_prefix_matches_full_parse_or_declines() -> Result<()> {
2245 let tree = "1111111111111111111111111111111111111111";
2246 let p1 = "2222222222222222222222222222222222222222";
2247 let p2 = "3333333333333333333333333333333333333333";
2248 let raw = format!(
2249 "tree {tree}\nparent {p1}\nparent {p2}\nauthor A <a@x> 1 +0000\n\
2250 committer C <c@x> 2 +0000\n\nmessage\n"
2251 );
2252 let commit = parse_commit(raw.as_bytes())?;
2253 let full = (commit.tree, commit.parents);
2254 assert_eq!(full.1.len(), 2);
2255 assert_eq!(
2256 parse_commit_tree_and_parents_lenient(raw.as_bytes(), HashAlgo::Sha1.hex_len()),
2257 Some(full.clone())
2258 );
2259 let hex_len = HashAlgo::Sha1.hex_len();
2260 let header_end = raw.find("author").expect("author line");
2261 for cut in 0..=raw.len() {
2262 let got = parse_commit_tree_and_parents_prefix(&raw.as_bytes()[..cut], hex_len);
2263 if cut > header_end {
2264 assert_eq!(got.as_ref(), Some(&full), "cut at {cut}");
2265 } else {
2266 assert_eq!(
2267 got, None,
2268 "cut at {cut} must not claim a complete parent list"
2269 );
2270 }
2271 }
2272 Ok(())
2273 }
2274
2275 #[test]
2276 fn commit_tree_and_parents_prefix_never_disagrees_with_whole_object() {
2277 let tree = "tree 1111111111111111111111111111111111111111\n";
2278 let parent = "parent 2222222222222222222222222222222222222222\n";
2279 let parent2 = "parent 3333333333333333333333333333333333333333\n";
2280 let bodies = [
2281 format!("{tree}{parent}author A <a@x> 1 +0000\n\nmessage\n"),
2282 format!("{tree}{parent}{parent2}committer C <c@x> 2 +0000\n\nno author\n"),
2283 format!("{tree}{parent}"),
2284 format!("{tree}{parent}{parent2}"),
2285 format!("{tree}author A <a@x> 1 +0000\n{parent}\nlate parent is ignored\n"),
2286 format!("{tree}{parent}not a header\n"),
2287 format!("{tree}parent zz\n\n"),
2288 format!("{tree}{parent}parent 2222"),
2289 format!("author A <a@x> 1 +0000\n{tree}\n"),
2290 ];
2291 let hex_len = HashAlgo::Sha1.hex_len();
2292 for body in &bodies {
2293 let whole = parse_commit_tree_and_parents_lenient(body.as_bytes(), hex_len);
2294 for cut in 0..=body.len() {
2295 if let Some(got) =
2296 parse_commit_tree_and_parents_prefix(&body.as_bytes()[..cut], hex_len)
2297 {
2298 assert_eq!(Some(got), whole, "cut {cut} of {body:?}");
2299 }
2300 }
2301 }
2302 let parents_of = |body: &str| {
2303 parse_commit_tree_and_parents_lenient(body.as_bytes(), hex_len).map(|(_, p)| p.len())
2304 };
2305 assert_eq!(parents_of(&bodies[1]), Some(2), "missing author is fine");
2306 assert_eq!(
2307 parents_of(&bodies[3]),
2308 Some(2),
2309 "object may end after parents"
2310 );
2311 assert_eq!(
2312 parents_of(&bodies[4]),
2313 Some(0),
2314 "only leading parents count"
2315 );
2316 assert_eq!(parents_of(&bodies[6]), None, "malformed parent line");
2317 assert_eq!(parents_of(&bodies[7]), None, "truncated parent line");
2318 assert_eq!(parents_of(&bodies[8]), None, "tree must come first");
2319 }
2320
2321 #[test]
2322 fn parse_commit_tree_and_parents_prefix_handles_root_and_sha256() -> Result<()> {
2323 let root = b"tree 1111111111111111111111111111111111111111\nauthor A <a@x> 1 +0000\n";
2324 let (tree, parents) =
2325 parse_commit_tree_and_parents_prefix(root, HashAlgo::Sha1.hex_len()).expect("root");
2326 assert_eq!(tree.to_hex(), "1111111111111111111111111111111111111111");
2327 assert!(parents.is_empty());
2328
2329 let t = "a".repeat(64);
2330 let p = "b".repeat(64);
2331 let raw = format!("tree {t}\nparent {p}\ncommitter C <c@x> 2 +0000\n");
2332 let (tree, parents) =
2333 parse_commit_tree_and_parents_prefix(raw.as_bytes(), HashAlgo::Sha256.hex_len())
2334 .expect("sha256");
2335 assert_eq!(tree.to_hex(), t);
2336 assert_eq!(parents.len(), 1);
2337 assert_eq!(parents[0].to_hex(), p);
2338
2339 assert!(parse_commit_tree_and_parents_prefix(raw.as_bytes(), 40).is_none());
2340 assert!(parse_commit_tree_and_parents_prefix(b"parent x\n", 40).is_none());
2341 assert!(parse_commit_tree_and_parents_prefix(
2342 b"tree zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz\nauthor\n",
2343 40
2344 )
2345 .is_none());
2346 Ok(())
2347 }
2348
2349 #[test]
2350 fn object_id_loose_path_helpers() {
2351 let oid = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap();
2352 assert_eq!(oid.loose_prefix(), "4b");
2353 assert_eq!(oid.loose_suffix().len(), 38);
2354 assert!(ObjectId::is_hex_len(40));
2355 assert!(ObjectId::is_loose_suffix_len(38));
2356 }
2357
2358 #[test]
2359 fn tree_ref_iter_yields_at_most_one_error() {
2360 let bad = b"100644 incomplete";
2361 let mut iter = TreeRefIter::with_oid_len(bad, HashAlgo::Sha1.len());
2362 assert!(iter.next().unwrap().is_err());
2363 assert!(iter.next().is_none());
2364 }
2365
2366 #[test]
2367 fn tree_ref_iter_public_api_smoke() {
2368 let data = serialize_tree(&[TreeEntry {
2369 mode: 0o100644,
2370 name: b"a".to_vec(),
2371 oid: ObjectId::from_hex("0123456789012345678901234567890123456789").unwrap(),
2372 }]);
2373 let mut iter = TreeRefIter::infer_oid_len(&data).expect("infer");
2374 let entry = iter.next().unwrap().expect("entry");
2375 assert_eq!(entry.name, b"a");
2376 let _owned = entry.to_owned();
2377 let _vec = parse_tree(&data).expect("parse");
2378 let _ = TreeRefIter::with_oid_len(&data, HashAlgo::Sha1.len());
2379 }
2380
2381 #[test]
2382 fn find_tree_entry_by_name_in_data_locates_directory_in_large_tree() -> Result<()> {
2383 let dummy = ObjectId::from_hex("0123456789012345678901234567890123456789")?;
2384 let mut entries: Vec<TreeEntry> = (1..=60)
2385 .map(|i| TreeEntry {
2386 mode: 0o100644,
2387 name: format!("f{i}.txt").into_bytes(),
2388 oid: dummy,
2389 })
2390 .collect();
2391 entries.push(TreeEntry {
2392 mode: 0o040000,
2393 name: b"sub".to_vec(),
2394 oid: dummy,
2395 });
2396 entries.sort_by(|a, b| {
2397 tree_entry_cmp(&a.name, a.mode == 0o040000, &b.name, b.mode == 0o040000)
2398 });
2399 let data = serialize_tree(&entries);
2400 assert!(entries.len() >= 48);
2401 let found = find_tree_entry_by_name_in_data(&data, HashAlgo::Sha1.len(), b"sub", true)?
2402 .expect("sub directory in wide tree");
2403 assert_eq!(found.mode, 0o040000);
2404 assert_eq!(found.name, b"sub");
2405 Ok(())
2406 }
2407
2408 fn sorted_tree_data(entries: &mut [TreeEntry]) -> Vec<u8> {
2409 entries.sort_by(|a, b| {
2410 tree_entry_cmp(&a.name, a.mode == 0o040000, &b.name, b.mode == 0o040000)
2411 });
2412 serialize_tree(entries)
2413 }
2414
2415 #[test]
2416 fn find_tree_entry_by_name_in_data_matches_parse_tree_for_every_name() -> Result<()> {
2417 let oid_a = ObjectId::from_hex("1111111111111111111111111111111111111111")?;
2418 let oid_b = ObjectId::from_hex("2222222222222222222222222222222222222222")?;
2419 let mut entries = vec![
2420 TreeEntry {
2421 mode: 0o040000,
2422 name: b"foo".to_vec(),
2423 oid: oid_a,
2424 },
2425 TreeEntry {
2426 mode: 0o100644,
2427 name: b"foo.c".to_vec(),
2428 oid: oid_b,
2429 },
2430 TreeEntry {
2431 mode: 0o100755,
2432 name: b"foo-bar".to_vec(),
2433 oid: oid_b,
2434 },
2435 TreeEntry {
2436 mode: 0o120000,
2437 name: b"link".to_vec(),
2438 oid: oid_a,
2439 },
2440 TreeEntry {
2441 mode: 0o160000,
2442 name: b"sub".to_vec(),
2443 oid: oid_b,
2444 },
2445 TreeEntry {
2446 mode: 0o100644,
2447 name: b"README".to_vec(),
2448 oid: oid_a,
2449 },
2450 ];
2451 let data = sorted_tree_data(&mut entries);
2452 for expected in parse_tree(&data)? {
2453 let name_is_tree = expected.mode == 0o040000;
2454 let found = find_tree_entry_by_name_in_data(
2455 &data,
2456 HashAlgo::Sha1.len(),
2457 &expected.name,
2458 name_is_tree,
2459 )?
2460 .expect("entry present");
2461 assert_eq!(found, expected);
2462 }
2463 for missing in [&b"fo"[..], b"foo/", b"foo.d", b"zzz", b"", b"AAA", b"lin"] {
2464 assert!(
2465 find_tree_entry_by_name_in_data(&data, HashAlgo::Sha1.len(), missing, false)?
2466 .is_none(),
2467 "unexpected match for {:?}",
2468 String::from_utf8_lossy(missing)
2469 );
2470 }
2471 Ok(())
2472 }
2473
2474 #[test]
2475 fn find_tree_entry_by_name_in_data_stops_after_sorted_position() -> Result<()> {
2476 let oid = ObjectId::from_hex("3333333333333333333333333333333333333333")?;
2477 let mut entries = vec![
2478 TreeEntry {
2479 mode: 0o100644,
2480 name: b"a".to_vec(),
2481 oid,
2482 },
2483 TreeEntry {
2484 mode: 0o100644,
2485 name: b"c".to_vec(),
2486 oid,
2487 },
2488 ];
2489 let mut data = sorted_tree_data(&mut entries);
2490 data.extend_from_slice(b"garbage without separators");
2491 let found = find_tree_entry_by_name_in_data(&data, HashAlgo::Sha1.len(), b"a", false)?;
2492 assert_eq!(found.map(|e| e.name), Some(b"a".to_vec()));
2493 assert!(
2494 find_tree_entry_by_name_in_data(&data, HashAlgo::Sha1.len(), b"b", false)?.is_none()
2495 );
2496 assert!(matches!(
2497 find_tree_entry_by_name_in_data(&data, HashAlgo::Sha1.len(), b"d", false),
2498 Err(Error::CorruptObject(_))
2499 ));
2500 Ok(())
2501 }
2502
2503 #[test]
2504 fn find_tree_entry_by_name_in_data_rejects_bad_mode_and_truncated_oid() {
2505 let bad_mode = b"10x644 a\0aaaaaaaaaaaaaaaaaaaa";
2506 assert!(matches!(
2507 find_tree_entry_by_name_in_data(bad_mode, HashAlgo::Sha1.len(), b"a", false),
2508 Err(Error::CorruptObject(_))
2509 ));
2510 let truncated = b"100644 a\0short";
2511 assert!(matches!(
2512 find_tree_entry_by_name_in_data(truncated, HashAlgo::Sha1.len(), b"a", false),
2513 Err(Error::CorruptObject(_))
2514 ));
2515 }
2516
2517 #[test]
2518 fn find_tree_entry_by_name_in_data_reads_sha256_width() -> Result<()> {
2519 let oid = ObjectId::from_bytes(&[0x5au8; 32])?;
2520 let mut raw = b"100644 file\0".to_vec();
2521 raw.extend_from_slice(oid.as_bytes());
2522 let found = find_tree_entry_by_name_in_data(&raw, HashAlgo::Sha256.len(), b"file", false)?
2523 .expect("file");
2524 assert_eq!(found.oid, oid);
2525 assert_eq!(found.mode, 0o100644);
2526 Ok(())
2527 }
2528
2529 #[test]
2530 fn tree_entry_span_matches_tree_ref_iter_and_defers_malformed_entries() {
2531 let mut data = Vec::new();
2532 for (mode, name) in [
2533 (&b"100644"[..], &b"a.txt"[..]),
2534 (b"40000", b"dir"),
2535 (b"+644", b"odd"),
2536 ] {
2537 data.extend_from_slice(mode);
2538 data.push(b' ');
2539 data.extend_from_slice(name);
2540 data.push(0);
2541 data.extend_from_slice(&[name[0]; 20]);
2542 }
2543 let mut iter = TreeRefIter::with_oid_len(&data, 20);
2544 let mut pos = 0;
2545 while let Some(entry) = iter.next() {
2546 let entry = entry.unwrap();
2547 let span = tree_entry_span(&data, pos, 20).expect("well-formed entry");
2548 assert_eq!(span.mode, entry.mode);
2549 assert_eq!(&data[span.name], entry.name);
2550 assert_eq!(span.end, iter.position());
2551 assert_eq!(&data[span.end - 20..span.end], entry.oid.as_bytes());
2552 assert_eq!(
2553 TreeRefIter::with_oid_len_at(&data, 20, pos)
2554 .next()
2555 .unwrap()
2556 .unwrap(),
2557 entry
2558 );
2559 pos = iter.position();
2560 }
2561 assert_eq!(pos, data.len());
2562 assert!(tree_entry_span(&data, pos, 20).is_none());
2563 let (first, second) = (6 + 1 + 5 + 1 + 20, 5 + 1 + 3 + 1 + 20);
2564 assert!(tree_entry_span(&data[..first + second - 1], first, 20).is_none());
2566 assert!(tree_entry_span(b"10064x f\0aaaaaaaaaaaaaaaaaaaa", 0, 20).is_none());
2567 assert!(tree_entry_span(b"100644 f", 0, 20).is_none());
2568 assert!(tree_entry_span(&data, 0, 19).is_none());
2569 for bad in [
2570 &data[..first + second - 1],
2571 b"10064x f\0aaaaaaaaaaaaaaaaaaaa",
2572 ] {
2573 assert!(TreeRefIter::with_oid_len(bad, 20).any(|entry| entry.is_err()));
2574 }
2575 }
2576
2577 #[test]
2578 fn object_id_from_bytes_accepts_only_digest_lengths() {
2579 let sha1 = ObjectId::from_bytes(&[0xab; 20]).unwrap();
2580 assert_eq!(sha1.as_bytes(), &[0xab; 20]);
2581 assert_eq!(sha1.algo(), HashAlgo::Sha1);
2582 let sha256 = ObjectId::from_bytes(&[0xcd; 32]).unwrap();
2583 assert_eq!(sha256.as_bytes(), &[0xcd; 32]);
2584 assert_eq!(sha256.algo(), HashAlgo::Sha256);
2585 assert!(matches!(
2586 ObjectId::from_bytes(&[1; 19]),
2587 Err(Error::InvalidObjectId(hex)) if hex == "01".repeat(19)
2588 ));
2589 }
2590
2591 #[test]
2592 fn parse_tree_mode_accepts_what_from_str_radix_accepts() {
2593 let cases: &[&[u8]] = &[
2594 b"100644",
2595 b"40000",
2596 b"040000",
2597 b"160000",
2598 b"120000",
2599 b"0",
2600 b"7777777777",
2601 b"37777777777",
2602 b"77777777777",
2603 b"777777777777",
2604 b"",
2605 b"+644",
2606 b"-644",
2607 b"100648",
2608 b"10064a",
2609 b"1 0",
2610 b"\xff",
2611 b"00000000000100644",
2612 ];
2613 for &case in cases {
2614 let expected = std::str::from_utf8(case)
2615 .ok()
2616 .and_then(|s| u32::from_str_radix(s, 8).ok());
2617 assert_eq!(
2618 parse_tree_mode(case),
2619 expected,
2620 "mode {:?}",
2621 String::from_utf8_lossy(case)
2622 );
2623 }
2624 }
2625}
2626
2627#[cfg(test)]
2628mod tests {
2629 use super::*;
2630
2631 const HEAD: &str = "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\nauthor A <a@example.com> 1 +0000\ncommitter C <c@example.com> 2 +0000\n";
2632
2633 fn len_of(message: &[u8]) -> Option<usize> {
2634 let mut data = HEAD.as_bytes().to_vec();
2635 data.extend_from_slice(message);
2636 commit_through_subject_len(&data).map(|len| len - HEAD.len())
2637 }
2638
2639 #[test]
2640 fn commit_through_subject_len_ends_after_the_first_non_blank_line() {
2641 assert_eq!(len_of(b"\nsubject\n\nbody\n"), Some(9));
2642 assert_eq!(len_of(b"\n\n \t\r\n subject\r\nmore\n"), Some(17));
2643 assert_eq!(len_of(b"\nx\n"), Some(3));
2644 let signed = b"gpgsig -----BEGIN-----\n \n abc\n -----END-----\n\nsigned\nbody\n";
2645 assert_eq!(len_of(signed), Some(signed.len() - 5));
2646 }
2647
2648 #[test]
2649 fn commit_through_subject_len_skips_lines_that_may_decode_blank() {
2650 let message = "\n\u{a0}\u{3000}\n\u{65e5}\u{672c}\nascii\nbody\n";
2651 assert_eq!(
2652 len_of(message.as_bytes()),
2653 Some(message.len() - "body\n".len())
2654 );
2655 }
2656
2657 #[test]
2658 fn commit_through_subject_len_needs_a_complete_prefix() {
2659 assert_eq!(len_of(b""), None);
2660 assert_eq!(len_of(b"\n"), None);
2661 assert_eq!(len_of(b"\n\n \n"), None);
2662 assert_eq!(len_of(b"\nsubject without newline"), None);
2663 assert_eq!(commit_through_subject_len(b"tree 4b82"), None);
2664 assert_eq!(commit_through_subject_len(HEAD.as_bytes()), None);
2665 }
2666
2667 #[test]
2668 fn commit_through_subject_len_gives_up_on_other_encodings() {
2669 assert_eq!(len_of(b"encoding ISO-8859-1\n\nS\xe9ance\n\nbody\n"), None);
2670 assert_eq!(len_of(b"encoding ISO-2022-JP\n\nsubject\n"), None);
2671 assert_eq!(len_of(b"encoding UTF-8\n\nsubject\nbody\n"), Some(24));
2672 assert_eq!(len_of(b"encoding utf8 \n\nsubject\nbody\n"), Some(24));
2673 }
2674
2675 #[test]
2676 fn commit_through_subject_prefix_parses_like_the_whole_commit() {
2677 let whole = format!("{HEAD}change-id zzzz\n\n\n Subject here \n\nlong body\n");
2678 let len = commit_through_subject_len(whole.as_bytes()).unwrap();
2679 let prefix = &whole.as_bytes()[..len];
2680 assert_eq!(
2681 read_commit_subject_line(prefix).unwrap(),
2682 read_commit_subject_line(whole.as_bytes()).unwrap()
2683 );
2684 let (short, full) = (
2685 parse_commit(prefix).unwrap(),
2686 parse_commit(whole.as_bytes()).unwrap(),
2687 );
2688 assert_eq!(short.extra_headers, full.extra_headers);
2689 assert_eq!(short.author, full.author);
2690 assert_eq!(short.message, "\n Subject here \n");
2691 }
2692}