use std::ffi::OsString;
use std::path::{Path, PathBuf};
#[cfg(windows)]
#[must_use]
pub fn strip_verbatim_prefix(path: &Path) -> std::borrow::Cow<'_, Path> {
use std::path::{Component, Prefix};
let mut components = path.components();
let Some(Component::Prefix(prefix)) = components.next() else {
return std::borrow::Cow::Borrowed(path);
};
let Prefix::VerbatimDisk(letter) = prefix.kind() else {
return std::borrow::Cow::Borrowed(path);
};
let mut rebuilt = PathBuf::from(format!("{}:\\", char::from(letter)));
rebuilt.extend(components.filter(|part| !matches!(part, Component::RootDir)));
std::borrow::Cow::Owned(rebuilt)
}
#[cfg(not(windows))]
#[must_use]
pub fn strip_verbatim_prefix(path: &Path) -> std::borrow::Cow<'_, Path> {
std::borrow::Cow::Borrowed(path)
}
#[must_use]
pub fn user_home(var: &dyn Fn(&str) -> Option<OsString>) -> Option<PathBuf> {
let named = |key: &str| var(key).filter(|value| !value.is_empty());
#[cfg(not(windows))]
{
named("HOME").map(PathBuf::from)
}
#[cfg(windows)]
{
let split = || {
let mut joined = named("HOMEDRIVE")?;
joined.push(named("HOMEPATH")?);
Some(joined)
};
named("HOME")
.or_else(|| named("USERPROFILE"))
.or_else(split)
.map(PathBuf::from)
}
}
#[cfg(not(windows))]
pub(crate) const HOME_DIR_VAR: &str = "$HOME";
#[cfg(windows)]
pub(crate) const HOME_DIR_VAR: &str = "%USERPROFILE%";
#[cfg(not(windows))]
pub(crate) const SHEP_HOME_VAR: &str = "$SHEP_HOME";
#[cfg(windows)]
pub(crate) const SHEP_HOME_VAR: &str = "%SHEP_HOME%";
const DEFAULT_HOME_DIR: &str = ".shep";
#[must_use]
pub fn shep_home(env: &dyn Fn(&str) -> Option<String>, home_dir: Option<&Path>) -> Option<PathBuf> {
match home_dir {
Some(dir) => Some(home_under(env, dir)),
None => env("SHEP_HOME").map(PathBuf::from),
}
}
fn home_under(env: &dyn Fn(&str) -> Option<String>, home_dir: &Path) -> PathBuf {
env("SHEP_HOME")
.map(PathBuf::from)
.unwrap_or_else(|| home_dir.join(DEFAULT_HOME_DIR))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ShepPaths {
pub home: PathBuf,
pub daemon_config: PathBuf,
pub dogs_config: PathBuf,
pub snapshot: PathBuf,
pub logs: PathBuf,
pub pids: PathBuf,
pub run: PathBuf,
pub socket: PathBuf,
pub barks: PathBuf,
pub kv: PathBuf,
pub overrides: PathBuf,
pub secrets: PathBuf,
pub secrets_cache: PathBuf,
}
fn fnv1a64(bytes: &[u8]) -> u64 {
bytes.iter().fold(0xcbf2_9ce4_8422_2325, |hash, &byte| {
(hash ^ u64::from(byte)).wrapping_mul(0x0000_0100_0000_01b3)
})
}
impl ShepPaths {
#[must_use]
pub fn pipe_name(&self) -> String {
const MAX_STEM: usize = 64;
let home = self.home.to_string_lossy();
let sanitized: String = home
.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
.collect();
let trimmed = sanitized.trim_matches('-');
let stem = trimmed[..trimmed.len().min(MAX_STEM)].trim_end_matches('-');
let digest = fnv1a64(home.as_bytes());
format!(r"\\.\pipe\shep-{stem}-{digest:016x}")
}
#[must_use]
pub fn resolve(env: &dyn Fn(&str) -> Option<String>, home_dir: &Path) -> Self {
let home = home_under(env, home_dir);
let run = home.join("run");
#[cfg_attr(not(windows), allow(unused_mut))]
let mut paths = Self {
daemon_config: home.join("shep.toml"),
dogs_config: home.join("dogs.toml"),
snapshot: home.join("flock.json"),
logs: home.join("logs"),
pids: home.join("pids"),
socket: run.join("shep.sock"),
barks: home.join("barks.jsonl"),
kv: home.join("kv.json"),
overrides: home.join("overrides.json"),
secrets: home.join("secrets.json"),
secrets_cache: home.join("secrets-cache.json"),
run,
home,
};
#[cfg(windows)]
{
paths.socket = PathBuf::from(paths.pipe_name());
}
paths
}
}
#[cfg(test)]
mod tests {
#[test]
fn shep_home_prefers_the_variable_and_falls_back_to_the_default() {
use std::path::{Path, PathBuf};
let named = |_: &str| Some("/srv/shep".to_string());
let unset = |_: &str| None;
let ada = Path::new("/home/ada");
assert_eq!(
super::shep_home(&named, Some(ada)),
Some(PathBuf::from("/srv/shep")),
"the variable wins over the default"
);
assert_eq!(
super::shep_home(&named, None),
Some(PathBuf::from("/srv/shep")),
"and needs no home directory of its own"
);
assert_eq!(
super::shep_home(&unset, Some(ada)),
Some(ada.join(".shep")),
"the default hangs off the user's home"
);
assert_eq!(
super::shep_home(&unset, None),
None,
"with neither, there is nothing to name"
);
assert_eq!(
super::shep_home(&|_| Some(String::new()), Some(ada)),
Some(PathBuf::new()),
"an empty value is taken at its word rather than read as unset"
);
assert_eq!(
super::ShepPaths::resolve(&named, ada).home,
super::shep_home(&named, Some(ada)).expect("a home directory was given"),
"and the layout is built from the same answer"
);
}
#[cfg(windows)]
#[test]
fn a_verbatim_prefix_is_stripped() {
let rewritten = super::strip_verbatim_prefix(std::path::Path::new(r"\\?\C:\tmp\flock.js"));
assert_eq!(
rewritten.as_os_str(),
std::ffi::OsStr::new(r"C:\tmp\flock.js"),
"node reads the leading `\\\\` as a UNC share and lstats `C:`, so \
the verbatim prefix must not reach it"
);
let plain = std::path::Path::new(r"C:\tmp\flock.js");
assert_eq!(
super::strip_verbatim_prefix(plain).as_os_str(),
plain.as_os_str(),
"a path with no verbatim prefix must pass through untouched"
);
}
#[cfg(windows)]
#[test]
fn a_real_canonicalized_path_comes_back_free_of_the_prefix() {
let dir = tempfile::tempdir().expect("temp dir");
let file = dir.path().join("dog.exe");
std::fs::write(&file, b"not really an exe").expect("write file");
let canonical = std::fs::canonicalize(&file).expect("canonicalize");
let rewritten = super::strip_verbatim_prefix(&canonical);
let shown = rewritten.display().to_string();
assert!(
!shown.starts_with(r"\\?\"),
"the path an operator will read still carries a verbatim prefix: {shown}"
);
assert!(
std::path::Path::new(&shown).is_file(),
"stripping the prefix must not break the path: {shown}"
);
}
#[cfg(not(windows))]
#[test]
fn a_unix_path_passes_through_untouched() {
let plain = std::path::Path::new("/tmp/flock.js");
assert_eq!(
super::strip_verbatim_prefix(plain).as_os_str(),
plain.as_os_str(),
"the non-Windows arm must be an identity"
);
}
use super::*;
use std::path::Path;
fn no_env(_: &str) -> Option<String> {
None
}
#[test]
fn default_layout_under_home_dir() {
let p = ShepPaths::resolve(&no_env, Path::new("/home/ada"));
assert_eq!(p.home, Path::new("/home/ada/.shep"));
assert_eq!(p.daemon_config, Path::new("/home/ada/.shep/shep.toml"));
assert_eq!(p.dogs_config, Path::new("/home/ada/.shep/dogs.toml"));
assert_eq!(p.snapshot, Path::new("/home/ada/.shep/flock.json"));
assert_eq!(p.logs, Path::new("/home/ada/.shep/logs"));
assert_eq!(p.pids, Path::new("/home/ada/.shep/pids"));
assert_eq!(p.run, Path::new("/home/ada/.shep/run"));
assert_eq!(p.barks, Path::new("/home/ada/.shep/barks.jsonl"));
assert_eq!(p.kv, Path::new("/home/ada/.shep/kv.json"));
assert_eq!(p.overrides, Path::new("/home/ada/.shep/overrides.json"));
assert_eq!(p.secrets, Path::new("/home/ada/.shep/secrets.json"));
assert_eq!(
p.secrets_cache,
Path::new("/home/ada/.shep/secrets-cache.json")
);
}
#[test]
fn the_control_address_is_a_socket_file_on_unix_and_a_pipe_name_on_windows() {
let p = ShepPaths::resolve(&no_env, Path::new("/home/ada"));
#[cfg(unix)]
assert_eq!(p.socket, Path::new("/home/ada/.shep/run/shep.sock"));
#[cfg(windows)]
assert_eq!(
p.socket,
Path::new(r"\\.\pipe\shep-home-ada--shep-fd394cfc5c93ad12")
);
#[cfg(windows)]
assert_eq!(
p.socket,
Path::new(&p.pipe_name()),
"the resolved address and `pipe_name` must not drift"
);
}
#[test]
fn shep_home_env_overrides_root() {
let env = |key: &str| (key == "SHEP_HOME").then(|| "/srv/shep".to_string());
let p = ShepPaths::resolve(&env, Path::new("/home/ada"));
assert_eq!(p.home, Path::new("/srv/shep"));
#[cfg(unix)]
assert_eq!(p.socket, Path::new("/srv/shep/run/shep.sock"));
#[cfg(windows)]
assert_eq!(
p.socket,
Path::new(r"\\.\pipe\shep-srv-shep-23b467803966a71a")
);
}
#[test]
fn pipe_name_is_per_home_and_sanitized() {
let env = |key: &str| (key == "SHEP_HOME").then(|| "/home/ada/.shep".to_string());
let p = ShepPaths::resolve(&env, Path::new("/home/ada"));
assert_eq!(
p.pipe_name(),
r"\\.\pipe\shep-home-ada--shep-626b4d544f86fe95"
);
let env = |key: &str| (key == "SHEP_HOME").then(|| "/srv/shep".to_string());
let q = ShepPaths::resolve(&env, Path::new("/home/ada"));
assert_eq!(q.pipe_name(), r"\\.\pipe\shep-srv-shep-23b467803966a71a");
}
#[test]
fn a_stem_longer_than_the_cap_is_cut_and_retrimmed() {
let long = format!("/{}/bbbb", "a".repeat(63));
let env = |key: &str| (key == "SHEP_HOME").then(|| long.clone());
let name = ShepPaths::resolve(&env, Path::new("/home/ada")).pipe_name();
let stem = name
.strip_prefix(r"\\.\pipe\shep-")
.expect("the fixed prefix")
.rsplit_once('-')
.expect("a digest after the stem")
.0;
assert_eq!(
stem,
"a".repeat(63),
"cut at the cap, then the dash removed"
);
assert!(
!name.contains("--"),
"a dash left by the cut would double against the digest's own: {name}"
);
}
#[test]
fn two_homes_differing_only_past_the_cap_stay_distinct() {
let base = "a".repeat(70);
let one = format!("/{base}/one");
let two = format!("/{base}/two");
let env_one = |key: &str| (key == "SHEP_HOME").then(|| one.clone());
let env_two = |key: &str| (key == "SHEP_HOME").then(|| two.clone());
let a = ShepPaths::resolve(&env_one, Path::new("/home/ada")).pipe_name();
let b = ShepPaths::resolve(&env_two, Path::new("/home/ada")).pipe_name();
assert_ne!(a, b, "two homes, two pipes, whatever the cut discarded");
}
#[test]
fn two_homes_that_sanitize_alike_get_distinct_pipe_names() {
let nested = |key: &str| (key == "SHEP_HOME").then(|| r"C:\a\b".to_string());
let dashed = |key: &str| (key == "SHEP_HOME").then(|| r"C:\a-b".to_string());
let n = ShepPaths::resolve(&nested, Path::new("/home/ada"));
let d = ShepPaths::resolve(&dashed, Path::new("/home/ada"));
assert!(
n.pipe_name().starts_with(r"\\.\pipe\shep-C--a-b-")
&& d.pipe_name().starts_with(r"\\.\pipe\shep-C--a-b-"),
"the readable stem is what collides, and it stays readable: {} vs {}",
n.pipe_name(),
d.pipe_name()
);
assert_ne!(
n.pipe_name(),
d.pipe_name(),
"only the digest keeps two homes that sanitize alike off one pipe"
);
}
fn os_env(pairs: &[(&str, &str)]) -> impl Fn(&str) -> Option<OsString> + use<> {
let owned: Vec<(String, OsString)> = pairs
.iter()
.map(|(k, v)| ((*k).to_string(), OsString::from(*v)))
.collect();
move |key: &str| owned.iter().find(|(k, _)| k == key).map(|(_, v)| v.clone())
}
#[test]
fn home_is_read_first_on_every_platform() {
assert_eq!(
user_home(&os_env(&[
("HOME", "/home/ada"),
("USERPROFILE", r"C:\Users\Ada")
])),
Some(PathBuf::from("/home/ada"))
);
}
#[test]
fn nothing_named_resolves_nothing() {
assert_eq!(user_home(&os_env(&[])), None);
}
#[test]
fn an_empty_value_counts_as_unset() {
assert_eq!(user_home(&os_env(&[("HOME", "")])), None);
}
#[cfg(windows)]
#[test]
fn windows_falls_back_to_userprofile_then_to_the_homedrive_pair() {
assert_eq!(
user_home(&os_env(&[("USERPROFILE", r"C:\Users\Ada")])),
Some(PathBuf::from(r"C:\Users\Ada"))
);
assert_eq!(
user_home(&os_env(&[("HOMEDRIVE", "C:"), ("HOMEPATH", r"\Users\Ada")])),
Some(PathBuf::from(r"C:\Users\Ada")),
"the two halves concatenate into one path, they do not nest"
);
assert_eq!(
user_home(&os_env(&[("HOMEDRIVE", "C:")])),
None,
"half the pair names no directory"
);
}
#[cfg(not(windows))]
#[test]
fn unix_ignores_the_windows_variables() {
assert_eq!(
user_home(&os_env(&[
("USERPROFILE", r"C:\Users\Ada"),
("HOMEDRIVE", "C:"),
("HOMEPATH", r"\Users\Ada"),
])),
None
);
}
}