diff --git a/crates/install-wheel-rs/src/lib.rs b/crates/install-wheel-rs/src/lib.rs index ae07fdc43..f8f625c24 100644 --- a/crates/install-wheel-rs/src/lib.rs +++ b/crates/install-wheel-rs/src/lib.rs @@ -51,16 +51,13 @@ pub enum Error { /// The wheel is broken #[error("The wheel is invalid: {0}")] InvalidWheel(String), - /// pyproject.toml or poetry.lock are broken - #[error("The poetry dependency specification (pyproject.toml or poetry.lock) is broken (try `poetry update`?): {0}")] - InvalidPoetry(String), /// Doesn't follow file name schema #[error(transparent)] InvalidWheelFileName(#[from] distribution_filename::WheelFilenameError), /// The caller must add the name of the zip file (See note on type). #[error("Failed to read {0} from zip file")] Zip(String, #[source] ZipError), - #[error("Failed to run python subcommand")] + #[error("Failed to run Python subcommand")] PythonSubcommand(#[source] io::Error), #[error("Failed to move data files")] WalkDir(#[from] walkdir::Error), @@ -86,6 +83,14 @@ pub enum Error { MultipleDistInfo(String), #[error("Invalid wheel size")] InvalidSize, + #[error("Invalid package name")] + InvalidName(#[from] puffin_normalize::InvalidNameError), + #[error("Invalid package version")] + InvalidVersion(#[from] pep440_rs::VersionParseError), + #[error("Wheel package name does not match filename: {0} != {1}")] + MismatchedName(PackageName, PackageName), + #[error("Wheel version does not match filename: {0} != {1}")] + MismatchedVersion(Version, Version), } /// Find the `dist-info` directory from a list of files. diff --git a/crates/install-wheel-rs/src/linker.rs b/crates/install-wheel-rs/src/linker.rs index d7f70e845..18b33b0ea 100644 --- a/crates/install-wheel-rs/src/linker.rs +++ b/crates/install-wheel-rs/src/linker.rs @@ -2,10 +2,14 @@ //! reading from a zip file. use std::path::Path; +use std::str::FromStr; use configparser::ini::Ini; +use distribution_filename::WheelFilename; use fs_err as fs; use fs_err::File; +use pep440_rs::Version; +use puffin_normalize::PackageName; use tempfile::tempdir_in; use tracing::{debug, instrument}; @@ -29,6 +33,7 @@ use crate::{read_record_file, Error, Script}; pub fn install_wheel( location: &InstallLocation>, wheel: impl AsRef, + filename: &WheelFilename, direct_url: Option<&DirectUrl>, installer: Option<&str>, link_mode: LinkMode, @@ -52,7 +57,20 @@ pub fn install_wheel( let dist_info_prefix = find_dist_info(&wheel)?; let metadata = dist_info_metadata(&dist_info_prefix, &wheel)?; - let (name, _version) = parse_metadata(&dist_info_prefix, &metadata)?; + let (name, version) = parse_metadata(&dist_info_prefix, &metadata)?; + + // Validate the wheel name and version. + { + let name = PackageName::from_str(&name)?; + if name != filename.name { + return Err(Error::MismatchedName(name, filename.name.clone())); + } + + let version = Version::from_str(&version)?; + if version != filename.version { + return Err(Error::MismatchedVersion(version, filename.version.clone())); + } + } // 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 diff --git a/crates/install-wheel-rs/src/wheel.rs b/crates/install-wheel-rs/src/wheel.rs index be19345d7..b5a75bd62 100644 --- a/crates/install-wheel-rs/src/wheel.rs +++ b/crates/install-wheel-rs/src/wheel.rs @@ -3,6 +3,7 @@ use std::collections::HashMap; use std::io::{BufRead, BufReader, BufWriter, Cursor, Read, Seek, Write}; use std::path::{Path, PathBuf}; use std::process::{Command, ExitStatus, Stdio}; +use std::str::FromStr; use std::{env, io, iter}; use configparser::ini::Ini; @@ -20,6 +21,8 @@ use zip::write::FileOptions; use zip::{ZipArchive, ZipWriter}; use distribution_filename::WheelFilename; +use pep440_rs::Version; +use puffin_normalize::PackageName; use pypi_types::DirectUrl; use crate::install_location::{InstallLocation, LockedDir}; @@ -955,7 +958,20 @@ pub fn install_wheel( .1 .to_string(); let metadata = dist_info_metadata(&dist_info_prefix, &mut archive)?; - let (name, _version) = parse_metadata(&dist_info_prefix, &metadata)?; + let (name, version) = parse_metadata(&dist_info_prefix, &metadata)?; + + // Validate the wheel name and version. + { + let name = PackageName::from_str(&name)?; + if name != filename.name { + return Err(Error::MismatchedName(name, filename.name.clone())); + } + + let version = Version::from_str(&version)?; + if version != filename.version { + return Err(Error::MismatchedVersion(version, filename.version.clone())); + } + } let record_path = format!("{dist_info_prefix}.dist-info/RECORD"); let mut record = read_record_file(&mut archive.by_name(&record_path).map_err(|err| { diff --git a/crates/puffin-installer/src/installer.rs b/crates/puffin-installer/src/installer.rs index 2bd1bd50a..bfc1d3081 100644 --- a/crates/puffin-installer/src/installer.rs +++ b/crates/puffin-installer/src/installer.rs @@ -49,6 +49,7 @@ impl<'a> Installer<'a> { install_wheel_rs::linker::install_wheel( &location, wheel.path(), + wheel.filename(), wheel .direct_url()? .as_ref() diff --git a/crates/puffin/tests/pip_sync.rs b/crates/puffin/tests/pip_sync.rs index da04484b0..47a2c7761 100644 --- a/crates/puffin/tests/pip_sync.rs +++ b/crates/puffin/tests/pip_sync.rs @@ -1210,6 +1210,102 @@ fn install_local_wheel() -> Result<()> { Ok(()) } +/// Install a wheel whose actual version doesn't match the version encoded in the filename. +#[test] +fn mismatched_version() -> Result<()> { + let temp_dir = assert_fs::TempDir::new()?; + let cache_dir = assert_fs::TempDir::new()?; + let venv = create_venv_py312(&temp_dir, &cache_dir); + + // Download a wheel. + let response = reqwest::blocking::get("https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl")?; + let archive = temp_dir.child("tomli-3.7.2-py3-none-any.whl"); + let mut archive_file = std::fs::File::create(&archive)?; + std::io::copy(&mut response.bytes()?.as_ref(), &mut archive_file)?; + + let requirements_txt = temp_dir.child("requirements.txt"); + requirements_txt.write_str(&format!("tomli @ file://{}", archive.path().display()))?; + + // In addition to the standard filters, remove the temporary directory from the snapshot. + let filters: Vec<_> = iter::once((r"file://.*/", "file://[TEMP_DIR]/")) + .chain(INSTA_FILTERS.to_vec()) + .collect(); + + insta::with_settings!({ + filters => filters.clone() + }, { + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .arg("pip") + .arg("sync") + .arg("requirements.txt") + .arg("--strict") + .arg("--cache-dir") + .arg(cache_dir.path()) + .env("VIRTUAL_ENV", venv.as_os_str()) + .current_dir(&temp_dir), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Downloaded 1 package in [TIME] + error: Failed to install: tomli-3.7.2-py3-none-any.whl (tomli==3.7.2 (from file://[TEMP_DIR]/tomli-3.7.2-py3-none-any.whl)) + Caused by: Wheel version does not match filename: 2.0.1 != 3.7.2 + "###); + }); + + Ok(()) +} + +/// Install a wheel whose actual name doesn't match the name encoded in the filename. +#[test] +fn mismatched_name() -> Result<()> { + let temp_dir = assert_fs::TempDir::new()?; + let cache_dir = assert_fs::TempDir::new()?; + let venv = create_venv_py312(&temp_dir, &cache_dir); + + // Download a wheel. + let response = reqwest::blocking::get("https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl")?; + let archive = temp_dir.child("foo-2.0.1-py3-none-any.whl"); + let mut archive_file = std::fs::File::create(&archive)?; + std::io::copy(&mut response.bytes()?.as_ref(), &mut archive_file)?; + + let requirements_txt = temp_dir.child("requirements.txt"); + requirements_txt.write_str(&format!("tomli @ file://{}", archive.path().display()))?; + + // In addition to the standard filters, remove the temporary directory from the snapshot. + let filters: Vec<_> = iter::once((r"file://.*/", "file://[TEMP_DIR]/")) + .chain(INSTA_FILTERS.to_vec()) + .collect(); + + insta::with_settings!({ + filters => filters.clone() + }, { + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .arg("pip") + .arg("sync") + .arg("requirements.txt") + .arg("--strict") + .arg("--cache-dir") + .arg(cache_dir.path()) + .env("VIRTUAL_ENV", venv.as_os_str()) + .current_dir(&temp_dir), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Downloaded 1 package in [TIME] + error: Failed to install: foo-2.0.1-py3-none-any.whl (foo==2.0.1 (from file://[TEMP_DIR]/foo-2.0.1-py3-none-any.whl)) + Caused by: Wheel package name does not match filename: tomli != foo + "###); + }); + + Ok(()) +} + /// Install a local source distribution. #[test] fn install_local_source_distribution() -> Result<()> { @@ -1847,7 +1943,7 @@ fn install_path_built_dist_cached() -> Result<()> { // Download a wheel. let response = reqwest::blocking::get("https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl")?; - let archive = temp_dir.child("tomli-3.0.1-py3-none-any.whl"); + let archive = temp_dir.child("tomli-2.0.1-py3-none-any.whl"); let mut archive_file = std::fs::File::create(&archive)?; std::io::copy(&mut response.bytes()?.as_ref(), &mut archive_file)?; @@ -1879,7 +1975,7 @@ fn install_path_built_dist_cached() -> Result<()> { Resolved 1 package in [TIME] Downloaded 1 package in [TIME] Installed 1 package in [TIME] - + tomli==3.0.1 (from file://[TEMP_DIR]/tomli-3.0.1-py3-none-any.whl) + + tomli==2.0.1 (from file://[TEMP_DIR]/tomli-2.0.1-py3-none-any.whl) "###); }); @@ -1907,7 +2003,7 @@ fn install_path_built_dist_cached() -> Result<()> { ----- stderr ----- Installed 1 package in [TIME] - + tomli==3.0.1 (from file://[TEMP_DIR]/tomli-3.0.1-py3-none-any.whl) + + tomli==2.0.1 (from file://[TEMP_DIR]/tomli-2.0.1-py3-none-any.whl) "###); }); @@ -1956,7 +2052,7 @@ fn install_path_built_dist_cached() -> Result<()> { Resolved 1 package in [TIME] Downloaded 1 package in [TIME] Installed 1 package in [TIME] - + tomli==3.0.1 (from file://[TEMP_DIR]/tomli-3.0.1-py3-none-any.whl) + + tomli==2.0.1 (from file://[TEMP_DIR]/tomli-2.0.1-py3-none-any.whl) "###); });