1234567891011121314module exchange1516import json217import src.events18import src.history19import src.snapshot20import src.storage21import strconv222324pub 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 bundle_key(repo string) string {45 return '${repo}/bundle'46}4748pub fn events_prefix(repo string) string {49 return '${repo}/events'50}5152pub struct FileRecord {53pub:54 path string55 hash string56 executable bool57}5859pub struct SaveRecord {60pub:61 id string62 parent string63 message string64 created_at i6465 machine string66 is_checkpoint bool67 size_bytes i6468 agent string69 session string70 restores string71 replayed_from string72pub mut:73 files []FileRecord74}75767778pub fn save_record(store &storage.Store, save history.Save) !SaveRecord {79 files := history.files_of(store, save.id)!80 mut records := []FileRecord{}81 for file in files {82 records << FileRecord{83 path: file.path84 hash: file.hash85 executable: file.executable86 }87 }88 return SaveRecord{89 id: save.id90 parent: save.parent91 message: save.message92 created_at: save.created_at93 machine: save.machine94 is_checkpoint: save.is_checkpoint95 size_bytes: save.size_bytes96 agent: save.agent97 session: save.session98 restores: save.restores99 replayed_from: save.replayed_from100 files: records101 }102}103104pub fn encode_save(record SaveRecord) string {105 return json2.encode(record)106}107108pub fn valid_save_id(id string) bool {109 return id.len in [6, 12]110 && id.bytes().all((it >= `0` && it <= `9`) || (it >= `a` && it <= `f`))111}112113pub fn valid_event_id(id string) bool {114 return id.len == 12 && valid_save_id(id)115}116117pub fn validate_save(record SaveRecord, expected string) ! {118 if !valid_save_id(expected) {119 return error('the remote has invalid save ID ${expected}')120 }121 if record.id != expected {122 return error('remote save record ID ${record.id} disagrees with key ${expected}')123 }124 if ![record.parent, record.restores, record.replayed_from].all(it == '' || valid_save_id(it)) {125 return error('remote save ${record.id} has an invalid save ID')126 }127 if record.created_at < 0 || record.size_bytes < 0 {128 return error('remote save ${record.id} has invalid metadata')129 }130}131132pub fn validate_hashes(id string, files []FileRecord) ! {133 for file in files {134 if !storage.valid_hash(file.hash) {135 return error('remote save ${id} has invalid object hash ${file.hash}')136 }137 }138}139140141142pub fn decode_save(body []u8) !SaveRecord {143 return json2.decode[SaveRecord](lone_surrogates_as_bytes(body).bytestr())!144}145146147148fn ignored_strip(event events.Event) bool {149 return event.kind == events.kind_strip && !snapshot.legal_path(event.path)150}151152153154155fn lone_surrogates_as_bytes(body []u8) []u8 {156 mut out := []u8{cap: body.len}157 mut i := 0158 for i < body.len {159 if body[i] != `\\` || i + 1 >= body.len {160 out << body[i]161 i++162 continue163 }164 unit := escaped_surrogate(body, i)165 if unit >= 0xd800 && unit <= 0xdbff && escaped_surrogate(body, i + 6) >= 0xdc00 {166 out << body[i..i + 12]167 i += 12168 } else if unit >= 0 {169 out << [u8(0xe0 | (unit >> 12)), u8(0x80 | ((unit >> 6) & 0x3f)), u8(0x80 | (unit & 0x3f))]170 i += 6171 } else {172 173 out << body[i..i + 2]174 i += 2175 }176 }177 return out178}179180181fn escaped_surrogate(body []u8, at int) int {182 if at + 6 > body.len || body[at] != `\\` || body[at + 1] != `u` {183 return -1184 }185 unit := strconv.parse_int(body[at + 2..at + 6].bytestr(), 16, 32) or { return -1 }186 return if unit >= 0xd800 && unit <= 0xdfff { int(unit) } else { -1 }187}188189190pub fn drop_unsafe_paths(mut record SaveRecord) []string {191 mut kept := []FileRecord{}192 mut dropped := []string{}193 for file in record.files {194 if snapshot.legal_path(file.path) {195 kept << file196 } else {197 dropped << snapshot.escape_path(file.path)198 }199 }200 record.files = kept201 dropped.sort()202 return dropped203}204205pub fn to_save(record SaveRecord) history.Save {206 return history.Save{207 id: record.id208 parent: record.parent209 message: record.message210 created_at: record.created_at211 machine: record.machine212 is_checkpoint: record.is_checkpoint213 size_bytes: record.size_bytes214 agent: record.agent215 session: record.session216 restores: record.restores217 replayed_from: record.replayed_from218 }219}220221pub fn to_files(record SaveRecord) []snapshot.FileEntry {222 mut files := []snapshot.FileEntry{}223 for file in record.files {224 files << snapshot.FileEntry{225 path: file.path226 hash: file.hash227 executable: file.executable228 }229 }230 return files231}232233pub struct EventRecord {234pub:235 id string236 kind string237 save_id string238 path string239 reason string240 created_at i64241 machine string242}243244pub fn event_record(event events.Event) EventRecord {245 return EventRecord{246 id: event.id247 kind: event.kind248 save_id: event.save_id249 path: event.path250 reason: event.reason251 created_at: event.created_at252 machine: event.machine253 }254}255256pub fn encode_event(event events.Event) string {257 return json2.encode(event_record(event))258}259260261pub fn decode_event(body []u8) !events.Event {262 record := json2.decode[EventRecord](lone_surrogates_as_bytes(body).bytestr())!263 return events.Event{264 id: record.id265 kind: record.kind266 save_id: record.save_id267 path: record.path268 reason: record.reason269 created_at: record.created_at270 machine: record.machine271 }272}