Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/wright-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ workspace = true
name = "wright"
path = "src/main.rs"

[[test]]
name = "update_provider"
path = "tests/update/provider.rs"

[dependencies]
arboard = { version = "3.6", default-features = false }
clap = { version = "4.5", features = ["derive"] }
Expand Down
33 changes: 15 additions & 18 deletions crates/wright-cli/src/update.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
use std::cmp::Ordering;
use std::io::Read;
use std::path::Path;
use std::time::Duration;

use clap::{Args, Subcommand, ValueEnum};
use wright_driver::result::exit;
use wright_driver::{OpyProviderConfig, OpyProviderError, ResolvedOpyProvider, sha256_hex};
use wright_driver::{OpyProviderConfig, OpyProviderError, ResolvedOpyProvider};

const DEFAULT_BASE_URL: &str = "https://releases.wrightkit.dev/wright";
const USER_AGENT: &str = concat!("wright-update/", env!("CARGO_PKG_VERSION"));
const MAX_DOWNLOAD_BYTES: u64 = 128 * 1024 * 1024;

mod install;

Expand Down Expand Up @@ -501,15 +500,11 @@ fn parse_latest_version(body: &str) -> Option<String> {
}

fn update_client() -> Result<reqwest::blocking::Client, UpdateError> {
reqwest::blocking::Client::builder()
.user_agent(USER_AGENT)
.timeout(Duration::from_secs(60))
.build()
.map_err(|e| {
UpdateError::failed(format!(
"could not initialize HTTPS client for release downloads: {e}"
))
})
wright_driver::store::client(USER_AGENT).map_err(|e| {
UpdateError::failed(format!(
"could not initialize HTTPS client for release downloads: {e}"
))
})
}

fn fetch_text(client: &reqwest::blocking::Client, url: &str) -> Result<String, UpdateError> {
Expand All @@ -526,12 +521,14 @@ fn fetch(client: &reqwest::blocking::Client, url: &str) -> Result<Vec<u8>, Updat
response.status()
)));
}
let mut bytes = Vec::new();
response
.take(128 * 1024 * 1024)
.read_to_end(&mut bytes)
.map_err(|e| UpdateError::failed(format!("could not download {url}: {e}")))?;
Ok(bytes)
wright_driver::store::read_limited(response, MAX_DOWNLOAD_BYTES).map_err(|error| match error {
wright_driver::store::ReadError::Io(error) => {
UpdateError::failed(format!("could not download {url}: {error}"))
}
wright_driver::store::ReadError::OverLimit => UpdateError::failed(format!(
"download from {url} exceeds the 128 MiB size limit"
)),
})
}

#[derive(Debug, Clone, PartialEq, Eq)]
Expand Down
52 changes: 18 additions & 34 deletions crates/wright-cli/src/update/install.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::path::{Path, PathBuf};
use std::process::Command as Process;

use super::{Platform, UpdateError, fetch, fetch_text, sha256_hex, truncate};
use super::{Platform, UpdateError, fetch, fetch_text, truncate};

pub(super) fn install_version(
client: &reqwest::blocking::Client,
Expand Down Expand Up @@ -59,21 +59,8 @@ pub(super) fn install_version(
}

fn staging_dir(install_dir: &Path) -> Result<PathBuf, UpdateError> {
let staging = install_dir.join(format!(
".wright-update-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or_default()
));
std::fs::create_dir_all(&staging).map_err(|e| {
UpdateError::failed(format!(
"could not create a staging directory in {}: {e}",
install_dir.display()
))
})?;
Ok(staging)
wright_driver::store::reserve_staging_dir(install_dir, ".wright-update", "an update")
.map_err(UpdateError::failed)
}

fn unpack(archive: &[u8], dest: &Path, platform: Platform) -> Result<(), UpdateError> {
Expand Down Expand Up @@ -112,23 +99,20 @@ pub(super) fn verify_checksum(
checksum_file: &str,
archive_name: &str,
) -> Result<(), UpdateError> {
let published = checksum_file
.split_whitespace()
.next()
.ok_or_else(|| UpdateError::failed(format!("empty checksum file for {archive_name}")))?;
if published.len() != 64 || !published.bytes().all(|b| b.is_ascii_hexdigit()) {
return Err(UpdateError::failed(format!(
wright_driver::store::verify_sha256(archive, checksum_file).map_err(|error| match error {
wright_driver::store::ChecksumError::Empty => {
UpdateError::failed(format!("empty checksum file for {archive_name}"))
}
wright_driver::store::ChecksumError::Invalid(digest) => UpdateError::failed(format!(
"invalid checksum file for {archive_name}: expected one SHA-256 hex digest (got '{}')",
truncate(published)
)));
}
let actual = sha256_hex(archive);
if !actual.eq_ignore_ascii_case(published) {
return Err(UpdateError::failed(format!(
"checksum verification failed for {archive_name} (published {published}, got {actual}); the download may be corrupted or tampered with — nothing was changed"
)));
}
Ok(())
truncate(&digest)
)),
wright_driver::store::ChecksumError::Mismatch(published, actual) => {
UpdateError::failed(format!(
"checksum verification failed for {archive_name} (published {published}, got {actual}); the download may be corrupted or tampered with — nothing was changed"
))
}
})
}

fn ensure_writable(install_dir: &Path, platform: Platform) -> Result<(), UpdateError> {
Expand Down Expand Up @@ -223,10 +207,10 @@ mod tests {
fn checksum_verification_accepts_good_and_rejects_bad() {
let bytes = b"the archive bytes";
let name = "wright-9.9.9-x86_64-unknown-linux-gnu.tar.gz";
let good = format!("{} {name}\n", sha256_hex(bytes));
let good = format!("{} {name}\n", wright_driver::sha256_hex(bytes));
verify_checksum(bytes, &good, name).unwrap();

let bad = format!("{} archive\n", sha256_hex(b"other bytes"));
let bad = format!("{} archive\n", wright_driver::sha256_hex(b"other bytes"));
let error = verify_checksum(bytes, &bad, "archive").unwrap_err();
assert!(error.message().contains("checksum verification failed"));

Expand Down
Loading
Loading