Skip to main content

shep_core/
paths.rs

1//! On-disk layout of `$SHEP_HOME`
2//!
3//! One resolver, no hidden `std::env` reads — the environment comes in as a
4//! closure so tests and the daemon share one code path.
5
6use std::path::{Path, PathBuf};
7
8/// Resolved filesystem layout for one shep home
9///
10/// All paths are derived from `$SHEP_HOME` (default `<home>/.shep`); nothing
11/// here touches the filesystem. The root itself is created by the CLI's own
12/// `ensure_home`, for the commands that need it before any daemon exists
13/// (`startup` above all), and everything under it by
14/// `shep_daemon::boot::init_dirs` on each boot.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct ShepPaths {
17    /// Root: `$SHEP_HOME`
18    pub home: PathBuf,
19    /// Daemon config: `shep.toml`
20    pub daemon_config: PathBuf,
21    /// Flock snapshot (muster roll): `flock.json`
22    pub snapshot: PathBuf,
23    /// Log directory
24    pub logs: PathBuf,
25    /// Pid-file directory
26    pub pids: PathBuf,
27    /// Runtime dir (sockets; created 0700)
28    pub run: PathBuf,
29    /// Control socket: `run/shep.sock`
30    pub socket: PathBuf,
31    /// Bark history ring: `barks.jsonl`
32    pub barks: PathBuf,
33    /// Key/value store: `kv.json`
34    pub kv: PathBuf,
35}
36
37impl ShepPaths {
38    /// Windows named-pipe identity for this home: `\\.\pipe\shep-<sanitized>`
39    ///
40    /// Derived from the home path (non-alphanumerics become `-`) so distinct
41    /// `$SHEP_HOME`s never collide on the global pipe namespace.
42    #[must_use]
43    pub fn pipe_name(&self) -> String {
44        let sanitized: String = self
45            .home
46            .to_string_lossy()
47            .chars()
48            .map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
49            .collect();
50        let trimmed = sanitized.trim_matches('-');
51        format!(r"\\.\pipe\shep-{trimmed}")
52    }
53
54    /// Resolves the layout from an environment lookup and the user's home dir
55    #[must_use]
56    pub fn resolve(env: &dyn Fn(&str) -> Option<String>, home_dir: &Path) -> Self {
57        let home = env("SHEP_HOME")
58            .map(PathBuf::from)
59            .unwrap_or_else(|| home_dir.join(".shep"));
60        let run = home.join("run");
61        Self {
62            daemon_config: home.join("shep.toml"),
63            snapshot: home.join("flock.json"),
64            logs: home.join("logs"),
65            pids: home.join("pids"),
66            socket: run.join("shep.sock"),
67            barks: home.join("barks.jsonl"),
68            kv: home.join("kv.json"),
69            run,
70            home,
71        }
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78    use std::path::Path;
79
80    fn no_env(_: &str) -> Option<String> {
81        None
82    }
83
84    #[test]
85    fn default_layout_under_home_dir() {
86        let p = ShepPaths::resolve(&no_env, Path::new("/home/rin"));
87        assert_eq!(p.home, Path::new("/home/rin/.shep"));
88        assert_eq!(p.daemon_config, Path::new("/home/rin/.shep/shep.toml"));
89        assert_eq!(p.snapshot, Path::new("/home/rin/.shep/flock.json"));
90        assert_eq!(p.logs, Path::new("/home/rin/.shep/logs"));
91        assert_eq!(p.pids, Path::new("/home/rin/.shep/pids"));
92        assert_eq!(p.run, Path::new("/home/rin/.shep/run"));
93        assert_eq!(p.socket, Path::new("/home/rin/.shep/run/shep.sock"));
94        assert_eq!(p.barks, Path::new("/home/rin/.shep/barks.jsonl"));
95        assert_eq!(p.kv, Path::new("/home/rin/.shep/kv.json"));
96    }
97
98    #[test]
99    fn shep_home_env_overrides_root() {
100        let env = |key: &str| (key == "SHEP_HOME").then(|| "/srv/shep".to_string());
101        let p = ShepPaths::resolve(&env, Path::new("/home/rin"));
102        assert_eq!(p.home, Path::new("/srv/shep"));
103        assert_eq!(p.socket, Path::new("/srv/shep/run/shep.sock"));
104    }
105
106    #[test]
107    fn pipe_name_is_per_home_and_sanitized() {
108        // Windows transport identity (spec §6): derived from SHEP_HOME so
109        // two homes never share a pipe; non-alphanumerics collapse to '-'.
110        let p = ShepPaths::resolve(&no_env, Path::new("/home/rin"));
111        assert_eq!(p.pipe_name(), r"\\.\pipe\shep-home-rin--shep");
112        let env = |key: &str| (key == "SHEP_HOME").then(|| "/srv/shep".to_string());
113        let q = ShepPaths::resolve(&env, Path::new("/home/rin"));
114        assert_eq!(q.pipe_name(), r"\\.\pipe\shep-srv-shep");
115    }
116}