1#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
2
3use sley_core::{GitError, ObjectFormat, ObjectId, Result, Signature};
19use std::str::FromStr;
20
21pub use sley_core::BString;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24pub enum ObjectType {
25 Blob,
26 Tree,
27 Commit,
28 Tag,
29}
30
31impl ObjectType {
32 pub const fn as_str(self) -> &'static str {
33 match self {
34 Self::Blob => "blob",
35 Self::Tree => "tree",
36 Self::Commit => "commit",
37 Self::Tag => "tag",
38 }
39 }
40}
41
42impl FromStr for ObjectType {
43 type Err = GitError;
44
45 fn from_str(value: &str) -> Result<Self> {
46 match value {
47 "blob" => Ok(Self::Blob),
48 "tree" => Ok(Self::Tree),
49 "commit" => Ok(Self::Commit),
50 "tag" => Ok(Self::Tag),
51 other => Err(GitError::InvalidObject(format!(
52 "unknown object type {other}"
53 ))),
54 }
55 }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct EncodedObject {
60 pub object_type: ObjectType,
61 pub body: Vec<u8>,
62}
63
64impl EncodedObject {
65 pub fn new(object_type: ObjectType, body: impl Into<Vec<u8>>) -> Self {
72 Self {
73 object_type,
74 body: body.into(),
75 }
76 }
77
78 pub fn framed_bytes(&self) -> Vec<u8> {
80 let mut out = Vec::with_capacity(self.body.len() + 32);
81 out.extend_from_slice(self.object_type.as_str().as_bytes());
82 out.push(b' ');
83 out.extend_from_slice(self.body.len().to_string().as_bytes());
84 out.push(0);
85 out.extend_from_slice(&self.body);
86 out
87 }
88
89 pub fn object_id(&self, format: ObjectFormat) -> Result<ObjectId> {
91 sley_core::object_id_for_bytes(format, self.object_type.as_str(), &self.body)
92 }
93}
94
95#[derive(Debug, Clone, PartialEq, Eq)]
96pub struct Tree {
97 pub entries: Vec<TreeEntry>,
98}
99
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct TreeEntry {
102 pub mode: u32,
103 pub name: BString,
104 pub oid: ObjectId,
105}
106
107#[derive(Debug, Clone, PartialEq, Eq)]
113pub struct TreeEntryRef<'a> {
114 pub mode: u32,
115 pub name: &'a [u8],
116 pub oid: ObjectId,
117}
118
119#[derive(Debug, Clone)]
121pub struct TreeEntries<'a> {
122 format: ObjectFormat,
123 bytes: &'a [u8],
124 offset: usize,
125}
126
127impl<'a> TreeEntries<'a> {
128 pub const fn new(format: ObjectFormat, bytes: &'a [u8]) -> Self {
129 Self {
130 format,
131 bytes,
132 offset: 0,
133 }
134 }
135}
136
137impl<'a> Iterator for TreeEntries<'a> {
138 type Item = Result<TreeEntryRef<'a>>;
139
140 fn next(&mut self) -> Option<Self::Item> {
141 if self.offset >= self.bytes.len() {
142 return None;
143 }
144 match parse_tree_entry_ref(self.format, self.bytes, self.offset) {
145 Ok((entry, next_offset)) => {
146 self.offset = next_offset;
147 Some(Ok(entry))
148 }
149 Err(err) => {
150 self.offset = self.bytes.len();
151 Some(Err(err))
152 }
153 }
154 }
155}
156
157impl<'a> From<TreeEntryRef<'a>> for TreeEntry {
158 fn from(entry: TreeEntryRef<'a>) -> Self {
159 Self {
160 mode: entry.mode,
161 name: entry.name.into(),
162 oid: entry.oid,
163 }
164 }
165}
166
167impl Tree {
168 pub fn parse(format: ObjectFormat, bytes: &[u8]) -> Result<Self> {
169 let entries = TreeEntries::new(format, bytes)
170 .map(|entry| entry.map(TreeEntry::from))
171 .collect::<Result<Vec<_>>>()?;
172 Ok(Self { entries })
173 }
174
175 pub fn write(&self) -> Vec<u8> {
176 let mut out = Vec::new();
177 for entry in &self.entries {
178 out.extend_from_slice(format!("{:o}", entry.mode).as_bytes());
179 out.push(b' ');
180 out.extend_from_slice(entry.name.as_bytes());
181 out.push(0);
182 out.extend_from_slice(entry.oid.as_bytes());
183 }
184 out
185 }
186}
187
188fn parse_tree_entry_ref<'a>(
189 format: ObjectFormat,
190 bytes: &'a [u8],
191 offset: usize,
192) -> Result<(TreeEntryRef<'a>, usize)> {
193 let mode_end = bytes[offset..]
194 .iter()
195 .position(|byte| *byte == b' ')
196 .map(|relative| offset + relative)
197 .ok_or_else(|| GitError::InvalidFormat("unterminated tree mode".into()))?;
198 let mode_text = std::str::from_utf8(&bytes[offset..mode_end])
199 .map_err(|err| GitError::InvalidFormat(err.to_string()))?;
200 let mode = u32::from_str_radix(mode_text, 8)
201 .map_err(|_| GitError::InvalidFormat("invalid tree mode".into()))?;
202
203 let name_start = mode_end + 1;
204 let name_end = bytes[name_start..]
205 .iter()
206 .position(|byte| *byte == 0)
207 .map(|relative| name_start + relative)
208 .ok_or_else(|| GitError::InvalidFormat("unterminated tree path".into()))?;
209 if name_end == name_start {
210 return Err(GitError::InvalidFormat("empty tree path".into()));
211 }
212
213 let oid_start = name_end + 1;
214 let oid_end = oid_start
215 .checked_add(format.raw_len())
216 .ok_or_else(|| GitError::InvalidFormat("tree oid overflow".into()))?;
217 if oid_end > bytes.len() {
218 return Err(GitError::InvalidFormat("truncated tree object id".into()));
219 }
220
221 Ok((
222 TreeEntryRef {
223 mode,
224 name: &bytes[name_start..name_end],
225 oid: ObjectId::from_raw(format, &bytes[oid_start..oid_end])?,
226 },
227 oid_end,
228 ))
229}
230
231pub fn tree_entry_object_type(mode: u32) -> ObjectType {
232 match mode {
233 0o040000 => ObjectType::Tree,
234 0o160000 => ObjectType::Commit,
235 _ => ObjectType::Blob,
236 }
237}
238
239#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
246pub enum EntryKind {
247 Tree,
249 Blob,
251 BlobExecutable,
253 Symlink,
256 Commit,
258}
259
260impl EntryKind {
261 pub const fn mode(self) -> u32 {
263 match self {
264 Self::Tree => 0o040000,
265 Self::Blob => 0o100644,
266 Self::BlobExecutable => 0o100755,
267 Self::Symlink => 0o120000,
268 Self::Commit => 0o160000,
269 }
270 }
271
272 pub const fn from_mode(mode: u32) -> Option<Self> {
275 match mode {
276 0o040000 => Some(Self::Tree),
277 0o100644 => Some(Self::Blob),
278 0o100755 => Some(Self::BlobExecutable),
279 0o120000 => Some(Self::Symlink),
280 0o160000 => Some(Self::Commit),
281 _ => None,
282 }
283 }
284
285 pub const fn object_type(self) -> ObjectType {
288 match self {
289 Self::Tree => ObjectType::Tree,
290 Self::Commit => ObjectType::Commit,
291 _ => ObjectType::Blob,
292 }
293 }
294}
295
296impl From<EntryKind> for u32 {
297 fn from(kind: EntryKind) -> Self {
298 kind.mode()
299 }
300}
301
302impl TreeEntry {
303 pub fn kind(&self) -> Option<EntryKind> {
305 EntryKind::from_mode(self.mode)
306 }
307
308 pub fn is_tree(&self) -> bool {
309 self.mode == EntryKind::Tree.mode()
310 }
311
312 pub fn is_symlink(&self) -> bool {
313 self.mode == EntryKind::Symlink.mode()
314 }
315
316 pub fn is_gitlink(&self) -> bool {
317 self.mode == EntryKind::Commit.mode()
318 }
319
320 pub fn is_executable(&self) -> bool {
321 self.mode == EntryKind::BlobExecutable.mode()
322 }
323}
324
325impl TreeEntryRef<'_> {
326 pub fn kind(&self) -> Option<EntryKind> {
328 EntryKind::from_mode(self.mode)
329 }
330
331 pub fn is_tree(&self) -> bool {
332 self.mode == EntryKind::Tree.mode()
333 }
334
335 pub fn is_symlink(&self) -> bool {
336 self.mode == EntryKind::Symlink.mode()
337 }
338
339 pub fn is_gitlink(&self) -> bool {
340 self.mode == EntryKind::Commit.mode()
341 }
342
343 pub fn is_executable(&self) -> bool {
344 self.mode == EntryKind::BlobExecutable.mode()
345 }
346
347 pub fn to_owned(&self) -> TreeEntry {
348 TreeEntry {
349 mode: self.mode,
350 name: self.name.into(),
351 oid: self.oid,
352 }
353 }
354}
355
356pub fn tree_entry_cmp(
360 left_name: &[u8],
361 left_mode: u32,
362 right_name: &[u8],
363 right_mode: u32,
364) -> std::cmp::Ordering {
365 use std::cmp::Ordering;
366 let shared = left_name.len().min(right_name.len());
367 let name_order = left_name[..shared].cmp(&right_name[..shared]);
368 if name_order != Ordering::Equal {
369 return name_order;
370 }
371 let left_end = left_name.len() == shared;
372 let right_end = right_name.len() == shared;
373 match (left_end, right_end) {
374 (true, true) => Ordering::Equal,
375 (true, false) => tree_name_terminator(left_mode).cmp(&right_name[shared]),
376 (false, true) => left_name[shared].cmp(&tree_name_terminator(right_mode)),
377 (false, false) => Ordering::Equal,
378 }
379}
380
381fn tree_name_terminator(mode: u32) -> u8 {
382 if mode == 0o040000 { b'/' } else { 0 }
383}
384
385#[derive(Debug, Clone, Default)]
392pub struct TreeBuilder {
393 entries: Vec<TreeEntry>,
394}
395
396impl TreeBuilder {
397 pub fn new() -> Self {
398 Self {
399 entries: Vec::new(),
400 }
401 }
402
403 pub fn from_tree(tree: Tree) -> Self {
405 Self {
406 entries: tree.entries,
407 }
408 }
409
410 pub fn upsert(&mut self, name: impl Into<BString>, kind: EntryKind, oid: ObjectId) {
413 self.upsert_raw(name, kind.mode(), oid);
414 }
415
416 pub fn upsert_raw(&mut self, name: impl Into<BString>, mode: u32, oid: ObjectId) {
419 let name = name.into();
420 if let Some(entry) = self
421 .entries
422 .iter_mut()
423 .find(|entry| entry.name == name.as_bytes())
424 {
425 entry.mode = mode;
426 entry.oid = oid;
427 } else {
428 self.entries.push(TreeEntry { mode, name, oid });
429 }
430 }
431
432 pub fn remove(&mut self, name: &[u8]) -> bool {
434 if let Some(position) = self.entries.iter().position(|entry| entry.name == name) {
435 self.entries.swap_remove(position);
436 true
437 } else {
438 false
439 }
440 }
441
442 pub fn is_empty(&self) -> bool {
443 self.entries.is_empty()
444 }
445
446 pub fn len(&self) -> usize {
447 self.entries.len()
448 }
449
450 pub fn build(self) -> Tree {
452 let mut entries = self.entries;
453 entries.sort_by(|left, right| {
454 tree_entry_cmp(
455 left.name.as_bytes(),
456 left.mode,
457 right.name.as_bytes(),
458 right.mode,
459 )
460 });
461 Tree { entries }
462 }
463
464 pub fn write(self) -> Vec<u8> {
466 self.build().write()
467 }
468
469 pub fn object_id(self, format: ObjectFormat) -> Result<ObjectId> {
471 EncodedObject::new(ObjectType::Tree, self.write()).object_id(format)
472 }
473}
474
475#[derive(Debug, Clone, PartialEq, Eq)]
484pub struct Commit {
485 pub tree: ObjectId,
486 pub parents: Vec<ObjectId>,
487 pub author: Vec<u8>,
488 pub committer: Vec<u8>,
489 pub encoding: Option<Vec<u8>>,
490 pub message: Vec<u8>,
491}
492
493#[derive(Debug, Clone, PartialEq, Eq)]
501pub struct CommitRef<'a> {
502 pub tree: ObjectId,
503 pub parents: Vec<ObjectId>,
504 pub author: &'a [u8],
505 pub committer: &'a [u8],
506 pub encoding: Option<&'a [u8]>,
507 pub message: &'a [u8],
508}
509
510impl Commit {
511 pub fn parse(format: ObjectFormat, bytes: &[u8]) -> Result<Self> {
516 Ok(Self::parse_ref(format, bytes)?.into())
517 }
518
519 pub fn parse_ref<'a>(format: ObjectFormat, bytes: &'a [u8]) -> Result<CommitRef<'a>> {
520 CommitRef::parse(format, bytes)
521 }
522
523 pub fn write(&self) -> Vec<u8> {
529 let mut out = Vec::new();
530 out.extend_from_slice(format!("tree {}\n", self.tree).as_bytes());
531 for parent in &self.parents {
532 out.extend_from_slice(format!("parent {parent}\n").as_bytes());
533 }
534 out.extend_from_slice(b"author ");
535 out.extend_from_slice(&self.author);
536 out.push(b'\n');
537 out.extend_from_slice(b"committer ");
538 out.extend_from_slice(&self.committer);
539 if let Some(encoding) = &self.encoding {
540 out.extend_from_slice(b"\nencoding ");
541 out.extend_from_slice(encoding);
542 }
543 out.extend_from_slice(b"\n\n");
544 out.extend_from_slice(&self.message);
545 out
546 }
547
548 pub fn author_signature(&self) -> Option<Signature> {
557 Signature::from_ident_line(&self.author)
558 }
559
560 pub fn committer_signature(&self) -> Option<Signature> {
565 Signature::from_ident_line(&self.committer)
566 }
567}
568
569impl<'a> CommitRef<'a> {
570 pub fn parse(format: ObjectFormat, bytes: &'a [u8]) -> Result<Self> {
571 let split = bytes
572 .windows(2)
573 .position(|window| window == b"\n\n")
574 .ok_or_else(|| GitError::InvalidObject("commit missing message separator".into()))?;
575 let mut tree = None;
576 let mut parents = Vec::new();
577 let mut author = None;
578 let mut committer = None;
579 let mut encoding = None;
580 for line in bytes[..split].split(|byte| *byte == b'\n') {
581 if let Some(value) = line.strip_prefix(b"tree ") {
582 tree = Some(ObjectId::from_hex(format, ascii_header_value(value)?)?);
583 } else if let Some(value) = line.strip_prefix(b"parent ") {
584 parents.push(ObjectId::from_hex(format, ascii_header_value(value)?)?);
585 } else if let Some(value) = line.strip_prefix(b"author ") {
586 author = Some(value);
587 } else if let Some(value) = line.strip_prefix(b"committer ") {
588 committer = Some(value);
589 } else if let Some(value) = line.strip_prefix(b"encoding ") {
590 encoding = Some(value);
591 }
592 }
593 Ok(Self {
594 tree: tree.ok_or_else(|| GitError::InvalidObject("commit missing tree".into()))?,
595 parents,
596 author: author
597 .ok_or_else(|| GitError::InvalidObject("commit missing author".into()))?,
598 committer: committer
599 .ok_or_else(|| GitError::InvalidObject("commit missing committer".into()))?,
600 encoding,
601 message: &bytes[split + 2..],
602 })
603 }
604
605 pub fn to_owned(&self) -> Commit {
606 Commit {
607 tree: self.tree,
608 parents: self.parents.clone(),
609 author: self.author.to_vec(),
610 committer: self.committer.to_vec(),
611 encoding: self.encoding.map(<[u8]>::to_vec),
612 message: self.message.to_vec(),
613 }
614 }
615
616 pub fn author_signature(&self) -> Option<Signature> {
625 Signature::from_ident_line(self.author)
626 }
627
628 pub fn committer_signature(&self) -> Option<Signature> {
633 Signature::from_ident_line(self.committer)
634 }
635}
636
637impl<'a> From<CommitRef<'a>> for Commit {
638 fn from(commit: CommitRef<'a>) -> Self {
639 Self {
640 tree: commit.tree,
641 parents: commit.parents,
642 author: commit.author.to_vec(),
643 committer: commit.committer.to_vec(),
644 encoding: commit.encoding.map(<[u8]>::to_vec),
645 message: commit.message.to_vec(),
646 }
647 }
648}
649
650#[derive(Debug, Clone, Eq)]
657pub struct Tag {
658 pub object: ObjectId,
659 pub object_type: ObjectType,
660 pub name: Vec<u8>,
661 pub tagger: Option<Vec<u8>>,
662 pub message: Vec<u8>,
663 pub raw_body: Option<Vec<u8>>,
664}
665
666#[derive(Debug, Clone, PartialEq, Eq)]
674pub struct TagRef<'a> {
675 pub object: ObjectId,
676 pub object_type: ObjectType,
677 pub name: &'a [u8],
678 pub tagger: Option<&'a [u8]>,
679 pub message: &'a [u8],
680 pub raw_body: Option<&'a [u8]>,
681}
682
683impl PartialEq for Tag {
684 fn eq(&self, other: &Self) -> bool {
685 self.object == other.object
686 && self.object_type == other.object_type
687 && self.name == other.name
688 && self.tagger == other.tagger
689 && self.message == other.message
690 }
691}
692
693impl Tag {
694 pub fn parse(format: ObjectFormat, bytes: &[u8]) -> Result<Self> {
699 Ok(Self::parse_ref(format, bytes)?.into())
700 }
701
702 pub fn parse_ref<'a>(format: ObjectFormat, bytes: &'a [u8]) -> Result<TagRef<'a>> {
703 TagRef::parse(format, bytes)
704 }
705
706 pub fn write(&self) -> Vec<u8> {
712 if let Some(raw) = &self.raw_body {
713 return raw.clone();
714 }
715 let mut out = Vec::new();
716 out.extend_from_slice(format!("object {}\n", self.object).as_bytes());
717 out.extend_from_slice(format!("type {}\n", self.object_type.as_str()).as_bytes());
718 out.extend_from_slice(b"tag ");
719 out.extend_from_slice(&self.name);
720 out.push(b'\n');
721 if let Some(tagger) = &self.tagger {
722 out.extend_from_slice(b"tagger ");
723 out.extend_from_slice(tagger);
724 out.push(b'\n');
725 }
726 out.push(b'\n');
727 out.extend_from_slice(&self.message);
728 out
729 }
730
731 pub fn tagger_signature(&self) -> Option<Signature> {
741 Signature::from_ident_line(self.tagger.as_deref()?)
742 }
743}
744
745impl<'a> TagRef<'a> {
746 pub fn parse(format: ObjectFormat, bytes: &'a [u8]) -> Result<Self> {
747 let split = bytes.windows(2).position(|window| window == b"\n\n");
748 let (headers, message) = match split {
749 Some(split) => (&bytes[..split], &bytes[split + 2..]),
750 None => (bytes, &bytes[bytes.len()..]),
751 };
752 let mut object = None;
753 let mut object_type = None;
754 let mut name = None;
755 let mut tagger = None;
756 for line in headers.split(|byte| *byte == b'\n') {
757 if let Some(value) = line.strip_prefix(b"object ") {
758 object = Some(ObjectId::from_hex(format, ascii_header_value(value)?)?);
759 } else if let Some(value) = line.strip_prefix(b"type ") {
760 object_type = Some(ascii_header_value(value)?.parse()?);
761 } else if let Some(value) = line.strip_prefix(b"tag ") {
762 name = Some(value);
763 } else if let Some(value) = line.strip_prefix(b"tagger ") {
764 tagger = Some(value);
765 }
766 }
767 Ok(Self {
768 object: object.ok_or_else(|| GitError::InvalidObject("tag missing object".into()))?,
769 object_type: object_type
770 .ok_or_else(|| GitError::InvalidObject("tag missing type".into()))?,
771 name: name.ok_or_else(|| GitError::InvalidObject("tag missing name".into()))?,
772 tagger,
773 message,
774 raw_body: Some(bytes),
775 })
776 }
777
778 pub fn to_owned(&self) -> Tag {
779 Tag {
780 object: self.object,
781 object_type: self.object_type,
782 name: self.name.to_vec(),
783 tagger: self.tagger.map(<[u8]>::to_vec),
784 message: self.message.to_vec(),
785 raw_body: self.raw_body.map(<[u8]>::to_vec),
786 }
787 }
788
789 pub fn tagger_signature(&self) -> Option<Signature> {
799 Signature::from_ident_line(self.tagger?)
800 }
801}
802
803impl<'a> From<TagRef<'a>> for Tag {
804 fn from(tag: TagRef<'a>) -> Self {
805 Self {
806 object: tag.object,
807 object_type: tag.object_type,
808 name: tag.name.to_vec(),
809 tagger: tag.tagger.map(<[u8]>::to_vec),
810 message: tag.message.to_vec(),
811 raw_body: tag.raw_body.map(<[u8]>::to_vec),
812 }
813 }
814}
815
816fn ascii_header_value(value: &[u8]) -> Result<&str> {
817 std::str::from_utf8(value).map_err(|err| GitError::InvalidObject(err.to_string()))
818}
819
820pub fn parse_framed_object(bytes: &[u8]) -> Result<EncodedObject> {
821 let nul = bytes
822 .iter()
823 .position(|byte| *byte == 0)
824 .ok_or_else(|| GitError::InvalidObject("missing object header terminator".into()))?;
825 let header = std::str::from_utf8(&bytes[..nul])
826 .map_err(|err| GitError::InvalidObject(err.to_string()))?;
827 let (kind, size) = header
828 .split_once(' ')
829 .ok_or_else(|| GitError::InvalidObject("missing object size".into()))?;
830 let size: usize = size
831 .parse()
832 .map_err(|_| GitError::InvalidObject("invalid object size".into()))?;
833 let body = &bytes[nul + 1..];
834 if body.len() != size {
835 return Err(GitError::InvalidObject(format!(
836 "object declared {size} bytes, found {}",
837 body.len()
838 )));
839 }
840 Ok(EncodedObject::new(kind.parse()?, body.to_vec()))
841}
842
843#[cfg(test)]
844mod tests {
845 use super::*;
846
847 #[test]
848 fn tree_builder_sorts_canonically_and_dedups() {
849 let format = ObjectFormat::Sha1;
850 let blob = ObjectId::empty_blob(format);
851 let subtree = ObjectId::empty_tree(format);
852 assert_eq!(subtree.to_hex(), "4b825dc642cb6eb9a060e54bf8d69288fbee4904");
854 assert_eq!(blob.to_hex(), "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391");
855
856 let mut builder = TreeBuilder::new();
857 builder.upsert("foo", EntryKind::Tree, subtree);
861 builder.upsert("a.txt", EntryKind::Blob, blob.clone());
862 builder.upsert("foo.txt", EntryKind::Blob, blob.clone());
863 builder.upsert("a.txt", EntryKind::BlobExecutable, blob);
865
866 let tree = builder.build();
867 let names: Vec<&[u8]> = tree.entries.iter().map(|e| e.name.as_bytes()).collect();
868 assert_eq!(names, vec![&b"a.txt"[..], &b"foo.txt"[..], &b"foo"[..]]);
869 assert_eq!(tree.entries[0].mode, EntryKind::BlobExecutable.mode());
870 assert!(tree.entries[2].is_tree());
871 }
872
873 #[test]
874 fn entry_kind_round_trips_modes() {
875 for kind in [
876 EntryKind::Tree,
877 EntryKind::Blob,
878 EntryKind::BlobExecutable,
879 EntryKind::Symlink,
880 EntryKind::Commit,
881 ] {
882 assert_eq!(EntryKind::from_mode(kind.mode()), Some(kind));
883 }
884 assert_eq!(EntryKind::from_mode(0o100600), None);
885 }
886
887 #[test]
888 fn framed_object_round_trips() {
889 let object = EncodedObject::new(ObjectType::Blob, b"hello\n".to_vec());
890 assert_eq!(
891 parse_framed_object(&object.framed_bytes()).expect("test operation should succeed"),
892 object
893 );
894 }
895
896 #[test]
897 fn encoded_raw_commit_with_multiline_gpgsig_preserves_bytes_and_id() {
898 let format = ObjectFormat::Sha1;
899 let tree = ObjectId::empty_tree(format);
900 let body = format!(
901 concat!(
902 "tree {tree}\n",
903 "author Signer <signer@example.invalid> 1700000000 +0000\n",
904 "committer Signer <signer@example.invalid> 1700000000 +0000\n",
905 "gpgsig -----BEGIN PGP SIGNATURE-----\n",
906 " \n",
907 " iQEzBAABCgAdFiEErawcommitbytescontract\n",
908 " =abcd\n",
909 " -----END PGP SIGNATURE-----\n",
910 "\n",
911 "signed commit\n",
912 ),
913 tree = tree,
914 )
915 .into_bytes();
916
917 assert_encoded_preserves_framed_bytes_and_id(ObjectType::Commit, body, format);
918 }
919
920 #[test]
921 fn encoded_raw_commit_with_mergetag_and_custom_headers_preserves_bytes_and_id() {
922 let format = ObjectFormat::Sha1;
923 let tree = ObjectId::empty_tree(format);
924 let parent = ObjectId::empty_blob(format);
925 let body = format!(
926 concat!(
927 "tree {tree}\n",
928 "parent {parent}\n",
929 "author Merger <merger@example.invalid> 1700000000 +0000\n",
930 "committer Merger <merger@example.invalid> 1700000001 +0000\n",
931 "x-review-id 42\n",
932 "mergetag object {parent}\n",
933 " type commit\n",
934 " tag imported-v1\n",
935 " tagger Tagger <tagger@example.invalid> 1699999999 +0000\n",
936 " \n",
937 " imported tag body\n",
938 " gpgsig -----BEGIN PGP SIGNATURE-----\n",
939 " nested-signature-line\n",
940 " -----END PGP SIGNATURE-----\n",
941 "x-sley-extra raw bytes stay here\n",
942 "\n",
943 "merge commit\n",
944 ),
945 tree = tree,
946 parent = parent,
947 )
948 .into_bytes();
949
950 assert_encoded_preserves_framed_bytes_and_id(ObjectType::Commit, body, format);
951 }
952
953 #[test]
954 fn encoded_raw_annotated_tag_with_signature_and_custom_headers_preserves_bytes_and_id() {
955 let format = ObjectFormat::Sha1;
956 let object = ObjectId::empty_blob(format);
957 let body = format!(
958 concat!(
959 "object {object}\n",
960 "type blob\n",
961 "tag signed-v1\n",
962 "tagger Tagger <tagger@example.invalid> 1700000000 -0000\n",
963 "x-release-channel stable\n",
964 "gpgsig -----BEGIN PGP SIGNATURE-----\n",
965 " tag-signature-line-1\n",
966 " tag-signature-line-2\n",
967 " -----END PGP SIGNATURE-----\n",
968 "\n",
969 "release notes\n",
970 ),
971 object = object,
972 )
973 .into_bytes();
974
975 assert_encoded_preserves_framed_bytes_and_id(ObjectType::Tag, body, format);
976 }
977
978 #[test]
979 fn tree_round_trips_entries() {
980 let blob = ObjectId::from_hex(
981 ObjectFormat::Sha1,
982 "ce013625030ba8dba906f756967f9e9ca394464a",
983 )
984 .expect("test operation should succeed");
985 let tree = Tree {
986 entries: vec![TreeEntry {
987 mode: 0o100644,
988 name: BString::from(b"hello.txt"),
989 oid: blob,
990 }],
991 };
992 assert_eq!(
993 Tree::parse(ObjectFormat::Sha1, &tree.write()).expect("test operation should succeed"),
994 tree
995 );
996 }
997
998 #[test]
999 fn tree_entries_iterates_without_name_allocations() {
1000 let format = ObjectFormat::Sha1;
1001 let blob = ObjectId::from_hex(format, "ce013625030ba8dba906f756967f9e9ca394464a")
1002 .expect("test operation should succeed");
1003 let subtree = ObjectId::empty_tree(format);
1004 let mut bytes = Vec::new();
1005
1006 let first_name_start = b"100644 ".len();
1007 write_tree_entry(&mut bytes, EntryKind::Blob.mode(), b"hello.txt", &blob);
1008 let second_name_start = bytes.len() + b"40000 ".len();
1009 write_tree_entry(&mut bytes, EntryKind::Tree.mode(), b"src", &subtree);
1010
1011 let mut entries = TreeEntries::new(format, &bytes);
1012 let first = entries
1013 .next()
1014 .expect("first entry")
1015 .expect("test operation should succeed");
1016 assert_eq!(first.mode, EntryKind::Blob.mode());
1017 assert_eq!(first.name, b"hello.txt");
1018 assert_eq!(first.oid, blob);
1019 assert_eq!(first.kind(), Some(EntryKind::Blob));
1020 assert!(std::ptr::eq(
1021 first.name.as_ptr(),
1022 bytes[first_name_start..].as_ptr()
1023 ));
1024
1025 let second = entries
1026 .next()
1027 .expect("second entry")
1028 .expect("test operation should succeed");
1029 assert_eq!(second.mode, EntryKind::Tree.mode());
1030 assert_eq!(second.name, b"src");
1031 assert_eq!(second.oid, subtree);
1032 assert!(second.is_tree());
1033 assert!(std::ptr::eq(
1034 second.name.as_ptr(),
1035 bytes[second_name_start..].as_ptr()
1036 ));
1037 assert!(entries.next().is_none());
1038
1039 let owned = Tree::parse(format, &bytes).expect("test operation should succeed");
1040 assert_eq!(owned.entries, vec![first.to_owned(), second.to_owned()]);
1041 }
1042
1043 #[test]
1044 fn tree_entries_reports_invalid_mode_path_and_truncated_oid() {
1045 let format = ObjectFormat::Sha1;
1046 let oid = ObjectId::empty_blob(format);
1047
1048 let mut invalid_mode = b"10088 bad\0".to_vec();
1049 invalid_mode.extend_from_slice(oid.as_bytes());
1050 assert_invalid_tree_entry(
1051 TreeEntries::new(format, &invalid_mode)
1052 .next()
1053 .expect("invalid mode result"),
1054 "invalid tree mode",
1055 );
1056
1057 let mut empty_path = b"100644 \0".to_vec();
1058 empty_path.extend_from_slice(oid.as_bytes());
1059 assert_invalid_tree_entry(
1060 TreeEntries::new(format, &empty_path)
1061 .next()
1062 .expect("empty path result"),
1063 "empty tree path",
1064 );
1065
1066 let mut truncated_oid = b"100644 bad\0".to_vec();
1067 truncated_oid.extend_from_slice(&oid.as_bytes()[..format.raw_len() - 1]);
1068 assert_invalid_tree_entry(
1069 TreeEntries::new(format, &truncated_oid)
1070 .next()
1071 .expect("truncated oid result"),
1072 "truncated tree object id",
1073 );
1074 }
1075
1076 #[test]
1077 fn tree_entry_ref_kind_helpers_match_entry_kinds() {
1078 let oid = ObjectId::null(ObjectFormat::Sha1);
1079
1080 let tree = TreeEntryRef {
1081 mode: EntryKind::Tree.mode(),
1082 name: b"dir",
1083 oid,
1084 };
1085 assert_eq!(tree.kind(), Some(EntryKind::Tree));
1086 assert!(tree.is_tree());
1087 assert!(!tree.is_symlink());
1088 assert!(!tree.is_gitlink());
1089 assert!(!tree.is_executable());
1090
1091 let symlink = TreeEntryRef {
1092 mode: EntryKind::Symlink.mode(),
1093 name: b"link",
1094 oid,
1095 };
1096 assert_eq!(symlink.kind(), Some(EntryKind::Symlink));
1097 assert!(symlink.is_symlink());
1098 assert!(!symlink.is_tree());
1099 assert!(!symlink.is_gitlink());
1100 assert!(!symlink.is_executable());
1101
1102 let executable = TreeEntryRef {
1103 mode: EntryKind::BlobExecutable.mode(),
1104 name: b"run",
1105 oid,
1106 };
1107 assert_eq!(executable.kind(), Some(EntryKind::BlobExecutable));
1108 assert!(executable.is_executable());
1109 assert!(!executable.is_tree());
1110 assert!(!executable.is_symlink());
1111 assert!(!executable.is_gitlink());
1112
1113 let gitlink = TreeEntryRef {
1114 mode: EntryKind::Commit.mode(),
1115 name: b"submodule",
1116 oid,
1117 };
1118 assert_eq!(gitlink.kind(), Some(EntryKind::Commit));
1119 assert!(gitlink.is_gitlink());
1120 assert!(!gitlink.is_tree());
1121 assert!(!gitlink.is_symlink());
1122 assert!(!gitlink.is_executable());
1123 }
1124
1125 #[test]
1126 fn commit_round_trips_headers_and_message() {
1127 let tree = ObjectId::from_hex(
1128 ObjectFormat::Sha1,
1129 "4b825dc642cb6eb9a060e54bf8d69288fbee4904",
1130 )
1131 .expect("test operation should succeed");
1132 let commit = Commit {
1133 tree,
1134 parents: Vec::new(),
1135 author: b"A U Thor <a@example.invalid> 0 +0000".to_vec(),
1136 committer: b"C O Mitter <c@example.invalid> 0 +0000".to_vec(),
1137 encoding: Some(b"ISO-8859-1".to_vec()),
1138 message: b"subject\n\nbody\n".to_vec(),
1139 };
1140 assert_eq!(
1141 Commit::parse(ObjectFormat::Sha1, &commit.write())
1142 .expect("test operation should succeed"),
1143 commit
1144 );
1145 }
1146
1147 #[test]
1148 fn commit_ref_borrows_headers_and_message() {
1149 let format = ObjectFormat::Sha1;
1150 let tree_hex = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
1151 let parent_hex = "e7556fb3ba7b8f5b1f4772180772a4d6a7323e15";
1152 let body = format!(
1153 "tree {tree_hex}\n\
1154 parent {parent_hex}\n\
1155 author A U Thor <a@example.invalid> 0 +0000\n\
1156 committer C O Mitter <c@example.invalid> 1 -0000\n\
1157 encoding UTF-8\n\
1158 \n\
1159 subject\n\nbody\n"
1160 )
1161 .into_bytes();
1162
1163 let commit = CommitRef::parse(format, &body).expect("test operation should succeed");
1164 assert_eq!(
1165 commit.tree,
1166 ObjectId::from_hex(format, tree_hex).expect("test operation should succeed")
1167 );
1168 assert_eq!(
1169 commit.parents,
1170 vec![ObjectId::from_hex(format, parent_hex).expect("test operation should succeed")]
1171 );
1172 assert_borrows_from(
1173 &body,
1174 commit.author,
1175 b"A U Thor <a@example.invalid> 0 +0000",
1176 );
1177 assert_borrows_from(
1178 &body,
1179 commit.committer,
1180 b"C O Mitter <c@example.invalid> 1 -0000",
1181 );
1182 assert_borrows_from(
1183 &body,
1184 commit.encoding.expect("test operation should succeed"),
1185 b"UTF-8",
1186 );
1187 assert_borrows_from(&body, commit.message, b"subject\n\nbody\n");
1188
1189 assert_eq!(
1190 Commit::parse_ref(format, &body).expect("test operation should succeed"),
1191 commit
1192 );
1193 assert_eq!(
1194 commit.to_owned(),
1195 Commit::parse(format, &body).expect("test operation should succeed")
1196 );
1197 }
1198
1199 #[test]
1200 fn commit_ref_accepts_non_utf8_headers_and_message() {
1201 let format = ObjectFormat::Sha1;
1202 let tree = ObjectId::empty_tree(format);
1203 let mut body = Vec::new();
1204 body.extend_from_slice(format!("tree {tree}\n").as_bytes());
1205 body.extend_from_slice(b"author J\xF6rg <j@example.invalid> 0 +0000\n");
1206 body.extend_from_slice(b"committer M\xFCller <m@example.invalid> 1 +0000\n");
1207 body.extend_from_slice(b"encoding ISO-8859-1\n\n");
1208 body.extend_from_slice(b"caf\xE9\n");
1209
1210 let commit = CommitRef::parse(format, &body).expect("non-utf8 commit parses");
1211 assert_eq!(commit.tree, tree);
1212 assert_borrows_from(&body, commit.author, b"J\xF6rg <j@example.invalid> 0 +0000");
1213 assert_borrows_from(
1214 &body,
1215 commit.committer,
1216 b"M\xFCller <m@example.invalid> 1 +0000",
1217 );
1218 assert_borrows_from(&body, commit.encoding.expect("encoding"), b"ISO-8859-1");
1219 assert_borrows_from(&body, commit.message, b"caf\xE9\n");
1220 assert_eq!(commit.to_owned().write(), body);
1221 }
1222
1223 #[test]
1224 fn commit_ref_rejects_missing_or_malformed_required_headers() {
1225 let format = ObjectFormat::Sha1;
1226 let valid_tree = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
1227 let valid_idents =
1228 b"author A U Thor <a@example.invalid> 0 +0000\ncommitter C O Mitter <c@example.invalid> 0 +0000\n\nmessage\n";
1229 let mut missing_tree = Vec::new();
1230 missing_tree.extend_from_slice(valid_idents);
1231 assert_invalid_object(
1232 CommitRef::parse(format, &missing_tree),
1233 "commit missing tree",
1234 );
1235
1236 let malformed_tree = b"tree not-an-object-id\nauthor A U Thor <a@example.invalid> 0 +0000\ncommitter C O Mitter <c@example.invalid> 0 +0000\n\nmessage\n";
1237 assert!(matches!(
1238 CommitRef::parse(format, malformed_tree),
1239 Err(GitError::InvalidObjectId(_))
1240 ));
1241
1242 let missing_committer =
1243 format!("tree {valid_tree}\nauthor A U Thor <a@example.invalid> 0 +0000\n\nmessage\n")
1244 .into_bytes();
1245 assert_invalid_object(
1246 CommitRef::parse(format, &missing_committer),
1247 "commit missing committer",
1248 );
1249 }
1250
1251 #[test]
1252 fn tag_round_trips_headers_and_message() {
1253 let object = ObjectId::from_hex(
1254 ObjectFormat::Sha1,
1255 "e7556fb3ba7b8f5b1f4772180772a4d6a7323e15",
1256 )
1257 .expect("test operation should succeed");
1258 let tag = Tag {
1259 object,
1260 object_type: ObjectType::Commit,
1261 name: b"v1.0".to_vec(),
1262 tagger: Some(b"Example User <example@example.invalid> 0 +0000".to_vec()),
1263 message: b"release\n".to_vec(),
1264 raw_body: None,
1265 };
1266 assert_eq!(
1267 Tag::parse(ObjectFormat::Sha1, &tag.write()).expect("test operation should succeed"),
1268 tag
1269 );
1270 }
1271
1272 #[test]
1273 fn tag_ref_accepts_non_utf8_tagger_and_message() {
1274 let format = ObjectFormat::Sha1;
1275 let object = ObjectId::empty_blob(format);
1276 let mut body = Vec::new();
1277 body.extend_from_slice(format!("object {object}\n").as_bytes());
1278 body.extend_from_slice(b"type blob\n");
1279 body.extend_from_slice(b"tag v1.0\n");
1280 body.extend_from_slice(b"tagger J\xF6rg <j@example.invalid> 0 +0000\n\n");
1281 body.extend_from_slice(b"caf\xE9\n");
1282
1283 let tag = TagRef::parse(format, &body).expect("non-utf8 tag parses");
1284 assert_eq!(tag.object, object);
1285 assert_eq!(tag.object_type, ObjectType::Blob);
1286 assert_borrows_from(&body, tag.name, b"v1.0");
1287 assert_borrows_from(
1288 &body,
1289 tag.tagger.expect("tagger"),
1290 b"J\xF6rg <j@example.invalid> 0 +0000",
1291 );
1292 assert_borrows_from(&body, tag.message, b"caf\xE9\n");
1293 assert_eq!(tag.to_owned().write(), body);
1294 }
1295
1296 #[test]
1297 fn typed_commit_canonicalizes_but_tag_write_preserves_raw_body() {
1298 let format = ObjectFormat::Sha1;
1299 let tree = ObjectId::empty_tree(format);
1300 let raw_commit = format!(
1301 concat!(
1302 "tree {tree}\n",
1303 "author A U Thor <a@example.invalid> 0 +0000\n",
1304 "x-hidden keep only in raw encoded object\n",
1305 "committer C O Mitter <c@example.invalid> 0 +0000\n",
1306 "gpgsig -----BEGIN PGP SIGNATURE-----\n",
1307 " typed-parser-accepts-this\n",
1308 " -----END PGP SIGNATURE-----\n",
1309 "\n",
1310 "subject\n",
1311 ),
1312 tree = tree,
1313 )
1314 .into_bytes();
1315
1316 let commit = Commit::parse(format, &raw_commit).expect("test operation should succeed");
1317 assert_eq!(commit.tree, tree);
1318 assert_eq!(commit.author, b"A U Thor <a@example.invalid> 0 +0000");
1319 assert_eq!(commit.committer, b"C O Mitter <c@example.invalid> 0 +0000");
1320 assert_eq!(commit.message, b"subject\n");
1321
1322 let written_commit = commit.write();
1323 assert_ne!(written_commit, raw_commit);
1324 assert_bytes_not_contains(&written_commit, b"x-hidden");
1325 assert_bytes_not_contains(&written_commit, b"gpgsig");
1326
1327 let object = ObjectId::empty_blob(format);
1328 let raw_tag = format!(
1329 concat!(
1330 "object {object}\n",
1331 "type blob\n",
1332 "tag v1.0\n",
1333 "x-hidden keep only in raw encoded object\n",
1334 "tagger Example User <example@example.invalid> 0 +0000\n",
1335 "gpgsig -----BEGIN PGP SIGNATURE-----\n",
1336 " typed-parser-accepts-this-too\n",
1337 " -----END PGP SIGNATURE-----\n",
1338 "\n",
1339 "release\n",
1340 ),
1341 object = object,
1342 )
1343 .into_bytes();
1344
1345 let tag = Tag::parse(format, &raw_tag).expect("test operation should succeed");
1346 assert_eq!(tag.object, object);
1347 assert_eq!(tag.object_type, ObjectType::Blob);
1348 assert_eq!(tag.name, b"v1.0");
1349 assert_eq!(
1350 tag.tagger.as_deref(),
1351 Some(&b"Example User <example@example.invalid> 0 +0000"[..])
1352 );
1353 assert_eq!(tag.message, b"release\n");
1354
1355 let written_tag = tag.write();
1356 assert_eq!(written_tag, raw_tag);
1357 let original_oid = EncodedObject::new(ObjectType::Tag, raw_tag).object_id(format);
1358 let written_oid = EncodedObject::new(ObjectType::Tag, written_tag).object_id(format);
1359 assert_eq!(
1360 original_oid.expect("original tag oid"),
1361 written_oid.expect("written tag oid")
1362 );
1363 }
1364
1365 #[test]
1366 fn tag_parse_write_preserves_uppercase_object_and_header_only_body() {
1367 let format = ObjectFormat::Sha1;
1368 let object = ObjectId::empty_blob(format);
1369 let mut raw_tag = Vec::new();
1370 raw_tag.extend_from_slice(
1371 format!("object {}\n", object.to_string().to_uppercase()).as_bytes(),
1372 );
1373 raw_tag.extend_from_slice(b"type blob\n");
1374 raw_tag.extend_from_slice(b"tag v1.0\n");
1375 raw_tag.extend_from_slice(b"tagger Example <example@example.invalid> 0 +0000\n");
1376
1377 let tag = Tag::parse(format, &raw_tag).expect("header-only tag parses");
1378 assert_eq!(tag.object, object);
1379 assert_eq!(tag.message, b"");
1380 assert_eq!(tag.write(), raw_tag);
1381 }
1382
1383 #[test]
1384 fn tag_ref_borrows_name_tagger_and_message() {
1385 let format = ObjectFormat::Sha1;
1386 let object_hex = "e7556fb3ba7b8f5b1f4772180772a4d6a7323e15";
1387 let body = format!(
1388 "object {object_hex}\n\
1389 type commit\n\
1390 tag v1.0-borrowed\n\
1391 tagger Example User <example@example.invalid> 0 +0000\n\
1392 \n\
1393 release notes\n"
1394 )
1395 .into_bytes();
1396
1397 let tag = TagRef::parse(format, &body).expect("test operation should succeed");
1398 assert_eq!(
1399 tag.object,
1400 ObjectId::from_hex(format, object_hex).expect("test operation should succeed")
1401 );
1402 assert_eq!(tag.object_type, ObjectType::Commit);
1403 assert_borrows_from(&body, tag.name, b"v1.0-borrowed");
1404 assert_borrows_from(
1405 &body,
1406 tag.tagger.expect("test operation should succeed"),
1407 b"Example User <example@example.invalid> 0 +0000",
1408 );
1409 assert_borrows_from(&body, tag.message, b"release notes\n");
1410
1411 assert_eq!(
1412 Tag::parse_ref(format, &body).expect("test operation should succeed"),
1413 tag
1414 );
1415 assert_eq!(
1416 tag.to_owned(),
1417 Tag::parse(format, &body).expect("test operation should succeed")
1418 );
1419 }
1420
1421 #[test]
1422 fn tag_ref_rejects_missing_or_malformed_required_headers() {
1423 let format = ObjectFormat::Sha1;
1424 let object_hex = "e7556fb3ba7b8f5b1f4772180772a4d6a7323e15";
1425
1426 let missing_name = format!("object {object_hex}\ntype commit\n\nmessage\n").into_bytes();
1427 assert_invalid_object(TagRef::parse(format, &missing_name), "tag missing name");
1428
1429 let malformed_object = b"object not-an-object-id\ntype commit\ntag v1.0\n\nmessage\n";
1430 assert!(matches!(
1431 TagRef::parse(format, malformed_object),
1432 Err(GitError::InvalidObjectId(_))
1433 ));
1434
1435 let malformed_type =
1436 format!("object {object_hex}\ntype mystery\ntag v1.0\n\nmessage\n").into_bytes();
1437 assert_invalid_object(
1438 TagRef::parse(format, &malformed_type),
1439 "unknown object type mystery",
1440 );
1441 }
1442
1443 #[test]
1444 fn commit_signature_accessors_parse_raw_idents_without_changing_storage() {
1445 let tree = ObjectId::from_hex(
1446 ObjectFormat::Sha1,
1447 "4b825dc642cb6eb9a060e54bf8d69288fbee4904",
1448 )
1449 .expect("test operation should succeed");
1450 let author_raw = b"A U Thor <a@example.invalid> 1700000000 +0530".to_vec();
1451 let committer_raw = b"C O Mitter <c@example.invalid> 1700000001 -0000".to_vec();
1452 let commit = Commit {
1453 tree,
1454 parents: Vec::new(),
1455 author: author_raw.clone(),
1456 committer: committer_raw.clone(),
1457 encoding: None,
1458 message: b"subject\n".to_vec(),
1459 };
1460
1461 let author = commit.author_signature().expect("author parses");
1462 assert_eq!(author.name.as_bytes(), b"A U Thor");
1463 assert_eq!(author.email.as_bytes(), b"a@example.invalid");
1464 assert_eq!(author.time.seconds, 1_700_000_000);
1465 assert_eq!(author.time.timezone_offset_minutes, 330);
1466 assert!(!author.time.negative_utc);
1467 assert_eq!(author.to_ident_bytes(), author_raw);
1469
1470 let committer = commit.committer_signature().expect("committer parses");
1471 assert_eq!(committer.time.seconds, 1_700_000_001);
1472 assert!(committer.time.negative_utc);
1474 assert_eq!(committer.to_ident_bytes(), committer_raw);
1475
1476 assert_eq!(commit.author, author_raw);
1479 assert_eq!(commit.committer, committer_raw);
1480 let written = commit.write();
1481 assert_eq!(
1482 Commit::parse(ObjectFormat::Sha1, &written).expect("test operation should succeed"),
1483 commit
1484 );
1485 }
1486
1487 #[test]
1488 fn commit_signature_accessor_is_none_for_malformed_ident() {
1489 let tree = ObjectId::from_hex(
1490 ObjectFormat::Sha1,
1491 "4b825dc642cb6eb9a060e54bf8d69288fbee4904",
1492 )
1493 .expect("test operation should succeed");
1494 let commit = Commit {
1495 tree,
1496 parents: Vec::new(),
1497 author: b"garbage without an email or time".to_vec(),
1498 committer: b"C O Mitter <c@example.invalid> 0 +0000".to_vec(),
1499 encoding: None,
1500 message: b"x\n".to_vec(),
1501 };
1502 assert!(commit.author_signature().is_none());
1503 assert!(commit.committer_signature().is_some());
1504 }
1505
1506 #[test]
1507 fn tag_signature_accessor_parses_tagger_and_handles_absence() {
1508 let object = ObjectId::from_hex(
1509 ObjectFormat::Sha1,
1510 "e7556fb3ba7b8f5b1f4772180772a4d6a7323e15",
1511 )
1512 .expect("test operation should succeed");
1513 let tagger_raw = b"Example User <example@example.invalid> 1700000000 -0000".to_vec();
1514 let tag = Tag {
1515 object: object.clone(),
1516 object_type: ObjectType::Commit,
1517 name: b"v1.0".to_vec(),
1518 tagger: Some(tagger_raw.clone()),
1519 message: b"release\n".to_vec(),
1520 raw_body: None,
1521 };
1522 let tagger = tag.tagger_signature().expect("tagger parses");
1523 assert_eq!(tagger.name.as_bytes(), b"Example User");
1524 assert!(tagger.time.negative_utc);
1525 assert_eq!(tagger.to_ident_bytes(), tagger_raw);
1526 assert_eq!(tag.tagger.as_deref(), Some(tagger_raw.as_slice()));
1528
1529 let lightweight = Tag {
1531 object,
1532 object_type: ObjectType::Commit,
1533 name: b"v1.0".to_vec(),
1534 tagger: None,
1535 message: b"x\n".to_vec(),
1536 raw_body: None,
1537 };
1538 assert!(lightweight.tagger_signature().is_none());
1539 }
1540
1541 fn write_tree_entry(body: &mut Vec<u8>, mode: u32, name: &[u8], oid: &ObjectId) {
1542 body.extend_from_slice(format!("{:o}", mode).as_bytes());
1543 body.push(b' ');
1544 body.extend_from_slice(name);
1545 body.push(0);
1546 body.extend_from_slice(oid.as_bytes());
1547 }
1548
1549 fn assert_invalid_tree_entry(result: Result<TreeEntryRef<'_>>, expected: &str) {
1550 match result {
1551 Err(GitError::InvalidFormat(message)) => assert_eq!(message, expected),
1552 other => panic!("expected invalid format {expected:?}, got {other:?}"),
1553 }
1554 }
1555
1556 fn assert_invalid_object<T: std::fmt::Debug>(result: Result<T>, expected: &str) {
1557 match result {
1558 Err(GitError::InvalidObject(message)) => assert_eq!(message, expected),
1559 other => panic!("expected invalid object {expected:?}, got {other:?}"),
1560 }
1561 }
1562
1563 fn assert_encoded_preserves_framed_bytes_and_id(
1564 object_type: ObjectType,
1565 body: Vec<u8>,
1566 format: ObjectFormat,
1567 ) {
1568 let object = EncodedObject::new(object_type, body.clone());
1569 let expected_id = object
1570 .object_id(format)
1571 .expect("test operation should succeed");
1572 let framed = object.framed_bytes();
1573
1574 let parsed = parse_framed_object(&framed).expect("test operation should succeed");
1575 assert_eq!(parsed.object_type, object_type);
1576 assert_eq!(parsed.body, body);
1577 assert_eq!(
1578 parsed
1579 .object_id(format)
1580 .expect("test operation should succeed"),
1581 expected_id
1582 );
1583 assert_eq!(parsed.framed_bytes(), framed);
1584 }
1585
1586 fn assert_bytes_not_contains(haystack: &[u8], needle: &[u8]) {
1587 assert!(
1588 !haystack
1589 .windows(needle.len())
1590 .any(|window| window == needle),
1591 "expected bytes not to contain {:?}",
1592 String::from_utf8_lossy(needle)
1593 );
1594 }
1595
1596 fn assert_borrows_from(body: &[u8], slice: &[u8], expected: &[u8]) {
1597 assert_eq!(slice, expected);
1598 let offset = body
1599 .windows(expected.len())
1600 .position(|window| window == expected)
1601 .expect("expected slice appears in body");
1602 assert!(std::ptr::eq(slice.as_ptr(), body[offset..].as_ptr()));
1603 }
1604}