From ad19536953529b488c1cbab5ddac45dcf5d2654b Mon Sep 17 00:00:00 2001 From: konstin Date: Thu, 25 Jun 2026 13:13:56 +0200 Subject: [PATCH] Reject wheels with multiple dist-info directories --- crates/uv-install-wheel/src/wheel.rs | 48 ++++++++++------ crates/uv/tests/pip_install/pip_install.rs | 65 ++++++++++++++++++++++ 2 files changed, 97 insertions(+), 16 deletions(-) diff --git a/crates/uv-install-wheel/src/wheel.rs b/crates/uv-install-wheel/src/wheel.rs index ab1b5a42e5a..abd9372aead 100644 --- a/crates/uv-install-wheel/src/wheel.rs +++ b/crates/uv-install-wheel/src/wheel.rs @@ -997,30 +997,46 @@ fn parse_email_message_file( Ok(data) } -/// Find the prefix of the `dist-info` directory in an unzipped wheel. +/// Find the prefix of the unique `dist-info` directory in an unzipped wheel. /// /// See: /// /// See: pub(crate) fn find_dist_info(path: impl AsRef) -> Result { - // Iterate over `path` to find the `.dist-info` directory. It should be at the top-level. - let Some(dist_info) = fs::read_dir(path.as_ref())?.find_map(|entry| { - let entry = entry.ok()?; - let file_type = entry.file_type().ok()?; - if file_type.is_dir() { - let path = entry.path(); - if path.extension().is_some_and(|ext| ext == "dist-info") { - Some(path) - } else { - None + // Iterate over `path` to find the `.dist-info` directory. It should be at the top-level, + // and wheels must contain exactly one. + let mut dist_info = fs::read_dir(path.as_ref())? + .filter_map(|entry| { + let entry = entry.ok()?; + let file_type = entry.file_type().ok()?; + if file_type.is_dir() { + let path = entry.path(); + if path.extension().is_some_and(|ext| ext == "dist-info") { + return Some(path); + } } - } else { None + }) + .collect::>(); + dist_info.sort(); + + let dist_info = match dist_info.as_slice() { + [] => { + return Err(Error::InvalidWheel( + "Missing .dist-info directory".to_string(), + )); + } + [dist_info] => dist_info, + _ => { + return Err(Error::InvalidWheel(format!( + "Multiple .dist-info directories found: {}", + dist_info + .iter() + .filter_map(|path| path.file_stem()) + .map(|prefix| prefix.to_string_lossy()) + .join(", ") + ))); } - }) else { - return Err(Error::InvalidWheel( - "Missing .dist-info directory".to_string(), - )); }; let Some(dist_info_prefix) = dist_info.file_stem() else { diff --git a/crates/uv/tests/pip_install/pip_install.rs b/crates/uv/tests/pip_install/pip_install.rs index e65aa268a5a..96c57d6319d 100644 --- a/crates/uv/tests/pip_install/pip_install.rs +++ b/crates/uv/tests/pip_install/pip_install.rs @@ -7732,6 +7732,71 @@ async fn find_links_uppercase_html() -> Result<()> { Ok(()) } +/// Reject a wheel with multiple `.dist-info` directories when PEP 658 metadata bypasses +/// reading metadata from the wheel archive. +#[tokio::test] +async fn reject_wheel_with_multiple_dist_info_directories() -> Result<()> { + let context = uv_test::test_context!("3.12"); + let server = MockServer::start().await; + let wheel_filename = "validation-3.0.0-py3-none-any.whl"; + let wheel_path = context + .workspace_root + .join("test/links") + .join(wheel_filename); + + Mock::given(method("GET")) + .and(path("/validation/")) + .respond_with(ResponseTemplate::new(200).set_body_raw( + formatdoc! {r#" + {{ + "name": "validation", + "files": [{{ + "filename": "{wheel_filename}", + "url": "/{wheel_filename}", + "hashes": {{}}, + "core-metadata": true, + "upload-time": "2024-03-24T00:00:00Z" + }}] + }} + "#}, + "application/vnd.pypi.simple.v1+json", + )) + .mount(&server) + .await; + Mock::given(method("GET")) + .and(path(format!("/{wheel_filename}.metadata"))) + .respond_with(ResponseTemplate::new(200).set_body_string(indoc! {" + Metadata-Version: 2.1 + Name: validation + Version: 3.0.0 + "})) + .expect(1) + .mount(&server) + .await; + Mock::given(method("GET")) + .and(path(format!("/{wheel_filename}"))) + .respond_with(ResponseTemplate::new(200).set_body_bytes(fs::read(wheel_path)?)) + .mount(&server) + .await; + + uv_snapshot!(context.filters(), context.pip_install() + .arg("validation==3.0.0") + .arg("--index-url") + .arg(server.uri()), @" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + × Failed to download `validation==3.0.0` + ╰─▶ The wheel is invalid: Multiple .dist-info directories found: validation-2.0.0, validation-3.0.0 + " + ); + + Ok(()) +} + /// Sync using `--find-links` with a local directory, with wheels disabled. #[test] fn find_links_no_binary() {