12345678module update910import crypto.blake311import json212import net.http13import os1415pub const default_source = 'https://kepr.uk/koh/releases/latest.json'16pub const source_env = 'KOH_UPDATE_SOURCE'17pub const repo = 'koh'18const file_scheme = 'file://'192021struct PlatformFile {22 url string23 blake3 string24 sha256 string25}26272829struct Document {30 version string31 notes ?string32 platforms map[string]PlatformFile33}3435pub struct Available {36pub:37 version string38 notes string39 url string40 blake3 string41 platform string42}43444546pub fn source() string {47 named := os.getenv(source_env)48 if named != '' {49 return named50 }51 return default_source52}53545556pub struct Checked {57pub:58 update bool59 available Available60}61626364pub fn check(now string, platform string) !Checked {65 url := source()66 response := http.get(url) or { return error('could not reach ${url}: ${err.msg()}') }67 if response.status_code != 200 {68 return error('${url} answered ${response.status_code}')69 }70 document := json2.decode[Document](response.body) or {71 return error('${url} is not a release document: ${err.msg()}')72 }73 if document.version == '' || compare_versions(document.version, now) <= 0 {74 return Checked{}75 }76 file := document.platforms[platform] or {77 return error('koh ${document.version} has no ${platform} file — run it on another machine')78 }79 return Checked{80 update: true81 available: Available{82 version: document.version83 notes: document.notes or { '' }84 url: file.url85 blake3: file.blake386 platform: platform87 }88 }89}909192939495pub fn compare_versions(a string, b string) int {96 left := a.split('.')97 right := b.split('.')98 for index in 0 .. if left.len > right.len { left.len } else { right.len } {99 one := if index < left.len { left[index] } else { '0' }100 two := if index < right.len { right[index] } else { '0' }101 order := compare_component(one, two)102 if order != 0 {103 return order104 }105 }106 return 0107}108109fn compare_component(a string, b string) int {110 111 left := a.trim_left('0')112 right := b.trim_left('0')113 if left == right {114 return 0115 }116 if left.len != right.len {117 return if left.len > right.len { 1 } else { -1 }118 }119 return if left > right { 1 } else { -1 }120}121122123124125pub fn install(available Available) !string {126 bytes := download(available.url)!127 verify(bytes, available.blake3)!128 target := os.executable()129 staged := stage(os.dir(target), bytes)!130 if os.user_os() == 'macos' {131 132 133 signed := os.execute('codesign -f -s - ${sh_quote(staged)}')134 if signed.exit_code != 0 {135 os.rm(staged) or {}136 return error('codesign refused the downloaded binary: ${signed.output.trim_space()}')137 }138 }139 os.mv(staged, target)!140 return target141}142143fn download(url string) ![]u8 {144 if url.starts_with(file_scheme) {145 146 147 148 path := url[file_scheme.len..]149 return os.read_file(path) or {150 return error('could not read ${url}: ${err.msg()}')151 }.bytes()152 }153 response := http.get(url) or { return error('could not download ${url}: ${err.msg()}') }154 if response.status_code != 200 {155 return error('${url} answered ${response.status_code}')156 }157 return response.body.bytes()158}159160161162fn verify(bytes []u8, expected string) ! {163 if bytes.len == 0 {164 return error('the download was empty')165 }166 if expected == '' {167 return error('the release document carries no Blake3 checksum — refusing to install it')168 }169 got := blake3.sum256(bytes).hex()170 if got != expected {171 return error('the download is not the published file (Blake3 ${got} instead of ${expected}) — refusing to install it')172 }173}174175176177178fn stage(dir string, bytes []u8) !string {179 staged := os.join_path(dir, '.koh-new-${os.getpid()}')180 os.write_file_array(staged, bytes) or {181 return error('cannot write beside ${dir} — put koh where this user can replace it (${err.msg()})')182 }183 os.chmod(staged, 0o755)!184 return staged185}186187188189fn sh_quote(text string) string {190 return "'" + text.replace("'", "'\\''") + "'"191}