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'181920struct PlatformFile {21 url string22 blake3 string23 sha256 string24}25262728struct Document {29 version string30 notes ?string31 platforms map[string]PlatformFile32}3334pub struct Available {35pub:36 version string37 notes string38 url string39 blake3 string40 platform string41}42434445pub fn source() string {46 named := os.getenv(source_env)47 if named != '' {48 return named49 }50 return default_source51}52535455pub struct Checked {56pub:57 update bool58 available Available59}60616263pub fn check(now string, platform string) !Checked {64 url := source()65 response := http.get(url) or { return error('could not reach ${url}: ${err.msg()}') }66 if response.status_code != 200 {67 return error('${url} answered ${response.status_code}')68 }69 document := json2.decode[Document](response.body) or {70 return error('${url} is not a release document: ${err.msg()}')71 }72 if document.version == '' || compare_versions(document.version, now) <= 0 {73 return Checked{}74 }75 file := document.platforms[platform] or {76 return error('koh ${document.version} has no ${platform} file — run it on another machine')77 }78 return Checked{79 update: true80 available: Available{81 version: document.version82 notes: document.notes or { '' }83 url: file.url84 blake3: file.blake385 platform: platform86 }87 }88}899091929394pub fn compare_versions(a string, b string) int {95 left := a.split('.')96 right := b.split('.')97 for index in 0 .. if left.len > right.len { left.len } else { right.len } {98 one := if index < left.len { left[index] } else { '0' }99 two := if index < right.len { right[index] } else { '0' }100 order := compare_component(one, two)101 if order != 0 {102 return order103 }104 }105 return 0106}107108fn compare_component(a string, b string) int {109 110 left := a.trim_left('0')111 right := b.trim_left('0')112 if left == right {113 return 0114 }115 if left.len != right.len {116 return if left.len > right.len { 1 } else { -1 }117 }118 return if left > right { 1 } else { -1 }119}120121122123124pub fn install(available Available) !string {125 bytes := download(available.url)!126 verify(bytes, available.blake3)!127 target := os.executable()128 staged := stage(os.dir(target), bytes)!129 if os.user_os() == 'macos' {130 131 132 signed := os.execute('codesign -f -s - ${sh_quote(staged)}')133 if signed.exit_code != 0 {134 os.rm(staged) or {}135 return error('codesign refused the downloaded binary: ${signed.output.trim_space()}')136 }137 }138 os.mv(staged, target)!139 return target140}141142fn download(url string) ![]u8 {143 response := http.get(url) or { return error('could not download ${url}: ${err.msg()}') }144 if response.status_code != 200 {145 return error('${url} answered ${response.status_code}')146 }147 return response.body.bytes()148}149150151152fn verify(bytes []u8, expected string) ! {153 if bytes.len == 0 {154 return error('the download was empty')155 }156 if expected == '' {157 return error('the release document carries no Blake3 checksum — refusing to install it')158 }159 got := blake3.sum256(bytes).hex()160 if got != expected {161 return error('the download is not the published file (Blake3 ${got} instead of ${expected}) — refusing to install it')162 }163}164165166167168fn stage(dir string, bytes []u8) !string {169 staged := os.join_path(dir, '.koh-new-${os.getpid()}')170 os.write_file_array(staged, bytes) or {171 return error('cannot write beside ${dir} — put koh where this user can replace it (${err.msg()})')172 }173 os.chmod(staged, 0o755)!174 return staged175}176177178179fn sh_quote(text string) string {180 return "'" + text.replace("'", "'\\''") + "'"181}