1#![allow(clippy::wrong_self_convention)]
7
8use alloc::{
9 string::{String, ToString},
10 vec::Vec,
11};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
15pub(crate) struct ObjectId([u8; 20]);
16
17impl ObjectId {
18 pub const ZERO: Self = Self([0u8; 20]);
19
20 pub(crate) fn from_bytes(bytes: &[u8; 20]) -> Self {
21 Self(*bytes)
22 }
23
24 pub(crate) fn as_bytes(&self) -> &[u8; 20] {
25 &self.0
26 }
27
28 pub(crate) fn from_hex(hex: &str) -> Option<Self> {
30 if hex.len() != 40 {
31 return None;
32 }
33 let mut bytes = [0u8; 20];
34 let hex_bytes = hex.as_bytes();
35 for (i, byte) in bytes.iter_mut().enumerate() {
36 let hi = hex_digit(hex_bytes[i * 2])?;
37 let lo = hex_digit(hex_bytes[i * 2 + 1])?;
38 *byte = (hi << 4) | lo;
39 }
40 Some(Self(bytes))
41 }
42
43 pub(crate) fn to_hex(&self) -> String {
45 let mut s = String::with_capacity(40);
46 for &b in &self.0 {
47 s.push(hex_char((b >> 4) & 0x0F));
48 s.push(hex_char(b & 0x0F));
49 }
50 s
51 }
52
53 pub(crate) fn short(&self) -> String {
55 self.to_hex()[..7].to_string()
56 }
57}
58
59impl core::fmt::Display for ObjectId {
60 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
61 write!(f, "{}", self.to_hex())
62 }
63}
64
65fn hex_digit(c: u8) -> Option<u8> {
66 match c {
67 b'0'..=b'9' => Some(c - b'0'),
68 b'a'..=b'f' => Some(c - b'a' + 10),
69 b'A'..=b'F' => Some(c - b'A' + 10),
70 _ => None,
71 }
72}
73
74fn hex_char(n: u8) -> char {
75 if n < 10 {
76 (b'0' + n) as char
77 } else {
78 (b'a' + n - 10) as char
79 }
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub(crate) enum ObjectType {
85 Blob,
86 Tree,
87 Commit,
88 Tag,
89}
90
91impl ObjectType {
92 pub(crate) fn as_str(&self) -> &'static str {
93 match self {
94 Self::Blob => "blob",
95 Self::Tree => "tree",
96 Self::Commit => "commit",
97 Self::Tag => "tag",
98 }
99 }
100
101 pub(crate) fn parse(s: &str) -> Option<Self> {
102 match s {
103 "blob" => Some(Self::Blob),
104 "tree" => Some(Self::Tree),
105 "commit" => Some(Self::Commit),
106 "tag" => Some(Self::Tag),
107 _ => None,
108 }
109 }
110}
111
112impl core::fmt::Display for ObjectType {
113 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
114 write!(f, "{}", self.as_str())
115 }
116}
117
118#[derive(Debug, Clone)]
120pub(crate) struct GitObject {
121 pub(crate) obj_type: ObjectType,
122 pub(crate) data: Vec<u8>,
123}
124
125impl GitObject {
126 pub(crate) fn new(obj_type: ObjectType, data: Vec<u8>) -> Self {
127 Self { obj_type, data }
128 }
129
130 pub(crate) fn serialize(&self) -> Vec<u8> {
132 let header = alloc::format!("{} {}\0", self.obj_type.as_str(), self.data.len());
133 let mut buf = Vec::with_capacity(header.len() + self.data.len());
134 buf.extend_from_slice(header.as_bytes());
135 buf.extend_from_slice(&self.data);
136 buf
137 }
138
139 pub(crate) fn compute_id(&self) -> ObjectId {
141 let serialized = self.serialize();
142 let hash = sha1_hash(&serialized);
143 ObjectId(hash)
144 }
145
146 pub(crate) fn deserialize(data: &[u8]) -> Option<Self> {
148 let null_pos = data.iter().position(|&b| b == 0)?;
150 let header = core::str::from_utf8(&data[..null_pos]).ok()?;
151
152 let space_pos = header.find(' ')?;
153 let type_str = &header[..space_pos];
154 let size_str = &header[space_pos + 1..];
155
156 let obj_type = ObjectType::parse(type_str)?;
157 let size: usize = size_str.parse().ok()?;
158
159 let content = &data[null_pos + 1..];
160 if content.len() != size {
161 return None;
162 }
163
164 Some(Self {
165 obj_type,
166 data: content.to_vec(),
167 })
168 }
169}
170
171#[derive(Debug, Clone)]
173pub(crate) struct Blob {
174 pub(crate) data: Vec<u8>,
175}
176
177impl Blob {
178 pub(crate) fn new(data: Vec<u8>) -> Self {
179 Self { data }
180 }
181
182 pub(crate) fn to_object(&self) -> GitObject {
183 GitObject::new(ObjectType::Blob, self.data.clone())
184 }
185}
186
187#[derive(Debug, Clone)]
189pub(crate) struct TreeEntry {
190 pub(crate) mode: u32,
191 pub(crate) name: String,
192 pub(crate) id: ObjectId,
193}
194
195impl TreeEntry {
196 pub(crate) fn new(mode: u32, name: &str, id: ObjectId) -> Self {
197 Self {
198 mode,
199 name: name.to_string(),
200 id,
201 }
202 }
203
204 pub(crate) fn is_tree(&self) -> bool {
206 self.mode == 0o040000
207 }
208
209 pub(crate) fn is_blob(&self) -> bool {
211 self.mode == 0o100644 || self.mode == 0o100755
212 }
213}
214
215#[derive(Debug, Clone, Default)]
217pub(crate) struct Tree {
218 pub(crate) entries: Vec<TreeEntry>,
219}
220
221impl Tree {
222 pub(crate) fn new() -> Self {
223 Self {
224 entries: Vec::new(),
225 }
226 }
227
228 pub(crate) fn add_entry(&mut self, entry: TreeEntry) {
229 self.entries.push(entry);
230 }
231
232 pub(crate) fn to_object(&self) -> GitObject {
234 let mut data = Vec::new();
235 let mut sorted = self.entries.clone();
236 sorted.sort_by(|a, b| {
237 let a_name = if a.is_tree() {
239 alloc::format!("{}/", a.name)
240 } else {
241 a.name.clone()
242 };
243 let b_name = if b.is_tree() {
244 alloc::format!("{}/", b.name)
245 } else {
246 b.name.clone()
247 };
248 a_name.cmp(&b_name)
249 });
250
251 for entry in &sorted {
252 let mode_str = alloc::format!("{:o}", entry.mode);
253 data.extend_from_slice(mode_str.as_bytes());
254 data.push(b' ');
255 data.extend_from_slice(entry.name.as_bytes());
256 data.push(0);
257 data.extend_from_slice(entry.id.as_bytes());
258 }
259
260 GitObject::new(ObjectType::Tree, data)
261 }
262
263 pub(crate) fn from_data(data: &[u8]) -> Option<Self> {
265 let mut tree = Self::new();
266 let mut pos = 0;
267
268 while pos < data.len() {
269 let space = data[pos..].iter().position(|&b| b == b' ')?;
271 let mode_str = core::str::from_utf8(&data[pos..pos + space]).ok()?;
272 let mode = u32::from_str_radix(mode_str, 8).ok()?;
273 pos += space + 1;
274
275 let null = data[pos..].iter().position(|&b| b == 0)?;
277 let name = core::str::from_utf8(&data[pos..pos + null]).ok()?;
278 pos += null + 1;
279
280 if pos + 20 > data.len() {
282 return None;
283 }
284 let mut id_bytes = [0u8; 20];
285 id_bytes.copy_from_slice(&data[pos..pos + 20]);
286 pos += 20;
287
288 tree.add_entry(TreeEntry::new(mode, name, ObjectId::from_bytes(&id_bytes)));
289 }
290
291 Some(tree)
292 }
293}
294
295#[derive(Debug, Clone)]
297pub(crate) struct Person {
298 pub(crate) name: String,
299 pub(crate) email: String,
300 pub(crate) timestamp: u64,
301 pub(crate) tz_offset: i16,
302}
303
304impl Person {
305 pub(crate) fn new(name: &str, email: &str, timestamp: u64) -> Self {
306 Self {
307 name: name.to_string(),
308 email: email.to_string(),
309 timestamp,
310 tz_offset: 0,
311 }
312 }
313
314 pub(crate) fn format(&self) -> String {
315 let sign = if self.tz_offset >= 0 { '+' } else { '-' };
316 let offset_abs = self.tz_offset.unsigned_abs();
317 let hours = offset_abs / 60;
318 let mins = offset_abs % 60;
319 alloc::format!(
320 "{} <{}> {} {}{:02}{:02}",
321 self.name,
322 self.email,
323 self.timestamp,
324 sign,
325 hours,
326 mins
327 )
328 }
329}
330
331#[derive(Debug, Clone)]
333pub(crate) struct Commit {
334 pub(crate) tree: ObjectId,
335 pub(crate) parents: Vec<ObjectId>,
336 pub(crate) author: Person,
337 pub(crate) committer: Person,
338 pub(crate) message: String,
339}
340
341impl Commit {
342 pub(crate) fn new(tree: ObjectId, author: Person, message: &str) -> Self {
343 Self {
344 tree,
345 parents: Vec::new(),
346 committer: author.clone(),
347 author,
348 message: message.to_string(),
349 }
350 }
351
352 pub(crate) fn to_object(&self) -> GitObject {
353 let mut data = String::new();
354 data.push_str(&alloc::format!("tree {}\n", self.tree));
355 for parent in &self.parents {
356 data.push_str(&alloc::format!("parent {}\n", parent));
357 }
358 data.push_str(&alloc::format!("author {}\n", self.author.format()));
359 data.push_str(&alloc::format!("committer {}\n", self.committer.format()));
360 data.push('\n');
361 data.push_str(&self.message);
362 if !self.message.ends_with('\n') {
363 data.push('\n');
364 }
365
366 GitObject::new(ObjectType::Commit, data.into_bytes())
367 }
368
369 pub(crate) fn from_data(data: &[u8]) -> Option<Self> {
371 let text = core::str::from_utf8(data).ok()?;
372 let mut tree = ObjectId::ZERO;
373 let mut parents = Vec::new();
374 let mut author = Person::new("", "", 0);
375 let mut committer = Person::new("", "", 0);
376
377 let mut lines = text.lines();
378 let mut message_start = false;
379 let mut message = String::new();
380
381 for line in &mut lines {
382 if message_start {
383 if !message.is_empty() {
384 message.push('\n');
385 }
386 message.push_str(line);
387 continue;
388 }
389
390 if line.is_empty() {
391 message_start = true;
392 continue;
393 }
394
395 if let Some(rest) = line.strip_prefix("tree ") {
396 tree = ObjectId::from_hex(rest)?;
397 } else if let Some(rest) = line.strip_prefix("parent ") {
398 parents.push(ObjectId::from_hex(rest)?);
399 } else if let Some(rest) = line.strip_prefix("author ") {
400 author = parse_person(rest)?;
401 } else if let Some(rest) = line.strip_prefix("committer ") {
402 committer = parse_person(rest)?;
403 }
404 }
405
406 for line in lines {
408 if !message.is_empty() {
409 message.push('\n');
410 }
411 message.push_str(line);
412 }
413
414 Some(Self {
415 tree,
416 parents,
417 author,
418 committer,
419 message,
420 })
421 }
422}
423
424#[derive(Debug, Clone)]
426pub(crate) struct Tag {
427 pub(crate) object: ObjectId,
428 pub(crate) obj_type: ObjectType,
429 pub(crate) tag_name: String,
430 pub(crate) tagger: Person,
431 pub(crate) message: String,
432}
433
434impl Tag {
435 pub(crate) fn new(object: ObjectId, tag_name: &str, tagger: Person, message: &str) -> Self {
436 Self {
437 object,
438 obj_type: ObjectType::Commit,
439 tag_name: tag_name.to_string(),
440 tagger,
441 message: message.to_string(),
442 }
443 }
444
445 pub(crate) fn to_object(&self) -> GitObject {
446 let mut data = String::new();
447 data.push_str(&alloc::format!("object {}\n", self.object));
448 data.push_str(&alloc::format!("type {}\n", self.obj_type));
449 data.push_str(&alloc::format!("tag {}\n", self.tag_name));
450 data.push_str(&alloc::format!("tagger {}\n", self.tagger.format()));
451 data.push('\n');
452 data.push_str(&self.message);
453 if !self.message.ends_with('\n') {
454 data.push('\n');
455 }
456
457 GitObject::new(ObjectType::Tag, data.into_bytes())
458 }
459}
460
461fn parse_person(s: &str) -> Option<Person> {
462 let lt = s.find('<')?;
464 let gt = s.find('>')?;
465 let name = s[..lt].trim();
466 let email = &s[lt + 1..gt];
467 let rest = s[gt + 1..].trim();
468
469 let parts: Vec<&str> = rest.split_whitespace().collect();
470 let timestamp: u64 = parts.first()?.parse().ok()?;
471 let tz_str = parts.get(1).unwrap_or(&"+0000");
472
473 let tz_sign = if tz_str.starts_with('-') { -1i16 } else { 1 };
474 let tz_val = &tz_str[1..];
475 let tz_hours: i16 = tz_val.get(..2).unwrap_or("0").parse().unwrap_or(0);
476 let tz_mins: i16 = tz_val.get(2..4).unwrap_or("0").parse().unwrap_or(0);
477 let tz_offset = tz_sign * (tz_hours * 60 + tz_mins);
478
479 Some(Person {
480 name: name.to_string(),
481 email: email.to_string(),
482 timestamp,
483 tz_offset,
484 })
485}
486
487pub(crate) fn sha1_hash(data: &[u8]) -> [u8; 20] {
493 let mut h0: u32 = 0x67452301;
494 let mut h1: u32 = 0xEFCDAB89;
495 let mut h2: u32 = 0x98BADCFE;
496 let mut h3: u32 = 0x10325476;
497 let mut h4: u32 = 0xC3D2E1F0;
498
499 let msg_len = data.len();
501 let bit_len = (msg_len as u64) * 8;
502
503 let mut padded = Vec::with_capacity(msg_len + 72);
504 padded.extend_from_slice(data);
505 padded.push(0x80);
506
507 while (padded.len() % 64) != 56 {
508 padded.push(0);
509 }
510
511 padded.extend_from_slice(&bit_len.to_be_bytes());
512
513 for chunk in padded.chunks(64) {
515 let mut w = [0u32; 80];
516 for i in 0..16 {
517 w[i] = u32::from_be_bytes([
518 chunk[i * 4],
519 chunk[i * 4 + 1],
520 chunk[i * 4 + 2],
521 chunk[i * 4 + 3],
522 ]);
523 }
524 for i in 16..80 {
525 w[i] = (w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16]).rotate_left(1);
526 }
527
528 let mut a = h0;
529 let mut b = h1;
530 let mut c = h2;
531 let mut d = h3;
532 let mut e = h4;
533
534 for (i, &wi) in w.iter().enumerate() {
535 let (f, k) = match i {
536 0..=19 => ((b & c) | ((!b) & d), 0x5A827999u32),
537 20..=39 => (b ^ c ^ d, 0x6ED9EBA1u32),
538 40..=59 => ((b & c) | (b & d) | (c & d), 0x8F1BBCDCu32),
539 _ => (b ^ c ^ d, 0xCA62C1D6u32),
540 };
541
542 let temp = a
543 .rotate_left(5)
544 .wrapping_add(f)
545 .wrapping_add(e)
546 .wrapping_add(k)
547 .wrapping_add(wi);
548 e = d;
549 d = c;
550 c = b.rotate_left(30);
551 b = a;
552 a = temp;
553 }
554
555 h0 = h0.wrapping_add(a);
556 h1 = h1.wrapping_add(b);
557 h2 = h2.wrapping_add(c);
558 h3 = h3.wrapping_add(d);
559 h4 = h4.wrapping_add(e);
560 }
561
562 let mut result = [0u8; 20];
563 result[0..4].copy_from_slice(&h0.to_be_bytes());
564 result[4..8].copy_from_slice(&h1.to_be_bytes());
565 result[8..12].copy_from_slice(&h2.to_be_bytes());
566 result[12..16].copy_from_slice(&h3.to_be_bytes());
567 result[16..20].copy_from_slice(&h4.to_be_bytes());
568 result
569}
570
571#[cfg(test)]
576mod tests {
577 use super::*;
578
579 #[test]
580 fn test_sha1_empty() {
581 let hash = sha1_hash(b"");
582 let expected = ObjectId::from_hex("da39a3ee5e6b4b0d3255bfef95601890afd80709").unwrap();
583 assert_eq!(&hash, expected.as_bytes());
584 }
585
586 #[test]
587 fn test_sha1_hello() {
588 let hash = sha1_hash(b"hello");
589 let oid = ObjectId(hash);
590 assert_eq!(oid.to_hex(), "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d");
591 }
592
593 #[test]
594 fn test_object_id_from_hex() {
595 let hex = "da39a3ee5e6b4b0d3255bfef95601890afd80709";
596 let oid = ObjectId::from_hex(hex).unwrap();
597 assert_eq!(oid.to_hex(), hex);
598 }
599
600 #[test]
601 fn test_object_id_short() {
602 let hex = "da39a3ee5e6b4b0d3255bfef95601890afd80709";
603 let oid = ObjectId::from_hex(hex).unwrap();
604 assert_eq!(oid.short(), "da39a3e");
605 }
606
607 #[test]
608 fn test_object_id_from_hex_invalid() {
609 assert!(ObjectId::from_hex("short").is_none());
610 assert!(ObjectId::from_hex("zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz").is_none());
611 }
612
613 #[test]
614 fn test_object_type_from_str() {
615 assert_eq!(ObjectType::parse("blob"), Some(ObjectType::Blob));
616 assert_eq!(ObjectType::parse("tree"), Some(ObjectType::Tree));
617 assert_eq!(ObjectType::parse("commit"), Some(ObjectType::Commit));
618 assert_eq!(ObjectType::parse("tag"), Some(ObjectType::Tag));
619 assert_eq!(ObjectType::parse("unknown"), None);
620 }
621
622 #[test]
623 fn test_blob_object() {
624 let blob = Blob::new(b"Hello, World!\n".to_vec());
625 let obj = blob.to_object();
626 assert_eq!(obj.obj_type, ObjectType::Blob);
627 assert_eq!(&obj.data, b"Hello, World!\n");
628 }
629
630 #[test]
631 fn test_git_object_serialize_deserialize() {
632 let obj = GitObject::new(ObjectType::Blob, b"test content".to_vec());
633 let serialized = obj.serialize();
634 let deserialized = GitObject::deserialize(&serialized).unwrap();
635 assert_eq!(deserialized.obj_type, ObjectType::Blob);
636 assert_eq!(&deserialized.data, b"test content");
637 }
638
639 #[test]
640 fn test_tree_entry() {
641 let entry = TreeEntry::new(0o100644, "file.txt", ObjectId::ZERO);
642 assert!(entry.is_blob());
643 assert!(!entry.is_tree());
644
645 let dir = TreeEntry::new(0o040000, "subdir", ObjectId::ZERO);
646 assert!(dir.is_tree());
647 assert!(!dir.is_blob());
648 }
649
650 #[test]
651 fn test_tree_serialize_deserialize() {
652 let mut tree = Tree::new();
653 tree.add_entry(TreeEntry::new(0o100644, "hello.txt", ObjectId::ZERO));
654 tree.add_entry(TreeEntry::new(0o040000, "src", ObjectId::ZERO));
655
656 let obj = tree.to_object();
657 let parsed = Tree::from_data(&obj.data).unwrap();
658 assert_eq!(parsed.entries.len(), 2);
659 }
660
661 #[test]
662 fn test_person_format() {
663 let p = Person::new("Alice", "alice@example.com", 1234567890);
664 let formatted = p.format();
665 assert!(formatted.contains("Alice"));
666 assert!(formatted.contains("<alice@example.com>"));
667 assert!(formatted.contains("1234567890"));
668 }
669
670 #[test]
671 fn test_commit_object() {
672 let author = Person::new("Test", "test@test.com", 1000000);
673 let commit = Commit::new(ObjectId::ZERO, author, "Initial commit");
674 let obj = commit.to_object();
675 assert_eq!(obj.obj_type, ObjectType::Commit);
676
677 let parsed = Commit::from_data(&obj.data).unwrap();
678 assert_eq!(parsed.tree, ObjectId::ZERO);
679 assert!(parsed.parents.is_empty());
680 assert_eq!(parsed.author.name, "Test");
681 assert!(parsed.message.contains("Initial commit"));
682 }
683
684 #[test]
685 fn test_commit_with_parent() {
686 let author = Person::new("Dev", "dev@os.com", 2000000);
687 let mut commit = Commit::new(ObjectId::ZERO, author, "Second commit");
688 let parent_hex = "da39a3ee5e6b4b0d3255bfef95601890afd80709";
689 commit.parents.push(ObjectId::from_hex(parent_hex).unwrap());
690
691 let obj = commit.to_object();
692 let parsed = Commit::from_data(&obj.data).unwrap();
693 assert_eq!(parsed.parents.len(), 1);
694 assert_eq!(parsed.parents[0].to_hex(), parent_hex);
695 }
696
697 #[test]
698 fn test_tag_object() {
699 let tagger = Person::new("Release", "rel@os.com", 3000000);
700 let tag = Tag::new(ObjectId::ZERO, "v1.0", tagger, "Release 1.0");
701 let obj = tag.to_object();
702 assert_eq!(obj.obj_type, ObjectType::Tag);
703 }
704
705 #[test]
706 fn test_object_id_display() {
707 let oid = ObjectId::ZERO;
708 let s = alloc::format!("{}", oid);
709 assert_eq!(s, "0000000000000000000000000000000000000000");
710 }
711}