diff --git a/crates/uv-publish/src/lib.rs b/crates/uv-publish/src/lib.rs index 5ea430f2f124d..a1409ccf430a3 100644 --- a/crates/uv-publish/src/lib.rs +++ b/crates/uv-publish/src/lib.rs @@ -584,14 +584,14 @@ pub async fn check_url( if let Some(remote_hash) = archived_file.hashes.first() { // We accept the risk for TOCTOU errors here, since we already read the file once before the // streaming upload to compute the hash for the form metadata. - let local_hash = hash_file(file, Hasher::from(remote_hash.algorithm)) + let local_hash = &hash_file(file, vec![Hasher::from(remote_hash.algorithm)]) .await .map_err(|err| { PublishError::PublishPrepare( file.to_path_buf(), Box::new(PublishPrepareError::Io(err)), ) - })?; + })?[0]; if local_hash.digest == remote_hash.digest { debug!( "Found {filename} in the registry with matching hash {}", @@ -611,13 +611,20 @@ pub async fn check_url( } } -/// Calculate the SHA256 of a file. -async fn hash_file(path: impl AsRef, hasher: Hasher) -> Result { +/// Calculate the requested hashes of a file. +async fn hash_file( + path: impl AsRef, + hashers: Vec, +) -> Result, io::Error> { debug!("Hashing {}", path.as_ref().display()); let file = BufReader::new(File::open(path.as_ref()).await?); - let mut hashers = vec![hasher]; + let mut hashers = hashers; HashReader::new(file, &mut hashers).finish().await?; - Ok(HashDigest::from(hashers.remove(0))) + + Ok(hashers + .into_iter() + .map(HashDigest::from) + .collect::>()) } // Not in `uv-metadata` because we only support tar files here. @@ -693,7 +700,24 @@ impl FormMetadata { file: &Path, filename: &DistFilename, ) -> Result { - let hash_hex = hash_file(file, Hasher::from(HashAlgorithm::Sha256)).await?; + let hashes = hash_file( + file, + vec![ + Hasher::from(HashAlgorithm::Sha256), + Hasher::from(HashAlgorithm::Blake2b), + ], + ) + .await?; + + let sha256_hash = hashes + .iter() + .find(|hash| hash.algorithm == HashAlgorithm::Sha256) + .unwrap(); + + let blake2b_hash = hashes + .iter() + .find(|hash| hash.algorithm == HashAlgorithm::Blake2b) + .unwrap(); let Metadata23 { metadata_version, @@ -728,7 +752,8 @@ impl FormMetadata { let mut form_metadata = vec![ (":action", "file_upload".to_string()), - ("sha256_digest", hash_hex.digest.to_string()), + ("sha256_digest", sha256_hash.digest.to_string()), + ("blake2_256_digest", blake2b_hash.digest.to_string()), ("protocol_version", "1".to_string()), ("metadata_version", metadata_version.clone()), // Twine transforms the name with `re.sub("[^A-Za-z0-9.]+", "-", name)` @@ -1040,9 +1065,10 @@ mod tests { .iter() .map(|(k, v)| format!("{k}: {v}")) .join("\n"); - assert_snapshot!(&formatted_metadata, @r###" + assert_snapshot!(&formatted_metadata, @r" :action: file_upload sha256_digest: 89fa05cffa7f457658373b85de302d24d0c205ceda2819a8739e324b75e9430b + blake2_256_digest: 40ab79b48c4e289e4990f7e689177adae4096c07a634034eb1d10c0b6700e4d2 protocol_version: 1 metadata_version: 2.3 name: tqdm @@ -1088,7 +1114,7 @@ mod tests { project_urls: Documentation, https://github.com/unknown/tqdm#readme project_urls: Issues, https://github.com/unknown/tqdm/issues project_urls: Source, https://github.com/unknown/tqdm - "###); + "); let client = BaseClientBuilder::default().build(); let (request, _) = build_upload_request( @@ -1128,7 +1154,7 @@ mod tests { }, headers: { "content-type": "multipart/form-data; boundary=[...]", - "content-length": "6803", + "content-length": "7000", "accept": "application/json;q=0.9, text/plain;q=0.8, text/html;q=0.7", "authorization": Sensitive, }, @@ -1154,9 +1180,10 @@ mod tests { .iter() .map(|(k, v)| format!("{k}: {v}")) .join("\n"); - assert_snapshot!(&formatted_metadata, @r###" + assert_snapshot!(&formatted_metadata, @r#" :action: file_upload sha256_digest: 0d88ca657bc6b64995ca416e0c59c71af85cc10015d940fa446c42a8b485ee1c + blake2_256_digest: 33d4e92517a16e3fa0c0893de0c7e4d46a2c38adab148dd2ff66eb47481d19cd protocol_version: 1 metadata_version: 2.1 name: tqdm @@ -1240,7 +1267,7 @@ mod tests { requires_dist: ipywidgets >=6 ; extra == 'notebook' requires_dist: slack-sdk ; extra == 'slack' requires_dist: requests ; extra == 'telegram' - "###); + "#); let client = BaseClientBuilder::default().build(); let (request, _) = build_upload_request( @@ -1280,7 +1307,7 @@ mod tests { }, headers: { "content-type": "multipart/form-data; boundary=[...]", - "content-length": "19330", + "content-length": "19527", "accept": "application/json;q=0.9, text/plain;q=0.8, text/html;q=0.7", "authorization": Sensitive, },