Skip to main content

zlayer_paths/
lib.rs

1use std::path::{Path, PathBuf};
2
3/// Centralized filesystem path resolution for ZLayer.
4///
5/// All ZLayer crates should use this instead of hardcoding paths.
6pub struct ZLayerDirs {
7    data_dir: PathBuf,
8}
9
10impl ZLayerDirs {
11    /// Create from an explicit data directory.
12    pub fn new(data_dir: impl Into<PathBuf>) -> Self {
13        Self {
14            data_dir: data_dir.into(),
15        }
16    }
17
18    /// Create using the platform default data directory.
19    pub fn system_default() -> Self {
20        Self::new(Self::default_data_dir())
21    }
22
23    // -- Platform defaults (associated functions) ----------------------------
24
25    /// Platform-aware default data directory.
26    ///
27    /// - macOS: `~/.zlayer`
28    /// - Linux (root): `/var/lib/zlayer`
29    /// - Linux (user): `~/.zlayer`
30    /// - Windows: `%ProgramData%\ZLayer` (system) or `C:\ProgramData\ZLayer`
31    ///   fallback. HCS-backed nodes run as SYSTEM so the system-wide
32    ///   `ProgramData` location is the right default.
33    pub fn default_data_dir() -> PathBuf {
34        #[cfg(target_os = "macos")]
35        {
36            home_dir_or_tmp().join(".zlayer")
37        }
38        #[cfg(target_os = "windows")]
39        {
40            windows_program_data_root()
41        }
42        #[cfg(not(any(target_os = "macos", target_os = "windows")))]
43        {
44            if is_root() {
45                PathBuf::from("/var/lib/zlayer")
46            } else {
47                home_dir_or_tmp().join(".zlayer")
48            }
49        }
50    }
51
52    /// Detect the data directory of an existing installation.
53    ///
54    /// On Linux, if not root, checks whether `/var/lib/zlayer/daemon.json`
55    /// exists (indicating a system-level install) and returns
56    /// `/var/lib/zlayer` if so. On Windows, probes `%ProgramData%\ZLayer`
57    /// for a `daemon.json` marker in case the caller lacks the env var but
58    /// a prior system install is present. Otherwise falls back to
59    /// [`default_data_dir`].
60    pub fn detect_data_dir() -> PathBuf {
61        #[cfg(not(any(target_os = "macos", target_os = "windows")))]
62        {
63            if !is_root() {
64                let system_data = PathBuf::from("/var/lib/zlayer");
65                if system_data.join("daemon.json").exists() {
66                    return system_data;
67                }
68            }
69        }
70        #[cfg(target_os = "windows")]
71        {
72            let system_data = windows_program_data_root();
73            if system_data.join("daemon.json").exists() {
74                return system_data;
75            }
76        }
77        Self::default_data_dir()
78    }
79
80    /// Default runtime directory.
81    ///
82    /// - Linux: `/var/run/zlayer`
83    /// - macOS: `{default_data_dir}/run`
84    /// - Windows: `{default_data_dir}\run` (i.e. `%ProgramData%\ZLayer\run`)
85    pub fn default_run_dir() -> PathBuf {
86        Self::default_run_dir_for(&Self::default_data_dir())
87    }
88
89    /// Data-dir-aware default run directory.
90    ///
91    /// Returns the platform's system default (e.g. `/var/run/zlayer` on Linux)
92    /// when `data_dir` matches [`Self::default_data_dir`]; otherwise returns
93    /// `{data_dir}/run`. This preserves the FHS layout for stock installs while
94    /// letting `--data-dir /tmp/foo` get a fully isolated runtime directory.
95    pub fn default_run_dir_for(data_dir: &Path) -> PathBuf {
96        let system_default = Self::default_data_dir();
97        if data_dir == system_default.as_path() {
98            #[cfg(not(any(target_os = "macos", target_os = "windows")))]
99            {
100                return PathBuf::from("/var/run/zlayer");
101            }
102            #[cfg(any(target_os = "macos", target_os = "windows"))]
103            {
104                return system_default.join("run");
105            }
106        }
107        data_dir.join("run")
108    }
109
110    /// Default log directory.
111    ///
112    /// - Linux: `/var/log/zlayer`
113    /// - macOS: `{default_data_dir}/logs`
114    /// - Windows: `{default_data_dir}\logs` (i.e. `%ProgramData%\ZLayer\logs`)
115    pub fn default_log_dir() -> PathBuf {
116        Self::default_log_dir_for(&Self::default_data_dir())
117    }
118
119    /// Data-dir-aware default log directory.
120    ///
121    /// Returns the platform's system default (e.g. `/var/log/zlayer` on Linux)
122    /// when `data_dir` matches [`Self::default_data_dir`]; otherwise returns
123    /// `{data_dir}/logs`. This preserves the FHS layout for stock installs
124    /// while letting `--data-dir /tmp/foo` get a fully isolated log directory.
125    pub fn default_log_dir_for(data_dir: &Path) -> PathBuf {
126        let system_default = Self::default_data_dir();
127        if data_dir == system_default.as_path() {
128            #[cfg(not(any(target_os = "macos", target_os = "windows")))]
129            {
130                return PathBuf::from("/var/log/zlayer");
131            }
132            #[cfg(any(target_os = "macos", target_os = "windows"))]
133            {
134                return system_default.join("logs");
135            }
136        }
137        data_dir.join("logs")
138    }
139
140    /// Default Unix socket path.
141    ///
142    /// - Linux: `/var/run/zlayer.sock`
143    /// - macOS: `{default_data_dir}/run/zlayer.sock`
144    /// - Windows: `tcp://127.0.0.1:3669`
145    pub fn default_socket_path() -> String {
146        Self::default_socket_path_for(&Self::default_data_dir())
147    }
148
149    /// Data-dir-aware default daemon socket path.
150    ///
151    /// On Windows always returns `tcp://127.0.0.1:3669` regardless of
152    /// `data_dir` (the daemon listens on TCP loopback, not a filesystem
153    /// socket). On Unix, returns the platform's system default when
154    /// `data_dir` matches [`Self::default_data_dir`]; otherwise returns
155    /// `{data_dir}/run/zlayer.sock`. Stock installs keep their FHS-style
156    /// path while `--data-dir /tmp/foo` gets an isolated socket.
157    pub fn default_socket_path_for(data_dir: &Path) -> String {
158        #[cfg(target_os = "windows")]
159        {
160            let _ = data_dir;
161            "tcp://127.0.0.1:3669".to_string()
162        }
163        #[cfg(not(target_os = "windows"))]
164        {
165            let system_default = Self::default_data_dir();
166            if data_dir == system_default.as_path() {
167                #[cfg(target_os = "macos")]
168                {
169                    return system_default
170                        .join("run")
171                        .join("zlayer.sock")
172                        .to_string_lossy()
173                        .into_owned();
174                }
175                #[cfg(not(target_os = "macos"))]
176                {
177                    return "/var/run/zlayer.sock".to_string();
178                }
179            }
180            data_dir
181                .join("run")
182                .join("zlayer.sock")
183                .to_string_lossy()
184                .into_owned()
185        }
186    }
187
188    /// Default Docker-compatible API socket path.
189    ///
190    /// - Linux (root): `/var/run/zlayer/docker.sock`
191    /// - Linux (user, `XDG_RUNTIME_DIR` set): `{XDG_RUNTIME_DIR}/zlayer/docker.sock`
192    /// - Linux (user, no `XDG_RUNTIME_DIR`): `{default_data_dir}/run/docker.sock`
193    /// - macOS: `{default_data_dir}/run/docker.sock`
194    /// - Windows: `\\.\pipe\zlayer-docker`
195    pub fn default_docker_socket_path() -> String {
196        #[cfg(target_os = "windows")]
197        {
198            r"\\.\pipe\zlayer-docker".to_string()
199        }
200        #[cfg(not(target_os = "windows"))]
201        {
202            #[cfg(target_os = "macos")]
203            {
204                Self::default_data_dir()
205                    .join("run")
206                    .join("docker.sock")
207                    .to_string_lossy()
208                    .into_owned()
209            }
210            #[cfg(not(target_os = "macos"))]
211            {
212                if is_root() {
213                    "/var/run/zlayer/docker.sock".to_string()
214                } else if let Some(xdg) = std::env::var_os("XDG_RUNTIME_DIR") {
215                    let mut p = PathBuf::from(xdg);
216                    p.push("zlayer");
217                    p.push("docker.sock");
218                    p.to_string_lossy().into_owned()
219                } else {
220                    Self::default_data_dir()
221                        .join("run")
222                        .join("docker.sock")
223                        .to_string_lossy()
224                        .into_owned()
225                }
226            }
227        }
228    }
229
230    /// Preferred system directory for the `zlayer` binary.
231    ///
232    /// Tries `/usr/local/bin` first (standard FHS, writable on most systems).
233    /// Falls back to `{data_dir}/bin` (`/var/lib/zlayer/bin` on Linux as root)
234    /// which is always writable since `ZLayer` owns that directory.
235    ///
236    /// On macOS and Windows, returns `/usr/local/bin` or the data-dir `bin`
237    /// subdirectory respectively.
238    pub fn default_binary_dir() -> PathBuf {
239        // Probe /usr/local/bin writability — metadata mode bits lie on overlayfs
240        #[cfg(unix)]
241        {
242            let probe = PathBuf::from("/usr/local/bin/.zlayer_write_probe");
243            if std::fs::write(&probe, b"").is_ok() {
244                let _ = std::fs::remove_file(&probe);
245                return PathBuf::from("/usr/local/bin");
246            }
247        }
248        // Fallback: our own bin dir (always writable)
249        let dirs = Self::system_default();
250        let bin_dir = dirs.bin();
251        let _ = std::fs::create_dir_all(&bin_dir);
252        bin_dir
253    }
254
255    // -- Core subdirectories -------------------------------------------------
256
257    /// Root data directory.
258    pub fn data_dir(&self) -> &Path {
259        &self.data_dir
260    }
261
262    /// Container state directory (`{data}/containers`).
263    pub fn containers(&self) -> PathBuf {
264        self.data_dir.join("containers")
265    }
266
267    /// Unpacked image rootfs directory (`{data}/rootfs`).
268    pub fn rootfs(&self) -> PathBuf {
269        self.data_dir.join("rootfs")
270    }
271
272    /// OCI bundle directory (`{data}/bundles`).
273    pub fn bundles(&self) -> PathBuf {
274        self.data_dir.join("bundles")
275    }
276
277    /// Image/blob cache directory (`{data}/cache`).
278    pub fn cache(&self) -> PathBuf {
279        self.data_dir.join("cache")
280    }
281
282    /// Named volumes directory (`{data}/volumes`).
283    pub fn volumes(&self) -> PathBuf {
284        self.data_dir.join("volumes")
285    }
286
287    /// WASM module cache directory (`{data}/wasm`).
288    pub fn wasm(&self) -> PathBuf {
289        self.data_dir.join("wasm")
290    }
291
292    /// AOT-compiled WASM cache directory (`{data}/wasm/compiled`).
293    pub fn wasm_compiled(&self) -> PathBuf {
294        self.data_dir.join("wasm").join("compiled")
295    }
296
297    /// Encrypted secrets store directory (`{data}/secrets`).
298    pub fn secrets(&self) -> PathBuf {
299        self.data_dir.join("secrets")
300    }
301
302    /// TLS certificate storage directory (`{data}/certs`).
303    pub fn certs(&self) -> PathBuf {
304        self.data_dir.join("certs")
305    }
306
307    /// Raft consensus data directory (`{data}/raft`).
308    pub fn raft(&self) -> PathBuf {
309        self.data_dir.join("raft")
310    }
311
312    /// Admin password file path (`{data}/admin_password`).
313    pub fn admin_password(&self) -> PathBuf {
314        self.data_dir.join("admin_password")
315    }
316
317    /// Path to the persisted local-admin bearer token file.
318    ///
319    /// On Linux/macOS this file is informational — the daemon's UDS middleware
320    /// already injects the bearer into UDS-originated requests. On Windows the
321    /// `DaemonClient` reads this file on connect to authenticate against the
322    /// loopback TCP listener (which has no socket-path-based local-admin
323    /// bypass).
324    ///
325    /// Default: `<data_dir>/admin_bearer.token`
326    ///
327    /// On Windows this resolves under `%ProgramData%\ZLayer` so the file
328    /// inherits the parent ACL (SYSTEM + Administrators write, Users read),
329    /// which is adequate for the local-admin bearer.
330    #[must_use]
331    pub fn admin_bearer_path(&self) -> PathBuf {
332        self.data_dir.join("admin_bearer.token")
333    }
334
335    /// Daemon metadata file path (`{data}/daemon.json`).
336    pub fn daemon_json(&self) -> PathBuf {
337        self.data_dir.join("daemon.json")
338    }
339
340    /// Path to the agent's local IPAM (per-node slice allocator) state file.
341    pub fn agent_ipam_state(&self) -> PathBuf {
342        self.data_dir.join("agent_ipam.json")
343    }
344
345    /// Logs subdirectory under data_dir (`{data}/logs`).
346    /// Used on macOS where logs live under the user data dir.
347    pub fn logs(&self) -> PathBuf {
348        self.data_dir.join("logs")
349    }
350
351    // -- macOS sandbox / builder paths ---------------------------------------
352
353    /// macOS VM state directory (`{data}/vms`).
354    pub fn vms(&self) -> PathBuf {
355        self.data_dir.join("vms")
356    }
357
358    /// OCI image storage directory (`{data}/images`).
359    pub fn images(&self) -> PathBuf {
360        self.data_dir.join("images")
361    }
362
363    /// Local binary directory (`{data}/bin`).
364    pub fn bin(&self) -> PathBuf {
365        self.data_dir.join("bin")
366    }
367
368    /// Toolchain download cache directory (`{data}/toolchain-cache`).
369    pub fn toolchain_cache(&self) -> PathBuf {
370        self.data_dir.join("toolchain-cache")
371    }
372
373    /// Temporary build directory (`{data}/tmp`).
374    pub fn tmp(&self) -> PathBuf {
375        self.data_dir.join("tmp")
376    }
377
378    /// Data-dir-aware WireGuard UAPI socket directory.
379    ///
380    /// When `data_dir == Self::default_data_dir()`, returns
381    /// `/var/run/wireguard` (FHS default — also where `wg(8)` looks).
382    /// Otherwise returns `{data_dir}/run/wireguard` so an isolated
383    /// install (e.g. `--data-dir /tmp/foo`) does not collide with a
384    /// system install on the same host.
385    ///
386    /// macOS / Windows: always returns `{data_dir}/run/wireguard`
387    /// since the FHS path doesn't apply.
388    pub fn wireguard(&self) -> PathBuf {
389        #[cfg(any(target_os = "macos", target_os = "windows"))]
390        {
391            // No FHS convention; always derive.
392            self.data_dir.join("run").join("wireguard")
393        }
394        #[cfg(not(any(target_os = "macos", target_os = "windows")))]
395        {
396            if self.data_dir == Self::default_data_dir() {
397                PathBuf::from("/var/run/wireguard")
398            } else {
399                self.data_dir.join("run").join("wireguard")
400            }
401        }
402    }
403}
404
405/// Convenience: `ZLayerDirs::system_default().admin_bearer_path()`.
406#[must_use]
407pub fn default_admin_bearer_path() -> PathBuf {
408    ZLayerDirs::system_default().admin_bearer_path()
409}
410
411// -- Internal helpers --------------------------------------------------------
412
413#[cfg(not(target_os = "windows"))]
414fn home_dir_or_tmp() -> PathBuf {
415    std::env::var_os("HOME")
416        .map(PathBuf::from)
417        .unwrap_or_else(|| PathBuf::from("/tmp"))
418}
419
420/// Resolve the Windows system-wide ZLayer data root.
421///
422/// Uses `%ProgramData%` (typically `C:\ProgramData`) when present, falling
423/// back to the literal `C:\ProgramData\ZLayer` path when the env var is
424/// missing (as can happen under a stripped-down service account).
425#[cfg(target_os = "windows")]
426fn windows_program_data_root() -> PathBuf {
427    if let Some(program_data) = std::env::var_os("PROGRAMDATA") {
428        let mut p = PathBuf::from(program_data);
429        p.push("ZLayer");
430        p
431    } else {
432        PathBuf::from(r"C:\ProgramData\ZLayer")
433    }
434}
435
436/// Returns `true` when the current process is running with superuser /
437/// Administrator privileges.
438///
439/// - Unix: true when the effective UID is `0`.
440/// - Windows: true when the current process token is a member of the
441///   built-in Administrators group (checked via `IsUserAnAdmin`).
442/// - Other targets: always returns `false`.
443#[cfg(unix)]
444#[must_use]
445pub fn is_root() -> bool {
446    // SAFETY: `geteuid` is always safe to call and is thread-safe.
447    unsafe { libc::geteuid() == 0 }
448}
449
450/// Returns `true` when the current process is running with superuser /
451/// Administrator privileges.
452#[cfg(windows)]
453#[must_use]
454pub fn is_root() -> bool {
455    use windows::Win32::UI::Shell::IsUserAnAdmin;
456    // SAFETY: `IsUserAnAdmin` has no preconditions and returns a BOOL.
457    unsafe { IsUserAnAdmin().as_bool() }
458}
459
460/// Fallback for non-unix, non-windows targets.
461#[cfg(not(any(unix, windows)))]
462#[must_use]
463pub fn is_root() -> bool {
464    false
465}
466
467#[cfg(test)]
468mod tests {
469    use super::*;
470
471    // Windows tests below mutate the `PROGRAMDATA` env var to exercise
472    // platform-default path resolution. Cargo runs tests concurrently,
473    // so readers (`system_default`, `default_admin_bearer_path`) must
474    // serialize against the mutators or they race and observe a mix of
475    // pre- and post-mutation env state.
476    #[cfg(target_os = "windows")]
477    static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
478
479    #[test]
480    fn subdirectories_are_relative_to_data_dir() {
481        let dirs = ZLayerDirs::new("/test/data");
482        assert_eq!(dirs.containers(), PathBuf::from("/test/data/containers"));
483        assert_eq!(dirs.rootfs(), PathBuf::from("/test/data/rootfs"));
484        assert_eq!(dirs.bundles(), PathBuf::from("/test/data/bundles"));
485        assert_eq!(dirs.cache(), PathBuf::from("/test/data/cache"));
486        assert_eq!(dirs.volumes(), PathBuf::from("/test/data/volumes"));
487        assert_eq!(dirs.wasm(), PathBuf::from("/test/data/wasm"));
488        assert_eq!(
489            dirs.wasm_compiled(),
490            PathBuf::from("/test/data/wasm/compiled")
491        );
492        assert_eq!(dirs.secrets(), PathBuf::from("/test/data/secrets"));
493        assert_eq!(dirs.certs(), PathBuf::from("/test/data/certs"));
494        assert_eq!(dirs.raft(), PathBuf::from("/test/data/raft"));
495        assert_eq!(
496            dirs.admin_password(),
497            PathBuf::from("/test/data/admin_password")
498        );
499        assert_eq!(dirs.daemon_json(), PathBuf::from("/test/data/daemon.json"));
500        assert_eq!(dirs.logs(), PathBuf::from("/test/data/logs"));
501        assert_eq!(dirs.vms(), PathBuf::from("/test/data/vms"));
502        assert_eq!(dirs.images(), PathBuf::from("/test/data/images"));
503        assert_eq!(dirs.bin(), PathBuf::from("/test/data/bin"));
504        assert_eq!(
505            dirs.toolchain_cache(),
506            PathBuf::from("/test/data/toolchain-cache")
507        );
508        assert_eq!(dirs.tmp(), PathBuf::from("/test/data/tmp"));
509    }
510
511    #[test]
512    fn system_default_uses_default_data_dir() {
513        #[cfg(target_os = "windows")]
514        let _env_guard = ENV_LOCK.lock().unwrap();
515        let dirs = ZLayerDirs::system_default();
516        assert_eq!(dirs.data_dir(), ZLayerDirs::default_data_dir().as_path());
517    }
518
519    #[test]
520    fn admin_bearer_path_is_under_data_dir() {
521        let dirs = ZLayerDirs::new(PathBuf::from("/tmp/zlayer-test"));
522        assert_eq!(
523            dirs.admin_bearer_path(),
524            PathBuf::from("/tmp/zlayer-test/admin_bearer.token")
525        );
526    }
527
528    #[test]
529    fn default_admin_bearer_path_matches_system_default() {
530        #[cfg(target_os = "windows")]
531        let _env_guard = ENV_LOCK.lock().unwrap();
532        assert_eq!(
533            default_admin_bearer_path(),
534            ZLayerDirs::system_default().admin_bearer_path()
535        );
536    }
537
538    #[cfg(target_os = "windows")]
539    #[test]
540    fn windows_default_data_dir_uses_program_data() {
541        let _env_guard = ENV_LOCK.lock().unwrap();
542        let prev = std::env::var_os("PROGRAMDATA");
543        std::env::set_var("PROGRAMDATA", r"C:\TestProgramData");
544
545        let data = ZLayerDirs::default_data_dir();
546        assert_eq!(data, PathBuf::from(r"C:\TestProgramData\ZLayer"));
547
548        // Sub-paths should live under the ProgramData root.
549        let dirs = ZLayerDirs::system_default();
550        assert_eq!(dirs.certs(), data.join("certs"));
551        assert_eq!(dirs.secrets(), data.join("secrets"));
552        assert_eq!(dirs.logs(), data.join("logs"));
553
554        // Run/log helpers should also honour the ProgramData root.
555        assert_eq!(ZLayerDirs::default_run_dir(), data.join("run"));
556        assert_eq!(ZLayerDirs::default_log_dir(), data.join("logs"));
557
558        // Socket path on Windows is a TCP loopback endpoint, not a filesystem
559        // path.
560        assert_eq!(ZLayerDirs::default_socket_path(), "tcp://127.0.0.1:3669");
561
562        match prev {
563            Some(v) => std::env::set_var("PROGRAMDATA", v),
564            None => std::env::remove_var("PROGRAMDATA"),
565        }
566    }
567
568    #[test]
569    fn default_log_dir_for_returns_system_path_when_data_dir_is_default() {
570        #[cfg(target_os = "windows")]
571        let _env_guard = ENV_LOCK.lock().unwrap();
572        let system_default = ZLayerDirs::default_data_dir();
573        let result = ZLayerDirs::default_log_dir_for(&system_default);
574        assert_eq!(result, ZLayerDirs::default_log_dir());
575    }
576
577    #[test]
578    fn default_log_dir_for_returns_data_subdir_when_data_dir_overridden() {
579        let tmp = tempfile::tempdir().expect("create tempdir");
580        let custom = tmp.path().to_path_buf();
581        let result = ZLayerDirs::default_log_dir_for(&custom);
582        assert_eq!(result, custom.join("logs"));
583        // Sanity: must NOT be the system default path on Linux/macOS/Windows.
584        assert_ne!(result, ZLayerDirs::default_log_dir());
585    }
586
587    #[test]
588    fn default_run_dir_for_returns_system_path_when_data_dir_is_default() {
589        #[cfg(target_os = "windows")]
590        let _env_guard = ENV_LOCK.lock().unwrap();
591        let system_default = ZLayerDirs::default_data_dir();
592        let result = ZLayerDirs::default_run_dir_for(&system_default);
593        assert_eq!(result, ZLayerDirs::default_run_dir());
594    }
595
596    #[test]
597    fn default_run_dir_for_returns_data_subdir_when_data_dir_overridden() {
598        let tmp = tempfile::tempdir().expect("create tempdir");
599        let custom = tmp.path().to_path_buf();
600        let result = ZLayerDirs::default_run_dir_for(&custom);
601        assert_eq!(result, custom.join("run"));
602        assert_ne!(result, ZLayerDirs::default_run_dir());
603    }
604
605    #[test]
606    fn default_socket_path_for_returns_system_path_when_data_dir_is_default() {
607        #[cfg(target_os = "windows")]
608        let _env_guard = ENV_LOCK.lock().unwrap();
609        let system_default = ZLayerDirs::default_data_dir();
610        let result = ZLayerDirs::default_socket_path_for(&system_default);
611        assert_eq!(result, ZLayerDirs::default_socket_path());
612    }
613
614    #[cfg(not(target_os = "windows"))]
615    #[test]
616    fn default_socket_path_for_returns_data_subdir_when_data_dir_overridden() {
617        let tmp = tempfile::tempdir().expect("create tempdir");
618        let custom = tmp.path().to_path_buf();
619        let result = ZLayerDirs::default_socket_path_for(&custom);
620        let expected = custom
621            .join("run")
622            .join("zlayer.sock")
623            .to_string_lossy()
624            .into_owned();
625        assert_eq!(result, expected);
626        assert_ne!(result, ZLayerDirs::default_socket_path());
627    }
628
629    #[cfg(target_os = "windows")]
630    #[test]
631    fn default_socket_path_for_always_tcp_on_windows() {
632        let _env_guard = ENV_LOCK.lock().unwrap();
633        let tmp = tempfile::tempdir().expect("create tempdir");
634        let custom = tmp.path().to_path_buf();
635        // On Windows the daemon listens on TCP loopback regardless of data_dir.
636        assert_eq!(
637            ZLayerDirs::default_socket_path_for(&custom),
638            "tcp://127.0.0.1:3669"
639        );
640    }
641
642    #[cfg(target_os = "windows")]
643    #[test]
644    fn windows_default_data_dir_fallback_when_env_missing() {
645        let _env_guard = ENV_LOCK.lock().unwrap();
646        let prev = std::env::var_os("PROGRAMDATA");
647        std::env::remove_var("PROGRAMDATA");
648
649        let data = ZLayerDirs::default_data_dir();
650        assert_eq!(data, PathBuf::from(r"C:\ProgramData\ZLayer"));
651
652        if let Some(v) = prev {
653            std::env::set_var("PROGRAMDATA", v);
654        }
655    }
656
657    #[test]
658    fn default_docker_socket_path_not_empty() {
659        let result = ZLayerDirs::default_docker_socket_path();
660        assert!(!result.is_empty());
661    }
662
663    #[cfg(target_os = "windows")]
664    #[test]
665    fn default_docker_socket_path_platform_shape() {
666        let result = ZLayerDirs::default_docker_socket_path();
667        assert!(result.starts_with(r"\\.\pipe"));
668    }
669
670    #[cfg(target_os = "macos")]
671    #[test]
672    fn default_docker_socket_path_platform_shape() {
673        let result = ZLayerDirs::default_docker_socket_path();
674        assert!(result.ends_with("/docker.sock"));
675    }
676
677    #[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
678    #[test]
679    fn default_docker_socket_path_platform_shape() {
680        let result = ZLayerDirs::default_docker_socket_path();
681        assert!(result.ends_with("/docker.sock"));
682    }
683
684    #[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
685    #[test]
686    fn wireguard_returns_fhs_path_on_default_data_dir() {
687        let dirs = ZLayerDirs::system_default();
688        assert_eq!(dirs.wireguard(), PathBuf::from("/var/run/wireguard"));
689    }
690
691    #[test]
692    fn wireguard_returns_data_subdir_when_overridden() {
693        let tmp = tempfile::tempdir().expect("create tempdir");
694        let custom = tmp.path().to_path_buf();
695        let dirs = ZLayerDirs::new(&custom);
696        let result = dirs.wireguard();
697        assert_eq!(result, custom.join("run").join("wireguard"));
698        // Sanity: must NOT be the FHS path on Linux when the data dir is custom.
699        #[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
700        assert_ne!(result, PathBuf::from("/var/run/wireguard"));
701    }
702
703    #[cfg(target_os = "macos")]
704    #[test]
705    fn wireguard_always_returns_data_subdir_on_macos() {
706        // On macOS there is no FHS convention; even the system-default data
707        // dir gets `{data_dir}/run/wireguard`.
708        let dirs = ZLayerDirs::system_default();
709        let expected = ZLayerDirs::default_data_dir().join("run").join("wireguard");
710        assert_eq!(dirs.wireguard(), expected);
711    }
712}