Skip to main content

lit/core/
objects.rs

1use crate::crypto::signatures::PQSignature;
2use blake3;
3use serde::{Deserialize, Serialize};
4use sha3::{Digest, Sha3_512};
5use std::fmt;
6
7/// Object hash type (SHA3-512 + BLAKE3 composite)
8/// Uses NIST-approved SHA-3 as primary hash with BLAKE3 for quantum resistance
9/// Format: sha3_512(data) || blake3(data) = 128 hex chars (64 + 64)
10#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
11pub struct ObjectHash(pub String);
12
13impl ObjectHash {
14    /// Create a quantum-resistant hash from bytes
15    /// Combines SHA3-512 (NIST FIPS 202) and BLAKE3 for defense-in-depth
16    pub fn from_bytes(data: &[u8]) -> Self {
17        // SHA3-512 (NIST standard, quantum-resistant hash function)
18        let mut sha3_hasher = Sha3_512::new();
19        sha3_hasher.update(data);
20        let sha3_result = sha3_hasher.finalize();
21
22        // BLAKE3 (additional quantum-resistant security)
23        let blake3_result = blake3::hash(data);
24
25        // Combine both hashes for maximum security
26        let combined = format!(
27            "{}{}",
28            hex::encode(sha3_result),
29            hex::encode(blake3_result.as_bytes())
30        );
31        ObjectHash(combined)
32    }
33
34    /// Get the hash as a string
35    pub fn as_str(&self) -> &str {
36        &self.0
37    }
38
39    /// Create from a hex string
40    pub fn from_hex(hex: String) -> Self {
41        ObjectHash(hex)
42    }
43
44    /// Get short hash (first 16 characters for quantum-resistant display)
45    pub fn short(&self) -> String {
46        self.0.chars().take(16).collect()
47    }
48
49    /// Get hash length in characters
50    pub fn len(&self) -> usize {
51        self.0.len()
52    }
53
54    pub fn is_empty(&self) -> bool {
55        self.0.is_empty()
56    }
57}
58
59impl fmt::Display for ObjectHash {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        write!(f, "{}", self.0)
62    }
63}
64
65/// Lit object types
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub enum Object {
68    /// File content
69    Blob(Blob),
70    /// Directory structure
71    Tree(Tree),
72    /// Commit snapshot
73    Commit(Commit),
74    /// Annotated tag
75    Tag(Tag),
76}
77
78impl Object {
79    /// Get the object type name
80    pub fn type_name(&self) -> &str {
81        match self {
82            Object::Blob(_) => "blob",
83            Object::Tree(_) => "tree",
84            Object::Commit(_) => "commit",
85            Object::Tag(_) => "tag",
86        }
87    }
88
89    /// Serialize the object to bytes
90    pub fn to_bytes(&self) -> Vec<u8> {
91        serde_json::to_vec(self).expect("Failed to serialize object")
92    }
93
94    /// Deserialize from bytes
95    pub fn from_bytes(bytes: &[u8]) -> Result<Self, String> {
96        serde_json::from_slice(bytes).map_err(|e| format!("Failed to deserialize object: {}", e))
97    }
98
99    /// Calculate the hash of this object
100    pub fn hash(&self) -> ObjectHash {
101        ObjectHash::from_bytes(&self.to_bytes())
102    }
103}
104
105/// Blob - stores file content
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct Blob {
108    pub content: Vec<u8>,
109}
110
111impl Blob {
112    pub fn new(content: Vec<u8>) -> Self {
113        Blob { content }
114    }
115}
116
117/// Tree entry - represents a file or subdirectory in a tree
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct TreeEntry {
120    pub mode: String,
121    pub name: String,
122    pub hash: ObjectHash,
123    pub object_type: String,
124}
125
126/// Tree - represents a directory structure
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct Tree {
129    pub entries: Vec<TreeEntry>,
130}
131
132impl Default for Tree {
133    fn default() -> Self {
134        Self::new()
135    }
136}
137
138impl Tree {
139    pub fn new() -> Self {
140        Tree {
141            entries: Vec::new(),
142        }
143    }
144
145    pub fn add_entry(&mut self, mode: String, name: String, hash: ObjectHash, object_type: String) {
146        self.entries.push(TreeEntry {
147            mode,
148            name,
149            hash,
150            object_type,
151        });
152    }
153}
154
155/// Commit - represents a snapshot in history with quantum-resistant signatures
156#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct Commit {
158    pub tree: ObjectHash,
159    pub parents: Vec<ObjectHash>,
160    pub author: String,
161    pub committer: String,
162    pub timestamp: i64,
163    pub message: String,
164    /// Optional post-quantum signature (ML-DSA/Dilithium)
165    /// Provides quantum-resistant verification of commit authenticity
166    #[serde(skip_serializing_if = "Option::is_none")]
167    pub pq_signature: Option<PQSignature>,
168    /// Optional metadata (agent annotations, tool context, etc.)
169    #[serde(skip_serializing_if = "Option::is_none")]
170    pub metadata: Option<serde_json::Value>,
171    /// Original Git timezone offset (e.g. `-0700`) for a commit imported from
172    /// Git, so re-exporting it reproduces the same Git hash. Commits Lit
173    /// creates itself record UTC and leave this unset — and because the field
174    /// is skipped when absent, their serialization is unchanged.
175    #[serde(default, skip_serializing_if = "Option::is_none")]
176    pub timezone: Option<String>,
177}
178
179impl Commit {
180    pub fn new(
181        tree: ObjectHash,
182        parents: Vec<ObjectHash>,
183        author: String,
184        message: String,
185    ) -> Self {
186        let timestamp = chrono::Utc::now().timestamp();
187        Commit {
188            tree,
189            parents,
190            author: author.clone(),
191            committer: author,
192            timestamp,
193            message,
194            pq_signature: None, // Can be added after creation
195            metadata: None,
196            timezone: None, // Lit's own commits are UTC
197        }
198    }
199
200    /// Sign this commit with post-quantum signature
201    pub fn sign(&mut self, keypair: &crate::crypto::signatures::PQKeyPair) {
202        // Serialize commit data (excluding signature)
203        let mut commit_data = self.clone();
204        commit_data.pq_signature = None;
205        let data = serde_json::to_vec(&commit_data).expect("Failed to serialize commit");
206
207        // Generate quantum-resistant signature
208        self.pq_signature = Some(keypair.sign(&data));
209    }
210
211    /// Verify post-quantum signature
212    pub fn verify_signature(&self) -> Result<(), String> {
213        match &self.pq_signature {
214            Some(sig) => {
215                // Reconstruct commit without signature
216                let mut commit_data = self.clone();
217                commit_data.pq_signature = None;
218                let data = serde_json::to_vec(&commit_data)
219                    .map_err(|e| format!("Failed to serialize: {}", e))?;
220
221                sig.verify(&data)
222            }
223            None => Err("Commit is not signed".to_string()),
224        }
225    }
226}
227
228/// Tag - annotated tag object with metadata and optional signature
229#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct Tag {
231    /// Hash of the tagged object (usually a commit)
232    pub target: ObjectHash,
233    /// Type of the tagged object
234    pub target_type: String,
235    /// Tag name
236    pub tag_name: String,
237    /// Tagger identity
238    pub tagger: String,
239    /// Creation timestamp
240    pub timestamp: i64,
241    /// Tag message
242    pub message: String,
243    /// Optional post-quantum signature
244    #[serde(skip_serializing_if = "Option::is_none")]
245    pub pq_signature: Option<PQSignature>,
246    /// Optional metadata (agent annotations, tool context, etc.)
247    #[serde(skip_serializing_if = "Option::is_none")]
248    pub metadata: Option<serde_json::Value>,
249    /// Original Git timezone offset for a tag imported from Git, so that
250    /// re-exporting it reproduces the same Git hash. See `Commit::timezone`.
251    #[serde(default, skip_serializing_if = "Option::is_none")]
252    pub timezone: Option<String>,
253}
254
255impl Tag {
256    pub fn new(
257        target: ObjectHash,
258        target_type: String,
259        tag_name: String,
260        tagger: String,
261        message: String,
262    ) -> Self {
263        Tag {
264            target,
265            target_type,
266            tag_name,
267            tagger,
268            timestamp: chrono::Utc::now().timestamp(),
269            message,
270            pq_signature: None,
271            metadata: None,
272            timezone: None, // Lit's own tags are UTC
273        }
274    }
275
276    /// Sign this tag with post-quantum signature
277    pub fn sign(&mut self, keypair: &crate::crypto::signatures::PQKeyPair) {
278        let mut tag_data = self.clone();
279        tag_data.pq_signature = None;
280        let data = serde_json::to_vec(&tag_data).expect("Failed to serialize tag");
281        self.pq_signature = Some(keypair.sign(&data));
282    }
283
284    /// Verify post-quantum signature
285    pub fn verify_signature(&self) -> Result<(), String> {
286        match &self.pq_signature {
287            Some(sig) => {
288                let mut tag_data = self.clone();
289                tag_data.pq_signature = None;
290                let data = serde_json::to_vec(&tag_data)
291                    .map_err(|e| format!("Failed to serialize: {}", e))?;
292                sig.verify(&data)
293            }
294            None => Err("Tag is not signed".to_string()),
295        }
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302
303    #[test]
304    fn test_object_hash() {
305        let data = b"test content";
306        let hash = ObjectHash::from_bytes(data);
307        // SHA3-512 (128 hex) + BLAKE3 (64 hex) = 192 hex chars total
308        // Note: BLAKE3 produces 32 bytes = 64 hex chars
309        assert_eq!(hash.as_str().len(), 192); // Composite quantum-resistant hash
310    }
311
312    #[test]
313    fn test_blob() {
314        let content = b"Hello, world!".to_vec();
315        let blob = Blob::new(content.clone());
316        assert_eq!(blob.content, content);
317    }
318
319    #[test]
320    fn test_tree() {
321        let mut tree = Tree::new();
322        let hash = ObjectHash::from_hex("abc123".to_string());
323        tree.add_entry(
324            "100644".to_string(),
325            "file.txt".to_string(),
326            hash,
327            "blob".to_string(),
328        );
329        assert_eq!(tree.entries.len(), 1);
330    }
331}