Skip to content

Commit af97783

Browse files
committed
fix(settings): handle PermissionDenied during config discovery
Add PermissionDenied to error handling patterns in user(), system(), and find() methods to prevent crashes when configuration files are inaccessible.
1 parent 0959763 commit af97783

File tree

2 files changed

+164
-4
lines changed

2 files changed

+164
-4
lines changed

crates/uv-settings/src/lib.rs

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,13 @@ impl FilesystemOptions {
5858
| std::io::ErrorKind::PermissionDenied
5959
) =>
6060
{
61+
// For PermissionDenied, warn the user about inaccessible config.
62+
if err.kind() == std::io::ErrorKind::PermissionDenied {
63+
warn_user!(
64+
"Permission denied while reading user configuration in `{}`; using defaults.",
65+
file.user_display().cyan()
66+
);
67+
}
6168
Ok(None)
6269
}
6370
Err(err) => Err(err),
@@ -69,10 +76,35 @@ impl FilesystemOptions {
6976
return Ok(None);
7077
};
7178

72-
tracing::debug!("Found system configuration in: `{}`", file.display());
73-
let options = read_file(&file)?;
74-
validate_uv_toml(&file, &options)?;
75-
Ok(Some(Self(options)))
79+
tracing::debug!(
80+
"Searching for system configuration in: `{}`",
81+
file.display()
82+
);
83+
match read_file(&file) {
84+
Ok(options) => {
85+
tracing::debug!("Found system configuration in: `{}`", file.display());
86+
validate_uv_toml(&file, &options)?;
87+
Ok(Some(Self(options)))
88+
}
89+
Err(Error::Io(err))
90+
if matches!(
91+
err.kind(),
92+
std::io::ErrorKind::NotFound
93+
| std::io::ErrorKind::NotADirectory
94+
| std::io::ErrorKind::PermissionDenied
95+
) =>
96+
{
97+
// For PermissionDenied, warn the user about inaccessible config.
98+
if err.kind() == std::io::ErrorKind::PermissionDenied {
99+
warn_user!(
100+
"Permission denied while reading system configuration in `{}`; using defaults.",
101+
file.user_display().cyan()
102+
);
103+
}
104+
Ok(None)
105+
}
106+
Err(err) => Err(err),
107+
}
76108
}
77109

