records.v

5.9 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 strconv2223pub fn format_key(repo string) string {24	return '${repo}/format'25}2627pub fn object_key(repo string, hash string) string {28	return '${repo}/objects/${hash}'29}3031pub fn save_key(repo string, id string) string {32	return '${repo}/saves/${id}'33}3435pub fn event_key(repo string, id string) string {36	return '${repo}/events/${id}'37}3839pub fn head_key(repo string) string {40	return '${repo}/head'41}4243pub fn events_prefix(repo string) string {44	return '${repo}/events'45}4647pub struct FileRecord {48pub:49	path       string50	hash       string51	executable bool52}5354pub struct SaveRecord {55pub:56	id            string57	parent        string58	message       string59	created_at    i6460	machine       string61	is_checkpoint bool62	size_bytes    i6463	agent         string64	session       string65	restores      string66	replayed_from string67pub mut:68	files []FileRecord69}7071// save_record turns a local save into what travels. The file list is part of72// the record, which is why a save needs one round trip, not one per file.73pub fn save_record(store &storage.Store, save history.Save) !SaveRecord {74	files := history.files_of(store, save.id)!75	mut records := []FileRecord{}76	for file in files {77		records << FileRecord{78			path:       file.path79			hash:       file.hash80			executable: file.executable81		}82	}83	return SaveRecord{84		id:            save.id85		parent:        save.parent86		message:       save.message87		created_at:    save.created_at88		machine:       save.machine89		is_checkpoint: save.is_checkpoint90		size_bytes:    save.size_bytes91		agent:         save.agent92		session:       save.session93		restores:      save.restores94		replayed_from: save.replayed_from95		files:         records96	}97}9899pub fn encode_save(record SaveRecord) string {100	return json2.encode(record)101}102103pub fn decode_save(body []u8) !SaveRecord {104	return json2.decode[SaveRecord](lone_surrogates_as_bytes(body).bytestr())!105}106107// ignored_strip is a strip whose path breaks §5.5. No save can hold such a path,108// so Koh and Kepr both leave the event out rather than guess what it meant.109fn ignored_strip(event events.Event) bool {110	return event.kind == events.kind_strip && !snapshot.legal_path(event.path)111}112113// lone_surrogates_as_bytes writes a `\\uXXXX` surrogate escape with no partner as its114// three bytes. json2 refuses such an escape outright, which would refuse the whole115// save; as bytes it decodes, and the one path holding it fails legal_path (§5.5).116fn lone_surrogates_as_bytes(body []u8) []u8 {117	mut out := []u8{cap: body.len}118	mut i := 0119	for i < body.len {120		if body[i] != `\\` || i + 1 >= body.len {121			out << body[i]122			i++123			continue124		}125		unit := escaped_surrogate(body, i)126		if unit >= 0xd800 && unit <= 0xdbff && escaped_surrogate(body, i + 6) >= 0xdc00 {127			out << body[i..i + 12]128			i += 12129		} else if unit >= 0 {130			out << [u8(0xe0 | (unit >> 12)), u8(0x80 | ((unit >> 6) & 0x3f)), u8(0x80 | (unit & 0x3f))]131			i += 6132		} else {133			// Any other escape, `\\\\` included, passes through whole.134			out << body[i..i + 2]135			i += 2136		}137	}138	return out139}140141// escaped_surrogate reads a `\\uD800`–`\\uDFFF` escape at `at`, or returns -1.142fn escaped_surrogate(body []u8, at int) int {143	if at + 6 > body.len || body[at] != `\\` || body[at + 1] != `u` {144		return -1145	}146	unit := strconv.parse_int(body[at + 2..at + 6].bytestr(), 16, 32) or { return -1 }147	return if unit >= 0xd800 && unit <= 0xdfff { int(unit) } else { -1 }148}149150// drop_unsafe_paths applies the shared path contract before an arriving record is used.151pub fn drop_unsafe_paths(mut record SaveRecord) []string {152	mut kept := []FileRecord{}153	mut dropped := []string{}154	for file in record.files {155		if snapshot.legal_path(file.path) {156			kept << file157		} else {158			dropped << snapshot.escape_path(file.path)159		}160	}161	record.files = kept162	dropped.sort()163	return dropped164}165166pub fn to_save(record SaveRecord) history.Save {167	return history.Save{168		id:            record.id169		parent:        record.parent170		message:       record.message171		created_at:    record.created_at172		machine:       record.machine173		is_checkpoint: record.is_checkpoint174		size_bytes:    record.size_bytes175		agent:         record.agent176		session:       record.session177		restores:      record.restores178		replayed_from: record.replayed_from179	}180}181182pub fn to_files(record SaveRecord) []snapshot.FileEntry {183	mut files := []snapshot.FileEntry{}184	for file in record.files {185		files << snapshot.FileEntry{186			path:       file.path187			hash:       file.hash188			executable: file.executable189		}190	}191	return files192}193194pub struct EventRecord {195pub:196	id         string197	kind       string198	save_id    string199	path       string200	reason     string201	created_at i64202	machine    string203}204205pub fn event_record(event events.Event) EventRecord {206	return EventRecord{207		id:         event.id208		kind:       event.kind209		save_id:    event.save_id210		path:       event.path211		reason:     event.reason212		created_at: event.created_at213		machine:    event.machine214	}215}216217pub fn encode_event(event events.Event) string {218	return json2.encode(event_record(event))219}220221pub fn decode_event(body []u8) !events.Event {222	record := json2.decode[EventRecord](lone_surrogates_as_bytes(body).bytestr())!223	return events.Event{224		id:         record.id225		kind:       record.kind226		save_id:    record.save_id227		path:       record.path228		reason:     record.reason229		created_at: record.created_at230		machine:    record.machine231	}232}