From b4c705f0c269bffe2302f847ff36907667715dbf Mon Sep 17 00:00:00 2001 From: konsti Date: Tue, 14 Jul 2026 11:39:20 +0000 Subject: [PATCH 1/4] Avoid panicking for non-UTF-8 virtualenv paths --- crates/uv-virtualenv/src/lib.rs | 5 +++++ crates/uv-virtualenv/src/virtualenv.rs | 6 ++++++ crates/uv/tests/python/venv.rs | 27 ++++++++++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/crates/uv-virtualenv/src/lib.rs b/crates/uv-virtualenv/src/lib.rs index 1eac4eeca0c66..9c485c7ae6c64 100644 --- a/crates/uv-virtualenv/src/lib.rs +++ b/crates/uv-virtualenv/src/lib.rs @@ -32,6 +32,11 @@ pub enum Error { /// The non-virtual environment directory that would have been cleared. path: PathBuf, }, + #[error("Virtual environment path is not valid UTF-8: {}", path.user_display())] + NonUtf8Path { + /// The non-UTF-8 virtual environment path. + path: PathBuf, + }, } impl uv_errors::Hint for Error { diff --git a/crates/uv-virtualenv/src/virtualenv.rs b/crates/uv-virtualenv/src/virtualenv.rs index 33ffb4856ae6e..e64a05662f658 100644 --- a/crates/uv-virtualenv/src/virtualenv.rs +++ b/crates/uv-virtualenv/src/virtualenv.rs @@ -101,6 +101,12 @@ pub(crate) fn create( }; let absolute = std::path::absolute(location)?; + if absolute.to_str().is_none() { + return Err(Error::NonUtf8Path { + path: location.to_path_buf(), + }); + } + // Validate the existing location. match location.metadata() { Ok(metadata) if metadata.is_file() => { diff --git a/crates/uv/tests/python/venv.rs b/crates/uv/tests/python/venv.rs index aeeb54423244f..54e3d859f4ad9 100644 --- a/crates/uv/tests/python/venv.rs +++ b/crates/uv/tests/python/venv.rs @@ -10,6 +10,8 @@ use uv_static::EnvVars; #[cfg(unix)] use fs_err::os::unix::fs::symlink; +#[cfg(unix)] +use std::{ffi::OsStr, os::unix::ffi::OsStrExt}; use uv_test::{site_packages_path, uv_snapshot}; @@ -1489,6 +1491,31 @@ fn file_exists() -> Result<()> { Ok(()) } +#[cfg(unix)] +#[test] +fn non_utf8_path() { + let context = uv_test::test_context_with_versions!(&["3.12"]); + let path = OsStr::from_bytes(b".venv-\xff"); + + uv_snapshot!(context.filters(), context.venv() + .arg(path) + .arg("--python") + .arg("3.12"), @" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Creating virtual environment at: .venv-� + error: Failed to create virtual environment + Caused by: Virtual environment path is not valid UTF-8: .venv-� + " + ); + + assert!(!context.temp_dir.path().join(path).exists()); +} + #[test] fn empty_dir_exists() -> Result<()> { let context = uv_test::test_context_with_versions!(&["3.12"]); From 1019b876ea9e8875c37bda9e1066da14a0c45fee Mon Sep 17 00:00:00 2001 From: konsti Date: Tue, 14 Jul 2026 12:02:08 +0000 Subject: [PATCH 2/4] Handle non-UTF-8 paths when generating activators --- crates/uv-virtualenv/src/virtualenv.rs | 16 ++++++++-------- crates/uv/tests/python/venv.rs | 2 -- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/crates/uv-virtualenv/src/virtualenv.rs b/crates/uv-virtualenv/src/virtualenv.rs index e64a05662f658..92cecb83a794a 100644 --- a/crates/uv-virtualenv/src/virtualenv.rs +++ b/crates/uv-virtualenv/src/virtualenv.rs @@ -101,12 +101,6 @@ pub(crate) fn create( }; let absolute = std::path::absolute(location)?; - if absolute.to_str().is_none() { - return Err(Error::NonUtf8Path { - path: location.to_path_buf(), - }); - } - // Validate the existing location. match location.metadata() { Ok(metadata) if metadata.is_file() => { @@ -486,6 +480,12 @@ pub(crate) fn create( .map(|path| path.simplified().to_str().unwrap().replace('\\', "\\\\")) .join(path_sep); + let location_string = location + .simplified() + .to_str() + .ok_or_else(|| Error::NonUtf8Path { + path: location.clone(), + })?; let virtual_env_dir = match (relocatable, name.to_owned()) { (true, "activate") => Cow::Borrowed( r#"'"$(dirname -- "$(dirname -- "$(realpath -- "$SCRIPT_PATH")")")"'"#, @@ -497,10 +497,10 @@ pub(crate) fn create( (true, "activate.nu") => Cow::Borrowed(r"(path self | path dirname | path dirname)"), (false, "activate.nu") => Cow::Owned(format!( "'{}'", - escape_posix_for_single_quotes(location.simplified().to_str().unwrap()) + escape_posix_for_single_quotes(location_string) )), // Note: `activate.ps1` is already relocatable by default. - _ => escape_posix_for_single_quotes(location.simplified().to_str().unwrap()), + _ => escape_posix_for_single_quotes(location_string), }; let activator = template diff --git a/crates/uv/tests/python/venv.rs b/crates/uv/tests/python/venv.rs index 54e3d859f4ad9..9a53c0cd6eebb 100644 --- a/crates/uv/tests/python/venv.rs +++ b/crates/uv/tests/python/venv.rs @@ -1512,8 +1512,6 @@ fn non_utf8_path() { Caused by: Virtual environment path is not valid UTF-8: .venv-� " ); - - assert!(!context.temp_dir.path().join(path).exists()); } #[test] From 21cd6aa5f563360741d909ea63c54db74b42291c Mon Sep 17 00:00:00 2001 From: konsti Date: Tue, 14 Jul 2026 18:40:31 +0000 Subject: [PATCH 3/4] Cover non-UTF-8 venv paths on Windows --- crates/uv/tests/python/venv.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/uv/tests/python/venv.rs b/crates/uv/tests/python/venv.rs index 9a53c0cd6eebb..dfae2cdfd238b 100644 --- a/crates/uv/tests/python/venv.rs +++ b/crates/uv/tests/python/venv.rs @@ -12,6 +12,8 @@ use uv_static::EnvVars; use fs_err::os::unix::fs::symlink; #[cfg(unix)] use std::{ffi::OsStr, os::unix::ffi::OsStrExt}; +#[cfg(windows)] +use std::{ffi::OsString, os::windows::ffi::OsStringExt}; use uv_test::{site_packages_path, uv_snapshot}; @@ -1491,11 +1493,13 @@ fn file_exists() -> Result<()> { Ok(()) } -#[cfg(unix)] #[test] fn non_utf8_path() { let context = uv_test::test_context_with_versions!(&["3.12"]); + #[cfg(unix)] let path = OsStr::from_bytes(b".venv-\xff"); + #[cfg(windows)] + let path = OsString::from_wide(&[0x002e, 0x0076, 0x0065, 0x006e, 0x0076, 0x002d, 0xd800]); uv_snapshot!(context.filters(), context.venv() .arg(path) From d30b6ab3fcba4399c88f23cc5aff84460fe33cca Mon Sep 17 00:00:00 2001 From: konsti Date: Tue, 14 Jul 2026 18:59:29 +0000 Subject: [PATCH 4/4] Use cfg_select for non-UTF-8 venv test --- crates/uv/tests/python/venv.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/uv/tests/python/venv.rs b/crates/uv/tests/python/venv.rs index dfae2cdfd238b..0df85574351c8 100644 --- a/crates/uv/tests/python/venv.rs +++ b/crates/uv/tests/python/venv.rs @@ -1496,10 +1496,10 @@ fn file_exists() -> Result<()> { #[test] fn non_utf8_path() { let context = uv_test::test_context_with_versions!(&["3.12"]); - #[cfg(unix)] - let path = OsStr::from_bytes(b".venv-\xff"); - #[cfg(windows)] - let path = OsString::from_wide(&[0x002e, 0x0076, 0x0065, 0x006e, 0x0076, 0x002d, 0xd800]); + let path = cfg_select! { + unix => OsStr::from_bytes(b".venv-\xff"), + windows => OsString::from_wide(&[0x002e, 0x0076, 0x0065, 0x006e, 0x0076, 0x002d, 0xd800]), + }; uv_snapshot!(context.filters(), context.venv() .arg(path)