Skip to content

Commit 5b2c359

Browse files
authored
Require disambiguated relative paths for --index (#14152)
We do not currently support passing index names to `--index` for installing packages. However, we do accept relative paths that can look like index names. This PR adds the requirement that `--index` values must be disambiguated with a prefix (`./` or `../` on Unix and Windows or `.\\` or `..\\` on Windows). For now, if an ambiguous value is provided, uv will warn that this will not be supported in the future. Currently, if you provide an index name like `--index test` when there is no `test` directory, uv will error with a `Directory not found...` error. That's not very informative if you thought index names were supported. The new warning makes the context clearer. Closes #13921
1 parent 283323a commit 5b2c359

File tree

7 files changed

+162
-4
lines changed

7 files changed

+162
-4
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/uv-cli/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5130,6 +5130,9 @@ pub struct IndexArgs {
51305130
/// All indexes provided via this flag take priority over the index specified by
51315131
/// `--default-index` (which defaults to PyPI). When multiple `--index` flags are provided,
51325132
/// earlier values take priority.
5133+
///
5134+
/// Index names are not supported as values. Relative paths must be disambiguated from index
5135+
/// names with `./` or `../` on Unix or `.\\`, `..\\`, `./` or `../` on Windows.
51335136
//
51345137
// The nested Vec structure (`Vec<Vec<Maybe<Index>>>`) is required for clap's
51355138
// value parsing mechanism, which processes one value at a time, in order to handle

crates/uv-distribution-types/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ uv-platform-tags = { workspace = true }
2929
uv-pypi-types = { workspace = true }
3030
uv-redacted = { workspace = true }
3131
uv-small-str = { workspace = true }
32+
uv-warnings = { workspace = true }
3233

3334
arcstr = { workspace = true }
3435
bitflags = { workspace = true }

crates/uv-distribution-types/src/index_url.rs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use url::{ParseError, Url};
1212

1313
use uv_pep508::{Scheme, VerbatimUrl, VerbatimUrlError, split_scheme};
1414
use uv_redacted::DisplaySafeUrl;
15+
use uv_warnings::warn_user;
1516

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

@@ -135,6 +136,30 @@ impl IndexUrl {
135136
Cow::Owned(url)
136137
}
137138
}
139+
140+
/// Warn user if the given URL was provided as an ambiguous relative path.
141+
///
142+
/// This is a temporary warning. Ambiguous values will not be
143+
/// accepted in the future.
144+
pub fn warn_on_disambiguated_relative_path(&self) {
145+
let Self::Path(verbatim_url) = &self else {
146+
return;
147+
};
148+
149+
if let Some(path) = verbatim_url.given() {
150+
if !is_disambiguated_path(path) {
151+
if cfg!(windows) {
152+
warn_user!(
153+
"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"
154+
);
155+
} else {
156+
warn_user!(
157+
"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"
158+
);
159+
}
160+
}
161+
}
162+
}
138163
}
139164

140165
impl Display for IndexUrl {
@@ -157,6 +182,28 @@ impl Verbatim for IndexUrl {
157182
}
158183
}
159184

185+
/// Checks if a path is disambiguated.
186+
///
187+
/// Disambiguated paths are absolute paths, paths with valid schemes,
188+
/// and paths starting with "./" or "../" on Unix or ".\\", "..\\",
189+
/// "./", or "../" on Windows.
190+
fn is_disambiguated_path(path: &str) -> bool {
191+
if cfg!(windows) {
192+
if path.starts_with(".\\") || path.starts_with("..\\") || path.starts_with('/') {
193+
return true;
194+
}
195+
}
196+
if path.starts_with("./") || path.starts_with("../") || Path::new(path).is_absolute() {
197+
return true;
198+
}
199+
// Check if the path has a scheme (like `file://`)
200+
if let Some((scheme, _)) = split_scheme(path) {
201+
return Scheme::parse(scheme).is_some();
202+
}
203+
// This is an ambiguous relative path
204+
false
205+
}
206+
160207
/// An error that can occur when parsing an [`IndexUrl`].
161208
#[derive(Error, Debug)]
162209
pub enum IndexUrlError {
@@ -620,3 +667,41 @@ impl IndexCapabilities {
620667
.insert(Flags::FORBIDDEN);
621668
}
622669
}
670+
671+
#[cfg(test)]
672+
mod tests {
673+
use super::*;
674+
675+
#[test]
676+
fn test_index_url_parse_valid_paths() {
677+
// Absolute path
678+
assert!(is_disambiguated_path("/absolute/path"));
679+
// Relative path
680+
assert!(is_disambiguated_path("./relative/path"));
681+
assert!(is_disambiguated_path("../../relative/path"));
682+
if cfg!(windows) {
683+
// Windows absolute path
684+
assert!(is_disambiguated_path("C:/absolute/path"));
685+
// Windows relative path
686+
assert!(is_disambiguated_path(".\\relative\\path"));
687+
assert!(is_disambiguated_path("..\\..\\relative\\path"));
688+
}
689+
}
690+
691+
#[test]
692+
fn test_index_url_parse_ambiguous_paths() {
693+
// Test single-segment ambiguous path
694+
assert!(!is_disambiguated_path("index"));
695+
// Test multi-segment ambiguous path
696+
assert!(!is_disambiguated_path("relative/path"));
697+
}
698+
699+
#[test]
700+
fn test_index_url_parse_with_schemes() {
701+
assert!(is_disambiguated_path("file:///absolute/path"));
702+
assert!(is_disambiguated_path("https://registry.com/simple/"));
703+
assert!(is_disambiguated_path(
704+
"git+https://github.com/example/repo.git"
705+
));
706+
}
707+
}