78110
/// Find the [`FilesystemOptions`] for the given path.
@@ -96,6 +128,23 @@ impl FilesystemOptions {
96128
textwrap::indent(&err.to_string(), " ")
97129
);
98130
}
131+
Err(Error::Io(err))
132+
if matches!(
133+
err.kind(),
134+
std::io::ErrorKind::NotFound
135+
| std::io::ErrorKind::NotADirectory
136+
| std::io::ErrorKind::PermissionDenied
137+
) =>
138+
{
139+
// For PermissionDenied, warn the user about inaccessible workspace config.
140+
if err.kind() == std::io::ErrorKind::PermissionDenied {
141+
warn_user!(
142+
"Permission denied while reading workspace configuration in `{}`; continuing search.",
143+
ancestor.user_display().cyan()
144+
);
145+
}
146+
// Continue traversing the directory tree.
147+
}
99148
Err(err) => {
100149
// Otherwise, warn and stop.
101150
return Err(err);

crates/uv/tests/it/show_settings.rs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
use std::path::Path;
22
use std::process::Command;
33

4+
#[cfg(unix)]
5+
use std::os::unix::fs::PermissionsExt;
6+
47
use assert_fs::prelude::*;
58
use uv_static::EnvVars;
69

@@ -10662,3 +10665,111 @@ fn build_isolation_override() -> anyhow::Result<()> {
1066210665

1066310666
Ok(())
1066410667
}
10668+
10669+
/// Skip configuration in parent directory when permissions are denied.
10670+
#[test]
10671+
#[cfg_attr(
10672+
windows,
10673+
ignore = "Configuration tests are not yet supported on Windows"
10674+
)]
10675+
#[cfg(unix)]
10676+
fn resolve_permission_denied() -> anyhow::Result<()> {
10677+
// RAII guard to ensure permissions are restored even if the test fails or panics.
10678+
struct PermissionGuard<'a> {
10679+
path: &'a std::path::Path,
10680+
should_restore: bool,
10681+
}
10682+
10683+
impl Drop for PermissionGuard<'_> {
10684+
fn drop(&mut self) {
10685+
if self.should_restore {
10686+
let _ = fs_err::set_permissions(self.path, std::fs::Permissions::from_mode(0o755));
10687+
}
10688+
}
10689+
}
10690+
10691+
let context = TestContext::new("3.12");
10692+
10693+
// Create a parent directory with a `uv.toml` file that sets a non-default resolution.
10694+
let parent = context.temp_dir.child("parent");
10695+
fs_err::create_dir(&parent)?;
10696+
let config = parent.child("uv.toml");
10697+
config.write_str(indoc::indoc! {r#"
10698+
[pip]
10699+
resolution = "lowest-direct"
10700+
index-url = "https://test.pypi.org/simple"
10701+
"#})?;
10702+
10703+
// Create a child directory to run the command from.
10704+
let child = parent.child("child");
10705+
fs_err::create_dir(&child)?;
10706+
let requirements_in = child.child("requirements.in");
10707+
requirements_in.write_str("anyio>3.0.0")?;
10708+
10709+
// Test 1: Normal permissions - should find and use parent config.
10710+
let result = context
10711+
.pip_compile()
10712+
.arg("--show-settings")
10713+
.arg("requirements.in")
10714+
.current_dir(&child)
10715+
.output()?;
10716+
assert!(result.status.success());
10717+
let stdout = std::str::from_utf8(&result.stdout)?;
10718+
// Should contain the parent config's resolution.
10719+
assert!(stdout.contains("resolution: LowestDirect"));
10720+
assert!(stdout.contains("test.pypi.org"));
10721+
10722+
// Test 2: Permission denied - should gracefully skip parent config.
10723+
// Try to remove read permissions from the config file itself (more targeted approach)
10724+
let permission_denied_setup =
10725+
fs_err::set_permissions(config.path(), std::fs::Permissions::from_mode(0o000)).is_ok();
10726+
10727+
let _guard = PermissionGuard {
10728+
path: config.path(),
10729+
should_restore: permission_denied_setup,
10730+
};
10731+
10732+
let result = context
10733+
.pip_compile()
10734+
.arg("--show-settings")
10735+
.arg("requirements.in")
10736+
.current_dir(&child)
10737+
.output()?;
10738+
10739+
assert!(result.status.success());
10740+
let stdout = std::str::from_utf8(&result.stdout)?;
10741+
let stderr = std::str::from_utf8(&result.stderr)?;
10742+
10743+
// Should use default resolution (not parent config) if permissions were denied.
10744+
if permission_denied_setup {
10745+
assert!(stdout.contains("resolution: Highest"));
10746+
assert!(!stdout.contains("test.pypi.org"));
10747+
10748+
// Should warn about permission denied (it's an error condition).
10749+
assert!(stderr.contains("Permission denied while reading workspace configuration"));
10750+
assert!(stderr.contains("continuing search"));
10751+
} else {
10752+
// If we couldn't set permissions, the test behaves like normal case
10753+
assert!(stdout.contains("resolution: LowestDirect"));
10754+
assert!(stdout.contains("test.pypi.org"));
10755+
assert!(!stderr.contains("Permission denied"));
10756+
}
10757+
10758+
// Test 3: UV_NO_CONFIG - should skip all config discovery silently.
10759+
let result = context
10760+
.pip_compile()
10761+
.arg("--show-settings")
10762+
.arg("requirements.in")
10763+
.arg("--no-config")
10764+
.current_dir(&child)
10765+
.output()?;
10766+
10767+
assert!(result.status.success());
10768+
let stdout = std::str::from_utf8(&result.stdout)?;
10769+
let stderr = std::str::from_utf8(&result.stderr)?;
10770+
assert!(stdout.contains("resolution: Highest"));
10771+
assert!(!stdout.contains("test.pypi.org"));
10772+
assert!(!stderr.contains("Permission denied")); // UV_NO_CONFIG is silent
10773+
10774+
Ok(())
10775+
}

0 commit comments

Comments
 (0)