Skip to content
Merged
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5068,6 +5068,9 @@ pub struct IndexArgs {
/// All indexes provided via this flag take priority over the index specified by
/// `--default-index` (which defaults to PyPI). When multiple `--index` flags are provided,
/// earlier values take priority.
///
/// Index names are not supported as values. Relative paths must be disambiguated from index
/// names with `./` or `../` on Unix or `.\\`, `..\\`, `./` or `../` on Windows.
//
// The nested Vec structure (`Vec<Vec<Maybe<Index>>>`) is required for clap's
// value parsing mechanism, which processes one value at a time, in order to handle
Expand Down
1 change: 1 addition & 0 deletions crates/uv-distribution-types/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ uv-platform-tags = { workspace = true }
uv-pypi-types = { workspace = true }
uv-redacted = { workspace = true }
uv-small-str = { workspace = true }
uv-warnings = { workspace = true }

arcstr = { workspace = true }
bitflags = { workspace = true }
Expand Down
85 changes: 85 additions & 0 deletions crates/uv-distribution-types/src/index_url.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use url::{ParseError, Url};

use uv_pep508::{Scheme, VerbatimUrl, VerbatimUrlError, split_scheme};
use uv_redacted::DisplaySafeUrl;
use uv_warnings::warn_user;

use crate::{Index, IndexStatusCodeStrategy, Verbatim};

Expand Down Expand Up @@ -140,6 +141,30 @@ impl IndexUrl {
Cow::Owned(url)
}
}

/// Warn user if the given URL was provided as an ambiguous relative path.
///
/// This is a temporary warning. Ambiguous values will not be
/// accepted in the future.
pub fn warn_on_disambiguated_relative_path(&self) {
let Self::Path(verbatim_url) = &self else {
return;
};

if let Some(path) = verbatim_url.given() {
if !is_disambiguated_path(path) {
if cfg!(windows) {
warn_user!(
"Relative paths passed to `--index` or `--default-index` should be disambiguated from index names (use `.\\{path}` or `./{path}`). Support for ambiguous values will be removed in the future"
);
} else {
warn_user!(
"Relative paths passed to `--index` or `--default-index` should be disambiguated from index names (use `./{path}`). Support for ambiguous values will be removed in the future"
);
}
}
}
}
}

impl Display for IndexUrl {
Expand All @@ -162,6 +187,28 @@ impl Verbatim for IndexUrl {
}
}

/// Checks if a path is disambiguated.
///
/// Disambiguated paths are absolute paths, paths with valid schemes,
/// and paths starting with "./" or "../" on Unix or ".\\", "..\\",
/// "./", or "../" on Windows.
fn is_disambiguated_path(path: &str) -> bool {
if cfg!(windows) {
if path.starts_with(".\\") || path.starts_with("..\\") || path.starts_with('/') {
return true;
}
}
if path.starts_with("./") || path.starts_with("../") || Path::new(path).is_absolute() {
return true;
}
// Check if the path has a scheme (like `file://`)
if let Some((scheme, _)) = split_scheme(path) {
return Scheme::parse(scheme).is_some();
}
// This is an ambiguous relative path
false
}

