Skip to content

Commit

Permalink
Add SHA384 and SHA512 hash algorithms (#2534)
Browse files Browse the repository at this point in the history
Closes #2533.
  • Loading branch information
charliermarsh committed Mar 19, 2024
1 parent a80d317 commit 80aa03d
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 16 deletions.
91 changes: 81 additions & 10 deletions crates/pypi-types/src/simple_json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -146,20 +145,36 @@ impl Default for Yanked {
pub struct Hashes {
pub md5: Option<String>,
pub sha256: Option<String>,
pub sha384: Option<String>,
pub sha512: Option<String>,
}

impl Hashes {
/// Format as `<algorithm>:<hash>`.
pub fn to_string(&self) -> Option<String> {
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())
}
}

Expand Down Expand Up @@ -188,13 +203,35 @@ impl FromStr for Hashes {
Ok(Hashes {
md5: Some(md5),
sha256: None,
sha384: None,
sha512: None,
})
}
"sha256" => {
let sha256 = value.to_string();
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())),
Expand All @@ -204,10 +241,12 @@ impl FromStr for Hashes {

#[derive(thiserror::Error, Debug)]
pub enum HashError {
#[error("Unexpected hash (expected `sha256:<hash>` or `md5:<hash>`) on: {0}")]
#[error("Unexpected hash (expected `<algorithm>:<hash>`): {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),
}

Expand All @@ -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!(
Expand All @@ -225,7 +292,9 @@ mod tests {
md5: None,
sha256: Some(
"40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f".to_string()
)
),
sha384: None,
sha512: None
}
);

Expand All @@ -237,15 +306,17 @@ mod tests {
md5: Some(
"090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2".to_string()
),
sha256: None
sha256: None,
sha384: None,
sha512: None
}
);

let result = "sha256=40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"
.parse::<Hashes>();
assert!(result.is_err());

let result = "sha512:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"
let result = "blake2:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"
.parse::<Hashes>();
assert!(result.is_err());

Expand Down
2 changes: 1 addition & 1 deletion crates/uv-cache/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}
Expand Down
62 changes: 57 additions & 5 deletions crates/uv-client/src/html.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ impl SimpleHtml {
Ok(Hashes {
md5: Some(md5),
sha256: None,
sha384: None,
sha512: None,
})
}
"sha256" => {
Expand All @@ -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())),
Expand Down Expand Up @@ -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}")]
Expand Down Expand Up @@ -274,6 +300,8 @@ mod tests {
sha256: Some(
"6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61",
),
sha384: None,
sha512: None,
},
requires_python: None,
size: None,
Expand Down Expand Up @@ -328,6 +356,8 @@ mod tests {
"6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61",
),
sha256: None,
sha384: None,
sha512: None,
},
requires_python: None,
size: None,
Expand Down Expand Up @@ -385,6 +415,8 @@ mod tests {
sha256: Some(
"6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61",
),
sha384: None,
sha512: None,
},
requires_python: None,
size: None,
Expand Down Expand Up @@ -439,6 +471,8 @@ mod tests {
sha256: Some(
"6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61",
),
sha384: None,
sha512: None,
},
requires_python: None,
size: None,
Expand Down Expand Up @@ -491,6 +525,8 @@ mod tests {
hashes: Hashes {
md5: None,
sha256: None,
sha384: None,
sha512: None,
},
requires_python: None,
size: None,
Expand Down Expand Up @@ -543,6 +579,8 @@ mod tests {
hashes: Hashes {
md5: None,
sha256: None,
sha384: None,
sha512: None,
},
requires_python: None,
size: None,
Expand Down Expand Up @@ -629,6 +667,8 @@ mod tests {
hashes: Hashes {
md5: None,
sha256: None,
sha384: None,
sha512: None,
},
requires_python: None,
size: None,
Expand All @@ -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]
Expand All @@ -665,14 +705,14 @@ mod tests {
<html>
<body>
<h1>Links for jinja2</h1>
<a href="/whl/Jinja2-3.1.2-py3-none-any.whl#sha512=6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61">Jinja2-3.1.2-py3-none-any.whl</a><br/>
<a href="/whl/Jinja2-3.1.2-py3-none-any.whl#blake2=6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61">Jinja2-3.1.2-py3-none-any.whl</a><br/>
</body>
</html>
<!--TIMESTAMP 1703347410-->
"#;
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]
Expand Down Expand Up @@ -716,6 +756,8 @@ mod tests {
hashes: Hashes {
md5: None,
sha256: None,
sha384: None,
sha512: None,
},
requires_python: None,
size: None,
Expand All @@ -729,6 +771,8 @@ mod tests {
hashes: Hashes {
md5: None,
sha256: None,
sha384: None,
sha512: None,
},
requires_python: None,
size: None,
Expand Down Expand Up @@ -793,6 +837,8 @@ mod tests {
sha256: Some(
"9da884457e910bf0847d396cb4b778ad9f3c3d17db1c5997cb861937bd284237",
),
sha384: None,
sha512: None,
},
requires_python: None,
size: None,
Expand All @@ -808,6 +854,8 @@ mod tests {
sha256: Some(
"4c83829ff83d408b5e1d4995472265411d2c414112298f2eb4b359d9e4563373",
),
sha384: None,
sha512: None,
},
requires_python: None,
size: None,
Expand All @@ -823,6 +871,8 @@ mod tests {
sha256: Some(
"6489f51bb3666def6f314e15f19d50a1869a19ae0e8c9a3641ffe66c77d42403",
),
sha384: None,
sha512: None,
},
requires_python: Some(
Ok(
Expand Down Expand Up @@ -887,6 +937,8 @@ mod tests {
sha256: Some(
"6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61",
),
sha384: None,
sha512: None,
},
requires_python: Some(
Ok(
Expand Down

0 comments on commit 80aa03d

Please sign in to comment.