crates/uv/src/settings.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1386,6 +1386,12 @@ impl AddSettings {
13861386
)
13871387
.collect::<Vec<_>>();
13881388

1389+
// Warn user if an ambiguous relative path was passed as a value for
1390+
// `--index` or `--default-index`.
1391+
indexes
1392+
.iter()
1393+
.for_each(|index| index.url().warn_on_disambiguated_relative_path());
1394+
13891395
// If the user passed an `--index-url` or `--extra-index-url`, warn.
13901396
if installer
13911397
.index_args

crates/uv/tests/it/edit.rs

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9446,7 +9446,7 @@ fn add_index_with_existing_relative_path_index() -> Result<()> {
94469446
let wheel_dst = packages.child("ok-1.0.0-py3-none-any.whl");
94479447
fs_err::copy(&wheel_src, &wheel_dst)?;
94489448

9449-
uv_snapshot!(context.filters(), context.add().arg("iniconfig").arg("--index").arg("test-index"), @r"
9449+
uv_snapshot!(context.filters(), context.add().arg("iniconfig").arg("--index").arg("./test-index"), @r"
94509450
success: true
94519451
exit_code: 0
94529452
----- stdout -----
@@ -9475,7 +9475,7 @@ fn add_index_with_non_existent_relative_path() -> Result<()> {
94759475
dependencies = []
94769476
"#})?;
94779477

9478-
uv_snapshot!(context.filters(), context.add().arg("iniconfig").arg("--index").arg("test-index"), @r"
9478+
uv_snapshot!(context.filters(), context.add().arg("iniconfig").arg("--index").arg("./test-index"), @r"
94799479
success: false
94809480
exit_code: 2
94819481
----- stdout -----
@@ -9505,7 +9505,7 @@ fn add_index_with_non_existent_relative_path_with_same_name_as_index() -> Result
95059505
url = "https://pypi-proxy.fly.dev/simple"
95069506
"#})?;
95079507

9508-
uv_snapshot!(context.filters(), context.add().arg("iniconfig").arg("--index").arg("test-index"), @r"
9508+
uv_snapshot!(context.filters(), context.add().arg("iniconfig").arg("--index").arg("./test-index"), @r"
95099509
success: false
95109510
exit_code: 2
95119511
----- stdout -----
@@ -9528,12 +9528,16 @@ fn add_index_empty_directory() -> Result<()> {
95289528
version = "0.1.0"
95299529
requires-python = ">=3.12"
95309530
dependencies = []
9531+
9532+
[[tool.uv.index]]
9533+
name = "test-index"
9534+
url = "https://pypi-proxy.fly.dev/simple"
95319535
"#})?;
95329536

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

9536-
uv_snapshot!(context.filters(), context.add().arg("iniconfig").arg("--index").arg("test-index"), @r"
9540+
uv_snapshot!(context.filters(), context.add().arg("iniconfig").arg("--index").arg("./test-index"), @r"
95379541
success: true
95389542
exit_code: 0
95399543
----- stdout -----
@@ -9549,6 +9553,46 @@ fn add_index_empty_directory() -> Result<()> {
95499553
Ok(())
95509554
}
95519555

9556+
#[test]
9557+
fn add_index_with_ambiguous_relative_path() -> Result<()> {
9558+
let context = TestContext::new("3.12");
9559+
let mut filters = context.filters();
9560+
filters.push((r"\./|\.\\\\", r"[PREFIX]"));
9561+
9562+
let pyproject_toml = context.temp_dir.child("pyproject.toml");
9563+
pyproject_toml.write_str(indoc! {r#"
9564+
[project]
9565+
name = "project"
9566+
version = "0.1.0"
9567+
requires-python = ">=3.12"
9568+
dependencies = []
9569+
"#})?;
9570+
9571+
#[cfg(unix)]
9572+
uv_snapshot!(filters, context.add().arg("iniconfig").arg("--index").arg("test-index"), @r"
9573+
success: false
9574+
exit_code: 2
9575+
----- stdout -----
9576+
9577+
----- stderr -----
9578+
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
9579+
error: Directory not found for index: file://[TEMP_DIR]/test-index
9580+
");
9581+
9582+
#[cfg(windows)]
9583+
uv_snapshot!(filters, context.add().arg("iniconfig").arg("--index").arg("test-index"), @r"
9584+
success: false
9585+
exit_code: 2
9586+
----- stdout -----
9587+
9588+
----- stderr -----
9589+
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
9590+
error: Directory not found for index: file://[TEMP_DIR]/test-index
9591+
");
9592+
9593+
Ok(())
9594+
}
9595+
95529596
/// Add a PyPI requirement.
95539597
#[test]
95549598
fn add_group_comment() -> Result<()> {

0 commit comments

Comments
 (0)