Validate wheel metadata against filename (#1002)

Closes #983.
This commit is contained in:
Charlie Marsh 2024-01-19 00:48:55 -05:00 committed by GitHub
parent f86d9b1c31
commit 69c72b6fa1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 146 additions and 10 deletions

View file

@ -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.

View file

@ -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<impl AsRef<Path>>,
wheel: impl AsRef<Path>,
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

View file

@ -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| {

View file

@ -49,6 +49,7 @@ impl<'a> Installer<'a> {
install_wheel_rs::linker::install_wheel(
&location,
wheel.path(),
wheel.filename(),
wheel
.direct_url()?
.as_ref()

View file

@ -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)
"###);
});