⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/devtools/git/
objects.rs

1//! Git Object Model
2//!
3//! Implements the four Git object types (blob, tree, commit, tag),
4//! SHA-1 object IDs, and object storage/retrieval.
5
6#![allow(clippy::wrong_self_convention)]
7
8use alloc::{
9    string::{String, ToString},
10    vec::Vec,
11};
12
13/// SHA-1 object identifier (20 bytes / 40 hex chars)
14#[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    /// Parse from 40-character hex string
29    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    /// Convert to 40-character hex string
44    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    /// Return first 7 chars of hex (short form)
54    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/// Git object types
83#[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/// A raw Git object (type + data)
119#[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    /// Serialize to Git's loose object format: "type size\0data"
131    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    /// Compute the SHA-1 object ID
140    pub(crate) fn compute_id(&self) -> ObjectId {
141        let serialized = self.serialize();
142        let hash = sha1_hash(&serialized);
143        ObjectId(hash)
144    }
145
146    /// Parse from serialized format
147    pub(crate) fn deserialize(data: &[u8]) -> Option<Self> {
148        // Find null byte separating header from content
149        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/// Blob object (file content)
172#[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/// Tree entry (file mode + name + object ID)
188#[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    /// Check if this is a directory entry
205    pub(crate) fn is_tree(&self) -> bool {
206        self.mode == 0o040000
207    }
208
209    /// Check if this is a regular file
210    pub(crate) fn is_blob(&self) -> bool {
211        self.mode == 0o100644 || self.mode == 0o100755
212    }
213}
214
215/// Tree object (directory listing)
216#[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    /// Serialize to Git tree format: "mode name\0<20-byte-sha1>"
233    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            // Git sorts trees with trailing '/' for comparison
238            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    /// Parse from tree object data
264    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            // Find space after mode
270            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            // Find null after name
276            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            // Read 20-byte SHA-1
281            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/// Person info (author/committer)
296#[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/// Commit object
332#[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    /// Parse from commit object data
370    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        // Collect remaining lines for message
407        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/// Tag object
425#[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    // Format: "Name <email> timestamp +0000"
463    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
487// ---------------------------------------------------------------------------
488// SHA-1 Implementation
489// ---------------------------------------------------------------------------
490
491/// SHA-1 hash (for Git object IDs)
492pub(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    // Pre-processing: add padding
500    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    // Process each 512-bit (64-byte) chunk
514    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// ---------------------------------------------------------------------------
572// Tests
573// ---------------------------------------------------------------------------
574
575#[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}