1use anyhow::{anyhow, bail, Context, Result};
11use sha2::{Digest, Sha256};
12use std::io::Read;
13
14const DEFAULT_RELEASE_REPO: &str = "boomctl/git-ark";
17
18pub fn version() -> &'static str {
20 env!("CARGO_PKG_VERSION")
21}
22
23fn release_repo() -> String {
24 std::env::var("GIT_ARK_RELEASE_REPO").unwrap_or_else(|_| DEFAULT_RELEASE_REPO.to_string())
25}
26
27pub fn asset_name(triple: &str) -> String {
30 format!("git-ark-{triple}")
31}
32
33fn release_base(repo: &str, version: &str) -> String {
36 format!("https://github.com/{repo}/releases/download/v{version}")
37}
38
39pub fn expected_sha256(sums: &str, asset: &str) -> Option<String> {
43 sums.lines().find_map(|line| {
44 let mut it = line.split_whitespace();
45 let hash = it.next()?;
46 let name = it.next()?;
47 (name == asset).then(|| hash.to_ascii_lowercase())
48 })
49}
50
51pub fn sha256_hex(bytes: &[u8]) -> String {
53 let mut h = Sha256::new();
54 h.update(bytes);
55 h.finalize().iter().map(|b| format!("{b:02x}")).collect()
56}
57
58fn http_get_bytes(url: &str) -> Result<Vec<u8>> {
59 let resp = ureq::get(url).call().map_err(|e| match e {
60 ureq::Error::Status(404, _) => anyhow!("not found (404)"),
61 other => anyhow!("{other}"),
62 })?;
63 let mut buf = Vec::new();
64 resp.into_reader()
65 .read_to_end(&mut buf)
66 .with_context(|| format!("reading response body from {url}"))?;
67 Ok(buf)
68}
69
70pub fn fetch_host_binary(triple: &str) -> Result<Vec<u8>> {
74 let repo = release_repo();
75 let version = version();
76 let base = release_base(&repo, version);
77 let asset = asset_name(triple);
78
79 let bin_url = format!("{base}/{asset}");
80 let sums_url = format!("{base}/SHA256SUMS");
81
82 let bytes = http_get_bytes(&bin_url).map_err(|e| {
83 anyhow!(
84 "couldn't fetch the host binary for {triple}: {e} ({bin_url}). \
85 If there's no v{version} release for this platform, pass \
86 --binary <path> to supply one."
87 )
88 })?;
89 let sums =
90 String::from_utf8(http_get_bytes(&sums_url).context("fetching the release SHA256SUMS")?)
91 .context("release SHA256SUMS is not valid UTF-8")?;
92
93 let want = expected_sha256(&sums, &asset)
94 .ok_or_else(|| anyhow!("no checksum for {asset} in the release SHA256SUMS"))?;
95 let got = sha256_hex(&bytes);
96 if got != want {
97 bail!("checksum mismatch for {asset}: expected {want}, got {got}");
98 }
99 Ok(bytes)
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 #[test]
107 fn asset_name_is_git_ark_dash_triple() {
108 assert_eq!(
109 asset_name("x86_64-unknown-linux-musl"),
110 "git-ark-x86_64-unknown-linux-musl"
111 );
112 }
113
114 #[test]
115 fn release_base_pins_the_client_version_tag() {
116 assert_eq!(
117 release_base("acme/git-ark", "1.2.3"),
118 "https://github.com/acme/git-ark/releases/download/v1.2.3"
119 );
120 }
121
122 #[test]
123 fn expected_sha256_matches_by_trailing_filename() {
124 let sums = "\
126aaaa1111 git-ark-x86_64-unknown-linux-musl
127BBBB2222 git-ark-aarch64-unknown-linux-musl
128cccc3333 git-ark-x86_64-pc-windows-msvc.exe
129";
130 assert_eq!(
131 expected_sha256(sums, "git-ark-aarch64-unknown-linux-musl").as_deref(),
132 Some("bbbb2222"), );
134 assert_eq!(
135 expected_sha256(sums, "git-ark-x86_64-unknown-linux-musl").as_deref(),
136 Some("aaaa1111"),
137 );
138 }
139
140 #[test]
141 fn expected_sha256_none_for_absent_asset() {
142 let sums = "aaaa1111 git-ark-x86_64-unknown-linux-musl\n";
143 assert_eq!(expected_sha256(sums, "git-ark-nonesuch"), None);
144 }
145
146 #[test]
147 fn expected_sha256_does_not_match_a_name_substring() {
148 let sums = "aaaa1111 git-ark-x86_64-apple-darwin\n";
151 assert_eq!(expected_sha256(sums, "git-ark-x86_64-apple"), None);
152 assert_eq!(expected_sha256(sums, "apple-darwin"), None);
153 }
154
155 #[test]
156 fn sha256_hex_known_vector() {
157 assert_eq!(
159 sha256_hex(b""),
160 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
161 );
162 assert_eq!(
164 sha256_hex(b"abc"),
165 "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
166 );
167 }
168}