jj workspaces over the network
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(core): add object store - Tree and Blob abstractions

+181
+181
crates/tandem-core/src/object_store.rs
··· 1 + //! Content-addressed object storage for trees and blobs (git-compatible) 2 + 3 + use crate::types::{BlobHash, TreeHash}; 4 + use serde::{Deserialize, Serialize}; 5 + use sha1::{Digest, Sha1}; 6 + use std::collections::HashMap; 7 + 8 + /// File mode (simplified, git-compatible) 9 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 10 + pub enum FileMode { 11 + Regular, // 100644 12 + Executable, // 100755 13 + Symlink, // 120000 14 + Directory, // 040000 15 + } 16 + 17 + impl FileMode { 18 + pub fn as_str(&self) -> &str { 19 + match self { 20 + FileMode::Regular => "100644", 21 + FileMode::Executable => "100755", 22 + FileMode::Symlink => "120000", 23 + FileMode::Directory => "040000", 24 + } 25 + } 26 + } 27 + 28 + /// Reference to either a tree or blob 29 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 30 + pub enum ObjectRef { 31 + Tree(TreeHash), 32 + Blob(BlobHash), 33 + } 34 + 35 + /// Entry in a tree 36 + #[derive(Debug, Clone, Serialize, Deserialize)] 37 + pub struct TreeEntry { 38 + pub name: String, 39 + pub mode: FileMode, 40 + pub hash: ObjectRef, 41 + } 42 + 43 + /// A tree object (directory) 44 + #[derive(Debug, Clone, Serialize, Deserialize)] 45 + pub struct Tree { 46 + pub entries: Vec<TreeEntry>, 47 + } 48 + 49 + impl Tree { 50 + pub fn new() -> Self { 51 + Self { 52 + entries: Vec::new(), 53 + } 54 + } 55 + 56 + pub fn add_entry(&mut self, name: String, mode: FileMode, hash: ObjectRef) { 57 + self.entries.push(TreeEntry { name, mode, hash }); 58 + } 59 + 60 + /// Compute git-compatible hash of this tree 61 + pub fn hash(&self) -> TreeHash { 62 + let mut content = Vec::new(); 63 + 64 + // Sort entries by name (git requirement) 65 + let mut sorted_entries = self.entries.clone(); 66 + sorted_entries.sort_by(|a, b| a.name.cmp(&b.name)); 67 + 68 + // Build tree content in git format 69 + for entry in sorted_entries { 70 + // Mode (as string) 71 + content.extend_from_slice(entry.mode.as_str().as_bytes()); 72 + content.push(b' '); 73 + 74 + // Name 75 + content.extend_from_slice(entry.name.as_bytes()); 76 + content.push(b'\0'); 77 + 78 + // Hash (as raw bytes, 20 bytes for SHA1) 79 + match entry.hash { 80 + ObjectRef::Tree(h) => content.extend_from_slice(&h.0), 81 + ObjectRef::Blob(h) => content.extend_from_slice(&h.0), 82 + } 83 + } 84 + 85 + // Git format: "tree {size}\0{content}" 86 + let header = format!("tree {}\0", content.len()); 87 + let mut full_content = Vec::new(); 88 + full_content.extend_from_slice(header.as_bytes()); 89 + full_content.extend_from_slice(&content); 90 + 91 + // Compute SHA1 92 + let mut hasher = Sha1::new(); 93 + hasher.update(&full_content); 94 + let result = hasher.finalize(); 95 + 96 + // Convert to [u8; 20] 97 + let mut hash_bytes = [0u8; 20]; 98 + hash_bytes.copy_from_slice(&result); 99 + 100 + TreeHash(hash_bytes) 101 + } 102 + } 103 + 104 + impl Default for Tree { 105 + fn default() -> Self { 106 + Self::new() 107 + } 108 + } 109 + 110 + /// Compute git-compatible hash of blob content 111 + pub fn hash_blob(content: &[u8]) -> BlobHash { 112 + // Git format: "blob {size}\0{content}" 113 + let header = format!("blob {}\0", content.len()); 114 + let mut full_content = Vec::new(); 115 + full_content.extend_from_slice(header.as_bytes()); 116 + full_content.extend_from_slice(content); 117 + 118 + // Compute SHA1 119 + let mut hasher = Sha1::new(); 120 + hasher.update(&full_content); 121 + let result = hasher.finalize(); 122 + 123 + // Convert to [u8; 20] 124 + let mut hash_bytes = [0u8; 20]; 125 + hash_bytes.copy_from_slice(&result); 126 + 127 + BlobHash(hash_bytes) 128 + } 129 + 130 + /// Trait for content-addressed object storage 131 + pub trait ObjectStore: Send + Sync { 132 + fn get_tree(&self, hash: &TreeHash) -> Option<Tree>; 133 + fn get_blob(&self, hash: &BlobHash) -> Option<Vec<u8>>; 134 + fn put_tree(&mut self, tree: &Tree) -> TreeHash; 135 + fn put_blob(&mut self, content: &[u8]) -> BlobHash; 136 + fn has_tree(&self, hash: &TreeHash) -> bool; 137 + fn has_blob(&self, hash: &BlobHash) -> bool; 138 + } 139 + 140 + /// In-memory implementation for testing/prototyping 141 + #[derive(Debug, Default)] 142 + pub struct MemoryObjectStore { 143 + trees: HashMap<TreeHash, Tree>, 144 + blobs: HashMap<BlobHash, Vec<u8>>, 145 + } 146 + 147 + impl MemoryObjectStore { 148 + pub fn new() -> Self { 149 + Self::default() 150 + } 151 + } 152 + 153 + impl ObjectStore for MemoryObjectStore { 154 + fn get_tree(&self, hash: &TreeHash) -> Option<Tree> { 155 + self.trees.get(hash).cloned() 156 + } 157 + 158 + fn get_blob(&self, hash: &BlobHash) -> Option<Vec<u8>> { 159 + self.blobs.get(hash).cloned() 160 + } 161 + 162 + fn put_tree(&mut self, tree: &Tree) -> TreeHash { 163 + let hash = tree.hash(); 164 + self.trees.insert(hash, tree.clone()); 165 + hash 166 + } 167 + 168 + fn put_blob(&mut self, content: &[u8]) -> BlobHash { 169 + let hash = hash_blob(content); 170 + self.blobs.insert(hash, content.to_vec()); 171 + hash 172 + } 173 + 174 + fn has_tree(&self, hash: &TreeHash) -> bool { 175 + self.trees.contains_key(hash) 176 + } 177 + 178 + fn has_blob(&self, hash: &BlobHash) -> bool { 179 + self.blobs.contains_key(hash) 180 + } 181 + }