Skip to content
Closed
Show file tree
Hide file tree
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
25 changes: 25 additions & 0 deletions crates/uv-install-wheel/src/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use std::str::FromStr;

use fs_err::File;
use tracing::{instrument, trace};
use walkdir::WalkDir;

use uv_distribution_filename::WheelFilename;
use uv_pep440::Version;
Expand Down Expand Up @@ -82,6 +83,30 @@ pub fn install_wheel<Cache: serde::Serialize, Build: serde::Serialize>(
}
}

// Validate `.data` before linking the wheel into site-packages. Distribution metadata in a
// data scheme could otherwise replace the metadata that was just validated or inject another
// installed distribution.
let data_dir = wheel.join(format!("{dist_info_prefix}.data"));
if data_dir.is_dir() {
for entry in WalkDir::new(&data_dir).min_depth(1) {
let entry = entry?;
if entry
.path()
.extension()
.and_then(|extension| extension.to_str())
.is_some_and(|extension| {
extension.eq_ignore_ascii_case("dist-info")
|| extension.eq_ignore_ascii_case("egg-info")
})
{
return Err(Error::InvalidWheel(format!(
"Wheel data directory must not contain distribution metadata: {}",
entry.file_name().display()
)));
}
}
}

// We're going step by step though
// https://packaging.python.org/en/latest/specifications/binary-distribution-format/#installing-a-wheel-distribution-1-0-py32-none-any-whl
// > 1.a Parse distribution-1.0.dist-info/WHEEL.
Expand Down
138 changes: 138 additions & 0 deletions crates/uv/tests/pip_install/pip_install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,144 @@ fn write_many_files_wheel(path: &Path, source_files: usize) -> Result<()> {
Ok(())
}

fn write_data_metadata_wheel(
path: &Path,
name: &str,
data_metadata: &str,
metadata_file: bool,
) -> Result<()> {
let mut writer = ZipFileWriter::new(Vec::new());
let metadata = formatdoc! {"
Metadata-Version: 2.1
Name: {name}
Version: 1.0.0
"};
let wheel = indoc! {"
Wheel-Version: 1.0
Generator: uv-test
Root-Is-Purelib: true
Tag: py3-none-any
"};
let data_metadata = if metadata_file {
format!("{name}-1.0.0.data/purelib/{data_metadata}")
} else {
format!("{name}-1.0.0.data/purelib/{data_metadata}/METADATA")
};
let entries = [
(format!("{name}/__init__.py"), "VALUE = 1\n".to_string()),
(format!("{name}-1.0.0.dist-info/METADATA"), metadata.clone()),
(format!("{name}-1.0.0.dist-info/WHEEL"), wheel.to_string()),
(
data_metadata,
"Name: injected\nVersion: 9.9.9\n".to_string(),
),
];
let mut record = String::new();
for (entry_name, contents) in entries {
let entry = ZipEntryBuilder::new(entry_name.clone().into(), Compression::Stored);
block_on(writer.write_entry_whole(entry, contents.as_bytes()))?;
writeln!(record, "{entry_name},,")?;
}
writeln!(record, "{name}-1.0.0.dist-info/RECORD,,")?;
let record_name = format!("{name}-1.0.0.dist-info/RECORD");
let entry = ZipEntryBuilder::new(record_name.into(), Compression::Stored);
block_on(writer.write_entry_whole(entry, record.as_bytes()))?;
fs_err::write(path, block_on(writer.close())?)?;
Ok(())
}

#[test]
fn reject_wheel_data_distribution_metadata() -> Result<()> {
let context = uv_test::test_context!("3.12");

let overwrite = context.temp_dir.join("overwrite-1.0.0-py3-none-any.whl");
write_data_metadata_wheel(&overwrite, "overwrite", "overwrite-1.0.0.dist-info", false)?;
uv_snapshot!(context.filters(), context.pip_install().arg(&overwrite), @"
success: false
exit_code: 2
----- stdout -----

----- stderr -----
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
error: Failed to install: overwrite-1.0.0-py3-none-any.whl (overwrite==1.0.0 (from file://[TEMP_DIR]/overwrite-1.0.0-py3-none-any.whl))
Caused by: The wheel is invalid: Wheel data directory must not contain distribution metadata: overwrite-1.0.0.dist-info
");
assert!(
!context
.site_packages()
.join("overwrite-1.0.0.dist-info")
.exists()
);

let inject = context.temp_dir.join("inject-1.0.0-py3-none-any.whl");
write_data_metadata_wheel(&inject, "inject", "phantom-9.9.9.DIST-INFO", false)?;
uv_snapshot!(context.filters(), context.pip_install().arg(&inject), @"
success: false
exit_code: 2
----- stdout -----

----- stderr -----
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
error: Failed to install: inject-1.0.0-py3-none-any.whl (inject==1.0.0 (from file://[TEMP_DIR]/inject-1.0.0-py3-none-any.whl))
Caused by: The wheel is invalid: Wheel data directory must not contain distribution metadata: phantom-9.9.9.DIST-INFO
");
assert!(
!context
.site_packages()
.join("phantom-9.9.9.DIST-INFO")
.exists()
);

let egg_info = context.temp_dir.join("egg-1.0.0-py3-none-any.whl");
write_data_metadata_wheel(&egg_info, "egg", "phantom-9.9.9.EGG-INFO", false)?;
uv_snapshot!(context.filters(), context.pip_install().arg(&egg_info), @"
success: false
exit_code: 2
----- stdout -----

----- stderr -----
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
error: Failed to install: egg-1.0.0-py3-none-any.whl (egg==1.0.0 (from file://[TEMP_DIR]/egg-1.0.0-py3-none-any.whl))
Caused by: The wheel is invalid: Wheel data directory must not contain distribution metadata: phantom-9.9.9.EGG-INFO
");
assert!(
!context
.site_packages()
.join("phantom-9.9.9.EGG-INFO")
.exists()
);

let egg_info_file = context.temp_dir.join("eggfile-1.0.0-py3-none-any.whl");
write_data_metadata_wheel(
&egg_info_file,
"eggfile",
"phantom-file-9.9.9.egg-info",
true,
)?;
uv_snapshot!(context.filters(), context.pip_install().arg(&egg_info_file), @"
success: false
exit_code: 2
----- stdout -----

----- stderr -----
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
error: Failed to install: eggfile-1.0.0-py3-none-any.whl (eggfile==1.0.0 (from file://[TEMP_DIR]/eggfile-1.0.0-py3-none-any.whl))
Caused by: The wheel is invalid: Wheel data directory must not contain distribution metadata: phantom-file-9.9.9.egg-info
");
assert!(
!context
.site_packages()
.join("phantom-file-9.9.9.egg-info")
.exists()
);

Ok(())
}

#[test]
fn missing_requirements_txt() {
let context = uv_test::test_context!("3.12");
Expand Down
Loading