mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 10:58:28 +00:00
Loosen .dist-info
validation to accept arbitrary versions (#2441)
## Summary It turns out that pip does _not_ validate the normalization of the version specifier in the `.dist-info` directory. In particular, it seems that some tools replace the `+` in a local version segment with a `_`. Closes https://github.com/astral-sh/uv/issues/2424.
This commit is contained in:
parent
b5d9014918
commit
492ffbf997
3 changed files with 63 additions and 33 deletions
|
@ -86,6 +86,16 @@ pub enum Error {
|
||||||
MissingRecord(PathBuf),
|
MissingRecord(PathBuf),
|
||||||
#[error("Multiple .dist-info directories found: {0}")]
|
#[error("Multiple .dist-info directories found: {0}")]
|
||||||
MultipleDistInfo(String),
|
MultipleDistInfo(String),
|
||||||
|
#[error(
|
||||||
|
"The .dist-info directory {0} does not consist of the normalized package name and version"
|
||||||
|
)]
|
||||||
|
MissingDistInfoSegments(String),
|
||||||
|
#[error("The .dist-info directory {0} does not start with the normalized package name: {0}")]
|
||||||
|
MissingDistInfoPackageName(String, String),
|
||||||
|
#[error("The .dist-info directory {0} does not start with the normalized version: {0}")]
|
||||||
|
MissingDistInfoVersion(String, String),
|
||||||
|
#[error("The .dist-info directory name contains invalid characters")]
|
||||||
|
InvalidDistInfoPrefix,
|
||||||
#[error("Invalid wheel size")]
|
#[error("Invalid wheel size")]
|
||||||
InvalidSize,
|
InvalidSize,
|
||||||
#[error("Invalid package name")]
|
#[error("Invalid package name")]
|
||||||
|
|
|
@ -137,6 +137,8 @@ pub fn install_wheel(
|
||||||
/// Find the `dist-info` directory in an unzipped wheel.
|
/// Find the `dist-info` directory in an unzipped wheel.
|
||||||
///
|
///
|
||||||
/// See: <https://github.com/PyO3/python-pkginfo-rs>
|
/// See: <https://github.com/PyO3/python-pkginfo-rs>
|
||||||
|
///
|
||||||
|
/// See: <https://github.com/pypa/pip/blob/36823099a9cdd83261fdbc8c1d2a24fa2eea72ca/src/pip/_internal/utils/wheel.py#L38>
|
||||||
fn find_dist_info(path: impl AsRef<Path>) -> Result<String, Error> {
|
fn find_dist_info(path: impl AsRef<Path>) -> Result<String, Error> {
|
||||||
// Iterate over `path` to find the `.dist-info` directory. It should be at the top-level.
|
// 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 Some(dist_info) = fs::read_dir(path.as_ref())?.find_map(|entry| {
|
||||||
|
|
|
@ -2,6 +2,7 @@ use std::io::{Read, Seek};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use tracing::warn;
|
||||||
use zip::ZipArchive;
|
use zip::ZipArchive;
|
||||||
|
|
||||||
use distribution_filename::WheelFilename;
|
use distribution_filename::WheelFilename;
|
||||||
|
@ -42,13 +43,9 @@ pub fn is_metadata_entry(path: &str, filename: &WheelFilename) -> bool {
|
||||||
|
|
||||||
/// Find the `.dist-info` directory in a zipped wheel.
|
/// Find the `.dist-info` directory in a zipped wheel.
|
||||||
///
|
///
|
||||||
/// The metadata name may be uppercase, while the wheel and dist info names are lowercase, or
|
|
||||||
/// the metadata name and the dist info name are lowercase, while the wheel name is uppercase.
|
|
||||||
/// Either way, we just search the wheel for the name.
|
|
||||||
///
|
|
||||||
/// Returns the dist info dir prefix without the `.dist-info` extension.
|
/// Returns the dist info dir prefix without the `.dist-info` extension.
|
||||||
///
|
///
|
||||||
/// Reference implementation: <https://github.com/pypa/packaging/blob/2f83540272e79e3fe1f5d42abae8df0c14ddf4c2/src/packaging/utils.py#L146-L172>
|
/// Reference implementation: <https://github.com/pypa/pip/blob/36823099a9cdd83261fdbc8c1d2a24fa2eea72ca/src/pip/_internal/utils/wheel.py#L38>
|
||||||
pub fn find_archive_dist_info<'a, T: Copy>(
|
pub fn find_archive_dist_info<'a, T: Copy>(
|
||||||
filename: &WheelFilename,
|
filename: &WheelFilename,
|
||||||
files: impl Iterator<Item = (T, &'a str)>,
|
files: impl Iterator<Item = (T, &'a str)>,
|
||||||
|
@ -59,20 +56,12 @@ pub fn find_archive_dist_info<'a, T: Copy>(
|
||||||
if file != "METADATA" {
|
if file != "METADATA" {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
let dist_info_prefix = dist_info_dir.strip_suffix(".dist-info")?;
|
||||||
let dir_stem = dist_info_dir.strip_suffix(".dist-info")?;
|
Some((payload, dist_info_prefix))
|
||||||
let (name, version) = dir_stem.rsplit_once('-')?;
|
|
||||||
if PackageName::from_str(name).ok()? != filename.name {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
if Version::from_str(version).ok()? != filename.version {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some((payload, dir_stem))
|
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
// Like `pip`, assert that there is exactly one `.dist-info` directory.
|
||||||
let (payload, dist_info_prefix) = match metadatas[..] {
|
let (payload, dist_info_prefix) = match metadatas[..] {
|
||||||
[] => {
|
[] => {
|
||||||
return Err(Error::MissingDistInfo);
|
return Err(Error::MissingDistInfo);
|
||||||
|
@ -88,6 +77,28 @@ pub fn find_archive_dist_info<'a, T: Copy>(
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Like `pip`, validate that the `.dist-info` directory is prefixed with the canonical
|
||||||
|
// package name, but only warn if the version is not the normalized version.
|
||||||
|
let Some((name, version)) = dist_info_prefix.rsplit_once('-') else {
|
||||||
|
return Err(Error::MissingDistInfoSegments(dist_info_prefix.to_string()));
|
||||||
|
};
|
||||||
|
if PackageName::from_str(name)? != filename.name {
|
||||||
|
return Err(Error::MissingDistInfoPackageName(
|
||||||
|
dist_info_prefix.to_string(),
|
||||||
|
filename.name.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if !Version::from_str(version).is_ok_and(|version| version == filename.version) {
|
||||||
|
warn!(
|
||||||
|
"{}",
|
||||||
|
Error::MissingDistInfoVersion(
|
||||||
|
dist_info_prefix.to_string(),
|
||||||
|
filename.version.to_string(),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Ok((payload, dist_info_prefix))
|
Ok((payload, dist_info_prefix))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,7 +129,7 @@ pub fn find_flat_dist_info(
|
||||||
path: impl AsRef<Path>,
|
path: impl AsRef<Path>,
|
||||||
) -> Result<String, Error> {
|
) -> Result<String, Error> {
|
||||||
// Iterate over `path` to find the `.dist-info` directory. It should be at the top-level.
|
// Iterate over `path` to find the `.dist-info` directory. It should be at the top-level.
|
||||||
let Some(dist_info) = fs_err::read_dir(path.as_ref())?.find_map(|entry| {
|
let Some(dist_info_prefix) = fs_err::read_dir(path.as_ref())?.find_map(|entry| {
|
||||||
let entry = entry.ok()?;
|
let entry = entry.ok()?;
|
||||||
let file_type = entry.file_type().ok()?;
|
let file_type = entry.file_type().ok()?;
|
||||||
if file_type.is_dir() {
|
if file_type.is_dir() {
|
||||||
|
@ -129,16 +140,8 @@ pub fn find_flat_dist_info(
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let stem = path.file_stem()?;
|
let dist_info_prefix = path.file_stem()?.to_str()?;
|
||||||
let (name, version) = stem.to_str()?.rsplit_once('-')?;
|
Some(dist_info_prefix.to_string())
|
||||||
if PackageName::from_str(name).ok()? != filename.name {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
if Version::from_str(version).ok()? != filename.version {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(path)
|
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
@ -148,13 +151,28 @@ pub fn find_flat_dist_info(
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(dist_info_prefix) = dist_info.file_stem() else {
|
// Like `pip`, validate that the `.dist-info` directory is prefixed with the canonical
|
||||||
return Err(Error::InvalidWheel(
|
// package name, but only warn if the version is not the normalized version.
|
||||||
"Missing .dist-info directory".to_string(),
|
let Some((name, version)) = dist_info_prefix.rsplit_once('-') else {
|
||||||
));
|
return Err(Error::MissingDistInfoSegments(dist_info_prefix.to_string()));
|
||||||
};
|
};
|
||||||
|
if PackageName::from_str(name)? != filename.name {
|
||||||
|
return Err(Error::MissingDistInfoPackageName(
|
||||||
|
dist_info_prefix.to_string(),
|
||||||
|
filename.name.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if !Version::from_str(version).is_ok_and(|version| version == filename.version) {
|
||||||
|
warn!(
|
||||||
|
"{}",
|
||||||
|
Error::MissingDistInfoVersion(
|
||||||
|
dist_info_prefix.to_string(),
|
||||||
|
filename.version.to_string(),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(dist_info_prefix.to_string_lossy().to_string())
|
Ok(dist_info_prefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read the wheel `METADATA` metadata from a `.dist-info` directory.
|
/// Read the wheel `METADATA` metadata from a `.dist-info` directory.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue