Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 41 additions & 14 deletions crates/uv-publish/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}",
Expand All @@ -611,13 +611,20 @@ pub async fn check_url(
}
}

/// Calculate the SHA256 of a file.
async fn hash_file(path: impl AsRef<Path>, hasher: Hasher) -> Result<HashDigest, io::Error> {
/// Calculate the requested hashes of a file.
async fn hash_file(
path: impl AsRef<Path>,
hashers: Vec<Hasher>,
) -> Result<Vec<HashDigest>, 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::<Vec<_>>())
}

// Not in `uv-metadata` because we only support tar files here.
Expand Down Expand Up @@ -693,7 +700,24 @@ impl FormMetadata {
file: &Path,
filename: &DistFilename,
) -> Result<Self, PublishPrepareError> {
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();
Comment on lines +712 to +720
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flagging: I thought iter + find here would be slightly cleaner than indexing with [0] and [1], since we may not want to assume the return order is the same as the request order in the hash_file parameters.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can assume the order, but this is fine too


let Metadata23 {
metadata_version,
Expand Down Expand Up @@ -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)`
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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,
},
Expand All @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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,
},
Expand Down
Loading