records.v

6.2 KB · raw

1//! What a save and an event look like on the remote.2//!3//! Records are JSON so a person can read the bucket, and the layout is the4//! same for every backend:5//!6//   <repo>/format          the remote layout, `koh-remote 1`7//   <repo>/objects/<hash>   the object's compressed bytes, as in the table8//   <repo>/saves/<id>       the full save record, including its file list9//   <repo>/events/<id>      one flag, clear or strip event10//   <repo>/head             the ID of the newest save on the line11//12// Only `head` is ever overwritten, and only with a matching version tag.1314module exchange1516import json217import src.events18import src.history19import src.snapshot20import src.storage21import strconv2223// One spelling for each key of the layout above (spec §9.2), so no caller builds a path by hand.24pub fn format_key(repo string) string {25	return '${repo}/format'26}2728pub fn object_key(repo string, hash string) string {29	return '${repo}/objects/${hash}'30}3132pub fn save_key(repo string, id string) string {33	return '${repo}/saves/${id}'34}3536pub fn event_key(repo string, id string) string {37	return '${repo}/events/${id}'38}3940pub fn head_key(repo string) string {41	return '${repo}/head'42}4344pub fn events_prefix(repo string) string {45	return '${repo}/events'46}4748pub struct FileRecord {49pub:50	path       string51	hash       string52	executable bool53}5455pub struct SaveRecord {56pub:57	id            string58	parent        string59	message       string60	created_at    i6461	machine       string62	is_checkpoint bool63	size_bytes    i6464	agent         string65	session       string66	restores      string67	replayed_from string68pub mut:69	files []FileRecord70}7172// save_record turns a local save into what travels. The file list is part of73// the record, which is why a save needs one round trip, not one per file.74pub fn save_record(store &storage.Store, save history.Save) !SaveRecord {75	files := history.files_of(store, save.id)!76	mut records := []FileRecord{}77	for file in files {78		records << FileRecord{79			path:       file.path80			hash:       file.hash81			executable: file.executable82		}83	}84	return SaveRecord{85		id:            save.id86		parent:        save.parent87		message:       save.message88		created_at:    save.created_at89		machine:       save.machine90		is_checkpoint: save.is_checkpoint91		size_bytes:    save.size_bytes92		agent:         save.agent93		session:       save.session94		restores:      save.restores95		replayed_from: save.replayed_from96		files:         records97	}98}99100pub fn encode_save(record SaveRecord) string {101	return json2.encode(record)102}103104// decode_save reads a record the remote holds. A lone surrogate escape fails only its path,105// not the whole record (spec §5.5).106pub fn decode_save(body []u8) !SaveRecord {107	return json2.decode[SaveRecord](lone_surrogates_as_bytes(body).bytestr())!108}109110// ignored_strip is a strip whose path breaks §5.5. No save can hold such a path,111// so Koh and Kepr both leave the event out rather than guess what it meant.112fn ignored_strip(event events.Event) bool {113	return event.kind == events.kind_strip && !snapshot.legal_path(event.path)114}115116// lone_surrogates_as_bytes writes a `\\uXXXX` surrogate escape with no partner as its117// three bytes. json2 refuses such an escape outright, which would refuse the whole118// save; as bytes it decodes, and the one path holding it fails legal_path (§5.5).119fn lone_surrogates_as_bytes(body []u8) []u8 {120	mut out := []u8{cap: body.len}121	mut i := 0122	for i < body.len {123		if body[i] != `\\` || i + 1 >= body.len {124			out << body[i]125			i++126			continue127		}128		unit := escaped_surrogate(body, i)129		if unit >= 0xd800 && unit <= 0xdbff && escaped_surrogate(body, i + 6) >= 0xdc00 {130			out << body[i..i + 12]131			i += 12132		} else if unit >= 0 {133			out << [u8(0xe0 | (unit >> 12)), u8(0x80 | ((unit >> 6) & 0x3f)), u8(0x80 | (unit & 0x3f))]134			i += 6135		} else {136			// Any other escape, `\\\\` included, passes through whole.137			out << body[i..i + 2]138			i += 2139		}140	}141	return out142}143144// escaped_surrogate reads a `\\uD800`–`\\uDFFF` escape at `at`, or returns -1.145fn escaped_surrogate(body []u8, at int) int {146	if at + 6 > body.len || body[at] != `\\` || body[at + 1] != `u` {147		return -1148	}149	unit := strconv.parse_int(body[at + 2..at + 6].bytestr(), 16, 32) or { return -1 }150	return if unit >= 0xd800 && unit <= 0xdfff { int(unit) } else { -1 }151}152153// drop_unsafe_paths applies the shared path contract before an arriving record is used.154pub fn drop_unsafe_paths(mut record SaveRecord) []string {155	mut kept := []FileRecord{}156	mut dropped := []string{}157	for file in record.files {158		if snapshot.legal_path(file.path) {159			kept << file160		} else {161			dropped << snapshot.escape_path(file.path)162		}163	}164	record.files = kept165	dropped.sort()166	return dropped167}168169pub fn to_save(record SaveRecord) history.Save {170	return history.Save{171		id:            record.id172		parent:        record.parent173		message:       record.message174		created_at:    record.created_at175		machine:       record.machine176		is_checkpoint: record.is_checkpoint177		size_bytes:    record.size_bytes178		agent:         record.agent179		session:       record.session180		restores:      record.restores181		replayed_from: record.replayed_from182	}183}184185pub fn to_files(record SaveRecord) []snapshot.FileEntry {186	mut files := []snapshot.FileEntry{}187	for file in record.files {188		files << snapshot.FileEntry{189			path:       file.path190			hash:       file.hash191			executable: file.executable192		}193	}194	return files195}196197pub struct EventRecord {198pub:199	id         string200	kind       string201	save_id    string202	path       string203	reason     string204	created_at i64205	machine    string206}207208pub fn event_record(event events.Event) EventRecord {209	return EventRecord{210		id:         event.id211		kind:       event.kind212		save_id:    event.save_id213		path:       event.path214		reason:     event.reason215		created_at: event.created_at216		machine:    event.machine217	}218}219220pub fn encode_event(event events.Event) string {221	return json2.encode(event_record(event))222}223224// decode_event applies the same lone-surrogate rule as decode_save.225pub fn decode_event(body []u8) !events.Event {226	record := json2.decode[EventRecord](lone_surrogates_as_bytes(body).bytestr())!227	return events.Event{228		id:         record.id229		kind:       record.kind230		save_id:    record.save_id231		path:       record.path232		reason:     record.reason233		created_at: record.created_at234		machine:    record.machine235	}236}