link-assistant-router 1.14.2

Link.Assistant.Router — Claude MAX OAuth proxy and token gateway for Anthropic APIs
Documentation
//! 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;