Check hash of downloaded python toolchain (#4806)

## Summary

Check the sha256 checksum when downloading a managed python toolchain.

## Test Plan

```sh
$ cargo run -- python install 3.12

warning: `uv python install` is experimental and may change without warning.
Looking for installation Python 3.12.3 (any-3.12.3-any-any-any)
Downloading cpython-3.12.3-windows-x86_64-none
Installed Python 3.12.3 to C:\Users\jo\AppData\Roaming\uv\data\python\cpython-3.12.3-windows-x86_64-none
Installed 1 installation in 6s

$ cargo run -- python uninstall 3.12

$ # manually change the hash in `crates/uv-python/src/downloads.inc`

$ cargo run -- python install 3.12

warning: `uv python install` is experimental and may change without warning.
Looking for installation Python 3.12 (any-3.12-any-any-any)
Downloading cpython-3.12.3-windows-x86_64-none
error: Hash mismatch for `cpython-3.12.3-windows-x86_64-none`

Expected:
xx

Computed:
776568c92c5f3b47dbf5f17c1c58578f70d75a32654419a158aa8bdc6f95b09a
```
This commit is contained in:
Jo 2024-07-05 01:49:12 +08:00 committed by GitHub
parent 445d45b82c
commit dac3161f90
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -14,9 +14,11 @@ use uv_client::WrappedReqwestError;
use futures::TryStreamExt;
use pypi_types::{HashAlgorithm, HashDigest};
use tokio_util::compat::FuturesAsyncReadCompatExt;
use tracing::{debug, instrument};
use url::Url;
use uv_extract::hash::Hasher;
use uv_fs::{rename_with_retry, Simplified};
#[derive(Error, Debug)]
@ -35,6 +37,14 @@ pub enum Error {
NetworkMiddlewareError(#[source] anyhow::Error),
#[error("Failed to extract archive: {0}")]
ExtractError(String, #[source] uv_extract::Error),
#[error("Failed to hash installation")]
HashExhaustion(#[source] io::Error),
#[error("Hash mismatch for `{installation}`\n\nExpected:\n{expected}\n\nComputed:\n{actual}")]
HashMismatch {
installation: String,
expected: String,
actual: String,
},
#[error("Invalid download url")]
InvalidUrl(#[from] url::ParseError),
#[error("Failed to create download directory")]
@ -423,9 +433,34 @@ impl ManagedPythonDownload {
.into_async_read();
debug!("Extracting {filename}");
if let Some(expected) = self.sha256 {
let mut hashers = [Hasher::from(HashAlgorithm::Sha256)];
let mut hasher = uv_extract::hash::HashReader::new(reader.compat(), &mut hashers);
uv_extract::stream::archive(&mut hasher, filename, temp_dir.path())
.await
.map_err(|err| Error::ExtractError(filename.to_string(), err))?;
hasher.finish().await.map_err(Error::HashExhaustion)?;
let actual = hashers
.into_iter()
.map(HashDigest::from)
.next()
.unwrap()
.digest;
if !actual.eq_ignore_ascii_case(expected) {
return Err(Error::HashMismatch {
installation: self.key.to_string(),
expected: expected.to_string(),
actual: actual.to_string(),
});
}
} else {
uv_extract::stream::archive(reader.compat(), filename, temp_dir.path())
.await
.map_err(|err| Error::ExtractError(filename.to_string(), err))?;
}
// Extract the top-level directory.
let extracted = match uv_extract::strip_component(temp_dir.path()) {