1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
//! Reading directory paths from the environment.
//!
//! `std::env::var_os` returns `Some("")` for a variable that is *set but
//! empty*, which is a different thing from unset and was being treated as the
//! same. An empty `XDG_CONFIG_HOME` therefore became the config root, every
//! state path turned relative, and the router read and wrote its state — a
//! `server.json` holding a live `la_sk_` token — into whatever directory the
//! command happened to run from (issue #340).
//!
//! It is easy to hit unintentionally: `env: { XDG_CONFIG_HOME: '' }` is the
//! natural way to say "do not inherit the user's config" in most CI runners,
//! and it did the opposite here.
use std::path::{Path, PathBuf};
/// The value of `name`, treating a set-but-empty variable as unset.
///
/// This is the whole fix: with `Some("")` filtered out, an `or_else` chain
/// falls through to the next candidate and the "…are unset" error at its end
/// can actually fire.
#[must_use]
pub fn directory(name: &str) -> Option<PathBuf> {
from_value(std::env::var_os(name))
}
/// Configuration root used by persistent `router with` client homes.
/// Keep profile inventory on the same path, including Windows HOME/USERPROFILE.
pub fn router_client_config_root() -> Result<PathBuf, String> {
router_client_config_root_in(&directory)
}
/// [`router_client_config_root`] resolved from another environment, such as
/// the one captured from a running client process (issue #610).
pub fn router_client_config_root_in(
directory: &dyn Fn(&str) -> Option<PathBuf>,
) -> Result<PathBuf, String> {
let root = directory("XDG_CONFIG_HOME")
.or_else(|| {
directory("HOME")
.or_else(|| directory("USERPROFILE"))
.map(|home| home.join(".config"))
})
.ok_or("HOME, USERPROFILE and XDG_CONFIG_HOME are unset; cannot keep a client profile")?;
require_absolute(root, "the client profile directory")
}
/// Qwen documents relative and `~/` overrides; resolve them once so later
/// profile operations never write through a changing process directory.
pub fn qwen_directory(name: &str, home: &Path) -> Result<Option<PathBuf>, String> {
qwen_directory_in(&directory, name, home, || {
std::env::current_dir().map_err(|error| format!("cannot resolve {name}: {error}"))
})
}
/// [`qwen_directory`] resolved from another environment. A relative value is
/// joined to whatever `current` reports as that environment's directory.
pub fn qwen_directory_in(
directory: &dyn Fn(&str) -> Option<PathBuf>,
name: &str,
home: &Path,
current: impl FnOnce() -> Result<PathBuf, String>,
) -> Result<Option<PathBuf>, String> {
let Some(value) = directory(name) else {
return Ok(None);
};
if value.is_absolute() {
return Ok(Some(value));
}
if let Ok(suffix) = value.strip_prefix("~") {
return Ok(Some(home.join(suffix)));
}
Ok(Some(current()?.join(value)))
}
/// The rule itself, separated from the lookup so it can be tested.
///
/// Setting a variable is process-wide and this crate forbids `unsafe`, so a
/// test that manipulated the real environment could neither be written here
/// nor run reliably beside its neighbours.
#[must_use]
pub fn from_value(value: Option<std::ffi::OsString>) -> Option<PathBuf> {
value.filter(|value| !value.is_empty()).map(PathBuf::from)
}
/// Refuse a resolved root that is not absolute.
///
/// A relative root is a broken environment rather than a location anyone
/// chose, and failing loudly beats writing a credential into `$PWD`. The
/// message names the variable so the cause is findable.
///
/// # Errors
///
/// Returns a message when `root` is relative.
pub fn require_absolute(root: PathBuf, what: &str) -> Result<PathBuf, String> {
if root.is_absolute() {
return Ok(root);
}
Err(format!(
"refusing to use the relative path {} for {what}: check HOME, XDG_CONFIG_HOME and \
APPDATA, one of which is set to something relative or empty",
root.display()
))
}
#[cfg(test)]
#[path = "env_paths_tests.rs"]
mod tests;