diff --git a/Cargo.lock b/Cargo.lock index 6b6040705..91f88a1a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -382,6 +382,15 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -970,6 +979,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -5170,6 +5180,7 @@ dependencies = [ "astral-tokio-tar", "async-compression", "async_zip", + "blake2", "fs-err 3.1.0", "futures", "md-5", diff --git a/Cargo.toml b/Cargo.toml index 012cfefed..64fec20f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -84,6 +84,7 @@ axoupdater = { version = "0.9.0", default-features = false } backon = { version = "1.3.0" } base64 = { version = "0.22.1" } bitflags = { version = "2.6.0" } +blake2 = { version = "0.10.6" } boxcar = { version = "0.2.5" } bytecheck = { version = "0.8.0" } cargo-util = { version = "0.2.14" } diff --git a/crates/uv-client/src/html.rs b/crates/uv-client/src/html.rs index 56776c5ef..106bd35a3 100644 --- a/crates/uv-client/src/html.rs +++ b/crates/uv-client/src/html.rs @@ -305,6 +305,7 @@ mod tests { ), sha384: None, sha512: None, + blake2b: None, }, requires_python: None, size: None, @@ -361,6 +362,7 @@ mod tests { sha256: None, sha384: None, sha512: None, + blake2b: None, }, requires_python: None, size: None, @@ -420,6 +422,7 @@ mod tests { ), sha384: None, sha512: None, + blake2b: None, }, requires_python: None, size: None, @@ -476,6 +479,7 @@ mod tests { ), sha384: None, sha512: None, + blake2b: None, }, requires_python: None, size: None, @@ -532,6 +536,7 @@ mod tests { ), sha384: None, sha512: None, + blake2b: None, }, requires_python: None, size: None, @@ -558,7 +563,7 @@ mod tests { "#; let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); let result = SimpleHtml::parse(text, &base).unwrap(); - insta::assert_debug_snapshot!(result, @r###" + insta::assert_debug_snapshot!(result, @r#" SimpleHtml { base: BaseUrl( Url { @@ -586,6 +591,7 @@ mod tests { sha256: None, sha384: None, sha512: None, + blake2b: None, }, requires_python: None, size: None, @@ -595,7 +601,7 @@ mod tests { }, ], } - "###); + "#); } #[test] @@ -612,7 +618,7 @@ mod tests { "#; let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); let result = SimpleHtml::parse(text, &base).unwrap(); - insta::assert_debug_snapshot!(result, @r###" + insta::assert_debug_snapshot!(result, @r#" SimpleHtml { base: BaseUrl( Url { @@ -640,6 +646,7 @@ mod tests { sha256: None, sha384: None, sha512: None, + blake2b: None, }, requires_python: None, size: None, @@ -649,7 +656,7 @@ mod tests { }, ], } - "###); + "#); } #[test] @@ -770,6 +777,7 @@ mod tests { sha256: None, sha384: None, sha512: None, + blake2b: None, }, requires_python: None, size: None, @@ -796,7 +804,7 @@ mod tests { "#; let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); let result = SimpleHtml::parse(text, &base).unwrap(); - insta::assert_debug_snapshot!(result, @r###" + insta::assert_debug_snapshot!(result, @r#" SimpleHtml { base: BaseUrl( Url { @@ -824,6 +832,7 @@ mod tests { sha256: None, sha384: None, sha512: None, + blake2b: None, }, requires_python: None, size: None, @@ -833,7 +842,7 @@ mod tests { }, ], } - "###); + "#); } #[test] @@ -879,6 +888,7 @@ mod tests { sha256: None, sha384: None, sha512: None, + blake2b: None, }, requires_python: None, size: None, @@ -935,6 +945,7 @@ mod tests { sha256: None, sha384: None, sha512: None, + blake2b: None, }, requires_python: None, size: None, @@ -962,7 +973,7 @@ mod tests { "#; let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); let result = SimpleHtml::parse(text, &base).unwrap_err(); - insta::assert_snapshot!(result, @"Unsupported hash algorithm (expected one of: `md5`, `sha256`, `sha384`, or `sha512`) on: `blake2=6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61`"); + insta::assert_snapshot!(result, @"Unsupported hash algorithm (expected one of: `md5`, `sha256`, `sha384`, `sha512`, or `blake2b`) on: `blake2=6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61`"); } #[test] @@ -980,7 +991,7 @@ mod tests { let base = Url::parse("https://storage.googleapis.com/jax-releases/jax_cuda_releases.html") .unwrap(); let result = SimpleHtml::parse(text, &base).unwrap(); - insta::assert_debug_snapshot!(result, @r###" + insta::assert_debug_snapshot!(result, @r#" SimpleHtml { base: BaseUrl( Url { @@ -1008,6 +1019,7 @@ mod tests { sha256: None, sha384: None, sha512: None, + blake2b: None, }, requires_python: None, size: None, @@ -1023,6 +1035,7 @@ mod tests { sha256: None, sha384: None, sha512: None, + blake2b: None, }, requires_python: None, size: None, @@ -1032,7 +1045,7 @@ mod tests { }, ], } - "###); + "#); } /// Test for AWS Code Artifact @@ -1090,6 +1103,7 @@ mod tests { ), sha384: None, sha512: None, + blake2b: None, }, requires_python: None, size: None, @@ -1107,6 +1121,7 @@ mod tests { ), sha384: None, sha512: None, + blake2b: None, }, requires_python: None, size: None, @@ -1124,6 +1139,7 @@ mod tests { ), sha384: None, sha512: None, + blake2b: None, }, requires_python: Some( Ok( @@ -1190,6 +1206,7 @@ mod tests { ), sha384: None, sha512: None, + blake2b: None, }, requires_python: Some( Ok( @@ -1232,7 +1249,7 @@ mod tests { let base = Url::parse("https://account.d.codeartifact.us-west-2.amazonaws.com/pypi/shared-packages-pypi/simple/flask/") .unwrap(); let result = SimpleHtml::parse(text, &base).unwrap(); - insta::assert_debug_snapshot!(result, @r###" + insta::assert_debug_snapshot!(result, @r#" SimpleHtml { base: BaseUrl( Url { @@ -1264,6 +1281,7 @@ mod tests { sha256: None, sha384: None, sha512: None, + blake2b: None, }, requires_python: None, size: None, @@ -1283,6 +1301,7 @@ mod tests { sha256: None, sha384: None, sha512: None, + blake2b: None, }, requires_python: None, size: None, @@ -1302,6 +1321,7 @@ mod tests { sha256: None, sha384: None, sha512: None, + blake2b: None, }, requires_python: None, size: None, @@ -1321,6 +1341,7 @@ mod tests { sha256: None, sha384: None, sha512: None, + blake2b: None, }, requires_python: None, size: None, @@ -1340,6 +1361,7 @@ mod tests { sha256: None, sha384: None, sha512: None, + blake2b: None, }, requires_python: None, size: None, @@ -1349,6 +1371,6 @@ mod tests { }, ], } - "###); + "#); } } diff --git a/crates/uv-extract/Cargo.toml b/crates/uv-extract/Cargo.toml index fc6c3343b..fabd03c3b 100644 --- a/crates/uv-extract/Cargo.toml +++ b/crates/uv-extract/Cargo.toml @@ -23,6 +23,7 @@ uv-pypi-types = { workspace = true } astral-tokio-tar = { workspace = true } async-compression = { workspace = true, features = ["bzip2", "gzip", "zstd", "xz"] } async_zip = { workspace = true } +blake2 = { workspace = true } fs-err = { workspace = true, features = ["tokio"] } futures = { workspace = true } md-5 = { workspace = true } diff --git a/crates/uv-extract/src/hash.rs b/crates/uv-extract/src/hash.rs index 008b2bfa8..1bcebaa08 100644 --- a/crates/uv-extract/src/hash.rs +++ b/crates/uv-extract/src/hash.rs @@ -1,7 +1,7 @@ +use blake2::digest::consts::U32; +use sha2::Digest; use std::pin::Pin; use std::task::{Context, Poll}; - -use sha2::Digest; use tokio::io::{AsyncReadExt, ReadBuf}; use uv_pypi_types::{HashAlgorithm, HashDigest}; @@ -12,6 +12,7 @@ pub enum Hasher { Sha256(sha2::Sha256), Sha384(sha2::Sha384), Sha512(sha2::Sha512), + Blake2b(blake2::Blake2b), } impl Hasher { @@ -21,6 +22,7 @@ impl Hasher { Hasher::Sha256(hasher) => hasher.update(data), Hasher::Sha384(hasher) => hasher.update(data), Hasher::Sha512(hasher) => hasher.update(data), + Hasher::Blake2b(hasher) => hasher.update(data), } } } @@ -32,6 +34,7 @@ impl From for Hasher { HashAlgorithm::Sha256 => Hasher::Sha256(sha2::Sha256::new()), HashAlgorithm::Sha384 => Hasher::Sha384(sha2::Sha384::new()), HashAlgorithm::Sha512 => Hasher::Sha512(sha2::Sha512::new()), + HashAlgorithm::Blake2b => Hasher::Blake2b(blake2::Blake2b::new()), } } } @@ -55,6 +58,10 @@ impl From for HashDigest { algorithm: HashAlgorithm::Sha512, digest: format!("{:x}", hasher.finalize()).into(), }, + Hasher::Blake2b(hasher) => HashDigest { + algorithm: HashAlgorithm::Blake2b, + digest: format!("{:x}", hasher.finalize()).into(), + }, } } } diff --git a/crates/uv-pypi-types/src/simple_json.rs b/crates/uv-pypi-types/src/simple_json.rs index d8b82ef3f..45cc4e373 100644 --- a/crates/uv-pypi-types/src/simple_json.rs +++ b/crates/uv-pypi-types/src/simple_json.rs @@ -195,6 +195,8 @@ pub struct Hashes { pub sha384: Option, #[serde(skip_serializing_if = "Option::is_none")] pub sha512: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub blake2b: Option, } impl Hashes { @@ -221,24 +223,35 @@ impl Hashes { sha256: None, sha384: None, sha512: None, + blake2b: None, }), "sha256" => Ok(Hashes { md5: None, sha256: Some(SmallString::from(value)), sha384: None, sha512: None, + blake2b: None, }), "sha384" => Ok(Hashes { md5: None, sha256: None, sha384: Some(SmallString::from(value)), sha512: None, + blake2b: None, }), "sha512" => Ok(Hashes { md5: None, sha256: None, sha384: None, sha512: Some(SmallString::from(value)), + blake2b: None, + }), + "blake2b" => Ok(Hashes { + md5: None, + sha256: None, + sha384: None, + sha512: None, + blake2b: Some(SmallString::from(value)), }), _ => Err(HashError::UnsupportedHashAlgorithm(fragment.to_string())), } @@ -270,24 +283,35 @@ impl FromStr for Hashes { sha256: None, sha384: None, sha512: None, + blake2b: None, }), "sha256" => Ok(Hashes { md5: None, sha256: Some(SmallString::from(value)), sha384: None, sha512: None, + blake2b: None, }), "sha384" => Ok(Hashes { md5: None, sha256: None, sha384: Some(SmallString::from(value)), sha512: None, + blake2b: None, }), "sha512" => Ok(Hashes { md5: None, sha256: None, sha384: None, sha512: Some(SmallString::from(value)), + blake2b: None, + }), + "blake2b" => Ok(Hashes { + md5: None, + sha256: None, + sha384: None, + sha512: None, + blake2b: Some(SmallString::from(value)), }), _ => Err(HashError::UnsupportedHashAlgorithm(s.to_string())), } @@ -315,6 +339,7 @@ pub enum HashAlgorithm { Sha256, Sha384, Sha512, + Blake2b, } impl FromStr for HashAlgorithm { @@ -326,6 +351,7 @@ impl FromStr for HashAlgorithm { "sha256" => Ok(Self::Sha256), "sha384" => Ok(Self::Sha384), "sha512" => Ok(Self::Sha512), + "blake2b" => Ok(Self::Blake2b), _ => Err(HashError::UnsupportedHashAlgorithm(s.to_string())), } } @@ -338,6 +364,7 @@ impl std::fmt::Display for HashAlgorithm { Self::Sha256 => write!(f, "sha256"), Self::Sha384 => write!(f, "sha384"), Self::Sha512 => write!(f, "sha512"), + Self::Blake2b => write!(f, "blake2b"), } } } @@ -503,6 +530,7 @@ impl From for Hashes { HashAlgorithm::Sha256 => hashes.sha256 = Some(digest.digest), HashAlgorithm::Sha384 => hashes.sha384 = Some(digest.digest), HashAlgorithm::Sha512 => hashes.sha512 = Some(digest.digest), + HashAlgorithm::Blake2b => hashes.blake2b = Some(digest.digest), } } hashes @@ -551,7 +579,7 @@ pub enum HashError { InvalidFragment(String), #[error( - "Unsupported hash algorithm (expected one of: `md5`, `sha256`, `sha384`, or `sha512`) on: `{0}`" + "Unsupported hash algorithm (expected one of: `md5`, `sha256`, `sha384`, `sha512`, or `blake2b`) on: `{0}`" )] UnsupportedHashAlgorithm(String), } @@ -562,6 +590,21 @@ mod tests { #[test] fn parse_hashes() -> Result<(), HashError> { + let hashes: Hashes = + "blake2b:af4793213ee66ef8fae3b93b3e29206f6b251e65c97bd91d8e1c5596ef15af0a".parse()?; + assert_eq!( + hashes, + Hashes { + md5: None, + sha256: None, + sha384: None, + sha512: None, + blake2b: Some( + "af4793213ee66ef8fae3b93b3e29206f6b251e65c97bd91d8e1c5596ef15af0a".into() + ), + } + ); + let hashes: Hashes = "sha512:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f".parse()?; assert_eq!( @@ -573,6 +616,7 @@ mod tests { sha512: Some( "40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f".into() ), + blake2b: None, } ); @@ -586,7 +630,8 @@ mod tests { sha384: Some( "40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f".into() ), - sha512: None + sha512: None, + blake2b: None, } ); @@ -600,7 +645,8 @@ mod tests { "40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f".into() ), sha384: None, - sha512: None + sha512: None, + blake2b: None, } ); @@ -614,7 +660,8 @@ mod tests { ), sha256: None, sha384: None, - sha512: None + sha512: None, + blake2b: None, } ); diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index 897713f3e..bf999b5dd 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -4490,24 +4490,35 @@ impl From for Hashes { sha256: None, sha384: None, sha512: None, + blake2b: None, }, HashAlgorithm::Sha256 => Hashes { md5: None, sha256: Some(value.0.digest), sha384: None, sha512: None, + blake2b: None, }, HashAlgorithm::Sha384 => Hashes { md5: None, sha256: None, sha384: Some(value.0.digest), sha512: None, + blake2b: None, }, HashAlgorithm::Sha512 => Hashes { md5: None, sha256: None, sha384: None, sha512: Some(value.0.digest), + blake2b: None, + }, + HashAlgorithm::Blake2b => Hashes { + md5: None, + sha256: None, + sha384: None, + sha512: None, + blake2b: Some(value.0.digest), }, } } diff --git a/crates/uv/tests/it/pip_sync.rs b/crates/uv/tests/it/pip_sync.rs index bb8a0aa34..743a0bed1 100644 --- a/crates/uv/tests/it/pip_sync.rs +++ b/crates/uv/tests/it/pip_sync.rs @@ -3440,14 +3440,14 @@ fn require_hashes_unknown_algorithm() -> Result<()> { uv_snapshot!(context.pip_sync() .arg("requirements.txt") - .arg("--require-hashes"), @r###" + .arg("--require-hashes"), @r" success: false exit_code: 2 ----- stdout ----- ----- stderr ----- - error: Unsupported hash algorithm (expected one of: `md5`, `sha256`, `sha384`, or `sha512`) on: `foo` - "### + error: Unsupported hash algorithm (expected one of: `md5`, `sha256`, `sha384`, `sha512`, or `blake2b`) on: `foo` + " ); Ok(())