objects.v

4.7 KB · raw

1//! The object table: content-addressed file bytes.2//!3//! Objects are Blake3-addressed and stored zlib-compressed, exactly as Koh 24//! stored them on disk, so the same bytes migrate without a re-hash and a5//! future reader can decompress them with any zlib. Objects are shared between6//! saves: identical content is stored once.78module storage910import compress.zlib11import crypto.blake31213pub const max_file_size = 512 * 1024 * 10241415pub fn hash_hex(data []u8) string {16	return blake3.sum256(data).hex()17}1819// put_object stores `data` and returns its hash. Writing an object that is20// already present is a no-op, which is what makes repeated saves cheap.21pub fn (s &Store) put_object(data []u8) !string {22	hash := hash_hex(data)23	if s.has_object(hash)! {24		return hash25	}26	packed := zlib.compress(data)!27	mut stmt := s.db.prepare('INSERT OR IGNORE INTO objects (hash, size, data) VALUES (?, ?, ?)')!28	defer {29		stmt.finalize()30	}31	stmt.bind_text(1, hash)!32	stmt.bind_int(2, i64(data.len))!33	stmt.bind_blob(3, packed)!34	stmt.step()!35	return hash36}3738// object_blob returns the bytes exactly as they sit in the table — the39// zlib-compressed form a remote receives.40pub fn (s &Store) object_blob(hash string) ![]u8 {41	mut stmt := s.db.prepare('SELECT data FROM objects WHERE hash = ?')!42	defer {43		stmt.finalize()44	}45	stmt.bind_text(1, hash)!46	if !stmt.step()! {47		return error('object ${hash} is missing')48	}49	return stmt.blob(0)50}5152// put_object_blob stores a blob that came from a remote. The content is53// decompressed to check it hashes back to its key, and the blob itself is kept54// as it arrived.55pub fn (s &Store) put_object_blob(hash string, blob []u8) ! {56	if s.has_object(hash)! {57		return58	}59	data := zlib.decompress(blob) or { return error('object ${hash} is damaged: ${err.msg()}') }60	if hash_hex(data) != hash {61		return error('object ${hash} is damaged: it does not match its hash')62	}63	mut stmt := s.db.prepare('INSERT OR IGNORE INTO objects (hash, size, data) VALUES (?, ?, ?)')!64	defer {65		stmt.finalize()66	}67	stmt.bind_text(1, hash)!68	stmt.bind_int(2, i64(data.len))!69	stmt.bind_blob(3, blob)!70	stmt.step()!71}7273pub fn (s &Store) has_object(hash string) !bool {74	mut stmt := s.db.prepare('SELECT 1 FROM objects WHERE hash = ?')!75	defer {76		stmt.finalize()77	}78	stmt.bind_text(1, hash)!79	return stmt.step()!80}8182// get_object returns the uncompressed bytes and refuses anything that does not83// hash back to the key it was asked for.84pub fn (s &Store) get_object(hash string) ![]u8 {85	mut stmt := s.db.prepare('SELECT data FROM objects WHERE hash = ?')!86	defer {87		stmt.finalize()88	}89	stmt.bind_text(1, hash)!90	if !stmt.step()! {91		return error('object ${hash} is missing')92	}93	packed := stmt.blob(0)94	data := zlib.decompress(packed)!95	if hash_hex(data) != hash {96		return error('object ${hash} does not match its hash')97	}98	return data99}100101pub fn (s &Store) object_size(hash string) !i64 {102	mut stmt := s.db.prepare('SELECT size FROM objects WHERE hash = ?')!103	defer {104		stmt.finalize()105	}106	stmt.bind_text(1, hash)!107	if !stmt.step()! {108		return error('object ${hash} is missing')109	}110	return stmt.int(0)111}112113pub fn (s &Store) object_count() !i64 {114	mut stmt := s.db.prepare('SELECT COUNT(*) FROM objects')!115	defer {116		stmt.finalize()117	}118	if !stmt.step()! {119		return 0120	}121	return stmt.int(0)122}123124// drop_unreferenced_objects removes objects no save points at any more. Only125// `strip` leaves objects behind, so this is what makes a strip permanent.126pub fn (s &Store) drop_unreferenced_objects() !i64 {127	before := s.object_count()!128	mut stmt := s.db.prepare('DELETE FROM objects WHERE hash NOT IN (SELECT hash FROM save_files)')!129	defer {130		stmt.finalize()131	}132	stmt.step()!133	after := s.object_count()!134	return before - after135}136137// check_objects reads every object and hashes it back. Nothing else proves the138// store is intact; the hash is the point of the store.139pub fn (s &Store) check_objects() ![]string {140	mut problems := []string{}141	mut stmt := s.db.prepare('SELECT hash FROM objects ORDER BY hash')!142	defer {143		stmt.finalize()144	}145	mut hashes := []string{}146	for stmt.step()! {147		hashes << stmt.text(0)148	}149	for hash in hashes {150		s.get_object(hash) or { problems << 'object ${hash}: ${err.msg()}' }151	}152	return problems153}154155// check_save_files catches a save naming an object that is gone — the one156// inconsistency the schema cannot prevent on its own, because objects are157// shared between saves.158pub fn (s &Store) check_save_files() ![]string {159	mut problems := []string{}160	mut stmt := s.db.prepare('SELECT DISTINCT f.save_id, f.hash FROM save_files f161		LEFT JOIN objects o ON o.hash = f.hash WHERE o.hash IS NULL')!162	defer {163		stmt.finalize()164	}165	for stmt.step()! {166		problems << 'save ${stmt.text(0)} needs missing object ${stmt.text(1)}'167	}168	return problems169}