From aed497b9bf38850f7da6fdebacc2d9a8380209fc Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Jan 2026 19:43:37 +0000 Subject: [PATCH] Reject unnamed indexes with `explicit = true` An unnamed explicit index can never be used, since explicit indexes must be referenced by name in `sources`. This adds validation at deserialization time via a custom `Deserialize` impl on `Index`. Closes #14878 https://claude.ai/code/session_01RtuQ1bcsHbj8qebME8okgB --- crates/uv-distribution-types/src/index.rs | 52 ++++++++++++++++++++++- crates/uv/tests/it/lock.rs | 44 +++++++++++++++++++ 2 files changed, 95 insertions(+), 1 deletion(-) diff --git a/crates/uv-distribution-types/src/index.rs b/crates/uv-distribution-types/src/index.rs index d99b645d43aaf..1ac47c9f2f29e 100644 --- a/crates/uv-distribution-types/src/index.rs +++ b/crates/uv-distribution-types/src/index.rs @@ -51,7 +51,7 @@ impl IndexCacheControl { } } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(rename_all = "kebab-case")] pub struct Index { @@ -545,6 +545,56 @@ impl<'a> From<&'a IndexUrl> for IndexMetadataRef<'a> { } } +/// Wire type for deserializing an [`Index`] with validation. +#[derive(Deserialize)] +#[serde(rename_all = "kebab-case")] +struct IndexWire { + name: Option, + url: IndexUrl, + #[serde(default)] + explicit: bool, + #[serde(default)] + default: bool, + #[serde(default)] + format: IndexFormat, + publish_url: Option, + #[serde(default)] + authenticate: AuthPolicy, + #[serde(default)] + ignore_error_codes: Option>, + #[serde(default)] + cache_control: Option, +} + +impl<'de> Deserialize<'de> for Index { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let wire = IndexWire::deserialize(deserializer)?; + + if wire.explicit && wire.name.is_none() { + return Err(serde::de::Error::custom(format!( + "An index with `explicit = true` requires a `name`: {}", + wire.url + ))); + } + + Ok(Self { + name: wire.name, + url: wire.url, + explicit: wire.explicit, + default: wire.default, + origin: None, + format: wire.format, + publish_url: wire.publish_url, + authenticate: wire.authenticate, + ignore_error_codes: wire.ignore_error_codes, + cache_control: wire.cache_control, + }) + } +} + /// An error that can occur when parsing an [`Index`]. #[derive(Error, Debug)] pub enum IndexSourceError { diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index 8fff3c72588e4..1019772dc8e3e 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -18910,6 +18910,50 @@ fn lock_explicit_default_index() -> Result<()> { Ok(()) } +/// Error when an explicit index does not have a name. +#[test] +fn lock_unnamed_explicit_index() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig==2.0.0"] + + [[tool.uv.index]] + url = "https://test.pypi.org/simple" + explicit = true + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r#" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + warning: Failed to parse `pyproject.toml` during settings discovery: + TOML parse error at line 8, column 9 + | + 8 | [[tool.uv.index]] + | ^^^^^^^^^^^^^^^^^ + An index with `explicit = true` requires a `name`: https://test.pypi.org/simple + + error: Failed to parse: `pyproject.toml` + Caused by: TOML parse error at line 8, column 9 + | + 8 | [[tool.uv.index]] + | ^^^^^^^^^^^^^^^^^ + An index with `explicit = true` requires a `name`: https://test.pypi.org/simple + "#); + + Ok(()) +} + #[test] fn lock_named_index() -> Result<()> { let context = TestContext::new("3.12");