From 80aa03dcba66b9e5b647dc882e975cd8597d78a5 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 18 Mar 2024 19:23:16 -0700 Subject: [PATCH] Add SHA384 and SHA512 hash algorithms (#2534) Closes #2533. --- crates/pypi-types/src/simple_json.rs | 91 +++++++++++++++++++++++++--- crates/uv-cache/src/lib.rs | 2 +- crates/uv-client/src/html.rs | 62 +++++++++++++++++-- 3 files changed, 139 insertions(+), 16 deletions(-) diff --git a/crates/pypi-types/src/simple_json.rs b/crates/pypi-types/src/simple_json.rs index 5c629c551..be39179e3 100644 --- a/crates/pypi-types/src/simple_json.rs +++ b/crates/pypi-types/src/simple_json.rs @@ -124,8 +124,7 @@ impl Default for Yanked { /// A dictionary mapping a hash name to a hex encoded digest of the file. /// -/// PEP 691 says multiple hashes can be included and the interpretation is left to the client, we -/// only support SHA 256 atm. +/// PEP 691 says multiple hashes can be included and the interpretation is left to the client. #[derive( Debug, Clone, @@ -146,20 +145,36 @@ impl Default for Yanked { pub struct Hashes { pub md5: Option, pub sha256: Option, + pub sha384: Option, + pub sha512: Option, } impl Hashes { /// Format as `:`. pub fn to_string(&self) -> Option { - self.sha256 + self.sha512 .as_ref() - .map(|sha256| format!("sha256:{sha256}")) + .map(|sha512| format!("sha512:{sha512}")) + .or_else(|| { + self.sha384 + .as_ref() + .map(|sha384| format!("sha384:{sha384}")) + }) + .or_else(|| { + self.sha256 + .as_ref() + .map(|sha256| format!("sha256:{sha256}")) + }) .or_else(|| self.md5.as_ref().map(|md5| format!("md5:{md5}"))) } /// Return the hash digest. pub fn as_str(&self) -> Option<&str> { - self.sha256.as_deref().or(self.md5.as_deref()) + self.sha512 + .as_deref() + .or(self.sha384.as_deref()) + .or(self.sha256.as_deref()) + .or(self.md5.as_deref()) } } @@ -188,6 +203,8 @@ impl FromStr for Hashes { Ok(Hashes { md5: Some(md5), sha256: None, + sha384: None, + sha512: None, }) } "sha256" => { @@ -195,6 +212,26 @@ impl FromStr for Hashes { Ok(Hashes { md5: None, sha256: Some(sha256), + sha384: None, + sha512: None, + }) + } + "sha384" => { + let sha384 = value.to_string(); + Ok(Hashes { + md5: None, + sha256: None, + sha384: Some(sha384), + sha512: None, + }) + } + "sha512" => { + let sha512 = value.to_string(); + Ok(Hashes { + md5: None, + sha256: None, + sha384: None, + sha512: Some(sha512), }) } _ => Err(HashError::UnsupportedHashAlgorithm(s.to_string())), @@ -204,10 +241,12 @@ impl FromStr for Hashes { #[derive(thiserror::Error, Debug)] pub enum HashError { - #[error("Unexpected hash (expected `sha256:` or `md5:`) on: {0}")] + #[error("Unexpected hash (expected `:`): {0}")] InvalidStructure(String), - #[error("Unsupported hash algorithm (expected `sha256` or `md5`) on: {0}")] + #[error( + "Unsupported hash algorithm (expected `md5`, `sha256`, `sha384`, or `sha512`) on: {0}" + )] UnsupportedHashAlgorithm(String), } @@ -217,6 +256,34 @@ mod tests { #[test] fn parse_hashes() -> Result<(), HashError> { + let hashes: Hashes = + "sha512:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f".parse()?; + assert_eq!( + hashes, + Hashes { + md5: None, + sha256: None, + sha384: None, + sha512: Some( + "40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f".to_string() + ), + } + ); + + let hashes: Hashes = + "sha384:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f".parse()?; + assert_eq!( + hashes, + Hashes { + md5: None, + sha256: None, + sha384: Some( + "40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f".to_string() + ), + sha512: None + } + ); + let hashes: Hashes = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f".parse()?; assert_eq!( @@ -225,7 +292,9 @@ mod tests { md5: None, sha256: Some( "40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f".to_string() - ) + ), + sha384: None, + sha512: None } ); @@ -237,7 +306,9 @@ mod tests { md5: Some( "090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2".to_string() ), - sha256: None + sha256: None, + sha384: None, + sha512: None } ); @@ -245,7 +316,7 @@ mod tests { .parse::(); assert!(result.is_err()); - let result = "sha512:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619" + let result = "blake2:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619" .parse::(); assert!(result.is_err()); diff --git a/crates/uv-cache/src/lib.rs b/crates/uv-cache/src/lib.rs index c92bb00ad..98998623c 100644 --- a/crates/uv-cache/src/lib.rs +++ b/crates/uv-cache/src/lib.rs @@ -537,7 +537,7 @@ impl CacheBucket { Self::FlatIndex => "flat-index-v0", Self::Git => "git-v0", Self::Interpreter => "interpreter-v0", - Self::Simple => "simple-v4", + Self::Simple => "simple-v5", Self::Wheels => "wheels-v0", Self::Archive => "archive-v0", } diff --git a/crates/uv-client/src/html.rs b/crates/uv-client/src/html.rs index 3161de45e..299226adf 100644 --- a/crates/uv-client/src/html.rs +++ b/crates/uv-client/src/html.rs @@ -93,6 +93,8 @@ impl SimpleHtml { Ok(Hashes { md5: Some(md5), sha256: None, + sha384: None, + sha512: None, }) } "sha256" => { @@ -101,6 +103,28 @@ impl SimpleHtml { Ok(Hashes { md5: None, sha256: Some(sha256), + sha384: None, + sha512: None, + }) + } + "sha384" => { + let sha384 = std::str::from_utf8(value.as_bytes())?; + let sha384 = sha384.to_string(); + Ok(Hashes { + md5: None, + sha256: None, + sha384: Some(sha384), + sha512: None, + }) + } + "sha512" => { + let sha512 = std::str::from_utf8(value.as_bytes())?; + let sha512 = sha512.to_string(); + Ok(Hashes { + md5: None, + sha256: None, + sha384: None, + sha512: Some(sha512), }) } _ => Err(Error::UnsupportedHashAlgorithm(fragment.to_string())), @@ -218,10 +242,12 @@ pub enum Error { #[error("Missing hash attribute on URL: {0}")] MissingHash(String), - #[error("Unexpected fragment (expected `#sha256=...`) on URL: {0}")] + #[error("Unexpected fragment (expected `#sha256=...` or similar) on URL: {0}")] FragmentParse(String), - #[error("Unsupported hash algorithm (expected `sha256` or `md5`) on: {0}")] + #[error( + "Unsupported hash algorithm (expected `md5`, `sha256`, `sha384`, or `sha512`) on: {0}" + )] UnsupportedHashAlgorithm(String), #[error("Invalid `requires-python` specifier: {0}")] @@ -274,6 +300,8 @@ mod tests { sha256: Some( "6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61", ), + sha384: None, + sha512: None, }, requires_python: None, size: None, @@ -328,6 +356,8 @@ mod tests { "6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61", ), sha256: None, + sha384: None, + sha512: None, }, requires_python: None, size: None, @@ -385,6 +415,8 @@ mod tests { sha256: Some( "6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61", ), + sha384: None, + sha512: None, }, requires_python: None, size: None, @@ -439,6 +471,8 @@ mod tests { sha256: Some( "6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61", ), + sha384: None, + sha512: None, }, requires_python: None, size: None, @@ -491,6 +525,8 @@ mod tests { hashes: Hashes { md5: None, sha256: None, + sha384: None, + sha512: None, }, requires_python: None, size: None, @@ -543,6 +579,8 @@ mod tests { hashes: Hashes { md5: None, sha256: None, + sha384: None, + sha512: None, }, requires_python: None, size: None, @@ -629,6 +667,8 @@ mod tests { hashes: Hashes { md5: None, sha256: None, + sha384: None, + sha512: None, }, requires_python: None, size: None, @@ -655,7 +695,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, @"Unexpected fragment (expected `#sha256=...`) on URL: sha256"); + insta::assert_snapshot!(result, @"Unexpected fragment (expected `#sha256=...` or similar) on URL: sha256"); } #[test] @@ -665,14 +705,14 @@ mod tests {

Links for jinja2

- Jinja2-3.1.2-py3-none-any.whl
+ Jinja2-3.1.2-py3-none-any.whl
"#; 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 `sha256` or `md5`) on: sha512=6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"); + insta::assert_snapshot!(result, @"Unsupported hash algorithm (expected `md5`, `sha256`, `sha384`, or `sha512`) on: blake2=6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"); } #[test] @@ -716,6 +756,8 @@ mod tests { hashes: Hashes { md5: None, sha256: None, + sha384: None, + sha512: None, }, requires_python: None, size: None, @@ -729,6 +771,8 @@ mod tests { hashes: Hashes { md5: None, sha256: None, + sha384: None, + sha512: None, }, requires_python: None, size: None, @@ -793,6 +837,8 @@ mod tests { sha256: Some( "9da884457e910bf0847d396cb4b778ad9f3c3d17db1c5997cb861937bd284237", ), + sha384: None, + sha512: None, }, requires_python: None, size: None, @@ -808,6 +854,8 @@ mod tests { sha256: Some( "4c83829ff83d408b5e1d4995472265411d2c414112298f2eb4b359d9e4563373", ), + sha384: None, + sha512: None, }, requires_python: None, size: None, @@ -823,6 +871,8 @@ mod tests { sha256: Some( "6489f51bb3666def6f314e15f19d50a1869a19ae0e8c9a3641ffe66c77d42403", ), + sha384: None, + sha512: None, }, requires_python: Some( Ok( @@ -887,6 +937,8 @@ mod tests { sha256: Some( "6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61", ), + sha384: None, + sha512: None, }, requires_python: Some( Ok(