diff --git a/crates/uv-build-backend/src/lib.rs b/crates/uv-build-backend/src/lib.rs index aa90a88c5..683feb486 100644 --- a/crates/uv-build-backend/src/lib.rs +++ b/crates/uv-build-backend/src/lib.rs @@ -66,6 +66,9 @@ pub enum Error { /// Either an absolute path or a parent path through `..`. #[error("Module root must be inside the project: `{}`", _0.user_display())] InvalidModuleRoot(PathBuf), + /// Either an absolute path or a parent path through `..`. + #[error("The path for the data directory {} must be inside the project: `{}`", name, path.user_display())] + InvalidDataRoot { name: String, path: PathBuf }, #[error("Inconsistent metadata between prepare and build step: `{0}`")] InconsistentSteps(&'static str), #[error("Failed to write to {}", _0.user_display())] @@ -209,8 +212,10 @@ fn find_roots( namespace: bool, ) -> Result<(PathBuf, Vec), Error> { let relative_module_root = uv_fs::normalize_path(relative_module_root); - let src_root = source_tree.join(&relative_module_root); - if !src_root.starts_with(source_tree) { + // Check that even if a path contains `..`, we only include files below the module root. + if !uv_fs::normalize_path(&source_tree.join(&relative_module_root)) + .starts_with(uv_fs::normalize_path(source_tree)) + { return Err(Error::InvalidModuleRoot(relative_module_root.to_path_buf())); } let src_root = source_tree.join(&relative_module_root); diff --git a/crates/uv-build-backend/src/settings.rs b/crates/uv-build-backend/src/settings.rs index 33e6d8c45..da90b509f 100644 --- a/crates/uv-build-backend/src/settings.rs +++ b/crates/uv-build-backend/src/settings.rs @@ -1,5 +1,5 @@ use serde::{Deserialize, Serialize}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use uv_macros::OptionsMetadata; /// Settings for the uv build backend (`uv_build`). @@ -204,16 +204,16 @@ pub enum ModuleName { #[serde(default, rename_all = "kebab-case", deny_unknown_fields)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct WheelDataIncludes { - purelib: Option, - platlib: Option, - headers: Option, - scripts: Option, - data: Option, + purelib: Option, + platlib: Option, + headers: Option, + scripts: Option, + data: Option, } impl WheelDataIncludes { /// Yield all data directories name and corresponding paths. - pub fn iter(&self) -> impl Iterator { + pub fn iter(&self) -> impl Iterator { [ ("purelib", self.purelib.as_deref()), ("platlib", self.platlib.as_deref()), diff --git a/crates/uv-build-backend/src/source_dist.rs b/crates/uv-build-backend/src/source_dist.rs index 3b6d11ba4..81eaea5e1 100644 --- a/crates/uv-build-backend/src/source_dist.rs +++ b/crates/uv-build-backend/src/source_dist.rs @@ -9,7 +9,7 @@ use fs_err::File; use globset::{Glob, GlobSet}; use std::io; use std::io::{BufReader, Cursor}; -use std::path::{Path, PathBuf}; +use std::path::{Component, Path, PathBuf}; use tar::{EntryType, Header}; use tracing::{debug, trace}; use uv_distribution_filename::{SourceDistExtension, SourceDistFilename}; @@ -123,12 +123,22 @@ fn source_dist_matcher( // Include the data files for (name, directory) in settings.data.iter() { - let directory = uv_fs::normalize_path(Path::new(directory)); + let directory = uv_fs::normalize_path(directory); trace!( "Including data ({}) at: `{}`", name, directory.user_display() ); + if directory + .components() + .next() + .is_some_and(|component| !matches!(component, Component::CurDir | Component::Normal(_))) + { + return Err(Error::InvalidDataRoot { + name: name.to_string(), + path: directory.to_path_buf(), + }); + } let directory = directory.portable_display().to_string(); let glob = PortableGlobParser::Uv .parse(&format!("{}/**", globset::escape(&directory))) diff --git a/crates/uv-build-backend/src/wheel.rs b/crates/uv-build-backend/src/wheel.rs index 7009a6468..762424a26 100644 --- a/crates/uv-build-backend/src/wheel.rs +++ b/crates/uv-build-backend/src/wheel.rs @@ -5,7 +5,7 @@ use itertools::Itertools; use rustc_hash::FxHashSet; use sha2::{Digest, Sha256}; use std::io::{BufReader, Read, Write}; -use std::path::{Path, PathBuf}; +use std::path::{Component, Path, PathBuf}; use std::{io, mem}; use tracing::{debug, trace}; use walkdir::WalkDir; @@ -207,7 +207,20 @@ fn write_wheel( // Add the data files for (name, directory) in settings.data.iter() { - debug!("Adding {name} data files from: `{directory}`"); + debug!( + "Adding {name} data files from: `{}`", + directory.user_display() + ); + if directory + .components() + .next() + .is_some_and(|component| !matches!(component, Component::CurDir | Component::Normal(_))) + { + return Err(Error::InvalidDataRoot { + name: name.to_string(), + path: directory.to_path_buf(), + }); + } let data_dir = format!( "{}-{}.data/{}/", pyproject_toml.name().as_dist_info_name(), diff --git a/crates/uv/tests/it/build.rs b/crates/uv/tests/it/build.rs index 138a218bb..3759c5b5b 100644 --- a/crates/uv/tests/it/build.rs +++ b/crates/uv/tests/it/build.rs @@ -1442,7 +1442,7 @@ fn build_fast_path() -> Result<()> { uv_snapshot!(context.build() .arg(&built_by_uv) .arg("--out-dir") - .arg(context.temp_dir.join("output1")), @r###" + .arg(context.temp_dir.join("output1")), @r" success: true exit_code: 0 ----- stdout ----- @@ -1452,7 +1452,7 @@ fn build_fast_path() -> Result<()> { Building wheel from source distribution (uv build backend)... Successfully built output1/built_by_uv-0.1.0.tar.gz Successfully built output1/built_by_uv-0.1.0-py3-none-any.whl - "###); + "); context .temp_dir .child("output1") @@ -1487,7 +1487,7 @@ fn build_fast_path() -> Result<()> { .arg(&built_by_uv) .arg("--out-dir") .arg(context.temp_dir.join("output3")) - .arg("--wheel"), @r###" + .arg("--wheel"), @r" success: true exit_code: 0 ----- stdout ----- @@ -1495,7 +1495,7 @@ fn build_fast_path() -> Result<()> { ----- stderr ----- Building wheel (uv build backend)... Successfully built output3/built_by_uv-0.1.0-py3-none-any.whl - "###); + "); context .temp_dir .child("output3") @@ -1507,7 +1507,7 @@ fn build_fast_path() -> Result<()> { .arg("--out-dir") .arg(context.temp_dir.join("output4")) .arg("--sdist") - .arg("--wheel"), @r###" + .arg("--wheel"), @r" success: true exit_code: 0 ----- stdout ----- @@ -1517,7 +1517,7 @@ fn build_fast_path() -> Result<()> { Building wheel (uv build backend)... Successfully built output4/built_by_uv-0.1.0.tar.gz Successfully built output4/built_by_uv-0.1.0-py3-none-any.whl - "###); + "); context .temp_dir .child("output4") diff --git a/crates/uv/tests/it/build_backend.rs b/crates/uv/tests/it/build_backend.rs index ae3a7a740..350f3a522 100644 --- a/crates/uv/tests/it/build_backend.rs +++ b/crates/uv/tests/it/build_backend.rs @@ -1,7 +1,7 @@ use crate::common::{TestContext, uv_snapshot, venv_bin_path}; use anyhow::Result; use assert_cmd::assert::OutputAssertExt; -use assert_fs::fixture::{FileWriteStr, PathChild, PathCreateDir}; +use assert_fs::fixture::{FileTouch, FileWriteStr, PathChild, PathCreateDir}; use flate2::bufread::GzDecoder; use fs_err::File; use indoc::{formatdoc, indoc}; @@ -884,3 +884,106 @@ fn invalid_build_backend_settings_are_ignored() -> Result<()> { Ok(()) } + +/// Error when there is a relative module root outside the project root, such as +/// `tool.uv.build-backend.module-root = ".."`. +#[test] +fn error_on_relative_module_root_outside_project_root() -> Result<()> { + let context = TestContext::new("3.12"); + + 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" + + [tool.uv.build-backend] + module-root = ".." + + [build-system] + requires = ["uv_build>=0.7,<10000"] + build-backend = "uv_build" + "#})?; + + context.temp_dir.child("__init__.py").touch()?; + + uv_snapshot!(context.filters(), context.build(), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Building source distribution (uv build backend)... + × Failed to build `[TEMP_DIR]/` + ╰─▶ Module root must be inside the project: `..` + "); + + uv_snapshot!(context.filters(), context.build().arg("--wheel"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Building wheel (uv build backend)... + × Failed to build `[TEMP_DIR]/` + ╰─▶ Module root must be inside the project: `..` + "); + + Ok(()) +} + +/// Error when there is a relative data directory outside the project root, such as +/// `tool.uv.build-backend.data.headers = "../headers"`. +#[test] +fn error_on_relative_data_dir_outside_project_root() -> Result<()> { + let context = TestContext::new("3.12"); + + let project = context.temp_dir.child("project"); + project.create_dir_all()?; + + let pyproject_toml = project.child("pyproject.toml"); + pyproject_toml.write_str(indoc! {r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + + [tool.uv.build-backend.data] + headers = "../header" + + [build-system] + requires = ["uv_build>=0.7,<10000"] + build-backend = "uv_build" + "#})?; + + let project_module = project.child("src/project"); + project_module.create_dir_all()?; + project_module.child("__init__.py").touch()?; + + context.temp_dir.child("headers").create_dir_all()?; + + uv_snapshot!(context.filters(), context.build().arg("project"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Building source distribution (uv build backend)... + × Failed to build `[TEMP_DIR]/project` + ╰─▶ The path for the data directory headers must be inside the project: `../header` + "); + + uv_snapshot!(context.filters(), context.build().arg("project").arg("--wheel"), @r" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Building wheel (uv build backend)... + × Failed to build `[TEMP_DIR]/project` + ╰─▶ The path for the data directory headers must be inside the project: `../header` + "); + + Ok(()) +}