| 1 | //! Objects: file contents (blobs) and directories (trees), stored by the hash of their bytes. |
| 2 | //! |
| 3 | //! An object's file holds exactly the bytes its id is the hash of: a type prefix |
| 4 | //! (`grove blob\0` or `grove tree\0`) and the content. So any object can be checked by hashing |
| 5 | //! its file, and a blob and a tree can never share an id. |
| 6 | //! |
| 7 | //! Writing: temporary file, flush, rename into place, flush the directory. An object either |
| 8 | //! exists completely or not at all, and is never rewritten. |
| 9 | |
| 10 | use crate::blake3::Hasher; |
| 11 | use crate::hex; |
| 12 | use crate::log::sync_parent; |
| 13 | use std::fs::{self, File}; |
| 14 | use std::io::{self, Write}; |
| 15 | use std::path::{Path, PathBuf}; |
| 16 | |
| 17 | pub type Id = [u8; 32]; |
| 18 | |
| 19 | const BLOB: &[u8] = b"grove blob\0"; |
| 20 | const TREE: &[u8] = b"grove tree\0"; |
| 21 | |
| 22 | #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] |
| 23 | pub enum Kind { |
| 24 | File, |
| 25 | Exec, |
| 26 | Link, |
| 27 | Dir, |
| 28 | } |
| 29 | |
| 30 | impl Kind { |
| 31 | fn byte(self) -> u8 { |
| 32 | match self { |
| 33 | Kind::File => b'f', |
| 34 | Kind::Exec => b'x', |
| 35 | Kind::Link => b'l', |
| 36 | Kind::Dir => b'd', |
| 37 | } |
| 38 | } |
| 39 | fn from_byte(b: u8) -> Option<Kind> { |
| 40 | Some(match b { |
| 41 | b'f' => Kind::File, |
| 42 | b'x' => Kind::Exec, |
| 43 | b'l' => Kind::Link, |
| 44 | b'd' => Kind::Dir, |
| 45 | _ => return None, |
| 46 | }) |
| 47 | } |
| 48 | } |
| 49 | |
| 50 | #[derive(Clone, Debug, PartialEq, Eq)] |
| 51 | pub struct Entry { |
| 52 | pub name: String, |
| 53 | pub kind: Kind, |
| 54 | pub id: Id, |
| 55 | } |
| 56 | |
| 57 | #[derive(Debug)] |
| 58 | pub enum Error { |
| 59 | Io(io::Error), |
| 60 | Missing(Id), |
| 61 | Corrupt(Id), |
| 62 | BadName(String), |
| 63 | } |
| 64 | |
| 65 | impl std::fmt::Display for Error { |
| 66 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { |
| 67 | match self { |
| 68 | Error::Io(e) => write!(f, "{e}"), |
| 69 | Error::Missing(id) => write!(f, "object {} is missing", hex::encode(id)), |
| 70 | Error::Corrupt(id) => write!(f, "object {} is corrupt", hex::encode(id)), |
| 71 | Error::BadName(n) => write!(f, "invalid file name {n:?}"), |
| 72 | } |
| 73 | } |
| 74 | } |
| 75 | |
| 76 | impl From<io::Error> for Error { |
| 77 | fn from(e: io::Error) -> Self { |
| 78 | Error::Io(e) |
| 79 | } |
| 80 | } |
| 81 | |
| 82 | pub type Result<T> = std::result::Result<T, Error>; |
| 83 | |
| 84 | /// Names must be usable as one path component everywhere. |
| 85 | pub fn valid_name(name: &str) -> bool { |
| 86 | !name.is_empty() && name != "." && name != ".." && !name.contains('/') && !name.contains('\0') |
| 87 | } |
| 88 | |
| 89 | fn id_of(prefix: &[u8], content: &[u8]) -> Id { |
| 90 | let mut h = Hasher::new(); |
| 91 | h.update(prefix).update(content); |
| 92 | h.finalize() |
| 93 | } |
| 94 | |
| 95 | pub fn blob_id(content: &[u8]) -> Id { |
| 96 | id_of(BLOB, content) |
| 97 | } |
| 98 | |
| 99 | /// Canonical tree bytes: entries sorted by name, each `kind, name length (u32 LE), name, id`. |
| 100 | pub fn encode_tree(entries: &[Entry]) -> Result<Vec<u8>> { |
| 101 | let mut sorted: Vec<&Entry> = entries.iter().collect(); |
| 102 | sorted.sort_by(|a, b| a.name.as_bytes().cmp(b.name.as_bytes())); |
| 103 | let mut out = Vec::new(); |
| 104 | for (i, e) in sorted.iter().enumerate() { |
| 105 | if !valid_name(&e.name) { |
| 106 | return Err(Error::BadName(e.name.clone())); |
| 107 | } |
| 108 | if i > 0 && sorted[i - 1].name == e.name { |
| 109 | return Err(Error::BadName(format!("{} (twice)", e.name))); |
| 110 | } |
| 111 | out.push(e.kind.byte()); |
| 112 | out.extend_from_slice(&(e.name.len() as u32).to_le_bytes()); |
| 113 | out.extend_from_slice(e.name.as_bytes()); |
| 114 | out.extend_from_slice(&e.id); |
| 115 | } |
| 116 | Ok(out) |
| 117 | } |
| 118 | |
| 119 | fn decode_tree(id: &Id, mut bytes: &[u8]) -> Result<Vec<Entry>> { |
| 120 | let corrupt = || Error::Corrupt(*id); |
| 121 | let mut entries = Vec::new(); |
| 122 | while !bytes.is_empty() { |
| 123 | let kind = Kind::from_byte(bytes[0]).ok_or_else(corrupt)?; |
| 124 | let len = u32::from_le_bytes(bytes.get(1..5).ok_or_else(corrupt)?.try_into().unwrap()) as usize; |
| 125 | let name = std::str::from_utf8(bytes.get(5..5 + len).ok_or_else(corrupt)?).map_err(|_| corrupt())?; |
| 126 | let id_bytes = bytes.get(5 + len..5 + len + 32).ok_or_else(corrupt)?; |
| 127 | entries.push(Entry { name: name.to_string(), kind, id: id_bytes.try_into().unwrap() }); |
| 128 | bytes = &bytes[5 + len + 32..]; |
| 129 | } |
| 130 | Ok(entries) |
| 131 | } |
| 132 | |
| 133 | pub struct Objects { |
| 134 | root: PathBuf, |
| 135 | } |
| 136 | |
| 137 | impl Objects { |
| 138 | pub fn open(root: &Path) -> Result<Objects> { |
| 139 | fs::create_dir_all(root.join("tmp"))?; |
| 140 | Ok(Objects { root: root.to_path_buf() }) |
| 141 | } |
| 142 | |
| 143 | fn path(&self, id: &Id) -> PathBuf { |
| 144 | let h = hex::encode(id); |
| 145 | self.root.join(&h[..2]).join(&h[2..]) |
| 146 | } |
| 147 | |
| 148 | pub fn has(&self, id: &Id) -> bool { |
| 149 | self.path(id).is_file() |
| 150 | } |
| 151 | |
| 152 | fn put(&self, prefix: &[u8], content: &[u8]) -> Result<Id> { |
| 153 | let id = id_of(prefix, content); |
| 154 | let path = self.path(&id); |
| 155 | if path.is_file() { |
| 156 | return Ok(id); |
| 157 | } |
| 158 | fs::create_dir_all(path.parent().unwrap())?; |
| 159 | let tmp = self.root.join("tmp").join(format!("{}-{}", hex::encode(&id[..8]), crate::random::hex(8))); |
| 160 | { |
| 161 | let mut f = File::create(&tmp)?; |
| 162 | f.write_all(prefix)?; |
| 163 | f.write_all(content)?; |
| 164 | f.sync_all()?; |
| 165 | } |
| 166 | fs::rename(&tmp, &path)?; |
| 167 | sync_parent(&path)?; |
| 168 | Ok(id) |
| 169 | } |
| 170 | |
| 171 | /// Read an object's content, checking that it hashes to its id. |
| 172 | fn get(&self, prefix: &[u8], id: &Id) -> Result<Vec<u8>> { |
| 173 | let bytes = match fs::read(self.path(id)) { |
| 174 | Ok(b) => b, |
| 175 | Err(e) if e.kind() == io::ErrorKind::NotFound => return Err(Error::Missing(*id)), |
| 176 | Err(e) => return Err(e.into()), |
| 177 | }; |
| 178 | if !bytes.starts_with(prefix) || crate::blake3::hash(&bytes) != *id { |
| 179 | return Err(Error::Corrupt(*id)); |
| 180 | } |
| 181 | Ok(bytes[prefix.len()..].to_vec()) |
| 182 | } |
| 183 | |
| 184 | pub fn put_blob(&self, content: &[u8]) -> Result<Id> { |
| 185 | self.put(BLOB, content) |
| 186 | } |
| 187 | |
| 188 | pub fn get_blob(&self, id: &Id) -> Result<Vec<u8>> { |
| 189 | self.get(BLOB, id) |
| 190 | } |
| 191 | |
| 192 | pub fn put_tree(&self, entries: &[Entry]) -> Result<Id> { |
| 193 | self.put(TREE, &encode_tree(entries)?) |
| 194 | } |
| 195 | |
| 196 | /// Entries sorted by name. |
| 197 | pub fn get_tree(&self, id: &Id) -> Result<Vec<Entry>> { |
| 198 | let bytes = self.get(TREE, id)?; |
| 199 | decode_tree(id, &bytes) |
| 200 | } |
| 201 | |
| 202 | /// The id of an empty tree, stored. |
| 203 | pub fn empty_tree(&self) -> Result<Id> { |
| 204 | self.put_tree(&[]) |
| 205 | } |
| 206 | |
| 207 | /// Every file under a tree, as (path, kind, blob id), sorted by path. |
| 208 | pub fn flatten(&self, tree: &Id) -> Result<Vec<(String, Kind, Id)>> { |
| 209 | let mut out = Vec::new(); |
| 210 | self.flatten_into(tree, "", &mut out)?; |
| 211 | Ok(out) |
| 212 | } |
| 213 | |
| 214 | fn flatten_into(&self, tree: &Id, prefix: &str, out: &mut Vec<(String, Kind, Id)>) -> Result<()> { |
| 215 | for e in self.get_tree(tree)? { |
| 216 | let path = format!("{prefix}{}", e.name); |
| 217 | if e.kind == Kind::Dir { |
| 218 | self.flatten_into(&e.id, &format!("{path}/"), out)?; |
| 219 | } else { |
| 220 | out.push((path, e.kind, e.id)); |
| 221 | } |
| 222 | } |
| 223 | Ok(()) |
| 224 | } |
| 225 | |
| 226 | /// An object's stored bytes (type prefix and content), checked against its id. For transfer. |
| 227 | pub fn get_raw(&self, id: &Id) -> Result<Vec<u8>> { |
| 228 | let bytes = match fs::read(self.path(id)) { |
| 229 | Ok(b) => b, |
| 230 | Err(e) if e.kind() == io::ErrorKind::NotFound => return Err(Error::Missing(*id)), |
| 231 | Err(e) => return Err(e.into()), |
| 232 | }; |
| 233 | if crate::blake3::hash(&bytes) != *id { |
| 234 | return Err(Error::Corrupt(*id)); |
| 235 | } |
| 236 | Ok(bytes) |
| 237 | } |
| 238 | |
| 239 | /// Store an object received from elsewhere. Its id is the hash of its bytes; the bytes must |
| 240 | /// be a blob, or a tree that decodes. |
| 241 | pub fn put_raw(&self, bytes: &[u8]) -> Result<Id> { |
| 242 | let id = crate::blake3::hash(bytes); |
| 243 | if let Some(content) = bytes.strip_prefix(BLOB) { |
| 244 | return self.put(BLOB, content).map(|_| id); |
| 245 | } |
| 246 | if let Some(content) = bytes.strip_prefix(TREE) { |
| 247 | decode_tree(&id, content)?; |
| 248 | return self.put(TREE, content).map(|_| id); |
| 249 | } |
| 250 | Err(Error::Corrupt(id)) |
| 251 | } |
| 252 | |
| 253 | /// Every object id under a tree (trees and blobs), the tree itself first. |
| 254 | pub fn ids_under(&self, tree: &Id) -> Result<Vec<Id>> { |
| 255 | let mut out = vec![*tree]; |
| 256 | let mut seen = std::collections::HashSet::new(); |
| 257 | seen.insert(*tree); |
| 258 | let mut i = 0; |
| 259 | while i < out.len() { |
| 260 | let id = out[i]; |
| 261 | i += 1; |
| 262 | // Only trees have children; blobs fail to decode as trees and are skipped. |
| 263 | let Ok(bytes) = self.get_raw(&id) else { return Err(Error::Missing(id)) }; |
| 264 | if let Some(content) = bytes.strip_prefix(TREE) { |
| 265 | for e in decode_tree(&id, content)? { |
| 266 | if seen.insert(e.id) { |
| 267 | out.push(e.id); |
| 268 | } |
| 269 | } |
| 270 | } |
| 271 | } |
| 272 | Ok(out) |
| 273 | } |
| 274 | |
| 275 | /// Confirm every object under a tree exists and matches its id. |
| 276 | pub fn verify_tree(&self, tree: &Id) -> Result<usize> { |
| 277 | let mut count = 1; |
| 278 | for e in self.get_tree(tree)? { |
| 279 | count += if e.kind == Kind::Dir { self.verify_tree(&e.id)? } else { self.get_blob(&e.id).map(|_| 1)? }; |
| 280 | } |
| 281 | Ok(count) |
| 282 | } |
| 283 | } |
| 284 | |
| 285 | #[cfg(test)] |
| 286 | mod tests { |
| 287 | use super::*; |
| 288 | use crate::testdir::TestDir; |
| 289 | |
| 290 | #[test] |
| 291 | fn blobs_round_trip_and_dedupe() { |
| 292 | let dir = TestDir::new("objects-blobs"); |
| 293 | let objects = Objects::open(dir.path()).unwrap(); |
| 294 | let a = objects.put_blob(b"hello").unwrap(); |
| 295 | assert_eq!(objects.put_blob(b"hello").unwrap(), a); |
| 296 | assert_ne!(objects.put_blob(b"hello!").unwrap(), a); |
| 297 | assert_eq!(objects.get_blob(&a).unwrap(), b"hello"); |
| 298 | assert_eq!(a, blob_id(b"hello")); |
| 299 | assert!(objects.has(&a)); |
| 300 | } |
| 301 | |
| 302 | #[test] |
| 303 | fn trees_are_canonical() { |
| 304 | let dir = TestDir::new("objects-trees"); |
| 305 | let objects = Objects::open(dir.path()).unwrap(); |
| 306 | let f = objects.put_blob(b"x").unwrap(); |
| 307 | let e = |name: &str| Entry { name: name.into(), kind: Kind::File, id: f }; |
| 308 | let t1 = objects.put_tree(&[e("b"), e("a")]).unwrap(); |
| 309 | let t2 = objects.put_tree(&[e("a"), e("b")]).unwrap(); |
| 310 | assert_eq!(t1, t2, "order of input doesn't matter"); |
| 311 | let entries = objects.get_tree(&t1).unwrap(); |
| 312 | assert_eq!(entries.iter().map(|e| e.name.as_str()).collect::<Vec<_>>(), ["a", "b"]); |
| 313 | // A tree and a blob with identical bytes still differ. |
| 314 | assert_ne!(objects.empty_tree().unwrap(), objects.put_blob(b"").unwrap()); |
| 315 | } |
| 316 | |
| 317 | #[test] |
| 318 | fn rejects_bad_names() { |
| 319 | let dir = TestDir::new("objects-names"); |
| 320 | let objects = Objects::open(dir.path()).unwrap(); |
| 321 | let f = objects.put_blob(b"x").unwrap(); |
| 322 | for bad in ["", ".", "..", "a/b", "nul\0"] { |
| 323 | let e = Entry { name: bad.into(), kind: Kind::File, id: f }; |
| 324 | assert!(matches!(objects.put_tree(&[e]), Err(Error::BadName(_))), "{bad:?}"); |
| 325 | } |
| 326 | let e = Entry { name: "same".into(), kind: Kind::File, id: f }; |
| 327 | assert!(objects.put_tree(&[e.clone(), e]).is_err()); |
| 328 | } |
| 329 | |
| 330 | #[test] |
| 331 | fn corruption_and_absence_are_detected() { |
| 332 | let dir = TestDir::new("objects-corrupt"); |
| 333 | let objects = Objects::open(dir.path()).unwrap(); |
| 334 | let id = objects.put_blob(b"precious").unwrap(); |
| 335 | std::fs::write(objects.path(&id), b"grove blob\0tampered").unwrap(); |
| 336 | assert!(matches!(objects.get_blob(&id), Err(Error::Corrupt(_)))); |
| 337 | assert!(matches!(objects.get_blob(&blob_id(b"never stored")), Err(Error::Missing(_)))); |
| 338 | } |
| 339 | |
| 340 | #[test] |
| 341 | fn flatten_and_verify() { |
| 342 | let dir = TestDir::new("objects-flatten"); |
| 343 | let objects = Objects::open(dir.path()).unwrap(); |
| 344 | let a = objects.put_blob(b"a").unwrap(); |
| 345 | let b = objects.put_blob(b"b").unwrap(); |
| 346 | let sub = objects.put_tree(&[Entry { name: "inner.txt".into(), kind: Kind::File, id: b }]).unwrap(); |
| 347 | let root = objects |
| 348 | .put_tree(&[ |
| 349 | Entry { name: "run.sh".into(), kind: Kind::Exec, id: a }, |
| 350 | Entry { name: "dir".into(), kind: Kind::Dir, id: sub }, |
| 351 | ]) |
| 352 | .unwrap(); |
| 353 | let files = objects.flatten(&root).unwrap(); |
| 354 | assert_eq!(files, vec![("dir/inner.txt".into(), Kind::File, b), ("run.sh".into(), Kind::Exec, a)]); |
| 355 | assert_eq!(objects.verify_tree(&root).unwrap(), 4); |
| 356 | } |
| 357 | } |