hyperdb_bootstrap/verify.rs
1// Copyright (c) 2026, Salesforce, Inc. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! HEAD each supported platform's download URL to confirm the pinned
5//! release is still reachable on Tableau's CDN. Used by the `verify`
6//! CLI subcommand and by CI workflows that guard against silent yanks
7//! or URL-scheme changes.
8
9use std::process::Command;
10
11use crate::platform::Platform;
12use crate::release::PinnedRelease;
13use crate::url::build_download_url;
14use crate::Error;
15
16const PLATFORMS: &[Platform] = &[
17 Platform::MacosArm64,
18 Platform::MacosX86_64,
19 Platform::LinuxX86_64,
20 Platform::WindowsX86_64,
21];
22
23/// Result of a single platform reachability probe performed by
24/// [`verify_release`].
25#[derive(Debug, Clone)]
26pub struct VerifyOutcome {
27 /// Platform the probe targeted.
28 pub platform: Platform,
29 /// URL that was probed.
30 pub url: String,
31 /// HTTP status returned by the CDN, or `None` if the probe itself failed.
32 pub status: Option<u16>,
33 /// Error message when `status` is `None` (spawn failure, parse failure,
34 /// stderr from a failed `curl` invocation).
35 pub error: Option<String>,
36}
37
38impl VerifyOutcome {
39 /// Returns `true` when the probe observed a 2xx/3xx HTTP status, which
40 /// is what `curl --head --location` returns when the CDN serves the
41 /// release.
42 #[must_use]
43 pub fn ok(&self) -> bool {
44 matches!(self.status, Some(s) if (200..400).contains(&s))
45 }
46}
47
48/// HEAD every supported platform URL for `release`. Returns one outcome
49/// per platform; callers decide how to surface failures.
50///
51/// Uses `curl --head` rather than reqwest so that Akamai's bot-protection
52/// layer (which blocks reqwest's TLS fingerprint from GitHub-hosted runner
53/// IPs) does not cause false 403 failures.
54///
55/// # Errors
56///
57/// Currently always returns `Ok(_)` — individual platform failures are
58/// surfaced through [`VerifyOutcome::error`]. The `Result` wrapper is
59/// kept for forward compatibility if a top-level failure mode is added.
60pub fn verify_release(release: &PinnedRelease) -> Result<Vec<VerifyOutcome>, Error> {
61 let mut out = Vec::with_capacity(PLATFORMS.len());
62 for &platform in PLATFORMS {
63 let url = build_download_url(release, platform);
64 let outcome = curl_head(&url);
65 out.push(VerifyOutcome {
66 platform,
67 url,
68 status: outcome.0,
69 error: outcome.1,
70 });
71 }
72 Ok(out)
73}
74
75/// Run `curl --head --silent --show-error --location <url>` and parse the
76/// HTTP status from the first status line. Returns `(Some(status), None)`
77/// on success and `(None, Some(error))` on spawn/parse failure.
78fn curl_head(url: &str) -> (Option<u16>, Option<String>) {
79 let result = Command::new("curl")
80 .args(["--head", "--silent", "--show-error", "--location"])
81 .arg(url)
82 .output();
83
84 match result {
85 Err(e) => (None, Some(format!("failed to spawn curl: {e}"))),
86 Ok(output) => {
87 if !output.status.success() && output.stdout.is_empty() {
88 let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
89 return (None, Some(stderr));
90 }
91 // Parse the last "HTTP/x.x NNN" status line (curl --location may
92 // produce multiple status lines when following redirects).
93 let stdout = String::from_utf8_lossy(&output.stdout);
94 let status = stdout
95 .lines()
96 .filter_map(|line| {
97 let line = line.trim();
98 // Matches "HTTP/1.1 200 OK", "HTTP/2 403", etc.
99 if line.starts_with("HTTP/") {
100 line.split_whitespace().nth(1)?.parse::<u16>().ok()
101 } else {
102 None
103 }
104 })
105 .next_back();
106 match status {
107 Some(s) => (Some(s), None),
108 None => (
109 None,
110 Some(format!(
111 "could not parse HTTP status from curl output: {}",
112 stdout.chars().take(200).collect::<String>()
113 )),
114 ),
115 }
116 }
117 }
118}