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)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ShepPaths {
pub home: PathBuf,
pub daemon_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,
}
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 = env("SHEP_HOME")
.map(PathBuf::from)
.unwrap_or_else(|| home_dir.join(".shep"));
let run = home.join("run");
#[cfg_attr(not(windows), allow(unused_mut))]
let mut paths = Self {
daemon_config: home.join("shep.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"),
run,
home,
};
#[cfg(windows)]
{
paths.socket = PathBuf::from(paths.pipe_name());
}
paths
}
}
#[cfg(test)]
mod tests {
#[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.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"));
}
#[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 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"
);
}
}