Skip to content

Commit

Permalink
Always treat archive-like requirements as local files (#7364)
Browse files Browse the repository at this point in the history
## Summary

`uv pip install foo.tar.gz` will now always treat `foo.tar.gz` as a
local file. This matches pip's behavior.

Closes #7309.
  • Loading branch information
charliermarsh committed Sep 13, 2024
1 parent f822241 commit d9f53cc
Show file tree
Hide file tree
Showing 2 changed files with 414 additions and 3 deletions.
59 changes: 56 additions & 3 deletions crates/pep508-rs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -657,8 +657,13 @@ fn looks_like_unnamed_requirement(cursor: &mut Cursor) -> bool {
// Expand any environment variables in the path.
let expanded = expand_env_vars(url);

// Strip extras.
let url = split_extras(&expanded)
.map(|(url, _)| url)
.unwrap_or(&expanded);

// Analyze the path.
let mut chars = expanded.chars();
let mut chars = url.chars();

let Some(first_char) = chars.next() else {
return false;
Expand All @@ -670,18 +675,47 @@ fn looks_like_unnamed_requirement(cursor: &mut Cursor) -> bool {
}

// Ex) `https://` or `C:`
if split_scheme(&expanded).is_some() {
if split_scheme(url).is_some() {
return true;
}

// Ex) `foo/bar`
if expanded.contains('/') || expanded.contains('\\') {
if url.contains('/') || url.contains('\\') {
return true;
}

// Ex) `foo.tar.gz`
if looks_like_archive(url) {
return true;
}

false
}

/// Returns `true` if a file looks like an archive.
///
/// See <https://github.com/pypa/pip/blob/111eed14b6e9fba7c78a5ec2b7594812d17b5d2b/src/pip/_internal/utils/filetypes.py#L8>
/// for the list of supported archive extensions.
fn looks_like_archive(file: impl AsRef<Path>) -> bool {
let file = file.as_ref();

// E.g., `gz` in `foo.tar.gz`
let Some(extension) = file.extension().and_then(|ext| ext.to_str()) else {
return false;
};

// E.g., `tar` in `foo.tar.gz`
let pre_extension = file
.file_stem()
.and_then(|stem| Path::new(stem).extension().and_then(|ext| ext.to_str()));

matches!(
(pre_extension, extension),
(_, "whl" | "tbz" | "txz" | "tlz" | "zip" | "tgz" | "tar")
| (Some("tar"), "bz2" | "xz" | "lz" | "lzma" | "gz")
)
}

/// parses extras in the `[extra1,extra2] format`
fn parse_extras_cursor<T: Pep508Url>(
cursor: &mut Cursor,
Expand Down Expand Up @@ -970,7 +1004,9 @@ fn parse_pep508_requirement<T: Pep508Url>(
// wsp*
cursor.eat_whitespace();
// name
let name_start = cursor.pos();
let name = parse_name(cursor)?;
let name_end = cursor.pos();
// wsp*
cursor.eat_whitespace();
// extras?
Expand Down Expand Up @@ -1018,6 +1054,23 @@ fn parse_pep508_requirement<T: Pep508Url>(

let requirement_end = cursor.pos();

// If the requirement consists solely of a package name, and that name appears to be an archive,
// treat it as a URL requirement, for consistency and security. (E.g., `requests-2.26.0.tar.gz`
// is a valid Python package name, but we should treat it as a reference to a file.)
//
// See: https://github.com/pypa/pip/blob/111eed14b6e9fba7c78a5ec2b7594812d17b5d2b/src/pip/_internal/utils/filetypes.py#L8
if requirement_kind.is_none() {
if looks_like_archive(cursor.slice(name_start, name_end)) {
let clone = cursor.clone().at(start);
return Err(Pep508Error {
message: Pep508ErrorSource::UnsupportedRequirement("URL requirement must be preceded by a package name. Add the name of the package before the URL (e.g., `package_name @ https://...`).".to_string()),
start,
len: clone.pos() - start,
input: clone.to_string(),
});
}
}

// wsp*
cursor.eat_whitespace();
// quoted_marker?
Expand Down
Loading

0 comments on commit d9f53cc

Please sign in to comment.