/// An error that can occur when parsing an [`IndexUrl`].
#[derive(Error, Debug)]
pub enum IndexUrlError {
Expand Down Expand Up @@ -625,3 +672,41 @@ impl IndexCapabilities {
.insert(Flags::FORBIDDEN);
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_index_url_parse_valid_paths() {
// Absolute path
assert!(is_disambiguated_path("/absolute/path"));
// Relative path
assert!(is_disambiguated_path("./relative/path"));
assert!(is_disambiguated_path("../../relative/path"));
if cfg!(windows) {
// Windows absolute path
assert!(is_disambiguated_path("C:/absolute/path"));
// Windows relative path
assert!(is_disambiguated_path(".\\relative\\path"));
assert!(is_disambiguated_path("..\\..\\relative\\path"));
}
}

#[test]
fn test_index_url_parse_ambiguous_paths() {
// Test single-segment ambiguous path
assert!(!is_disambiguated_path("index"));
// Test multi-segment ambiguous path
assert!(!is_disambiguated_path("relative/path"));
}

#[test]
fn test_index_url_parse_with_schemes() {
assert!(is_disambiguated_path("file:///absolute/path"));
assert!(is_disambiguated_path("https://registry.com/simple/"));
assert!(is_disambiguated_path(
"git+https://github.com/example/repo.git"
));
}
}
6 changes: 6 additions & 0 deletions crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1333,6 +1333,12 @@ impl AddSettings {
)
.collect::<Vec<_>>();

// Warn user if an ambiguous relative path was passed as a value for
// `--index` or `--default-index`.
indexes
.iter()
.for_each(|index| index.url().warn_on_disambiguated_relative_path());

// If the user passed an `--index-url` or `--extra-index-url`, warn.
if installer
.index_args
Expand Down
52 changes: 48 additions & 4 deletions crates/uv/tests/it/edit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9364,7 +9364,7 @@ fn add_index_with_existing_relative_path_index() -> Result<()> {
let wheel_dst = packages.child("ok-1.0.0-py3-none-any.whl");
fs_err::copy(&wheel_src, &wheel_dst)?;

uv_snapshot!(context.filters(), context.add().arg("iniconfig").arg("--index").arg("test-index"), @r"
uv_snapshot!(context.filters(), context.add().arg("iniconfig").arg("--index").arg("./test-index"), @r"
success: true
exit_code: 0
----- stdout -----
Expand Down Expand Up @@ -9393,7 +9393,7 @@ fn add_index_with_non_existent_relative_path() -> Result<()> {
dependencies = []
"#})?;

uv_snapshot!(context.filters(), context.add().arg("iniconfig").arg("--index").arg("test-index"), @r"
uv_snapshot!(context.filters(), context.add().arg("iniconfig").arg("--index").arg("./test-index"), @r"
success: false
exit_code: 2
----- stdout -----
Expand Down Expand Up @@ -9423,7 +9423,7 @@ fn add_index_with_non_existent_relative_path_with_same_name_as_index() -> Result
url = "https://pypi-proxy.fly.dev/simple"
"#})?;

uv_snapshot!(context.filters(), context.add().arg("iniconfig").arg("--index").arg("test-index"), @r"
uv_snapshot!(context.filters(), context.add().arg("iniconfig").arg("--index").arg("./test-index"), @r"
success: false
exit_code: 2
----- stdout -----
Expand All @@ -9446,12 +9446,16 @@ fn add_index_empty_directory() -> Result<()> {
version = "0.1.0"
requires-python = ">=3.12"
dependencies = []

[[tool.uv.index]]
name = "test-index"
url = "https://pypi-proxy.fly.dev/simple"
"#})?;

let packages = context.temp_dir.child("test-index");
packages.create_dir_all()?;

uv_snapshot!(context.filters(), context.add().arg("iniconfig").arg("--index").arg("test-index"), @r"
uv_snapshot!(context.filters(), context.add().arg("iniconfig").arg("--index").arg("./test-index"), @r"
success: true
exit_code: 0
----- stdout -----
Expand All @@ -9467,6 +9471,46 @@ fn add_index_empty_directory() -> Result<()> {
Ok(())
}

#[test]
fn add_index_with_ambiguous_relative_path() -> Result<()> {
let context = TestContext::new("3.12");
let mut filters = context.filters();
filters.push((r"\./|\.\\\\", r"[PREFIX]"));

let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! {r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = []
"#})?;

#[cfg(unix)]
uv_snapshot!(filters, context.add().arg("iniconfig").arg("--index").arg("test-index"), @r"
success: false
exit_code: 2
----- stdout -----

----- stderr -----
warning: Relative paths passed to `--index` or `--default-index` should be disambiguated from index names (use `[PREFIX]test-index`). Support for ambiguous values will be removed in the future
error: Directory not found for index: file://[TEMP_DIR]/test-index
");

#[cfg(windows)]
uv_snapshot!(filters, context.add().arg("iniconfig").arg("--index").arg("test-index"), @r"
success: false
exit_code: 2
----- stdout -----

----- stderr -----
warning: Relative paths passed to `--index` or `--default-index` should be disambiguated from index names (use `[PREFIX]test-index` or `[PREFIX]test-index`). Support for ambiguous values will be removed in the future
error: Directory not found for index: file://[TEMP_DIR]/test-index
");

Ok(())
}

/// Add a PyPI requirement.
#[test]
fn add_group_comment() -> Result<()> {
Expand Down
Loading
Loading