1use std::fmt;
22use std::str::FromStr;
23
24use crate::commit_encoding;
25use crate::error::{Error, Result};
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
33pub enum HashAlgo {
34 #[default]
36 Sha1,
37 Sha256,
39}
40
41impl HashAlgo {
42 #[must_use]
44 pub const fn len(self) -> usize {
45 match self {
46 Self::Sha1 => 20,
47 Self::Sha256 => 32,
48 }
49 }
50
51 #[must_use]
53 pub const fn is_empty(self) -> bool {
54 false
55 }
56
57 #[must_use]
59 pub const fn hex_len(self) -> usize {
60 self.len() * 2
61 }
62
63 #[must_use]
65 pub const fn name(self) -> &'static str {
66 match self {
67 Self::Sha1 => "sha1",
68 Self::Sha256 => "sha256",
69 }
70 }
71
72 #[must_use]
75 pub const fn oid_version(self) -> u8 {
76 match self {
77 Self::Sha1 => 1,
78 Self::Sha256 => 2,
79 }
80 }
81
82 #[must_use]
84 pub fn from_name(name: &str) -> Option<Self> {
85 match name.trim() {
86 v if v.eq_ignore_ascii_case("sha1") => Some(Self::Sha1),
87 v if v.eq_ignore_ascii_case("sha256") => Some(Self::Sha256),
88 _ => None,
89 }
90 }
91
92 #[must_use]
94 pub const fn from_len(len: usize) -> Option<Self> {
95 match len {
96 20 => Some(Self::Sha1),
97 32 => Some(Self::Sha256),
98 _ => None,
99 }
100 }
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
105#[error("unknown Git hash version byte {0} (expected 1 for SHA-1 or 2 for SHA-256)")]
106pub struct HashVersionError(pub u8);
107
108impl From<HashAlgo> for u8 {
109 fn from(algo: HashAlgo) -> Self {
110 algo.oid_version()
111 }
112}
113
114impl TryFrom<u8> for HashAlgo {
115 type Error = HashVersionError;
116
117 fn try_from(version: u8) -> std::result::Result<Self, Self::Error> {
118 match version {
119 1 => Ok(Self::Sha1),
120 2 => Ok(Self::Sha256),
121 other => Err(HashVersionError(other)),
122 }
123 }
124}
125
126const MAX_OID_LEN: usize = 32;
128
129const WELL_KNOWN_EMPTY_TREE_CANON: [u8; 20] = [
131 0x4b, 0x82, 0x5d, 0xc6, 0x42, 0xcb, 0x6e, 0xb9, 0xa0, 0x60, 0xe5, 0x4b, 0xf8, 0xd6, 0x92, 0x88,
132 0xfb, 0xee, 0x49, 0x04,
133];
134const WELL_KNOWN_EMPTY_TREE_LEGACY: [u8; 20] = [
136 0x4b, 0x82, 0x5d, 0xc6, 0x42, 0xcb, 0x6e, 0xb9, 0xa0, 0x60, 0xe5, 0x4b, 0xf8, 0x99, 0xd6, 0x9f,
137 0x7c, 0x69, 0x48, 0xd4,
138];
139const EMPTY_TREE_SHA256: [u8; 32] = [
141 0x6e, 0xf1, 0x9b, 0x41, 0x22, 0x5c, 0x53, 0x69, 0xf1, 0xc1, 0x04, 0xd4, 0x5d, 0x8d, 0x85, 0xef,
142 0xa9, 0xb0, 0x57, 0xb5, 0x3b, 0x14, 0xb4, 0xb9, 0xb9, 0x39, 0xdd, 0x74, 0xde, 0xcc, 0x53, 0x21,
143];
144const EMPTY_BLOB_SHA1: [u8; 20] = [
146 0xe6, 0x9d, 0xe2, 0x9b, 0xb2, 0xd1, 0xd6, 0x43, 0x4b, 0x8b, 0x29, 0xae, 0x77, 0x5a, 0xd8, 0xc2,
147 0xe4, 0x8c, 0x53, 0x91,
148];
149const EMPTY_BLOB_SHA256: [u8; 32] = [
151 0x47, 0x3a, 0x0f, 0x4c, 0x3b, 0xe8, 0xa9, 0x36, 0x81, 0xa2, 0x67, 0xe3, 0xb1, 0xe9, 0xa7, 0xdc,
152 0xda, 0x11, 0x85, 0x43, 0x6f, 0xe1, 0x41, 0xf7, 0x74, 0x91, 0x20, 0xa3, 0x03, 0x72, 0x18, 0x13,
153];
154
155#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
173pub struct ObjectId {
174 bytes: [u8; MAX_OID_LEN],
175 len: u8,
176}
177
178impl ObjectId {
179 #[must_use]
185 pub const fn zero() -> Self {
186 Self {
187 bytes: [0u8; MAX_OID_LEN],
188 len: 20,
189 }
190 }
191
192 #[must_use]
194 pub const fn null(algo: HashAlgo) -> Self {
195 Self {
196 bytes: [0u8; MAX_OID_LEN],
197 len: algo.len() as u8,
198 }
199 }
200
201 pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
208 if HashAlgo::from_len(bytes.len()).is_none() {
209 return Err(Error::InvalidObjectId(hex::encode(bytes)));
210 }
211 let mut buf = [0u8; MAX_OID_LEN];
212 buf[..bytes.len()].copy_from_slice(bytes);
213 Ok(Self {
214 bytes: buf,
215 len: bytes.len() as u8,
216 })
217 }
218
219 #[must_use]
221 pub fn as_bytes(&self) -> &[u8] {
222 &self.bytes[..self.len as usize]
223 }
224
225 #[must_use]
227 pub fn algo(&self) -> HashAlgo {
228 HashAlgo::from_len(self.len as usize).unwrap_or(HashAlgo::Sha1)
229 }
230
231 #[must_use]
233 pub fn is_zero(&self) -> bool {
234 self.as_bytes().iter().all(|&b| b == 0)
235 }
236
237 #[must_use]
239 pub(crate) fn common_prefix_nibbles(&self, other: &Self) -> usize {
240 let (a, b) = (self.as_bytes(), other.as_bytes());
241 match a.iter().zip(b).position(|(x, y)| x != y) {
242 Some(i) => i * 2 + usize::from((a[i] ^ b[i]) & 0xf0 == 0),
243 None => a.len().min(b.len()) * 2,
244 }
245 }
246
247 #[must_use]
249 pub(crate) fn is_empty_blob(&self) -> bool {
250 let bytes = self.as_bytes();
251 bytes == EMPTY_BLOB_SHA1 || bytes == EMPTY_BLOB_SHA256
252 }
253
254 #[must_use]
256 pub(crate) fn is_well_known_empty_tree(&self) -> bool {
257 self.is_canonical_empty_tree()
258 || (self.as_bytes().len() == HashAlgo::Sha1.len()
259 && self.as_bytes() == WELL_KNOWN_EMPTY_TREE_LEGACY)
260 }
261
262 #[must_use]
265 pub(crate) fn is_empty_tree_for(&self, algo: HashAlgo) -> bool {
266 match algo {
267 HashAlgo::Sha1 => self.is_well_known_empty_tree(),
268 HashAlgo::Sha256 => self.as_bytes() == EMPTY_TREE_SHA256,
269 }
270 }
271
272 #[must_use]
274 pub(crate) fn is_canonical_empty_tree(&self) -> bool {
275 self.as_bytes().len() == HashAlgo::Sha1.len()
276 && self.as_bytes() == WELL_KNOWN_EMPTY_TREE_CANON
277 }
278
279 #[must_use]
285 pub const fn empty_tree() -> Self {
286 let mut bytes = [0u8; MAX_OID_LEN];
287 let mut i = 0;
288 while i < WELL_KNOWN_EMPTY_TREE_CANON.len() {
289 bytes[i] = WELL_KNOWN_EMPTY_TREE_CANON[i];
290 i += 1;
291 }
292 Self { bytes, len: 20 }
293 }
294
295 pub(crate) fn push_loose_relative(&self, path: &mut std::path::PathBuf) {
297 const HEX: &[u8; 16] = b"0123456789abcdef";
298 let bytes = self.as_bytes();
299 let prefix = [
300 HEX[(bytes[0] >> 4) as usize],
301 HEX[(bytes[0] & 0xf) as usize],
302 ];
303 path.push(unsafe { std::str::from_utf8_unchecked(&prefix) });
305 let suffix_len = (bytes.len() - 1) * 2;
306 let mut suffix = [0u8; 62];
307 for (i, &b) in bytes[1..].iter().enumerate() {
308 suffix[i * 2] = HEX[(b >> 4) as usize];
309 suffix[i * 2 + 1] = HEX[(b & 0xf) as usize];
310 }
311 path.push(unsafe { std::str::from_utf8_unchecked(&suffix[..suffix_len]) });
312 }
313
314 #[must_use]
316 pub(crate) fn loose_path_in(&self, objects_dir: &std::path::Path) -> std::path::PathBuf {
317 let mut path = objects_dir.to_path_buf();
318 self.push_loose_relative(&mut path);
319 path
320 }
321
322 #[must_use]
324 pub fn to_hex(&self) -> String {
325 hex::encode(self.as_bytes())
326 }
327
328 #[must_use]
332 pub fn loose_prefix(&self) -> String {
333 hex::encode(&self.bytes[..1])
334 }
335
336 pub fn from_hex(s: &str) -> Result<Self> {
343 Self::decode_hex(s.as_bytes()).ok_or_else(|| Error::InvalidObjectId(s.to_owned()))
344 }
345
346 pub fn from_hex_ascii(bytes: &[u8]) -> Result<Self> {
352 if bytes.len() != HashAlgo::Sha1.hex_len() {
353 return Err(Error::InvalidObjectId(
354 String::from_utf8_lossy(bytes).into_owned(),
355 ));
356 }
357 Self::decode_hex(bytes)
358 .ok_or_else(|| Error::InvalidObjectId(String::from_utf8_lossy(bytes).into_owned()))
359 }
360
361 fn decode_hex(hex: &[u8]) -> Option<Self> {
366 let len = hex.len() / 2;
367 if !Self::is_hex_len(hex.len()) {
368 return None;
369 }
370 let mut bytes = [0u8; MAX_OID_LEN];
371 let mut invalid = 0u8;
372 for (out, pair) in bytes.iter_mut().zip(hex.as_chunks::<2>().0) {
373 let hi = HEX_DECODE[usize::from(pair[0])];
374 let lo = HEX_DECODE[usize::from(pair[1])];
375 invalid |= hi | lo;
376 *out = (hi << 4) | lo;
377 }
378 (invalid & HEX_INVALID == 0).then_some(Self {
379 bytes,
380 len: len as u8,
381 })
382 }
383
384 #[must_use]
387 pub fn loose_suffix(&self) -> String {
388 hex::encode(&self.bytes[1..self.len as usize])
389 }
390
391 #[must_use]
394 pub fn is_full_hex(s: &str) -> bool {
395 (s.len() == HashAlgo::Sha1.hex_len() || s.len() == HashAlgo::Sha256.hex_len())
396 && s.bytes().all(|b| b.is_ascii_hexdigit())
397 }
398
399 #[must_use]
401 pub const fn is_hex_len(len: usize) -> bool {
402 len == HashAlgo::Sha1.hex_len() || len == HashAlgo::Sha256.hex_len()
403 }
404
405 #[must_use]
408 pub const fn is_loose_suffix_len(len: usize) -> bool {
409 len == HashAlgo::Sha1.hex_len() - 2 || len == HashAlgo::Sha256.hex_len() - 2
410 }
411}
412
413impl std::hash::Hash for ObjectId {
414 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
415 state.write(self.as_bytes());
416 }
417}
418
419impl fmt::Display for ObjectId {
420 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
421 f.write_str(&self.to_hex())
422 }
423}
424
425impl fmt::Debug for ObjectId {
426 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
427 write!(f, "ObjectId({})", self.to_hex())
428 }
429}
430
431#[cfg(feature = "serde-models")]
432impl serde::Serialize for ObjectId {
433 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
434 where
435 S: serde::Serializer,
436 {
437 serializer.serialize_str(&self.to_hex())
438 }
439}
440
441const HEX_INVALID: u8 = 0x10;
443
444const HEX_DECODE: [u8; 256] = {
446 let mut table = [HEX_INVALID; 256];
447 let mut i = 0u8;
448 while i < 10 {
449 table[(b'0' + i) as usize] = i;
450 i += 1;
451 }
452 let mut i = 0u8;
453 while i < 6 {
454 table[(b'a' + i) as usize] = 10 + i;
455 table[(b'A' + i) as usize] = 10 + i;
456 i += 1;
457 }
458 table
459};
460
461impl FromStr for ObjectId {
462 type Err = Error;
463
464 fn from_str(s: &str) -> Result<Self> {
465 Self::from_hex(s)
466 }
467}
468
469#[derive(Clone, Copy, PartialEq, Eq)]
493pub struct ObjectIdPrefix {
494 bytes: [u8; MAX_OID_LEN],
497 nibbles: u8,
498 algo: HashAlgo,
499}
500
501impl ObjectIdPrefix {
502 pub fn from_hex(hex: &str, algo: HashAlgo) -> Result<Self> {
511 let digits = hex.as_bytes();
512 if digits.len() > algo.hex_len() {
513 return Err(Error::InvalidObjectId(hex.to_owned()));
514 }
515 let mut bytes = [0u8; MAX_OID_LEN];
516 let mut invalid = 0u8;
517 for (i, &digit) in digits.iter().enumerate() {
518 let nibble = HEX_DECODE[usize::from(digit)];
519 invalid |= nibble;
520 let shift = if i.is_multiple_of(2) { 4 } else { 0 };
521 bytes[i / 2] |= (nibble & 0x0f) << shift;
522 }
523 if invalid & HEX_INVALID != 0 {
524 return Err(Error::InvalidObjectId(hex.to_owned()));
525 }
526 Ok(Self {
527 bytes,
528 nibbles: digits.len() as u8,
529 algo,
530 })
531 }
532
533 #[must_use]
535 pub fn nibbles(&self) -> usize {
536 usize::from(self.nibbles)
537 }
538
539 #[must_use]
541 pub fn is_empty(&self) -> bool {
542 self.nibbles == 0
543 }
544
545 #[must_use]
547 pub fn algo(&self) -> HashAlgo {
548 self.algo
549 }
550
551 #[must_use]
556 pub fn matches(&self, oid: &ObjectId) -> bool {
557 self.matches_digest(oid.as_bytes())
558 }
559
560 #[must_use]
562 pub(crate) fn matches_digest(&self, digest: &[u8]) -> bool {
563 let whole = self.nibbles() / 2;
564 let Some(head) = digest.get(..whole) else {
565 return false;
566 };
567 if head != &self.bytes[..whole] {
568 return false;
569 }
570 if self.nibbles.is_multiple_of(2) {
571 return true;
572 }
573 digest
574 .get(whole)
575 .is_some_and(|&b| b & 0xf0 == self.bytes[whole])
576 }
577
578 #[must_use]
580 pub fn lowest(&self) -> ObjectId {
581 ObjectId {
582 bytes: self.bytes,
583 len: self.algo.len() as u8,
584 }
585 }
586
587 #[must_use]
589 pub fn highest(&self) -> ObjectId {
590 let mut bytes = self.bytes;
591 let whole = self.nibbles() / 2;
592 if !self.nibbles.is_multiple_of(2) {
593 bytes[whole] |= 0x0f;
594 }
595 let first_free = whole + usize::from(self.nibbles % 2);
596 bytes[first_free..self.algo.len()].fill(0xff);
597 ObjectId {
598 bytes,
599 len: self.algo.len() as u8,
600 }
601 }
602}
603
604impl fmt::Display for ObjectIdPrefix {
605 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
606 let hex = hex::encode(&self.bytes[..self.nibbles().div_ceil(2)]);
607 f.write_str(&hex[..self.nibbles()])
608 }
609}
610
611impl fmt::Debug for ObjectIdPrefix {
612 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
613 write!(f, "ObjectIdPrefix({self})")
614 }
615}
616
617#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
619pub enum ObjectKind {
620 Blob,
622 Tree,
624 Commit,
626 Tag,
628}
629
630impl ObjectKind {
631 pub fn from_bytes(b: &[u8]) -> Result<Self> {
637 match b {
638 b"blob" => Ok(Self::Blob),
639 b"tree" => Ok(Self::Tree),
640 b"commit" => Ok(Self::Commit),
641 b"tag" => Ok(Self::Tag),
642 other => Err(Error::UnknownObjectType(
643 String::from_utf8_lossy(other).into_owned(),
644 )),
645 }
646 }
647
648 #[must_use]
653 pub fn from_tag_type_field(line: &[u8]) -> Option<Self> {
654 fn keyword_matches(canonical: &[u8], field: &[u8]) -> bool {
655 if field.is_empty() {
656 return false;
657 }
658 for (i, &bc) in field.iter().enumerate() {
659 let sc = canonical.get(i).copied().unwrap_or(0);
660 if sc != bc {
661 return false;
662 }
663 }
664 canonical.get(field.len()).copied().unwrap_or(0) == 0
665 }
666
667 const NAMES: &[(ObjectKind, &[u8])] = &[
668 (ObjectKind::Blob, b"blob"),
669 (ObjectKind::Tree, b"tree"),
670 (ObjectKind::Commit, b"commit"),
671 (ObjectKind::Tag, b"tag"),
672 ];
673 for &(kind, name) in NAMES {
674 if keyword_matches(name, line) {
675 return Some(kind);
676 }
677 }
678 None
679 }
680
681 #[must_use]
683 pub fn as_str(&self) -> &'static str {
684 match self {
685 Self::Blob => "blob",
686 Self::Tree => "tree",
687 Self::Commit => "commit",
688 Self::Tag => "tag",
689 }
690 }
691}
692
693impl fmt::Display for ObjectKind {
694 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
695 f.write_str(self.as_str())
696 }
697}
698
699impl FromStr for ObjectKind {
700 type Err = Error;
701
702 fn from_str(s: &str) -> Result<Self> {
703 Self::from_bytes(s.as_bytes())
704 }
705}
706
707#[derive(Debug, Clone, Copy, PartialEq, Eq)]
709pub struct ObjectInfo {
710 pub kind: ObjectKind,
712 pub size: u64,
714}
715
716#[derive(Debug, Clone)]
718pub struct Object {
719 pub kind: ObjectKind,
721 pub data: Vec<u8>,
723}
724
725#[derive(Debug, Clone, Copy, PartialEq, Eq)]
731pub struct ObjectRef<'a> {
732 pub kind: ObjectKind,
734 pub data: &'a [u8],
736}
737
738impl<'a> ObjectRef<'a> {
739 #[must_use]
741 pub fn from_buf(kind: ObjectKind, buf: &'a [u8]) -> Self {
742 Self { kind, data: buf }
743 }
744}
745
746impl Object {
747 #[must_use]
749 pub fn new(kind: ObjectKind, data: Vec<u8>) -> Self {
750 Self { kind, data }
751 }
752
753 #[must_use]
755 pub fn as_ref(&self) -> ObjectRef<'_> {
756 ObjectRef {
757 kind: self.kind,
758 data: self.data.as_slice(),
759 }
760 }
761
762 #[must_use]
764 pub fn to_store_bytes(&self) -> Vec<u8> {
765 let header = format!("{} {}\0", self.kind, self.data.len());
766 let mut out = Vec::with_capacity(header.len() + self.data.len());
767 out.extend_from_slice(header.as_bytes());
768 out.extend_from_slice(&self.data);
769 out
770 }
771}
772
773#[derive(Debug, Clone, PartialEq, Eq)]
775pub struct TreeEntry {
776 pub mode: u32,
778 pub name: Vec<u8>,
780 pub oid: ObjectId,
782}
783
784impl TreeEntry {
785 #[must_use]
789 pub fn mode_str(&self) -> String {
790 if self.mode == 0o040000 {
792 "40000".to_owned()
793 } else {
794 format!("{:o}", self.mode)
795 }
796 }
797}
798
799#[derive(Debug, Clone, Copy, PartialEq, Eq)]
801pub struct TreeEntryRef<'a> {
802 pub mode: u32,
804 pub name: &'a [u8],
806 pub oid: ObjectId,
808}
809
810impl TreeEntryRef<'_> {
811 #[must_use]
813 pub fn to_owned(&self) -> TreeEntry {
814 TreeEntry {
815 mode: self.mode,
816 name: self.name.to_vec(),
817 oid: self.oid,
818 }
819 }
820}
821
822#[derive(Debug, Clone)]
827pub struct TreeRefIter<'a> {
828 data: &'a [u8],
829 pos: usize,
830 oid_len: usize,
831 failed: bool,
833}
834
835impl<'a> TreeRefIter<'a> {
836 #[must_use]
838 pub fn with_oid_len(data: &'a [u8], oid_len: usize) -> Self {
839 Self {
840 data,
841 pos: 0,
842 oid_len,
843 failed: false,
844 }
845 }
846
847 pub fn infer_oid_len(data: &'a [u8]) -> Result<Self> {
853 match try_parse_tree_oid_len(data, HashAlgo::Sha1.len()) {
854 Ok(()) => Ok(Self::with_oid_len(data, HashAlgo::Sha1.len())),
855 Err(sha1_err) => match try_parse_tree_oid_len(data, HashAlgo::Sha256.len()) {
856 Ok(()) => Ok(Self::with_oid_len(data, HashAlgo::Sha256.len())),
857 Err(_) => Err(sha1_err),
858 },
859 }
860 }
861
862 fn next_entry(&mut self) -> Result<Option<TreeEntryRef<'a>>> {
863 parse_tree_entry_at(self.data, &mut self.pos, self.oid_len)
864 }
865}
866
867fn parse_tree_entry_at<'a>(
869 data: &'a [u8],
870 pos: &mut usize,
871 oid_len: usize,
872) -> Result<Option<TreeEntryRef<'a>>> {
873 if *pos >= data.len() {
874 return Ok(None);
875 }
876 let sp = data[*pos..]
877 .iter()
878 .position(|&b| b == b' ')
879 .ok_or_else(|| Error::CorruptObject("tree entry missing space".to_owned()))?;
880 let mode_bytes = &data[*pos..*pos + sp];
881 let mode = std::str::from_utf8(mode_bytes)
882 .ok()
883 .and_then(|s| u32::from_str_radix(s, 8).ok())
884 .ok_or_else(|| {
885 Error::CorruptObject(format!(
886 "invalid tree mode: {}",
887 String::from_utf8_lossy(mode_bytes)
888 ))
889 })?;
890 *pos += sp + 1;
891
892 let nul = data[*pos..]
893 .iter()
894 .position(|&b| b == 0)
895 .ok_or_else(|| Error::CorruptObject("tree entry missing NUL".to_owned()))?;
896 let name = &data[*pos..*pos + nul];
897 *pos += nul + 1;
898
899 if *pos + oid_len > data.len() {
900 return Err(Error::CorruptObject("tree entry truncated SHA".to_owned()));
901 }
902 let oid = ObjectId::from_bytes(&data[*pos..*pos + oid_len])?;
903 *pos += oid_len;
904
905 Ok(Some(TreeEntryRef { mode, name, oid }))
906}
907
908impl<'a> Iterator for TreeRefIter<'a> {
909 type Item = Result<TreeEntryRef<'a>>;
910
911 fn next(&mut self) -> Option<Self::Item> {
912 if self.failed || self.pos >= self.data.len() {
913 return None;
914 }
915 match self.next_entry() {
916 Ok(Some(entry)) => Some(Ok(entry)),
917 Ok(None) => None,
918 Err(err) => {
919 self.failed = true;
920 self.pos = self.data.len();
921 Some(Err(err))
922 }
923 }
924 }
925}
926
927fn try_parse_tree_oid_len(data: &[u8], oid_len: usize) -> Result<()> {
929 let mut pos = 0usize;
930 while parse_tree_entry_at(data, &mut pos, oid_len)?.is_some() {}
931 Ok(())
932}
933
934fn collect_tree_with_oid_len(data: &[u8], oid_len: usize) -> Result<Vec<TreeEntry>> {
935 let mut pos = 0usize;
936 let mut entries = Vec::with_capacity(data.len() / 32);
937 while let Some(entry) = parse_tree_entry_at(data, &mut pos, oid_len)? {
938 entries.push(TreeEntry {
939 mode: entry.mode,
940 name: entry.name.to_vec(),
941 oid: entry.oid,
942 });
943 }
944 Ok(entries)
945}
946
947pub fn parse_tree(data: &[u8]) -> Result<Vec<TreeEntry>> {
958 match collect_tree_with_oid_len(data, HashAlgo::Sha1.len()) {
959 Ok(entries) => Ok(entries),
960 Err(sha1_err) => {
961 collect_tree_with_oid_len(data, HashAlgo::Sha256.len()).map_err(|_| sha1_err)
962 }
963 }
964}
965
966pub fn parse_tree_with_oid_len(data: &[u8], oid_len: usize) -> Result<Vec<TreeEntry>> {
971 collect_tree_with_oid_len(data, oid_len)
972}
973
974#[must_use]
981pub fn serialize_tree(entries: &[TreeEntry]) -> Vec<u8> {
982 let len = entries
983 .iter()
984 .map(|e| 8 + e.name.len() + e.oid.as_bytes().len())
985 .sum();
986 let mut out = Vec::with_capacity(len);
987 for e in entries {
988 push_tree_mode(&mut out, e.mode);
989 out.push(b' ');
990 out.extend_from_slice(&e.name);
991 out.push(0);
992 out.extend_from_slice(e.oid.as_bytes());
993 }
994 out
995}
996
997fn push_tree_mode(out: &mut Vec<u8>, mode: u32) {
999 let mut digits = [0u8; 11];
1000 let mut start = digits.len();
1001 let mut rest = mode;
1002 loop {
1003 start -= 1;
1004 digits[start] = b'0' + (rest & 7) as u8;
1005 rest >>= 3;
1006 if rest == 0 {
1007 break;
1008 }
1009 }
1010 out.extend_from_slice(&digits[start..]);
1011}
1012
1013#[must_use]
1026pub fn tree_entry_cmp(
1027 a_name: &[u8],
1028 a_is_tree: bool,
1029 b_name: &[u8],
1030 b_is_tree: bool,
1031) -> std::cmp::Ordering {
1032 let a_trailer = if a_is_tree { b'/' } else { 0u8 };
1033 let b_trailer = if b_is_tree { b'/' } else { 0u8 };
1034
1035 let min_len = a_name.len().min(b_name.len());
1036 let cmp = a_name[..min_len].cmp(&b_name[..min_len]);
1037 if cmp != std::cmp::Ordering::Equal {
1038 return cmp;
1039 }
1040 let ac = a_name.get(min_len).copied().unwrap_or(a_trailer);
1042 let bc = b_name.get(min_len).copied().unwrap_or(b_trailer);
1043 ac.cmp(&bc)
1044}
1045
1046fn parse_tree_mode_octal(bytes: &[u8]) -> Option<u32> {
1048 if bytes.is_empty() || bytes.len() > 7 {
1049 return None;
1050 }
1051 bytes.iter().try_fold(0u32, |acc, &b| match b {
1052 b'0'..=b'7' => Some(acc * 8 + u32::from(b - b'0')),
1053 _ => None,
1054 })
1055}
1056
1057pub(crate) fn find_tree_entry_by_name_in_data(
1069 data: &[u8],
1070 oid_len: usize,
1071 name: &[u8],
1072 name_is_tree: bool,
1073) -> Result<Option<TreeEntry>> {
1074 let mut pos = 0usize;
1075 while pos < data.len() {
1076 let rest = &data[pos..];
1077 let sp = memchr::memchr(b' ', rest)
1078 .ok_or_else(|| Error::CorruptObject("tree entry missing space".to_owned()))?;
1079 let mode = parse_tree_mode_octal(&rest[..sp])
1080 .ok_or_else(|| Error::CorruptObject("invalid tree mode".to_owned()))?;
1081 let name_start = sp + 1;
1082 let nul = memchr::memchr(0, &rest[name_start..])
1083 .ok_or_else(|| Error::CorruptObject("tree entry missing NUL".to_owned()))?;
1084 let entry_name = &rest[name_start..name_start + nul];
1085 let oid_start = name_start + nul + 1;
1086 let oid_end = oid_start + oid_len;
1087 if oid_end > rest.len() {
1088 return Err(Error::CorruptObject("tree entry truncated SHA".to_owned()));
1089 }
1090 if entry_name == name {
1091 return Ok(Some(TreeEntry {
1092 mode,
1093 name: entry_name.to_vec(),
1094 oid: ObjectId::from_bytes(&rest[oid_start..oid_end])?,
1095 }));
1096 }
1097 if tree_entry_cmp(entry_name, mode == 0o040000, name, name_is_tree)
1098 == std::cmp::Ordering::Greater
1099 {
1100 return Ok(None);
1101 }
1102 pos += oid_end;
1103 }
1104 Ok(None)
1105}
1106
1107pub(crate) fn find_tree_entry_by_name_dir_or_file_in_data(
1109 data: &[u8],
1110 oid_len: usize,
1111 name: &[u8],
1112) -> Result<Option<TreeEntry>> {
1113 if let Some(entry) = find_tree_entry_by_name_in_data(data, oid_len, name, true)? {
1114 if entry.mode == 0o040000 {
1115 return Ok(Some(entry));
1116 }
1117 }
1118 find_tree_entry_by_name_in_data(data, oid_len, name, false)
1119}
1120
1121#[derive(Debug, Clone, PartialEq, Eq)]
1127pub struct CommitData {
1128 pub tree: ObjectId,
1130 pub parents: Vec<ObjectId>,
1132 pub author: String,
1134 pub committer: String,
1136 pub author_raw: Vec<u8>,
1140 pub committer_raw: Vec<u8>,
1142 pub encoding: Option<String>,
1144 pub message: String,
1146 #[doc = "Optional raw message bytes for non-UTF-8 messages."]
1149 pub raw_message: Option<Vec<u8>>,
1150 pub extra_headers: Vec<u8>,
1156}
1157
1158#[must_use]
1165pub fn commit_subject_pretty(message: &str) -> String {
1166 let mut parts = Vec::new();
1167 for line in message.lines() {
1168 if line.trim().is_empty() {
1169 break;
1170 }
1171 parts.push(line.trim_end());
1172 }
1173 parts.join(" ")
1174}
1175
1176pub fn read_commit_subject_pretty(data: &[u8]) -> Result<String> {
1182 let message_start = data
1183 .windows(2)
1184 .position(|w| w == b"\n\n")
1185 .map(|p| p + 2)
1186 .ok_or_else(|| {
1187 Error::CorruptObject("commit missing blank line before message".to_owned())
1188 })?;
1189 let body = std::str::from_utf8(data.get(message_start..).unwrap_or_default()).unwrap_or("");
1190 Ok(commit_subject_pretty(body))
1191}
1192
1193pub fn read_commit_subject_line(data: &[u8]) -> Result<String> {
1201 let mut pos = 0usize;
1202 let mut encoding: Option<String> = None;
1203 while pos < data.len() {
1204 let line_start = pos;
1205 let mut line_end = pos;
1206 while line_end < data.len() && data[line_end] != b'\n' {
1207 line_end += 1;
1208 }
1209 let line = &data[line_start..line_end];
1210 let after_nl = line_end.saturating_add(1);
1211 if line.is_empty() {
1212 let body = data.get(after_nl..).unwrap_or_default();
1213 let message = commit_encoding::decode_bytes(encoding.as_deref(), body);
1214 return Ok(message
1215 .lines()
1216 .find(|l| !l.trim().is_empty())
1217 .map(str::trim)
1218 .unwrap_or("(no subject)")
1219 .to_owned());
1220 }
1221 if line.first() == Some(&b' ') {
1222 pos = after_nl;
1223 continue;
1224 }
1225 if let Some(rest) = line.strip_prefix(b"encoding ") {
1226 encoding = Some(
1227 std::str::from_utf8(rest)
1228 .map_err(|_| Error::CorruptObject("encoding is not UTF-8".to_owned()))?
1229 .trim()
1230 .to_owned(),
1231 );
1232 }
1233 pos = after_nl;
1234 }
1235 Err(Error::CorruptObject(
1236 "commit missing blank line before message".to_owned(),
1237 ))
1238}
1239
1240pub(crate) fn parse_oid_field(value: &[u8]) -> Result<ObjectId> {
1241 let s = std::str::from_utf8(value)
1242 .map_err(|_| Error::CorruptObject("commit header object id is not UTF-8".to_owned()))?;
1243 s.trim()
1244 .parse::<ObjectId>()
1245 .map_err(|_| Error::CorruptObject("invalid object id in commit header".to_owned()))
1246}
1247
1248pub(crate) fn parse_commit_tree_and_parents_prefix(
1258 prefix: &[u8],
1259 hex_len: usize,
1260) -> Option<(ObjectId, Vec<ObjectId>)> {
1261 canonical_commit_tree_and_parents(prefix, hex_len, false)
1262}
1263
1264pub(crate) fn parse_commit_tree_and_parents_lenient(
1271 data: &[u8],
1272 hex_len: usize,
1273) -> Option<(ObjectId, Vec<ObjectId>)> {
1274 canonical_commit_tree_and_parents(data, hex_len, true)
1275}
1276
1277fn canonical_commit_tree_and_parents(
1278 data: &[u8],
1279 hex_len: usize,
1280 complete: bool,
1281) -> Option<(ObjectId, Vec<ObjectId>)> {
1282 fn header_oid<'a>(rest: &'a [u8], key: &[u8], hex_len: usize) -> Option<(ObjectId, &'a [u8])> {
1283 let line = rest.strip_prefix(key)?;
1284 if line.len() <= hex_len || line[hex_len] != b'\n' {
1285 return None;
1286 }
1287 let oid = ObjectId::from_hex(std::str::from_utf8(&line[..hex_len]).ok()?).ok()?;
1288 Some((oid, &line[hex_len + 1..]))
1289 }
1290 const PARENT: &[u8] = b"parent ";
1291 let (tree, mut rest) = header_oid(data, b"tree ", hex_len)?;
1292 let mut parents = Vec::new();
1293 loop {
1294 if !complete && rest.len() < PARENT.len() && PARENT.starts_with(rest) {
1295 return None;
1296 }
1297 if !rest.starts_with(PARENT) {
1298 return Some((tree, parents));
1299 }
1300 let (parent, next) = header_oid(rest, PARENT, hex_len)?;
1301 parents.push(parent);
1302 rest = next;
1303 }
1304}
1305
1306pub fn parse_commit(data: &[u8]) -> Result<CommitData> {
1312 Ok(super::CommitRef::parse(data)?.to_owned())
1313}
1314
1315#[must_use]
1318pub fn tag_header_field(data: &[u8], prefix: &[u8]) -> Option<String> {
1319 let mut pos = 0usize;
1320 while pos < data.len() {
1321 let rest = &data[pos..];
1322 let nl = rest.iter().position(|&b| b == b'\n');
1323 let line = if let Some(i) = nl { &rest[..i] } else { rest };
1324 if line.is_empty() {
1325 break;
1326 }
1327 if let Some(after) = line.strip_prefix(prefix) {
1328 return Some(String::from_utf8_lossy(after).trim().to_owned());
1329 }
1330 pos += line.len().saturating_add(nl.map(|_| 1).unwrap_or(0));
1331 if nl.is_none() {
1332 break;
1333 }
1334 }
1335 None
1336}
1337
1338#[must_use]
1340pub fn tag_object_line_oid(data: &[u8]) -> Option<ObjectId> {
1341 let s = tag_header_field(data, b"object ")?;
1342 s.parse().ok()
1343}
1344
1345#[derive(Debug, Clone)]
1347pub struct TagData {
1348 pub object: ObjectId,
1350 pub object_type: String,
1352 pub tag: String,
1354 pub tagger: Option<String>,
1356 pub extra_headers: Vec<u8>,
1358 pub message: String,
1360}
1361
1362pub fn parse_tag(data: &[u8]) -> Result<TagData> {
1368 use super::extra_headers::next_header_block;
1369
1370 #[derive(Clone, Copy)]
1371 enum Continuation {
1372 None,
1373 Extra,
1374 }
1375
1376 let mut pos = 0usize;
1377 let mut object = None;
1378 let mut object_type = None;
1379 let mut tag_name = None;
1380 let mut tagger = None;
1381 let mut extra_headers = Vec::new();
1382 let mut cont = Continuation::None;
1383 let mut past_tagger = false;
1384
1385 while pos < data.len() {
1386 let line_start = pos;
1387 let mut line_end = pos;
1388 while line_end < data.len() && data[line_end] != b'\n' {
1389 line_end += 1;
1390 }
1391 let line = &data[line_start..line_end];
1392 let after_nl = (line_end + 1).min(data.len());
1393 if line.is_empty() {
1394 let body = data.get(after_nl..).unwrap_or_default();
1395 let message = std::str::from_utf8(body)
1396 .map_err(|_| Error::CorruptObject("tag message is not valid UTF-8".to_owned()))?
1397 .to_owned();
1398 return Ok(TagData {
1399 object: object
1400 .ok_or_else(|| Error::CorruptObject("tag missing object header".to_owned()))?,
1401 object_type: object_type
1402 .ok_or_else(|| Error::CorruptObject("tag missing type header".to_owned()))?,
1403 tag: tag_name
1404 .ok_or_else(|| Error::CorruptObject("tag missing tag header".to_owned()))?,
1405 tagger,
1406 extra_headers,
1407 message,
1408 });
1409 }
1410
1411 if past_tagger {
1412 let (_, block_end, _) = next_header_block(data, line_start);
1413 extra_headers.extend_from_slice(&data[line_start..block_end]);
1414 pos = block_end;
1415 continue;
1416 }
1417
1418 if line.first() == Some(&b' ') {
1419 match cont {
1420 Continuation::Extra => {
1421 extra_headers.extend_from_slice(&data[line_start..after_nl]);
1422 }
1423 Continuation::None => {}
1424 }
1425 pos = after_nl;
1426 continue;
1427 }
1428
1429 let key_end = line
1430 .iter()
1431 .position(|&b| b == b' ')
1432 .ok_or_else(|| Error::CorruptObject("malformed tag header line".to_owned()))?;
1433 let key = &line[..key_end];
1434 let rest = line.get(key_end + 1..).unwrap_or_default();
1435
1436 match key {
1437 b"object" => {
1438 let line_str = std::str::from_utf8(rest).map_err(|_| {
1439 Error::CorruptObject("tag object line is not valid UTF-8".to_owned())
1440 })?;
1441 object = Some(line_str.trim().parse::<ObjectId>()?);
1442 cont = Continuation::None;
1443 }
1444 b"type" => {
1445 let typ = std::str::from_utf8(rest).map_err(|_| {
1446 Error::CorruptObject("tag type line is not valid UTF-8".to_owned())
1447 })?;
1448 let typ = typ.trim();
1449 if ObjectKind::from_tag_type_field(typ.as_bytes()).is_none() {
1450 return Err(Error::CorruptObject(format!(
1451 "invalid 'type' value in tag: {typ}"
1452 )));
1453 }
1454 object_type = Some(typ.to_owned());
1455 cont = Continuation::None;
1456 }
1457 b"tag" => {
1458 let line_str = std::str::from_utf8(rest).map_err(|_| {
1459 Error::CorruptObject("tag name line is not valid UTF-8".to_owned())
1460 })?;
1461 tag_name = Some(line_str.trim().to_owned());
1462 cont = Continuation::None;
1463 }
1464 b"tagger" => {
1465 tagger = Some(
1466 std::str::from_utf8(rest)
1467 .map_err(|_| {
1468 Error::CorruptObject("tag tagger line is not valid UTF-8".to_owned())
1469 })?
1470 .to_owned(),
1471 );
1472 past_tagger = true;
1473 cont = Continuation::None;
1474 }
1475 _ => {
1476 cont = Continuation::Extra;
1477 extra_headers.extend_from_slice(&data[line_start..after_nl]);
1478 }
1479 }
1480 pos = after_nl;
1481 }
1482
1483 Err(Error::CorruptObject(
1484 "tag missing blank line before message".to_owned(),
1485 ))
1486}
1487
1488#[must_use]
1493pub fn serialize_tag(t: &TagData) -> Vec<u8> {
1494 let mut out = Vec::new();
1495 out.extend_from_slice(format!("object {}\n", t.object).as_bytes());
1496 out.extend_from_slice(format!("type {}\n", t.object_type).as_bytes());
1497 out.extend_from_slice(format!("tag {}\n", t.tag).as_bytes());
1498 if let Some(ref tagger) = t.tagger {
1499 out.extend_from_slice(format!("tagger {tagger}\n").as_bytes());
1500 }
1501 out.extend_from_slice(&t.extra_headers);
1502 out.push(b'\n');
1503 let msg = t.message.trim_end_matches('\n');
1504 if !msg.is_empty() {
1505 out.extend_from_slice(msg.as_bytes());
1506 out.push(b'\n');
1507 }
1508 out
1509}
1510
1511#[must_use]
1521pub fn serialize_commit(c: &CommitData) -> Vec<u8> {
1522 let mut out = Vec::new();
1523 out.extend_from_slice(format!("tree {}\n", c.tree).as_bytes());
1524 for p in &c.parents {
1525 out.extend_from_slice(format!("parent {p}\n").as_bytes());
1526 }
1527 out.extend_from_slice(b"author ");
1528 if c.author_raw.is_empty() {
1529 out.extend_from_slice(c.author.as_bytes());
1530 } else {
1531 out.extend_from_slice(&c.author_raw);
1532 }
1533 out.push(b'\n');
1534 out.extend_from_slice(b"committer ");
1535 if c.committer_raw.is_empty() {
1536 out.extend_from_slice(c.committer.as_bytes());
1537 } else {
1538 out.extend_from_slice(&c.committer_raw);
1539 }
1540 out.push(b'\n');
1541 if let Some(enc) = &c.encoding {
1542 out.extend_from_slice(format!("encoding {enc}\n").as_bytes());
1543 }
1544 out.extend_from_slice(&c.extra_headers);
1545 out.push(b'\n');
1546 if let Some(raw) = &c.raw_message {
1547 out.extend_from_slice(raw);
1548 } else if !c.message.is_empty() {
1549 out.extend_from_slice(c.message.as_bytes());
1550 }
1551 out
1552}
1553
1554#[cfg(test)]
1555mod object_id_prefix_tests {
1556 use super::*;
1557
1558 const ID: &str = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
1559
1560 fn id() -> ObjectId {
1561 ObjectId::from_hex(ID).unwrap()
1562 }
1563
1564 fn prefix(hex: &str) -> ObjectIdPrefix {
1565 ObjectIdPrefix::from_hex(hex, HashAlgo::Sha1).unwrap()
1566 }
1567
1568 #[test]
1569 fn matches_every_length_of_the_id_and_rejects_a_changed_last_digit() {
1570 let id = id();
1571 for n in 0..=ID.len() {
1572 let p = prefix(&ID[..n]);
1573 assert_eq!(p.nibbles(), n);
1574 assert_eq!(p.is_empty(), n == 0);
1575 assert!(p.matches(&id), "{n} digits");
1576 assert_eq!(p.to_string(), ID[..n]);
1577 if n > 0 {
1578 let mut changed = ID[..n].to_owned();
1579 let last = changed.pop().unwrap();
1580 changed.push(if last == '0' { '1' } else { '0' });
1581 assert!(!prefix(&changed).matches(&id), "{changed}");
1582 }
1583 }
1584 }
1585
1586 #[test]
1587 fn odd_prefix_ignores_the_low_nibble_of_its_last_byte() {
1588 let p = prefix("4b8");
1589 assert!(p.matches(&ObjectId::from_hex("4b80000000000000000000000000000000000000").unwrap()));
1590 assert!(p.matches(&ObjectId::from_hex("4b8fffffffffffffffffffffffffffffffffffff").unwrap()));
1591 assert!(
1592 !p.matches(&ObjectId::from_hex("4b90000000000000000000000000000000000000").unwrap())
1593 );
1594 }
1595
1596 #[test]
1597 fn parsing_is_case_insensitive_and_validates() {
1598 assert_eq!(prefix("4B825DC"), prefix("4b825dc"));
1599 assert!(matches!(
1600 ObjectIdPrefix::from_hex("4b8g", HashAlgo::Sha1),
1601 Err(Error::InvalidObjectId(_))
1602 ));
1603 assert!(matches!(
1604 ObjectIdPrefix::from_hex(&format!("{ID}0"), HashAlgo::Sha1),
1605 Err(Error::InvalidObjectId(_))
1606 ));
1607 let sha256 = "a".repeat(64);
1608 assert_eq!(
1609 ObjectIdPrefix::from_hex(&sha256, HashAlgo::Sha256)
1610 .unwrap()
1611 .nibbles(),
1612 64
1613 );
1614 assert_eq!(prefix("").algo(), HashAlgo::Sha1);
1615 }
1616
1617 #[test]
1618 fn longer_prefix_never_matches_a_shorter_digest() {
1619 let long = ObjectIdPrefix::from_hex(&format!("{ID}0000"), HashAlgo::Sha256).unwrap();
1620 assert!(!long.matches(&id()));
1621 assert!(ObjectIdPrefix::from_hex(ID, HashAlgo::Sha256)
1622 .unwrap()
1623 .matches(&id()));
1624 }
1625
1626 #[test]
1627 fn bounds_are_the_first_and_last_matching_ids() {
1628 let p = prefix("4b8");
1629 assert_eq!(
1630 p.lowest().to_hex(),
1631 "4b80000000000000000000000000000000000000"
1632 );
1633 assert_eq!(
1634 p.highest().to_hex(),
1635 "4b8fffffffffffffffffffffffffffffffffffff"
1636 );
1637 let even = prefix("4b82");
1638 assert_eq!(
1639 even.lowest().to_hex(),
1640 "4b82000000000000000000000000000000000000"
1641 );
1642 assert_eq!(
1643 even.highest().to_hex(),
1644 "4b82ffffffffffffffffffffffffffffffffffff"
1645 );
1646 assert_eq!(prefix("").lowest(), ObjectId::null(HashAlgo::Sha1));
1647 assert_eq!(prefix("").highest().to_hex(), "f".repeat(40));
1648 assert_eq!(prefix(ID).lowest(), id());
1649 assert_eq!(prefix(ID).highest(), id());
1650 let sha256 = ObjectIdPrefix::from_hex("abc", HashAlgo::Sha256).unwrap();
1651 assert_eq!(sha256.highest().to_hex(), format!("abc{}", "f".repeat(61)));
1652 assert_eq!(format!("{sha256:?}"), "ObjectIdPrefix(abc)");
1653 }
1654
1655 #[test]
1656 fn common_prefix_nibbles_counts_shared_leading_digits() {
1657 let a = id();
1658 let hex = |s: &str| ObjectId::from_hex(s).unwrap();
1659 assert_eq!(a.common_prefix_nibbles(&a), 40);
1660 assert_eq!(
1661 a.common_prefix_nibbles(&hex("4b825dc642cb6eb9a060e54bf8d69288fbee4905")),
1662 39
1663 );
1664 assert_eq!(
1665 a.common_prefix_nibbles(&hex("4b815dc642cb6eb9a060e54bf8d69288fbee4904")),
1666 3
1667 );
1668 assert_eq!(
1669 a.common_prefix_nibbles(&hex("4b925dc642cb6eb9a060e54bf8d69288fbee4904")),
1670 2
1671 );
1672 assert_eq!(
1673 a.common_prefix_nibbles(&hex("5b825dc642cb6eb9a060e54bf8d69288fbee4904")),
1674 0
1675 );
1676 assert_eq!(
1677 a.common_prefix_nibbles(&hex("4c825dc642cb6eb9a060e54bf8d69288fbee4904")),
1678 1
1679 );
1680 }
1681}
1682
1683#[cfg(test)]
1684mod commit_parse_tests {
1685 use super::*;
1686
1687 #[test]
1688 fn empty_tree_constant_is_canonical_sha1_digest() {
1689 assert_eq!(
1690 ObjectId::empty_tree().to_hex(),
1691 "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
1692 );
1693 }
1694
1695 #[test]
1696 fn is_empty_tree_for_matches_hashed_empty_tree() {
1697 for algo in [HashAlgo::Sha1, HashAlgo::Sha256] {
1698 let hashed =
1699 crate::hash::hash_object(algo, ObjectKind::Tree, b"").expect("hash empty tree");
1700 assert!(hashed.is_empty_tree_for(algo), "{algo:?}");
1701 let other = match algo {
1702 HashAlgo::Sha1 => HashAlgo::Sha256,
1703 HashAlgo::Sha256 => HashAlgo::Sha1,
1704 };
1705 assert!(!hashed.is_empty_tree_for(other), "{algo:?}");
1706 let empty_blob =
1707 crate::hash::hash_object(algo, ObjectKind::Blob, b"").expect("hash empty blob");
1708 assert!(!empty_blob.is_empty_tree_for(algo), "{algo:?}");
1709 }
1710 let legacy = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf899d69f7c6948d4").expect("hex");
1711 assert!(legacy.is_empty_tree_for(HashAlgo::Sha1));
1712 assert!(!legacy.is_empty_tree_for(HashAlgo::Sha256));
1713 }
1714
1715 #[test]
1716 fn serialize_tree_mode_matches_mode_str() {
1717 for mode in [
1718 0,
1719 0o7,
1720 0o100644,
1721 0o100755,
1722 0o120000,
1723 0o040000,
1724 0o160000,
1725 u32::MAX,
1726 ] {
1727 let mut out = Vec::new();
1728 push_tree_mode(&mut out, mode);
1729 let entry = TreeEntry {
1730 mode,
1731 name: Vec::new(),
1732 oid: ObjectId::empty_tree(),
1733 };
1734 assert_eq!(out, entry.mode_str().into_bytes(), "mode {mode:o}");
1735 }
1736 }
1737
1738 #[test]
1739 fn is_empty_blob_matches_both_hash_algorithms() {
1740 for algo in [HashAlgo::Sha1, HashAlgo::Sha256] {
1741 let oid = algo.hash_object(ObjectKind::Blob, b"").unwrap();
1742 assert!(oid.is_empty_blob(), "{algo:?} empty blob {}", oid.to_hex());
1743 let other = algo.hash_object(ObjectKind::Blob, b"x").unwrap();
1744 assert!(!other.is_empty_blob());
1745 }
1746 assert!(!ObjectId::empty_tree().is_empty_blob());
1747 assert!(!ObjectId::zero().is_empty_blob());
1748 }
1749
1750 #[test]
1751 fn commit_subject_pretty_folds_first_paragraph() {
1752 assert_eq!(commit_subject_pretty("hello\nworld\n\nbody"), "hello world");
1753 assert_eq!(commit_subject_pretty(""), "");
1754 assert_eq!(
1755 commit_subject_pretty(" hello world\tthere \n next\t line \n\nbody\n"),
1756 " hello world\tthere next\t line"
1757 );
1758 }
1759
1760 #[test]
1761 fn commit_ref_matches_parse_commit_headers() {
1762 let raw = concat!(
1763 "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
1764 "parent 1111111111111111111111111111111111111111\n",
1765 "author A <a@example.com> 1 +0000\n",
1766 "committer C <c@example.com> 2 +0000\n",
1767 "\n",
1768 "ignored body\n",
1769 );
1770 let full = parse_commit(raw.as_bytes()).expect("parse");
1771 let link = crate::objects::CommitRef::parse(raw.as_bytes()).expect("CommitRef");
1772 assert_eq!(link.tree(), full.tree);
1773 assert_eq!(link.parents(), full.parents.as_slice());
1774 assert_eq!(link.author(), full.author);
1775 assert_eq!(link.committer(), full.committer);
1776 }
1777
1778 #[test]
1779 fn parse_commit_skips_multiline_gpgsig_continuation() {
1780 let raw = concat!(
1781 "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
1782 "author A U Thor <author@example.com> 1 +0000\n",
1783 "committer C O Mitter <committer@example.com> 1 +0000\n",
1784 "gpgsig -----BEGIN PGP SIGNATURE-----\n",
1785 " abcdef\n",
1786 " -----END PGP SIGNATURE-----\n",
1787 "\n",
1788 "msg\n",
1789 );
1790 let c = parse_commit(raw.as_bytes()).expect("parse signed commit");
1791 assert_eq!(c.tree.to_hex(), "4b825dc642cb6eb9a060e54bf8d69288fbee4904");
1792 assert_eq!(c.message, "msg\n");
1793 assert_eq!(serialize_commit(&c), raw.as_bytes());
1794 }
1795
1796 #[test]
1797 fn hex_parsing_accepts_both_cases_and_lengths_and_rejects_the_rest() {
1798 let sha1 = "0123456789abcdefABCDEF0123456789abcdef01";
1799 let oid = ObjectId::from_hex(sha1).unwrap();
1800 assert_eq!(oid.algo(), HashAlgo::Sha1);
1801 assert_eq!(oid.to_hex(), sha1.to_ascii_lowercase());
1802 assert_eq!(ObjectId::from_hex_ascii(sha1.as_bytes()).unwrap(), oid);
1803 assert_eq!(sha1.parse::<ObjectId>().unwrap(), oid);
1804
1805 let sha256 = "f".repeat(64);
1806 let oid = sha256.parse::<ObjectId>().unwrap();
1807 assert_eq!(oid.algo(), HashAlgo::Sha256);
1808 assert_eq!(oid.as_bytes(), [0xff; 32]);
1809 assert!(ObjectId::from_hex_ascii(sha256.as_bytes()).is_err());
1810
1811 for bad in [
1812 "",
1813 &sha1[..39],
1814 &format!("{sha1}0"),
1815 "g123456789abcdef0123456789abcdef01234567",
1816 "0123456789abcdef0123456789abcdef0123456 ",
1817 "0123456789abcdef0123456789abcdef012345/7",
1818 "0123456789abcdef0123456789abcdef012345:7",
1819 "0123456789abcdef0123456789abcdef012345`7",
1820 "0123456789abcdef0123456789abcdef012345G7",
1821 ] {
1822 assert!(
1823 matches!(ObjectId::from_hex(bad), Err(Error::InvalidObjectId(s)) if s == bad),
1824 "{bad:?}"
1825 );
1826 assert!(bad.parse::<ObjectId>().is_err(), "{bad:?}");
1827 assert!(ObjectId::from_hex_ascii(bad.as_bytes()).is_err(), "{bad:?}");
1828 }
1829 let mut non_ascii = sha1.as_bytes().to_vec();
1830 non_ascii[3] = 0xb0;
1831 assert!(ObjectId::from_hex_ascii(&non_ascii).is_err());
1832 }
1833
1834 #[test]
1835 fn serialize_commit_applies_public_field_edits_after_parse() {
1836 let raw = concat!(
1837 "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
1838 "author A <a@example.com> 1 +0000\n",
1839 "committer C <c@example.com> 1 +0000\n",
1840 "\n",
1841 "msg\n",
1842 );
1843 let mut commit = parse_commit(raw.as_bytes()).expect("parse");
1844 let zero = ObjectId::from_hex("0000000000000000000000000000000000000000").unwrap();
1845 commit.tree = zero;
1846 let serialized = serialize_commit(&commit);
1847 let tree_line = format!("tree {}\n", zero.to_hex());
1848 assert!(
1849 serialized.starts_with(tree_line.as_bytes()),
1850 "serialize_commit ignored the updated public tree field"
1851 );
1852 }
1853
1854 #[test]
1855 fn parse_commit_preserves_gpgsig_mergetag_bytes_exactly() {
1856 let raw = concat!(
1857 "tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
1858 "author A <a@example.com> 1 +0000\n",
1859 "committer C <c@example.com> 1 +0000\n",
1860 "encoding ISO-8859-1\n",
1861 "gpgsig -----BEGIN PGP SIGNATURE-----\n",
1862 " line2\n",
1863 " -----END PGP SIGNATURE-----\n",
1864 "mergetag object 4b825dc642cb6eb9a060e54bf8d69288fbee4904\n",
1865 " type commit\n",
1866 " tag merge-tag\n",
1867 " tagger T <t@example.com> 1 +0000\n",
1868 " -----BEGIN PGP SIGNATURE-----\n",
1869 " inner\n",
1870 " -----END PGP SIGNATURE-----\n",
1871 "\n",
1872 "msg-only\n",
1873 );
1874 let parsed = parse_commit(raw.as_bytes()).expect("parse");
1875 assert_eq!(serialize_commit(&parsed), raw.as_bytes());
1876 }
1877
1878 #[test]
1879 fn tree_entry_mode_str_and_tag_object_line_oid() {
1880 let oid = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap();
1881 let entry = TreeEntry {
1882 mode: 0o040000,
1883 name: b"dir".to_vec(),
1884 oid,
1885 };
1886 assert_eq!(entry.mode_str(), "40000");
1887 let tag_body = format!("object {}\ntype commit\ntag t\n\n", oid.to_hex());
1888 assert_eq!(tag_object_line_oid(tag_body.as_bytes()), Some(oid));
1889 }
1890
1891 #[test]
1892 fn parse_commit_non_commit_bytes_does_not_panic() {
1893 let tree_bytes = b"100644 blob 0123456789012345678901234567890123456789\tf";
1894 let err = parse_commit(tree_bytes).expect_err("tree bytes are not a commit");
1895 assert!(matches!(err, Error::CorruptObject(_)));
1896 }
1897
1898 #[test]
1899 fn parse_tree_with_sha256_oid_width() {
1900 let oid_bytes = [0x6eu8; 32];
1901 let oid = ObjectId::from_bytes(&oid_bytes).expect("oid");
1902 let mut raw = b"100644 file\0".to_vec();
1903 raw.extend_from_slice(oid.as_bytes());
1904 let entries = parse_tree_with_oid_len(&raw, HashAlgo::Sha256.len()).expect("tree");
1905 assert_eq!(entries.len(), 1);
1906 assert_eq!(entries[0].oid, oid);
1907 }
1908
1909 #[test]
1910 fn parse_commit_tree_and_parents_prefix_matches_full_parse_or_declines() -> Result<()> {
1911 let tree = "1111111111111111111111111111111111111111";
1912 let p1 = "2222222222222222222222222222222222222222";
1913 let p2 = "3333333333333333333333333333333333333333";
1914 let raw = format!(
1915 "tree {tree}\nparent {p1}\nparent {p2}\nauthor A <a@x> 1 +0000\n\
1916 committer C <c@x> 2 +0000\n\nmessage\n"
1917 );
1918 let commit = parse_commit(raw.as_bytes())?;
1919 let full = (commit.tree, commit.parents);
1920 assert_eq!(full.1.len(), 2);
1921 assert_eq!(
1922 parse_commit_tree_and_parents_lenient(raw.as_bytes(), HashAlgo::Sha1.hex_len()),
1923 Some(full.clone())
1924 );
1925 let hex_len = HashAlgo::Sha1.hex_len();
1926 let header_end = raw.find("author").expect("author line");
1927 for cut in 0..=raw.len() {
1928 let got = parse_commit_tree_and_parents_prefix(&raw.as_bytes()[..cut], hex_len);
1929 if cut > header_end {
1930 assert_eq!(got.as_ref(), Some(&full), "cut at {cut}");
1931 } else {
1932 assert_eq!(
1933 got, None,
1934 "cut at {cut} must not claim a complete parent list"
1935 );
1936 }
1937 }
1938 Ok(())
1939 }
1940
1941 #[test]
1942 fn commit_tree_and_parents_prefix_never_disagrees_with_whole_object() {
1943 let tree = "tree 1111111111111111111111111111111111111111\n";
1944 let parent = "parent 2222222222222222222222222222222222222222\n";
1945 let parent2 = "parent 3333333333333333333333333333333333333333\n";
1946 let bodies = [
1947 format!("{tree}{parent}author A <a@x> 1 +0000\n\nmessage\n"),
1948 format!("{tree}{parent}{parent2}committer C <c@x> 2 +0000\n\nno author\n"),
1949 format!("{tree}{parent}"),
1950 format!("{tree}{parent}{parent2}"),
1951 format!("{tree}author A <a@x> 1 +0000\n{parent}\nlate parent is ignored\n"),
1952 format!("{tree}{parent}not a header\n"),
1953 format!("{tree}parent zz\n\n"),
1954 format!("{tree}{parent}parent 2222"),
1955 format!("author A <a@x> 1 +0000\n{tree}\n"),
1956 ];
1957 let hex_len = HashAlgo::Sha1.hex_len();
1958 for body in &bodies {
1959 let whole = parse_commit_tree_and_parents_lenient(body.as_bytes(), hex_len);
1960 for cut in 0..=body.len() {
1961 if let Some(got) =
1962 parse_commit_tree_and_parents_prefix(&body.as_bytes()[..cut], hex_len)
1963 {
1964 assert_eq!(Some(got), whole, "cut {cut} of {body:?}");
1965 }
1966 }
1967 }
1968 let parents_of = |body: &str| {
1969 parse_commit_tree_and_parents_lenient(body.as_bytes(), hex_len).map(|(_, p)| p.len())
1970 };
1971 assert_eq!(parents_of(&bodies[1]), Some(2), "missing author is fine");
1972 assert_eq!(
1973 parents_of(&bodies[3]),
1974 Some(2),
1975 "object may end after parents"
1976 );
1977 assert_eq!(
1978 parents_of(&bodies[4]),
1979 Some(0),
1980 "only leading parents count"
1981 );
1982 assert_eq!(parents_of(&bodies[6]), None, "malformed parent line");
1983 assert_eq!(parents_of(&bodies[7]), None, "truncated parent line");
1984 assert_eq!(parents_of(&bodies[8]), None, "tree must come first");
1985 }
1986
1987 #[test]
1988 fn parse_commit_tree_and_parents_prefix_handles_root_and_sha256() -> Result<()> {
1989 let root = b"tree 1111111111111111111111111111111111111111\nauthor A <a@x> 1 +0000\n";
1990 let (tree, parents) =
1991 parse_commit_tree_and_parents_prefix(root, HashAlgo::Sha1.hex_len()).expect("root");
1992 assert_eq!(tree.to_hex(), "1111111111111111111111111111111111111111");
1993 assert!(parents.is_empty());
1994
1995 let t = "a".repeat(64);
1996 let p = "b".repeat(64);
1997 let raw = format!("tree {t}\nparent {p}\ncommitter C <c@x> 2 +0000\n");
1998 let (tree, parents) =
1999 parse_commit_tree_and_parents_prefix(raw.as_bytes(), HashAlgo::Sha256.hex_len())
2000 .expect("sha256");
2001 assert_eq!(tree.to_hex(), t);
2002 assert_eq!(parents.len(), 1);
2003 assert_eq!(parents[0].to_hex(), p);
2004
2005 assert!(parse_commit_tree_and_parents_prefix(raw.as_bytes(), 40).is_none());
2006 assert!(parse_commit_tree_and_parents_prefix(b"parent x\n", 40).is_none());
2007 assert!(parse_commit_tree_and_parents_prefix(
2008 b"tree zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz\nauthor\n",
2009 40
2010 )
2011 .is_none());
2012 Ok(())
2013 }
2014
2015 #[test]
2016 fn object_id_loose_path_helpers() {
2017 let oid = ObjectId::from_hex("4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap();
2018 assert_eq!(oid.loose_prefix(), "4b");
2019 assert_eq!(oid.loose_suffix().len(), 38);
2020 assert!(ObjectId::is_hex_len(40));
2021 assert!(ObjectId::is_loose_suffix_len(38));
2022 }
2023
2024 #[test]
2025 fn tree_ref_iter_yields_at_most_one_error() {
2026 let bad = b"100644 incomplete";
2027 let mut iter = TreeRefIter::with_oid_len(bad, HashAlgo::Sha1.len());
2028 assert!(iter.next().unwrap().is_err());
2029 assert!(iter.next().is_none());
2030 }
2031
2032 #[test]
2033 fn tree_ref_iter_public_api_smoke() {
2034 let data = serialize_tree(&[TreeEntry {
2035 mode: 0o100644,
2036 name: b"a".to_vec(),
2037 oid: ObjectId::from_hex("0123456789012345678901234567890123456789").unwrap(),
2038 }]);
2039 let mut iter = TreeRefIter::infer_oid_len(&data).expect("infer");
2040 let entry = iter.next().unwrap().expect("entry");
2041 assert_eq!(entry.name, b"a");
2042 let _owned = entry.to_owned();
2043 let _vec = parse_tree(&data).expect("parse");
2044 let _ = TreeRefIter::with_oid_len(&data, HashAlgo::Sha1.len());
2045 }
2046
2047 #[test]
2048 fn find_tree_entry_by_name_in_data_locates_directory_in_large_tree() -> Result<()> {
2049 let dummy = ObjectId::from_hex("0123456789012345678901234567890123456789")?;
2050 let mut entries: Vec<TreeEntry> = (1..=60)
2051 .map(|i| TreeEntry {
2052 mode: 0o100644,
2053 name: format!("f{i}.txt").into_bytes(),
2054 oid: dummy,
2055 })
2056 .collect();
2057 entries.push(TreeEntry {
2058 mode: 0o040000,
2059 name: b"sub".to_vec(),
2060 oid: dummy,
2061 });
2062 entries.sort_by(|a, b| {
2063 tree_entry_cmp(&a.name, a.mode == 0o040000, &b.name, b.mode == 0o040000)
2064 });
2065 let data = serialize_tree(&entries);
2066 assert!(entries.len() >= 48);
2067 let found = find_tree_entry_by_name_in_data(&data, HashAlgo::Sha1.len(), b"sub", true)?
2068 .expect("sub directory in wide tree");
2069 assert_eq!(found.mode, 0o040000);
2070 assert_eq!(found.name, b"sub");
2071 Ok(())
2072 }
2073
2074 fn sorted_tree_data(entries: &mut [TreeEntry]) -> Vec<u8> {
2075 entries.sort_by(|a, b| {
2076 tree_entry_cmp(&a.name, a.mode == 0o040000, &b.name, b.mode == 0o040000)
2077 });
2078 serialize_tree(entries)
2079 }
2080
2081 #[test]
2082 fn find_tree_entry_by_name_in_data_matches_parse_tree_for_every_name() -> Result<()> {
2083 let oid_a = ObjectId::from_hex("1111111111111111111111111111111111111111")?;
2084 let oid_b = ObjectId::from_hex("2222222222222222222222222222222222222222")?;
2085 let mut entries = vec![
2086 TreeEntry {
2087 mode: 0o040000,
2088 name: b"foo".to_vec(),
2089 oid: oid_a,
2090 },
2091 TreeEntry {
2092 mode: 0o100644,
2093 name: b"foo.c".to_vec(),
2094 oid: oid_b,
2095 },
2096 TreeEntry {
2097 mode: 0o100755,
2098 name: b"foo-bar".to_vec(),
2099 oid: oid_b,
2100 },
2101 TreeEntry {
2102 mode: 0o120000,
2103 name: b"link".to_vec(),
2104 oid: oid_a,
2105 },
2106 TreeEntry {
2107 mode: 0o160000,
2108 name: b"sub".to_vec(),
2109 oid: oid_b,
2110 },
2111 TreeEntry {
2112 mode: 0o100644,
2113 name: b"README".to_vec(),
2114 oid: oid_a,
2115 },
2116 ];
2117 let data = sorted_tree_data(&mut entries);
2118 for expected in parse_tree(&data)? {
2119 let name_is_tree = expected.mode == 0o040000;
2120 let found = find_tree_entry_by_name_in_data(
2121 &data,
2122 HashAlgo::Sha1.len(),
2123 &expected.name,
2124 name_is_tree,
2125 )?
2126 .expect("entry present");
2127 assert_eq!(found, expected);
2128 }
2129 for missing in [&b"fo"[..], b"foo/", b"foo.d", b"zzz", b"", b"AAA", b"lin"] {
2130 assert!(
2131 find_tree_entry_by_name_in_data(&data, HashAlgo::Sha1.len(), missing, false)?
2132 .is_none(),
2133 "unexpected match for {:?}",
2134 String::from_utf8_lossy(missing)
2135 );
2136 }
2137 Ok(())
2138 }
2139
2140 #[test]
2141 fn find_tree_entry_by_name_in_data_stops_after_sorted_position() -> Result<()> {
2142 let oid = ObjectId::from_hex("3333333333333333333333333333333333333333")?;
2143 let mut entries = vec![
2144 TreeEntry {
2145 mode: 0o100644,
2146 name: b"a".to_vec(),
2147 oid,
2148 },
2149 TreeEntry {
2150 mode: 0o100644,
2151 name: b"c".to_vec(),
2152 oid,
2153 },
2154 ];
2155 let mut data = sorted_tree_data(&mut entries);
2156 data.extend_from_slice(b"garbage without separators");
2157 let found = find_tree_entry_by_name_in_data(&data, HashAlgo::Sha1.len(), b"a", false)?;
2158 assert_eq!(found.map(|e| e.name), Some(b"a".to_vec()));
2159 assert!(
2160 find_tree_entry_by_name_in_data(&data, HashAlgo::Sha1.len(), b"b", false)?.is_none()
2161 );
2162 assert!(matches!(
2163 find_tree_entry_by_name_in_data(&data, HashAlgo::Sha1.len(), b"d", false),
2164 Err(Error::CorruptObject(_))
2165 ));
2166 Ok(())
2167 }
2168
2169 #[test]
2170 fn find_tree_entry_by_name_in_data_rejects_bad_mode_and_truncated_oid() {
2171 let bad_mode = b"10x644 a\0aaaaaaaaaaaaaaaaaaaa";
2172 assert!(matches!(
2173 find_tree_entry_by_name_in_data(bad_mode, HashAlgo::Sha1.len(), b"a", false),
2174 Err(Error::CorruptObject(_))
2175 ));
2176 let truncated = b"100644 a\0short";
2177 assert!(matches!(
2178 find_tree_entry_by_name_in_data(truncated, HashAlgo::Sha1.len(), b"a", false),
2179 Err(Error::CorruptObject(_))
2180 ));
2181 }
2182
2183 #[test]
2184 fn find_tree_entry_by_name_in_data_reads_sha256_width() -> Result<()> {
2185 let oid = ObjectId::from_bytes(&[0x5au8; 32])?;
2186 let mut raw = b"100644 file\0".to_vec();
2187 raw.extend_from_slice(oid.as_bytes());
2188 let found = find_tree_entry_by_name_in_data(&raw, HashAlgo::Sha256.len(), b"file", false)?
2189 .expect("file");
2190 assert_eq!(found.oid, oid);
2191 assert_eq!(found.mode, 0o100644);
2192 Ok(())
2193 }
2194}