use std::path::PathBuf;
#[cfg(unix)]
fn xdg_dir(var: &str, home_segments: &[&str]) -> std::io::Result<PathBuf> {
let xdg = std::env::var(var).ok();
let home = std::env::var("HOME").ok();
xdg_dir_from(var, xdg.as_deref(), home.as_deref(), home_segments)
}
#[allow(dead_code)]
fn xdg_dir_from(
var: &str,
xdg: Option<&str>,
home: Option<&str>,
home_segments: &[&str],
) -> std::io::Result<PathBuf> {
if let Some(x) = xdg
&& !x.is_empty()
&& std::path::Path::new(x).is_absolute()
{
return Ok(PathBuf::from(x).join("mcpmesh"));
}
match home {
Some(h) if !h.is_empty() => {
let mut dir = PathBuf::from(h);
for seg in home_segments {
dir.push(seg);
}
dir.push("mcpmesh");
Ok(dir)
}
_ => Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("HOME is not set; set HOME or {var} to an absolute path"),
)),
}
}
#[allow(dead_code)]
fn win_dir_from(
xdg: Option<&str>,
base: Option<&str>,
segments: &[&str],
) -> std::io::Result<PathBuf> {
if let Some(x) = xdg
&& !x.is_empty()
&& std::path::Path::new(x).is_absolute()
{
return Ok(PathBuf::from(x).join("mcpmesh"));
}
match base {
Some(b) if !b.is_empty() => {
let mut dir = PathBuf::from(b);
dir.push("mcpmesh");
for seg in segments {
dir.push(seg);
}
Ok(dir)
}
_ => Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"APPDATA/LOCALAPPDATA is not set; set it or the XDG override to an absolute path",
)),
}
}
#[cfg(windows)]
fn win_dir(xdg_var: &str, base_var: &str, segments: &[&str]) -> std::io::Result<PathBuf> {
let xdg = std::env::var(xdg_var).ok();
let base = std::env::var(base_var).ok();
win_dir_from(xdg.as_deref(), base.as_deref(), segments)
}
#[allow(dead_code)]
fn fnv8(s: &str) -> String {
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for b in s.as_bytes() {
h ^= u64::from(*b);
h = h.wrapping_mul(0x100_0000_01b3);
}
format!("{:08x}", ((h >> 32) ^ h) as u32)
}
#[allow(dead_code)]
fn sanitize_pipe_component(s: &str) -> String {
s.chars()
.map(|c| match c {
'a'..='z' | '0'..='9' | '_' | '-' => c,
'A'..='Z' => c.to_ascii_lowercase(),
_ => '-',
})
.collect()
}
#[allow(dead_code)]
fn windows_pipe_name_from(
domain: Option<&str>,
user: Option<&str>,
xdg: Option<&str>,
) -> std::io::Result<PathBuf> {
let user = user.filter(|u| !u.is_empty()).ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::NotFound,
"USERNAME is not set; cannot derive a per-user pipe name",
)
})?;
let mut name = format!(
r"\\.\pipe\mcpmesh-{}-{}",
sanitize_pipe_component(domain.unwrap_or("local")),
sanitize_pipe_component(user)
);
if let Some(x) = xdg
&& !x.is_empty()
{
name.push('-');
name.push_str(&fnv8(x));
}
Ok(PathBuf::from(name))
}
#[cfg(windows)]
fn windows_pipe_name() -> std::io::Result<PathBuf> {
let domain = std::env::var("USERDOMAIN").ok();
let user = std::env::var("USERNAME").ok();
let xdg = std::env::var("XDG_RUNTIME_DIR").ok();
windows_pipe_name_from(domain.as_deref(), user.as_deref(), xdg.as_deref())
}
pub fn config_dir() -> std::io::Result<PathBuf> {
#[cfg(unix)]
return xdg_dir("XDG_CONFIG_HOME", &[".config"]);
#[cfg(windows)]
return win_dir("XDG_CONFIG_HOME", "APPDATA", &[]);
}
pub fn default_device_key_path() -> std::io::Result<PathBuf> {
Ok(config_dir()?.join("device.key"))
}
pub fn default_config_path() -> std::io::Result<PathBuf> {
Ok(config_dir()?.join("config.toml"))
}
pub fn runtime_dir() -> std::io::Result<PathBuf> {
let xdg = std::env::var("XDG_RUNTIME_DIR").ok();
let tmp = std::env::var("TMPDIR")
.ok()
.filter(|s| !s.is_empty())
.map(PathBuf::from)
.unwrap_or_else(std::env::temp_dir);
runtime_dir_from(xdg.as_deref(), tmp)
}
fn runtime_dir_from(xdg: Option<&str>, tmp: PathBuf) -> std::io::Result<PathBuf> {
if let Some(x) = xdg
&& !x.is_empty()
&& std::path::Path::new(x).is_absolute()
{
return Ok(PathBuf::from(x).join("mcpmesh"));
}
let dir = tmp.join("mcpmesh");
if !dir.is_absolute() {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"could not resolve a per-user runtime dir; \
set XDG_RUNTIME_DIR or TMPDIR to an absolute path",
));
}
Ok(dir)
}
pub fn default_endpoint() -> std::io::Result<PathBuf> {
#[cfg(unix)]
return Ok(runtime_dir()?.join("mcpmesh.sock"));
#[cfg(windows)]
return windows_pipe_name();
}
pub fn data_dir() -> std::io::Result<PathBuf> {
#[cfg(unix)]
return xdg_dir("XDG_DATA_HOME", &[".local", "share"]);
#[cfg(windows)]
return win_dir("XDG_DATA_HOME", "LOCALAPPDATA", &["data"]);
}
pub fn default_state_db_path() -> std::io::Result<PathBuf> {
Ok(data_dir()?.join("state.redb"))
}
pub fn default_blobs_dir() -> std::io::Result<PathBuf> {
Ok(data_dir()?.join("blobs"))
}
pub fn default_blob_scopes_path() -> std::io::Result<PathBuf> {
Ok(data_dir()?.join("blob-scopes.json"))
}
pub fn state_dir() -> std::io::Result<PathBuf> {
#[cfg(unix)]
return xdg_dir("XDG_STATE_HOME", &[".local", "state"]);
#[cfg(windows)]
return win_dir("XDG_STATE_HOME", "LOCALAPPDATA", &["state"]);
}
pub fn default_audit_dir() -> std::io::Result<PathBuf> {
Ok(state_dir()?.join("audit"))
}
pub fn default_roster_path() -> std::io::Result<PathBuf> {
Ok(config_dir()?.join("roster.json"))
}
pub fn default_roster_confirmed_path() -> std::io::Result<PathBuf> {
Ok(config_dir()?.join("roster.confirmed"))
}
pub fn default_org_root_key_path() -> std::io::Result<PathBuf> {
Ok(config_dir()?.join("org-root.key"))
}
pub fn default_user_key_path() -> std::io::Result<PathBuf> {
Ok(config_dir()?.join("user.key"))
}
#[cfg(test)]
mod path_tests {
use super::*;
#[cfg(unix)]
#[test]
fn runtime_dir_prefers_xdg_when_set() {
assert_eq!(
runtime_dir_from(Some("/run/user/1000"), PathBuf::from("/tmp")).unwrap(),
PathBuf::from("/run/user/1000/mcpmesh")
);
}
#[cfg(unix)]
#[test]
fn runtime_dir_falls_back_to_tmp_when_xdg_absent_empty_or_relative() {
let tmp = PathBuf::from("/var/folders/xx");
let want = PathBuf::from("/var/folders/xx/mcpmesh");
assert_eq!(runtime_dir_from(None, tmp.clone()).unwrap(), want);
assert_eq!(runtime_dir_from(Some(""), tmp.clone()).unwrap(), want);
assert_eq!(runtime_dir_from(Some("relative/dir"), tmp).unwrap(), want);
}
#[test]
fn empty_tmpdir_without_xdg_errors_not_relative() {
let err = runtime_dir_from(None, PathBuf::from("")).unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
}
#[cfg(unix)]
#[test]
fn default_endpoint_is_under_runtime_dir() {
let sock = default_endpoint().unwrap();
assert!(sock.ends_with("mcpmesh/mcpmesh.sock"));
assert!(sock.is_absolute());
}
#[cfg(windows)]
#[test]
fn default_endpoint_is_a_per_user_pipe_name() {
let sock = default_endpoint().unwrap();
assert!(sock.to_string_lossy().starts_with(r"\\.\pipe\mcpmesh-"));
}
#[test]
fn audit_dir_is_state_dir_slash_audit() {
let audit = default_audit_dir().unwrap();
assert_eq!(audit.file_name().unwrap(), "audit");
assert!(audit.is_absolute());
assert_eq!(audit.parent().unwrap(), state_dir().unwrap());
}
#[test]
fn state_dir_prefers_xdg_state_home_when_absolute() {
let sd = state_dir().unwrap();
assert!(sd.components().any(|c| c.as_os_str() == "mcpmesh"));
assert!(sd.is_absolute());
}
#[cfg(unix)]
#[test]
fn xdg_dir_prefers_the_var_when_absolute_else_home_segments() {
assert_eq!(
xdg_dir_from(
"XDG_DATA_HOME",
Some("/xdg/data"),
Some("/home/u"),
&[".local", "share"]
)
.unwrap(),
PathBuf::from("/xdg/data/mcpmesh")
);
for bad in [None, Some(""), Some("relative/dir")] {
assert_eq!(
xdg_dir_from("XDG_DATA_HOME", bad, Some("/home/u"), &[".local", "share"]).unwrap(),
PathBuf::from("/home/u/.local/share/mcpmesh")
);
}
}
#[test]
fn xdg_dir_without_home_errors_never_panics() {
for home in [None, Some("")] {
let err = xdg_dir_from("XDG_CONFIG_HOME", None, home, &[".config"]).unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
}
}
#[test]
fn win_dir_prefers_xdg_override_else_base_env() {
let abs_override = if cfg!(windows) {
r"C:\xdg\cfg"
} else {
"/xdg/cfg"
};
assert_eq!(
win_dir_from(Some(abs_override), Some(r"C:\Users\u\AppData\Roaming"), &[]).unwrap(),
PathBuf::from(abs_override).join("mcpmesh")
);
assert_eq!(
win_dir_from(None, Some(r"C:\Users\u\AppData\Local"), &["data"]).unwrap(),
PathBuf::from(r"C:\Users\u\AppData\Local")
.join("mcpmesh")
.join("data")
);
for base in [None, Some("")] {
let err = win_dir_from(None, base, &[]).unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
}
}
#[test]
fn windows_pipe_name_is_per_user_and_env_isolatable() {
assert_eq!(
windows_pipe_name_from(Some("DESKTOP-AB12"), Some("Jo Hn"), None).unwrap(),
PathBuf::from(r"\\.\pipe\mcpmesh-desktop-ab12-jo-hn")
);
let a = windows_pipe_name_from(Some("D"), Some("u"), Some(r"C:\tmp\t1")).unwrap();
let b = windows_pipe_name_from(Some("D"), Some("u"), Some(r"C:\tmp\t2")).unwrap();
assert_ne!(a, b);
assert!(a.to_string_lossy().starts_with(r"\\.\pipe\mcpmesh-d-u-"));
assert_eq!(
windows_pipe_name_from(Some("D"), Some("u"), Some(r"C:\tmp\t1")).unwrap(),
a
);
assert!(windows_pipe_name_from(None, None, None).is_err());
}
}