use std::path::{Path, PathBuf};
use std::process::Command;
use std::time::Duration;
use ureq::config::{ConfigBuilder, RedirectAuthHeaders};
use ureq::http::{StatusCode, Uri};
use ureq::typestate::AgentScope;
const REPO_API_LATEST: &str = "https://api.github.com/repos/zakuro-ai/zc/releases/latest";
const REPO_CLONE_HOST: &str = "github.com/zakuro-ai/zc";
const OCTET_STREAM: &str = "application/octet-stream";
const MAX_BINARY_BYTES: u64 = 256 * 1024 * 1024;
const PRIVATE_RELEASES: &str = "zc releases are private. Sign in with `gh auth login` (or set GH_PAT / GITHUB_TOKEN) and run zc update again.";
const FROM_SOURCE_FALLBACK: &str = "Fallback: try building from source instead — `zc update --from-source` (it also needs a GitHub token with access to zakuro-ai/zc).";
fn asset_name() -> &'static str {
match (cfg!(target_os = "macos"), cfg!(target_arch = "aarch64")) {
(true, true) => "zc-darwin-arm64",
(true, false) => "zc-darwin-x86_64",
(false, true) => "zc-linux-arm64",
(false, false) => "zc-linux-x86_64",
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
PrebuiltRelease,
FromSource,
}
pub fn parse_mode(args: &[String]) -> Mode {
if args.iter().any(|a| a == "--from-source") {
Mode::FromSource
} else {
Mode::PrebuiltRelease
}
}
pub fn resolve_token<F1, F2>(try_gh: F1, get_env: F2) -> Option<String>
where
F1: FnOnce() -> Option<String>,
F2: Fn(&str) -> Option<String>,
{
if let Some(t) = try_gh() {
if !t.trim().is_empty() {
return Some(t.trim().to_string());
}
}
if let Some(t) = get_env("GH_PAT") {
if !t.trim().is_empty() {
return Some(t.trim().to_string());
}
}
if let Some(t) = get_env("GITHUB_TOKEN") {
if !t.trim().is_empty() {
return Some(t.trim().to_string());
}
}
None
}
fn gh_auth_token() -> Option<String> {
let out = Command::new("gh").args(["auth", "token"]).output().ok()?;
if !out.status.success() {
return None;
}
let token = String::from_utf8_lossy(&out.stdout).trim().to_string();
if token.is_empty() {
None
} else {
Some(token)
}
}
fn real_env(key: &str) -> Option<String> {
std::env::var(key).ok()
}
pub fn run_cli(args: &[String]) -> i32 {
match parse_mode(args) {
Mode::PrebuiltRelease => update_from_release(),
Mode::FromSource => update_from_source(),
}
}
fn replace_binary_atomic(exe_path: &Path, bytes: &[u8]) -> std::io::Result<()> {
let dir = exe_path.parent().unwrap_or_else(|| Path::new("."));
let tmp_path: PathBuf = dir.join(format!(".zc-update-{}.tmp", std::process::id()));
std::fs::write(&tmp_path, bytes)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&tmp_path, std::fs::Permissions::from_mode(0o755))?;
}
std::fs::rename(&tmp_path, exe_path)?;
Ok(())
}
fn current_exe_or_bail() -> Option<PathBuf> {
match std::env::current_exe() {
Ok(p) => Some(p),
Err(e) => {
eprintln!(
"Error: could not determine the current executable path: {}",
e
);
None
}
}
}
fn dir_writable(dir: &Path) -> bool {
let probe = dir.join(format!(".zc-write-probe-{}", std::process::id()));
match std::fs::File::create(&probe) {
Ok(_) => {
let _ = std::fs::remove_file(&probe);
true
}
Err(_) => false,
}
}
fn local_bin_zc() -> PathBuf {
let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
PathBuf::from(home).join(".local").join("bin").join("zc")
}
fn resolve_install_target() -> (PathBuf, bool) {
if let Ok(exe) = std::env::current_exe() {
if let Some(dir) = exe.parent() {
if dir_writable(dir) {
return (exe, false);
}
}
}
(local_bin_zc(), true)
}
fn append_line_once(path: &Path, line: &str) -> std::io::Result<bool> {
let existing = std::fs::read_to_string(path).unwrap_or_default();
if existing.lines().any(|l| l.trim() == line.trim()) {
return Ok(false);
}
use std::io::Write;
let mut f = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)?;
let sep = if existing.is_empty() || existing.ends_with('\n') {
""
} else {
"\n"
};
writeln!(
f,
"{}# added by `zc update` — put ~/.local/bin on PATH\n{}",
sep, line
)?;
Ok(true)
}
fn shell_rc_path() -> Option<PathBuf> {
let home = std::env::var("HOME").ok()?;
let shell = std::env::var("SHELL").unwrap_or_default();
let file = if shell.contains("zsh") {
".zshrc"
} else if shell.contains("bash") {
".bashrc"
} else {
".profile"
};
Some(PathBuf::from(home).join(file))
}
fn ensure_local_bin_on_path(installed: &Path) {
let bin_dir = installed
.parent()
.map(|p| p.display().to_string())
.unwrap_or_default();
let on_path = std::env::var("PATH")
.map(|p| p.split(':').any(|d| d == bin_dir))
.unwrap_or(false);
if on_path {
println!("Installed to {}.", installed.display());
println!("Run `hash -r` (or open a new terminal) so your shell uses the new binary.");
return;
}
let export = "export PATH=\"$HOME/.local/bin:$PATH\"".to_string();
let appended = shell_rc_path().and_then(|rc| {
let ok = append_line_once(&rc, &export).unwrap_or(false);
ok.then_some(rc)
});
println!("Installed to {}.", installed.display());
match appended {
Some(rc) => {
println!("Added ~/.local/bin to your PATH in {}.", rc.display());
println!(
"Open a new terminal (or `source {}`) and run `zc`.",
rc.display()
);
}
None => {
println!("Add ~/.local/bin to your PATH so `zc` is found:");
println!(" {}", export);
}
}
}
#[derive(Debug, PartialEq, Eq)]
struct AssetSource {
url: String,
accept: &'static str,
}
fn select_asset(release: &serde_json::Value, name: &str) -> Option<AssetSource> {
let asset = release
.get("assets")?
.as_array()?
.iter()
.find(|a| a.get("name").and_then(|n| n.as_str()) == Some(name))?;
let field = |key: &str| asset.get(key).and_then(|v| v.as_str()).map(str::to_string);
match field("url") {
Some(url) => Some(AssetSource {
url,
accept: OCTET_STREAM,
}),
None => field("browser_download_url").map(|url| AssetSource { url, accept: "*/*" }),
}
}
fn token_for<'a>(url: &str, token: Option<&'a str>) -> Option<&'a str> {
let uri: Uri = url.parse().ok()?;
let github_api = uri.scheme_str() == Some("https")
&& uri
.host()
.is_some_and(|h| h.eq_ignore_ascii_case("api.github.com"));
token.filter(|_| github_api)
}
fn github_headers(accept: &str, token: Option<&str>) -> Vec<(&'static str, String)> {
let mut headers = vec![
("User-Agent", "zc-update".to_string()),
("Accept", accept.to_string()),
];
if let Some(t) = token {
headers.push(("Authorization", format!("Bearer {t}")));
}
headers
}
fn api_config() -> ConfigBuilder<AgentScope> {
ureq::Agent::config_builder()
.timeout_connect(Some(Duration::from_secs(10)))
.timeout_global(Some(Duration::from_secs(30)))
.http_status_as_error(false)
}
fn download_config() -> ConfigBuilder<AgentScope> {
ureq::Agent::config_builder()
.timeout_connect(Some(Duration::from_secs(10)))
.timeout_global(Some(Duration::from_secs(300)))
.http_status_as_error(false)
.redirect_auth_headers(RedirectAuthHeaders::Never)
}
fn api_status_message(status: StatusCode, have_token: bool) -> String {
if status != StatusCode::NOT_FOUND {
format!("GitHub releases API returned HTTP {status}\n{FROM_SOURCE_FALLBACK}")
} else if !have_token {
PRIVATE_RELEASES.to_string()
} else {
format!(
"GitHub releases API returned HTTP {status}: the GitHub token zc found may not have access to zakuro-ai/zc (the repo is private), or there is no release yet.\n\
Check its account with `gh auth status`; zc uses gh's token first, then GH_PAT, then GITHUB_TOKEN.\n\
{FROM_SOURCE_FALLBACK}"
)
}
}
fn download(
agent: &ureq::Agent,
src: &AssetSource,
token: Option<&str>,
) -> Result<Vec<u8>, String> {
let mut req = agent.get(&src.url);
for (name, value) in github_headers(src.accept, token) {
req = req.header(name, value);
}
let resp = req
.call()
.map_err(|e| format!("Failed to download release asset: {e}"))?;
if resp.status() != StatusCode::OK {
return Err(format!("Download failed: HTTP {}", resp.status()));
}
resp.into_body()
.with_config()
.limit(MAX_BINARY_BYTES)
.read_to_vec()
.map_err(|e| format!("Failed to read downloaded binary: {e}"))
}
fn update_from_release() -> i32 {
println!("Checking latest zc release...");
let token = resolve_token(gh_auth_token, real_env);
let api = ureq::Agent::new_with_config(api_config().build());
let mut req = api.get(REPO_API_LATEST);
let api_token = token_for(REPO_API_LATEST, token.as_deref());
for (name, value) in github_headers("application/vnd.github+json", api_token) {
req = req.header(name, value);
}
let resp = match req.call() {
Ok(r) => r,
Err(e) => {
eprintln!("Failed to reach GitHub releases API: {}", e);
eprintln!("{FROM_SOURCE_FALLBACK}");
return 1;
}
};
if resp.status() != StatusCode::OK {
eprintln!("{}", api_status_message(resp.status(), api_token.is_some()));
return 1;
}
let body = match resp.into_body().read_to_string() {
Ok(s) => s,
Err(e) => {
eprintln!("Failed to read releases API response: {}", e);
return 1;
}
};
let data: serde_json::Value = match serde_json::from_str(&body) {
Ok(v) => v,
Err(e) => {
eprintln!("Failed to parse releases API response: {}", e);
return 1;
}
};
let version = data
.get("tag_name")
.and_then(|v| v.as_str())
.unwrap_or("unknown")
.to_string();
let src = match select_asset(&data, asset_name()) {
Some(s) => s,
None => {
eprintln!("No {} asset found on release {}.", asset_name(), version);
eprintln!("{FROM_SOURCE_FALLBACK}");
return 1;
}
};
println!("Downloading {} ({})...", asset_name(), version);
let downloader = ureq::Agent::new_with_config(download_config().build());
let bytes = match download(&downloader, &src, token_for(&src.url, token.as_deref())) {
Ok(b) => b,
Err(e) => {
eprintln!("{e}");
eprintln!("{FROM_SOURCE_FALLBACK}");
return 1;
}
};
let (exe_path, to_local_bin) = resolve_install_target();
if let Some(parent) = exe_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
println!("Installing to {}...", exe_path.display());
if let Err(e) = replace_binary_atomic(&exe_path, &bytes) {
eprintln!("Failed to install the new binary: {}", e);
return 1;
}
if to_local_bin {
ensure_local_bin_on_path(&exe_path);
}
println!("updated to {}", version);
0
}
fn update_from_source() -> i32 {
let token = match resolve_token(gh_auth_token, real_env) {
Some(t) => t,
None => {
eprintln!("no GitHub token found; run `gh auth login` or set GH_PAT/GITHUB_TOKEN");
return 1;
}
};
let (exe_path, to_local_bin) = resolve_install_target();
if let Some(parent) = exe_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let tmp_dir = std::env::temp_dir().join(format!("zc-update-src-{}", std::process::id()));
if let Err(e) = std::fs::create_dir_all(&tmp_dir) {
eprintln!("Failed to create temp dir {}: {}", tmp_dir.display(), e);
return 1;
}
println!("Cloning zakuro-ai/zc (source build)...");
let clone_url = format!("https://x-access-token:{}@{}", token, REPO_CLONE_HOST);
let clone_status = Command::new("git")
.args(["clone", "--depth", "1", &clone_url, "."])
.current_dir(&tmp_dir)
.status();
match clone_status {
Ok(s) if s.success() => {}
Ok(s) => {
eprintln!("git clone failed (exit {}).", s);
let _ = std::fs::remove_dir_all(&tmp_dir);
return 1;
}
Err(e) => {
eprintln!("Failed to run git clone: {}", e);
let _ = std::fs::remove_dir_all(&tmp_dir);
return 1;
}
}
println!("Building (cargo build --release)... this can take a few minutes.");
let (prog, pre_args) = cargo_invocation();
let build_output = Command::new(&prog)
.args(pre_args.iter().map(String::as_str))
.args(["build", "--release"])
.current_dir(&tmp_dir)
.output();
let build_output = match build_output {
Ok(o) => o,
Err(e) => {
eprintln!("Failed to run cargo build: {}", e);
let _ = std::fs::remove_dir_all(&tmp_dir);
return 1;
}
};
if !build_output.status.success() {
let stderr = String::from_utf8_lossy(&build_output.stderr);
let tail: String = stderr
.lines()
.rev()
.take(30)
.collect::<Vec<_>>()
.into_iter()
.rev()
.collect::<Vec<_>>()
.join("\n");
eprintln!("cargo build --release failed. Build output (tail):");
eprintln!("{}", tail);
if stderr.contains("lock file version") {
eprintln!();
eprintln!(
"This is a toolchain problem, not a code problem: the cargo that ran is\n\
too old to read this repo's Cargo.lock (v4 needs Rust >= 1.78).\n\
Install rustup so the pinned `stable` toolchain is used, or run the\n\
build yourself with a newer cargo:\n\
\n\
\trustup run stable cargo build --release"
);
}
let _ = std::fs::remove_dir_all(&tmp_dir);
return 1;
}
let mut built_bin: Option<PathBuf> = None;
for c in ["zc", "zc2"] {
let p = tmp_dir.join("target/release").join(c);
if p.is_file() {
built_bin = Some(p);
break;
}
}
let built_bin = match built_bin {
Some(p) => p,
None => {
eprintln!("Build finished but no zc binary found under target/release/.");
let _ = std::fs::remove_dir_all(&tmp_dir);
return 1;
}
};
let bytes = match std::fs::read(&built_bin) {
Ok(b) => b,
Err(e) => {
eprintln!("Failed to read built binary: {}", e);
let _ = std::fs::remove_dir_all(&tmp_dir);
return 1;
}
};
println!("Installing to {}...", exe_path.display());
if let Err(e) = replace_binary_atomic(&exe_path, &bytes) {
eprintln!("Failed to install the new binary: {}", e);
let _ = std::fs::remove_dir_all(&tmp_dir);
return 1;
}
if to_local_bin {
ensure_local_bin_on_path(&exe_path);
}
let version = Command::new(&exe_path)
.arg("--version")
.output()
.ok()
.filter(|o| o.status.success())
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
.unwrap_or_else(|| "source build".to_string());
let _ = std::fs::remove_dir_all(&tmp_dir);
println!("updated to {}", version);
0
}
fn cargo_invocation() -> (String, Vec<String>) {
let rustup_ok = Command::new("rustup")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false);
if rustup_ok {
(
"rustup".to_string(),
["run", "stable", "cargo"]
.iter()
.map(|s| s.to_string())
.collect(),
)
} else {
("cargo".to_string(), Vec::new())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn append_line_once_is_idempotent() {
let dir = std::env::temp_dir().join(format!("zc-rc-test-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let rc = dir.join("rc");
let line = "export PATH=\"$HOME/.local/bin:$PATH\"";
assert!(super::append_line_once(&rc, line).unwrap()); assert!(!super::append_line_once(&rc, line).unwrap()); let body = std::fs::read_to_string(&rc).unwrap();
assert_eq!(body.matches(line).count(), 1);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn parse_mode_default_is_prebuilt_release() {
let args: Vec<String> = vec![];
assert_eq!(parse_mode(&args), Mode::PrebuiltRelease);
}
#[test]
fn parse_mode_other_flags_still_prebuilt_release() {
let args: Vec<String> = vec!["--verbose".to_string()];
assert_eq!(parse_mode(&args), Mode::PrebuiltRelease);
}
#[test]
fn parse_mode_from_source_flag_selects_source_build() {
let args: Vec<String> = vec!["--from-source".to_string()];
assert_eq!(parse_mode(&args), Mode::FromSource);
}
#[test]
fn parse_mode_from_source_among_other_args() {
let args: Vec<String> = vec!["--verbose".to_string(), "--from-source".to_string()];
assert_eq!(parse_mode(&args), Mode::FromSource);
}
#[test]
fn resolve_token_prefers_gh() {
let t = resolve_token(
|| Some("gh-token".to_string()),
|k| match k {
"GH_PAT" => Some("pat-token".to_string()),
"GITHUB_TOKEN" => Some("gh-env-token".to_string()),
_ => None,
},
);
assert_eq!(t.as_deref(), Some("gh-token"));
}
#[test]
fn resolve_token_falls_back_to_gh_pat() {
let t = resolve_token(
|| None,
|k| match k {
"GH_PAT" => Some("pat-token".to_string()),
"GITHUB_TOKEN" => Some("gh-env-token".to_string()),
_ => None,
},
);
assert_eq!(t.as_deref(), Some("pat-token"));
}
#[test]
fn resolve_token_falls_back_to_github_token() {
let t = resolve_token(
|| None,
|k| match k {
"GH_PAT" => None,
"GITHUB_TOKEN" => Some("gh-env-token".to_string()),
_ => None,
},
);
assert_eq!(t.as_deref(), Some("gh-env-token"));
}
#[test]
fn resolve_token_none_found() {
let t = resolve_token(|| None, |_k| None);
assert_eq!(t, None);
}
#[test]
fn resolve_token_skips_blank_values() {
let t = resolve_token(
|| Some(" ".to_string()),
|k| match k {
"GH_PAT" => Some("".to_string()),
"GITHUB_TOKEN" => Some("real-token".to_string()),
_ => None,
},
);
assert_eq!(t.as_deref(), Some("real-token"));
}
const API_ASSET: &str = "https://api.github.com/repos/zakuro-ai/zc/releases/assets/2";
const BROWSER_ASSET: &str =
"https://github.com/zakuro-ai/zc/releases/download/v0.0.26/zc-darwin-arm64";
#[test]
fn asset_selection_prefers_the_api_url_with_octet_stream() {
let release = serde_json::json!({ "tag_name": "v0.0.26", "assets": [
{
"name": "zc-linux-x86_64",
"url": "https://api.github.com/repos/zakuro-ai/zc/releases/assets/1",
"browser_download_url": "https://github.com/zakuro-ai/zc/releases/download/v0.0.26/zc-linux-x86_64",
},
{ "name": "zc-darwin-arm64", "url": API_ASSET, "browser_download_url": BROWSER_ASSET },
]});
assert_eq!(
select_asset(&release, "zc-darwin-arm64"),
Some(AssetSource {
url: API_ASSET.to_string(),
accept: "application/octet-stream",
})
);
assert_eq!(select_asset(&release, "zc-linux-arm64"), None);
}
#[test]
fn asset_selection_falls_back_to_the_browser_download_url() {
let release = serde_json::json!({ "assets": [
{ "name": "zc-darwin-arm64", "browser_download_url": BROWSER_ASSET },
]});
assert_eq!(
select_asset(&release, "zc-darwin-arm64"),
Some(AssetSource {
url: BROWSER_ASSET.to_string(),
accept: "*/*",
})
);
}
#[test]
fn a_token_becomes_an_authorization_header_on_github_requests_only() {
let with = github_headers(OCTET_STREAM, Some("t0ken"));
assert!(
with.contains(&("Authorization", "Bearer t0ken".to_string())),
"{with:?}"
);
assert!(
with.contains(&("Accept", OCTET_STREAM.to_string())),
"{with:?}"
);
assert!(
with.contains(&("User-Agent", "zc-update".to_string())),
"{with:?}"
);
let without = github_headers(OCTET_STREAM, None);
assert!(
without.iter().all(|(k, _)| *k != "Authorization"),
"{without:?}"
);
assert_eq!(
download_config().build().redirect_auth_headers(),
RedirectAuthHeaders::Never
);
}
#[test]
fn the_download_gets_300_s_and_the_api_call_30_s() {
assert_eq!(
download_config().build().timeouts().global,
Some(Duration::from_secs(300))
);
assert_eq!(
api_config().build().timeouts().global,
Some(Duration::from_secs(30))
);
}
#[test]
fn without_a_token_a_404_says_the_releases_are_private() {
assert_eq!(
api_status_message(StatusCode::NOT_FOUND, false),
"zc releases are private. Sign in with `gh auth login` (or set GH_PAT / GITHUB_TOKEN) and run zc update again."
);
for have_token in [false, true] {
let m = api_status_message(StatusCode::FORBIDDEN, have_token);
assert!(
m.starts_with("GitHub releases API returned HTTP 403 Forbidden\n"),
"{m}"
);
assert!(m.ends_with(FROM_SOURCE_FALLBACK), "{m}");
}
}
#[test]
fn with_a_token_a_404_says_it_may_lack_access_or_there_is_no_release() {
let m = api_status_message(StatusCode::NOT_FOUND, true);
for part in [
"HTTP 404 Not Found",
"may not have access to zakuro-ai/zc (the repo is private)",
"or there is no release yet",
"`gh auth status`",
"GH_PAT",
"GITHUB_TOKEN",
] {
assert!(m.contains(part), "missing {part:?} in: {m}");
}
assert!(m.ends_with(FROM_SOURCE_FALLBACK), "{m}");
}
#[test]
fn the_token_goes_to_api_github_com_only() {
let sends_token = |url: &str| {
github_headers(OCTET_STREAM, token_for(url, Some("t0ken")))
.iter()
.any(|(k, v)| *k == "Authorization" && v == "Bearer t0ken")
};
for url in [
REPO_API_LATEST,
API_ASSET,
"https://API.GitHub.com/repos/zakuro-ai/zc/releases/latest",
] {
assert!(sends_token(url), "{url} gets the token");
}
for url in [
BROWSER_ASSET, "https://objects.githubusercontent.com/github-production-release-asset/1?sig=x",
"https://api.github.com.evil.example/repos/zakuro-ai/zc", "https://evil.example/api.github.com/repos/zakuro-ai/zc", "https://api.github.com@evil.example/repos/zakuro-ai/zc", "http://api.github.com/repos/zakuro-ai/zc", "not a url",
] {
assert!(!sends_token(url), "{url} must not get the token");
}
assert_eq!(token_for(REPO_API_LATEST, None), None);
}
fn one_shot_server(response: Vec<u8>) -> (String, std::thread::JoinHandle<String>) {
use std::io::{Read, Write};
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let base = format!("http://{}", listener.local_addr().unwrap());
let handle = std::thread::spawn(move || {
let (mut s, _) = listener.accept().unwrap();
let mut head = Vec::new();
let mut b = [0u8; 1024];
while !head.windows(4).any(|w| w == b"\r\n\r\n") {
match s.read(&mut b) {
Ok(0) | Err(_) => break,
Ok(n) => head.extend_from_slice(&b[..n]),
}
}
s.write_all(&response).unwrap();
String::from_utf8_lossy(&head).into_owned()
});
(base, handle)
}
#[test]
fn the_download_follows_the_redirect_without_the_token() {
let blob = b"\x7fELF not really a zc binary".to_vec();
let mut ok = format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/octet-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
blob.len()
)
.into_bytes();
ok.extend_from_slice(&blob);
let (storage, storage_req) = one_shot_server(ok);
let (api, api_req) = one_shot_server(
format!(
"HTTP/1.1 302 Found\r\nLocation: {storage}/signed?sig=abc\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
)
.into_bytes(),
);
let agent = ureq::Agent::new_with_config(download_config().proxy(None).build());
let src = AssetSource {
url: format!("{api}/repos/zakuro-ai/zc/releases/assets/2"),
accept: OCTET_STREAM,
};
let bytes = download(&agent, &src, Some("t0ken")).expect("download succeeds");
assert_eq!(bytes, blob);
let api_req = api_req.join().unwrap().to_ascii_lowercase();
assert!(
api_req.contains("\r\nauthorization: bearer t0ken\r\n"),
"{api_req}"
);
assert!(
api_req.contains("\r\naccept: application/octet-stream\r\n"),
"{api_req}"
);
let storage_req = storage_req.join().unwrap().to_ascii_lowercase();
assert!(
storage_req.starts_with("get /signed?sig=abc http/1.1\r\n"),
"{storage_req}"
);
assert!(!storage_req.contains("authorization"), "{storage_req}");
}
}
#[cfg(test)]
mod cargo_invocation_tests {
#[test]
fn rustup_invocation_puts_cargo_after_run_stable() {
let (prog, pre) = super::cargo_invocation();
if prog == "rustup" {
assert_eq!(pre, vec!["run", "stable", "cargo"]);
} else {
assert_eq!(prog, "cargo");
assert!(pre.is_empty(), "plain cargo takes no leading args");
}
}
}