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    /// - `$ZLAYER_DATA_DIR` (if set and non-empty) overrides every other source.
28    /// - macOS: `~/.zlayer`
29    /// - Linux (root): `/var/lib/zlayer`
30    /// - Linux (user): `~/.zlayer`
31    /// - Windows: `%ProgramData%\ZLayer` (system) or `C:\ProgramData\ZLayer`
32    ///   fallback. HCS-backed nodes run as SYSTEM so the system-wide
33    ///   `ProgramData` location is the right default.
34    pub fn default_data_dir() -> PathBuf {
35        if let Some(env_dir) = std::env::var_os("ZLAYER_DATA_DIR") {
36            if !env_dir.is_empty() {
37                return PathBuf::from(env_dir);
38            }
39        }
40        platform_default_data_dir()
41    }
42
43    /// Detect the data directory of an existing installation.
44    ///
45    /// On Linux, if not root, checks whether `/var/lib/zlayer/daemon.json`
46    /// exists (indicating a system-level install) and returns
47    /// `/var/lib/zlayer` if so. On Windows, probes `%ProgramData%\ZLayer`
48    /// for a `daemon.json` marker in case the caller lacks the env var but
49    /// a prior system install is present. Otherwise falls back to
50    /// [`default_data_dir`].
51    pub fn detect_data_dir() -> PathBuf {
52        #[cfg(not(any(target_os = "macos", target_os = "windows")))]
53        {
54            if !is_root() {
55                let system_data = PathBuf::from("/var/lib/zlayer");
56                if system_data.join("daemon.json").exists() {
57                    return system_data;
58                }
59            }
60        }
61        #[cfg(target_os = "windows")]
62        {
63            let system_data = windows_program_data_root();
64            if system_data.join("daemon.json").exists() {
65                return system_data;
66            }
67        }
68        Self::default_data_dir()
69    }
70
71    /// Default runtime directory.
72    ///
73    /// - Linux: `/var/run/zlayer`
74    /// - macOS: `{default_data_dir}/run`
75    /// - Windows: `{default_data_dir}\run` (i.e. `%ProgramData%\ZLayer\run`)
76    pub fn default_run_dir() -> PathBuf {
77        Self::default_run_dir_for(&Self::default_data_dir())
78    }
79
80    /// Data-dir-aware default run directory.
81    ///
82    /// Returns the platform's system default (e.g. `/var/run/zlayer` on Linux)
83    /// when `data_dir` matches [`Self::default_data_dir`]; otherwise returns
84    /// `{data_dir}/run`. This preserves the FHS layout for stock installs while
85    /// letting `--data-dir /tmp/foo` get a fully isolated runtime directory.
86    pub fn default_run_dir_for(data_dir: &Path) -> PathBuf {
87        let system_default = platform_default_data_dir();
88        if data_dir == system_default.as_path() {
89            #[cfg(not(any(target_os = "macos", target_os = "windows")))]
90            {
91                return PathBuf::from("/var/run/zlayer");
92            }
93            #[cfg(any(target_os = "macos", target_os = "windows"))]
94            {
95                return system_default.join("run");
96            }
97        }
98        data_dir.join("run")
99    }
100
101    /// Default log directory.
102    ///
103    /// - Linux: `/var/log/zlayer`
104    /// - macOS: `{default_data_dir}/logs`
105    /// - Windows: `{default_data_dir}\logs` (i.e. `%ProgramData%\ZLayer\logs`)
106    pub fn default_log_dir() -> PathBuf {
107        Self::default_log_dir_for(&Self::default_data_dir())
108    }
109
110    /// Data-dir-aware default log directory.
111    ///
112    /// Returns the platform's system default (e.g. `/var/log/zlayer` on Linux)
113    /// when `data_dir` matches [`Self::default_data_dir`]; otherwise returns
114    /// `{data_dir}/logs`. This preserves the FHS layout for stock installs
115    /// while letting `--data-dir /tmp/foo` get a fully isolated log directory.
116    pub fn default_log_dir_for(data_dir: &Path) -> PathBuf {
117        let system_default = platform_default_data_dir();
118        if data_dir == system_default.as_path() {
119            #[cfg(not(any(target_os = "macos", target_os = "windows")))]
120            {
121                return PathBuf::from("/var/log/zlayer");
122            }
123            #[cfg(any(target_os = "macos", target_os = "windows"))]
124            {
125                return system_default.join("logs");
126            }
127        }
128        data_dir.join("logs")
129    }
130
131    /// Default Unix socket path.
132    ///
133    /// - Linux: `/var/run/zlayer.sock`
134    /// - macOS: `{default_data_dir}/run/zlayer.sock`
135    /// - Windows: `tcp://127.0.0.1:3669`
136    pub fn default_socket_path() -> String {
137        Self::default_socket_path_for(&Self::default_data_dir())
138    }
139
140    /// Data-dir-aware default daemon socket path.
141    ///
142    /// On Windows always returns `tcp://127.0.0.1:3669` regardless of
143    /// `data_dir` (the daemon listens on TCP loopback, not a filesystem
144    /// socket). On Unix, returns the platform's system default when
145    /// `data_dir` matches [`Self::default_data_dir`]; otherwise returns
146    /// `{data_dir}/run/zlayer.sock`. Stock installs keep their FHS-style
147    /// path while `--data-dir /tmp/foo` gets an isolated socket.
148    pub fn default_socket_path_for(data_dir: &Path) -> String {
149        #[cfg(target_os = "windows")]
150        {
151            let _ = data_dir;
152            "tcp://127.0.0.1:3669".to_string()
153        }
154        #[cfg(not(target_os = "windows"))]
155        {
156            let system_default = platform_default_data_dir();
157            if data_dir == system_default.as_path() {
158                #[cfg(target_os = "macos")]
159                {
160                    return system_default
161                        .join("run")
162                        .join("zlayer.sock")
163                        .to_string_lossy()
164                        .into_owned();
165                }
166                #[cfg(not(target_os = "macos"))]
167                {
168                    return "/var/run/zlayer.sock".to_string();
169                }
170            }
171            let natural = data_dir
172                .join("run")
173                .join("zlayer.sock")
174                .to_string_lossy()
175                .into_owned();
176            if natural.len() <= SUN_PATH_MAX {
177                natural
178            } else {
179                socket_safe_fallback(data_dir, "daemon")
180            }
181        }
182    }
183
184    /// Data-dir-aware default `zlayer-overlayd` IPC socket path.
185    ///
186    /// `zlayer-overlayd` is the standalone overlay daemon; the main daemon
187    /// drives it over this endpoint. Mirrors [`Self::default_socket_path_for`]:
188    ///
189    /// - Windows: always `\\.\pipe\zlayer-overlayd` (named pipe, not a file).
190    /// - Unix, default data dir: `/var/run/zlayer-overlayd.sock`.
191    /// - Unix, overridden data dir: `{data_dir}/run/zlayer-overlayd.sock`
192    ///   (falling back to a length-safe path if that would exceed `SUN_PATH`).
193    pub fn default_overlayd_socket_path_for(data_dir: &Path) -> String {
194        #[cfg(target_os = "windows")]
195        {
196            let _ = data_dir;
197            r"\\.\pipe\zlayer-overlayd".to_string()
198        }
199        #[cfg(not(target_os = "windows"))]
200        {
201            let system_default = platform_default_data_dir();
202            if data_dir == system_default.as_path() {
203                #[cfg(target_os = "macos")]
204                {
205                    return system_default
206                        .join("run")
207                        .join("zlayer-overlayd.sock")
208                        .to_string_lossy()
209                        .into_owned();
210                }
211                #[cfg(not(target_os = "macos"))]
212                {
213                    return "/var/run/zlayer-overlayd.sock".to_string();
214                }
215            }
216            let natural = data_dir
217                .join("run")
218                .join("zlayer-overlayd.sock")
219                .to_string_lossy()
220                .into_owned();
221            if natural.len() <= SUN_PATH_MAX {
222                natural
223            } else {
224                socket_safe_fallback(data_dir, "overlayd")
225            }
226        }
227    }
228
229    /// Default Docker-compatible API socket path.
230    ///
231    /// - Linux (root): `/var/run/zlayer/docker.sock`
232    /// - Linux (user, `XDG_RUNTIME_DIR` set): `{XDG_RUNTIME_DIR}/zlayer/docker.sock`
233    /// - Linux (user, no `XDG_RUNTIME_DIR`): `{default_data_dir}/run/docker.sock`
234    /// - macOS: `{default_data_dir}/run/docker.sock`
235    /// - Windows: `\\.\pipe\zlayer-docker`
236    pub fn default_docker_socket_path() -> String {
237        #[cfg(target_os = "windows")]
238        {
239            r"\\.\pipe\zlayer-docker".to_string()
240        }
241        #[cfg(not(target_os = "windows"))]
242        {
243            #[cfg(target_os = "macos")]
244            {
245                let path = Self::default_data_dir()
246                    .join("run")
247                    .join("docker.sock")
248                    .to_string_lossy()
249                    .into_owned();
250                if path.len() <= SUN_PATH_MAX {
251                    path
252                } else {
253                    socket_safe_fallback(&Self::default_data_dir(), "docker")
254                }
255            }
256            #[cfg(not(target_os = "macos"))]
257            {
258                if is_root() {
259                    "/var/run/zlayer/docker.sock".to_string()
260                } else if let Some(xdg) = std::env::var_os("XDG_RUNTIME_DIR") {
261                    let xdg_path = PathBuf::from(&xdg);
262                    let mut p = xdg_path.clone();
263                    p.push("zlayer");
264                    p.push("docker.sock");
265                    let path = p.to_string_lossy().into_owned();
266                    if path.len() <= SUN_PATH_MAX {
267                        path
268                    } else {
269                        socket_safe_fallback(&xdg_path, "docker")
270                    }
271                } else {
272                    let path = Self::default_data_dir()
273                        .join("run")
274                        .join("docker.sock")
275                        .to_string_lossy()
276                        .into_owned();
277                    if path.len() <= SUN_PATH_MAX {
278                        path
279                    } else {
280                        socket_safe_fallback(&Self::default_data_dir(), "docker")
281                    }
282                }
283            }
284        }
285    }
286
287    /// Preferred system directory for the `zlayer` binary.
288    ///
289    /// Tries `/usr/local/bin` first (standard FHS, writable on most systems).
290    /// Falls back to `{data_dir}/bin` (`/var/lib/zlayer/bin` on Linux as root)
291    /// which is always writable since `ZLayer` owns that directory.
292    ///
293    /// On macOS and Windows, returns `/usr/local/bin` or the data-dir `bin`
294    /// subdirectory respectively.
295    pub fn default_binary_dir() -> PathBuf {
296        // Probe /usr/local/bin writability — metadata mode bits lie on overlayfs
297        #[cfg(unix)]
298        {
299            let probe = PathBuf::from("/usr/local/bin/.zlayer_write_probe");
300            if std::fs::write(&probe, b"").is_ok() {
301                let _ = std::fs::remove_file(&probe);
302                return PathBuf::from("/usr/local/bin");
303            }
304        }
305        // Fallback: our own bin dir (always writable)
306        let dirs = Self::system_default();
307        let bin_dir = dirs.bin();
308        let _ = std::fs::create_dir_all(&bin_dir);
309        bin_dir
310    }
311
312    // -- Core subdirectories -------------------------------------------------
313
314    /// Root data directory.
315    pub fn data_dir(&self) -> &Path {
316        &self.data_dir
317    }
318
319    /// Container state directory (`{data}/containers`).
320    pub fn containers(&self) -> PathBuf {
321        self.data_dir.join("containers")
322    }
323
324    /// Unpacked image rootfs directory (`{data}/rootfs`).
325    pub fn rootfs(&self) -> PathBuf {
326        self.data_dir.join("rootfs")
327    }
328
329    /// OCI bundle directory (`{data}/bundles`).
330    pub fn bundles(&self) -> PathBuf {
331        self.data_dir.join("bundles")
332    }
333
334    /// Image/blob cache directory (`{data}/cache`).
335    pub fn cache(&self) -> PathBuf {
336        self.data_dir.join("cache")
337    }
338
339    /// Named volumes directory (`{data}/volumes`).
340    pub fn volumes(&self) -> PathBuf {
341        self.data_dir.join("volumes")
342    }
343
344    /// Project git clones directory (`{data}/projects`). Persistent state —
345    /// per-project working copies live at `{data}/projects/{project_id}`.
346    pub fn projects(&self) -> PathBuf {
347        self.data_dir.join("projects")
348    }
349
350    /// WASM module cache directory (`{data}/wasm`).
351    pub fn wasm(&self) -> PathBuf {
352        self.data_dir.join("wasm")
353    }
354
355    /// AOT-compiled WASM cache directory (`{data}/wasm/compiled`).
356    pub fn wasm_compiled(&self) -> PathBuf {
357        self.data_dir.join("wasm").join("compiled")
358    }
359
360    /// Encrypted secrets store directory (`{data}/secrets`).
361    pub fn secrets(&self) -> PathBuf {
362        self.data_dir.join("secrets")
363    }
364
365    /// TLS certificate storage directory (`{data}/certs`).
366    pub fn certs(&self) -> PathBuf {
367        self.data_dir.join("certs")
368    }
369
370    /// Raft consensus data directory (`{data}/raft`).
371    pub fn raft(&self) -> PathBuf {
372        self.data_dir.join("raft")
373    }
374
375    /// Admin password file path (`{data}/admin_password`).
376    pub fn admin_password(&self) -> PathBuf {
377        self.data_dir.join("admin_password")
378    }
379
380    /// Path to the persisted local-admin bearer token file.
381    ///
382    /// On Linux/macOS this file is informational — the daemon's UDS middleware
383    /// already injects the bearer into UDS-originated requests. On Windows the
384    /// `DaemonClient` reads this file on connect to authenticate against the
385    /// loopback TCP listener (which has no socket-path-based local-admin
386    /// bypass).
387    ///
388    /// Default: `<data_dir>/admin_bearer.token`
389    ///
390    /// On Windows this resolves under `%ProgramData%\ZLayer` so the file
391    /// inherits the parent ACL (SYSTEM + Administrators write, Users read),
392    /// which is adequate for the local-admin bearer.
393    #[must_use]
394    pub fn admin_bearer_path(&self) -> PathBuf {
395        self.data_dir.join("admin_bearer.token")
396    }
397
398    /// Daemon metadata file path (`{data}/daemon.json`).
399    pub fn daemon_json(&self) -> PathBuf {
400        self.data_dir.join("daemon.json")
401    }
402
403    /// Path to the agent's local IPAM (per-node slice allocator) state file.
404    pub fn agent_ipam_state(&self) -> PathBuf {
405        self.data_dir.join("agent_ipam.json")
406    }
407
408    /// Path to the agent's managed-network marker file
409    /// (`{data}/agent_network.json`).
410    ///
411    /// Records the host-level networks ZLayer creates (e.g. the Windows HCN
412    /// overlay network) so they can be reused across daemon restarts/updates
413    /// and torn down **only** on a full uninstall (`daemon uninstall --purge`),
414    /// not on every restart/reinstall.
415    pub fn agent_network_state(&self) -> PathBuf {
416        self.data_dir.join("agent_network.json")
417    }
418
419    /// Logs subdirectory under data_dir (`{data}/logs`).
420    /// Used on macOS where logs live under the user data dir.
421    pub fn logs(&self) -> PathBuf {
422        self.data_dir.join("logs")
423    }
424
425    // -- macOS sandbox / builder paths ---------------------------------------
426
427    /// macOS VM state directory (`{data}/vms`).
428    pub fn vms(&self) -> PathBuf {
429        self.data_dir.join("vms")
430    }
431
432    /// OCI image storage directory (`{data}/images`).
433    pub fn images(&self) -> PathBuf {
434        self.data_dir.join("images")
435    }
436
437    /// Local binary directory (`{data}/bin`).
438    pub fn bin(&self) -> PathBuf {
439        self.data_dir.join("bin")
440    }
441
442    /// Canonical install path for the `zlayer-buildd` sidecar binary:
443    /// `{data}/bin/zlayer-buildd`. Resolved by the buildah-sidecar
444    /// backend's discovery logic and written by the `zlayer install
445    /// --sidecar` installer.
446    #[must_use]
447    pub fn buildd_bin(&self) -> PathBuf {
448        self.bin().join("zlayer-buildd")
449    }
450
451    /// Directory holding sidecar mTLS material: `{data}/buildd`. Contains
452    /// `ca.pem`, `cert.pem`, and `key.pem` consumed by both ends of the
453    /// `zlayer-buildd` gRPC channel.
454    #[must_use]
455    pub fn buildd(&self) -> PathBuf {
456        self.data_dir.join("buildd")
457    }
458
459    /// Toolchain download cache directory (`{data}/toolchain-cache`).
460    pub fn toolchain_cache(&self) -> PathBuf {
461        self.data_dir.join("toolchain-cache")
462    }
463
464    /// Temporary build directory (`{data}/tmp`).
465    pub fn tmp(&self) -> PathBuf {
466        self.data_dir.join("tmp")
467    }
468
469    /// Create a uniquely-named scratch directory under `{data}/tmp`.
470    ///
471    /// Returns a [`zlayer_types::Scratch`] RAII guard — the directory is
472    /// removed when the guard is dropped. Use this instead of
473    /// `tempfile::tempdir()` so scratch data lives on the configured data
474    /// filesystem rather than `/tmp`, which is tmpfs (RAM-backed) on most
475    /// modern Linux distros and risks OOM for large scratch data
476    /// (build contexts, image tarballs, layer staging, etc.).
477    ///
478    /// # Errors
479    ///
480    /// Returns the underlying filesystem error if `{data}/tmp` can't be
481    /// created or the unique subdirectory can't be allocated.
482    pub fn scratch_dir(&self, prefix: &str) -> std::io::Result<zlayer_types::Scratch> {
483        std::fs::create_dir_all(self.tmp())?;
484        let td = tempfile::Builder::new()
485            .prefix(prefix)
486            .tempdir_in(self.tmp())?;
487        Ok(zlayer_types::Scratch::from_tempdir(td))
488    }
489
490    /// Create a uniquely-named scratch file under `{data}/tmp`.
491    ///
492    /// Returns a [`zlayer_types::ScratchFile`] RAII guard. Same rationale
493    /// as [`Self::scratch_dir`].
494    ///
495    /// # Errors
496    ///
497    /// Returns the underlying filesystem error if `{data}/tmp` can't be
498    /// created or the unique file can't be allocated.
499    pub fn scratch_file(&self, prefix: &str) -> std::io::Result<zlayer_types::ScratchFile> {
500        std::fs::create_dir_all(self.tmp())?;
501        let nf = tempfile::Builder::new()
502            .prefix(prefix)
503            .tempfile_in(self.tmp())?;
504        Ok(zlayer_types::ScratchFile::from_named(nf))
505    }
506
507    /// Data-dir-aware `WireGuard` UAPI socket directory.
508    ///
509    /// When `data_dir == Self::default_data_dir()`, returns
510    /// `/var/run/wireguard` (FHS default — also where `wg(8)` looks).
511    /// Otherwise returns `{data_dir}/run/wireguard` so an isolated
512    /// install (e.g. `--data-dir /tmp/foo`) does not collide with a
513    /// system install on the same host.
514    ///
515    /// macOS / Windows: always returns `{data_dir}/run/wireguard`
516    /// since the FHS path doesn't apply.
517    pub fn wireguard(&self) -> PathBuf {
518        #[cfg(any(target_os = "macos", target_os = "windows"))]
519        let natural = self.data_dir.join("run").join("wireguard");
520        #[cfg(not(any(target_os = "macos", target_os = "windows")))]
521        let natural = if self.data_dir == Self::default_data_dir() {
522            PathBuf::from("/var/run/wireguard")
523        } else {
524            self.data_dir.join("run").join("wireguard")
525        };
526        // 1 byte separator + IFNAMSIZ (15) + ".sock" (5) = 21
527        const WG_DIR_MAX: usize = SUN_PATH_MAX - 21;
528        if natural.to_string_lossy().len() <= WG_DIR_MAX {
529            natural
530        } else {
531            PathBuf::from(format!(
532                "/tmp/zlayer-wg-{:016x}",
533                hash_for_socket(&self.data_dir, "wg")
534            ))
535        }
536    }
537}
538
539/// Convenience: `ZLayerDirs::system_default().admin_bearer_path()`.
540#[must_use]
541pub fn default_admin_bearer_path() -> PathBuf {
542    ZLayerDirs::system_default().admin_bearer_path()
543}
544
545// -- Internal helpers --------------------------------------------------------
546
547/// Platform-default data directory, ignoring `$ZLAYER_DATA_DIR`.
548///
549/// Mirrors [`ZLayerDirs::default_data_dir`] but skips the env-var override so
550/// `default_run_dir_for` / `default_log_dir_for` / `default_socket_path_for`
551/// can compare a caller-supplied `data_dir` against the platform-hardcoded
552/// default rather than against whatever the env var currently resolves to
553/// (which would make both sides of the comparison identical when the env var
554/// is set, falsely triggering the FHS branch).
555pub(crate) fn platform_default_data_dir() -> PathBuf {
556    #[cfg(target_os = "macos")]
557    {
558        home_dir_or_fallback().join(".zlayer")
559    }
560    #[cfg(target_os = "windows")]
561    {
562        windows_program_data_root()
563    }
564    #[cfg(not(any(target_os = "macos", target_os = "windows")))]
565    {
566        if is_root() {
567            PathBuf::from("/var/lib/zlayer")
568        } else {
569            home_dir_or_fallback().join(".zlayer")
570        }
571    }
572}
573
574/// Max usable bytes in `sockaddr_un.sun_path` across our supported Unix
575/// targets. Linux's `sun_path` is `char[108]`; macOS's is `char[104]`.
576/// Pick the lower bound and reserve one byte for the trailing NUL so the
577/// check is trivial on both platforms.
578const SUN_PATH_MAX: usize = 103;
579
580/// FNV-1a hash of `data_dir`'s bytes plus a label. Dependency-free and
581/// deterministic across processes (unlike `DefaultHasher` which is
582/// keyed per-process). The daemon and any CLI client passing the same
583/// `data_dir` resolve to byte-identical paths.
584fn hash_for_socket(data_dir: &Path, label: &str) -> u64 {
585    let mut h: u64 = 0xcbf2_9ce4_8422_2325;
586    for b in data_dir.to_string_lossy().as_bytes() {
587        h ^= u64::from(*b);
588        h = h.wrapping_mul(0x0000_0100_0000_01b3);
589    }
590    for b in label.as_bytes() {
591        h ^= u64::from(*b);
592        h = h.wrapping_mul(0x0000_0100_0000_01b3);
593    }
594    h
595}
596
597/// Short, deterministic UDS path for callers whose natural
598/// `{data_dir}/run/...` path would overflow `sun_path`.
599///
600/// **Scope guardrail:** `/tmp` is acceptable here ONLY because a UDS
601/// endpoint file is just an inode (kernel metadata, no payload). Daemon
602/// state — databases, logs, blob caches, image rootfs, anything the
603/// daemon reads back later — must NEVER use `/tmp`, even as a fallback.
604/// Most modern Linux distros mount `/tmp` as tmpfs (RAM-backed) and
605/// silently landing state there courts OOM under load. Sockets are the
606/// narrow exception because the file itself stores ~256 bytes of inode
607/// metadata.
608#[cfg(not(target_os = "windows"))]
609fn socket_safe_fallback(data_dir: &Path, label: &str) -> String {
610    format!(
611        "/tmp/zlayer-{label}-{:016x}.sock",
612        hash_for_socket(data_dir, label)
613    )
614}
615
616#[cfg(not(target_os = "windows"))]
617fn home_dir_or_fallback() -> PathBuf {
618    // Falls back to the FHS system data dir rather than /tmp because /tmp is
619    // tmpfs (RAM-backed) on most modern Linux distros and silently landing
620    // daemon state there courts OOM.
621    std::env::var_os("HOME")
622        .map(PathBuf::from)
623        .unwrap_or_else(|| PathBuf::from("/var/lib/zlayer"))
624}
625
626/// Resolve the Windows system-wide ZLayer data root.
627///
628/// Uses `%ProgramData%` (typically `C:\ProgramData`) when present, falling
629/// back to the literal `C:\ProgramData\ZLayer` path when the env var is
630/// missing (as can happen under a stripped-down service account).
631#[cfg(target_os = "windows")]
632fn windows_program_data_root() -> PathBuf {
633    if let Some(program_data) = std::env::var_os("PROGRAMDATA") {
634        let mut p = PathBuf::from(program_data);
635        p.push("ZLayer");
636        p
637    } else {
638        PathBuf::from(r"C:\ProgramData\ZLayer")
639    }
640}
641
642/// Returns `true` when the current process is running with superuser /
643/// Administrator privileges.
644///
645/// - Unix: true when the effective UID is `0`.
646/// - Windows: true when the current process token is a member of the
647///   built-in Administrators group (checked via `IsUserAnAdmin`).
648/// - Other targets: always returns `false`.
649#[cfg(unix)]
650#[must_use]
651pub fn is_root() -> bool {
652    // SAFETY: `geteuid` is always safe to call and is thread-safe.
653    unsafe { libc::geteuid() == 0 }
654}
655
656/// Windows analogue of the Unix `is_root()` check.
657///
658/// Returns `true` when the current process token carries administrator
659/// privileges (elevated or running as LocalSystem). Backed by
660/// `IsUserAnAdmin` from `shell32`, which is the cheapest call that
661/// covers both interactive elevation and service-account scenarios.
662///
663/// The name `is_root()` is retained deliberately: callers across the
664/// workspace gate privileged filesystem/network setup on this
665/// function, and "admin" is the closest semantic analogue on Windows.
666///
667/// Exposed as `pub` so the Windows cluster-bootstrap CLI
668/// (`zlayer node init` / `zlayer node join` / `zlayer join`) can gate
669/// privileged subsystem setup (service-manager registration, firewall
670/// rules, Wintun adapter creation) on elevation before attempting them.
671#[cfg(windows)]
672#[must_use]
673pub fn is_root() -> bool {
674    use windows::Win32::UI::Shell::IsUserAnAdmin;
675    // SAFETY: `IsUserAnAdmin` has no preconditions and returns a BOOL.
676    unsafe { IsUserAnAdmin().as_bool() }
677}
678
679/// Fallback for non-unix, non-windows targets.
680#[cfg(not(any(unix, windows)))]
681#[must_use]
682pub fn is_root() -> bool {
683    false
684}
685
686#[cfg(test)]
687mod tests {
688    use super::*;
689
690    // Tests below mutate environment variables (`PROGRAMDATA` on Windows,
691    // `ZLAYER_DATA_DIR` on Linux/macOS) to exercise platform-default and
692    // env-override path resolution. Cargo runs tests concurrently, so
693    // readers (`system_default`, `default_admin_bearer_path`, the
694    // `default_*_for` helpers) must serialize against the mutators or
695    // they race and observe a mix of pre- and post-mutation env state.
696    static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
697
698    #[test]
699    fn subdirectories_are_relative_to_data_dir() {
700        let dirs = ZLayerDirs::new("/test/data");
701        assert_eq!(dirs.containers(), PathBuf::from("/test/data/containers"));
702        assert_eq!(dirs.rootfs(), PathBuf::from("/test/data/rootfs"));
703        assert_eq!(dirs.bundles(), PathBuf::from("/test/data/bundles"));
704        assert_eq!(dirs.cache(), PathBuf::from("/test/data/cache"));
705        assert_eq!(dirs.volumes(), PathBuf::from("/test/data/volumes"));
706        assert_eq!(dirs.wasm(), PathBuf::from("/test/data/wasm"));
707        assert_eq!(
708            dirs.wasm_compiled(),
709            PathBuf::from("/test/data/wasm/compiled")
710        );
711        assert_eq!(dirs.secrets(), PathBuf::from("/test/data/secrets"));
712        assert_eq!(dirs.certs(), PathBuf::from("/test/data/certs"));
713        assert_eq!(dirs.raft(), PathBuf::from("/test/data/raft"));
714        assert_eq!(
715            dirs.admin_password(),
716            PathBuf::from("/test/data/admin_password")
717        );
718        assert_eq!(dirs.daemon_json(), PathBuf::from("/test/data/daemon.json"));
719        assert_eq!(dirs.logs(), PathBuf::from("/test/data/logs"));
720        assert_eq!(dirs.vms(), PathBuf::from("/test/data/vms"));
721        assert_eq!(dirs.images(), PathBuf::from("/test/data/images"));
722        assert_eq!(dirs.bin(), PathBuf::from("/test/data/bin"));
723        assert_eq!(
724            dirs.buildd_bin(),
725            PathBuf::from("/test/data/bin/zlayer-buildd")
726        );
727        assert_eq!(dirs.buildd(), PathBuf::from("/test/data/buildd"));
728        assert_eq!(
729            dirs.toolchain_cache(),
730            PathBuf::from("/test/data/toolchain-cache")
731        );
732        assert_eq!(dirs.tmp(), PathBuf::from("/test/data/tmp"));
733    }
734
735    #[test]
736    fn system_default_uses_default_data_dir() {
737        let _env_guard = ENV_LOCK.lock().unwrap();
738        let dirs = ZLayerDirs::system_default();
739        assert_eq!(dirs.data_dir(), ZLayerDirs::default_data_dir().as_path());
740    }
741
742    #[test]
743    fn admin_bearer_path_is_under_data_dir() {
744        let dirs = ZLayerDirs::new(PathBuf::from("/var/lib/zlayer-test"));
745        assert_eq!(
746            dirs.admin_bearer_path(),
747            PathBuf::from("/var/lib/zlayer-test/admin_bearer.token")
748        );
749    }
750
751    #[test]
752    fn default_admin_bearer_path_matches_system_default() {
753        let _env_guard = ENV_LOCK.lock().unwrap();
754        assert_eq!(
755            default_admin_bearer_path(),
756            ZLayerDirs::system_default().admin_bearer_path()
757        );
758    }
759
760    #[cfg(target_os = "windows")]
761    #[test]
762    fn windows_default_data_dir_uses_program_data() {
763        let _env_guard = ENV_LOCK.lock().unwrap();
764        let prev = std::env::var_os("PROGRAMDATA");
765        std::env::set_var("PROGRAMDATA", r"C:\TestProgramData");
766
767        let data = ZLayerDirs::default_data_dir();
768        assert_eq!(data, PathBuf::from(r"C:\TestProgramData\ZLayer"));
769
770        // Sub-paths should live under the ProgramData root.
771        let dirs = ZLayerDirs::system_default();
772        assert_eq!(dirs.certs(), data.join("certs"));
773        assert_eq!(dirs.secrets(), data.join("secrets"));
774        assert_eq!(dirs.logs(), data.join("logs"));
775
776        // Run/log helpers should also honour the ProgramData root.
777        assert_eq!(ZLayerDirs::default_run_dir(), data.join("run"));
778        assert_eq!(ZLayerDirs::default_log_dir(), data.join("logs"));
779
780        // Socket path on Windows is a TCP loopback endpoint, not a filesystem
781        // path.
782        assert_eq!(ZLayerDirs::default_socket_path(), "tcp://127.0.0.1:3669");
783
784        match prev {
785            Some(v) => std::env::set_var("PROGRAMDATA", v),
786            None => std::env::remove_var("PROGRAMDATA"),
787        }
788    }
789
790    #[test]
791    fn default_log_dir_for_returns_system_path_when_data_dir_is_default() {
792        let _env_guard = ENV_LOCK.lock().unwrap();
793        let system_default = ZLayerDirs::default_data_dir();
794        let result = ZLayerDirs::default_log_dir_for(&system_default);
795        assert_eq!(result, ZLayerDirs::default_log_dir());
796    }
797
798    #[test]
799    fn default_log_dir_for_returns_data_subdir_when_data_dir_overridden() {
800        let _env_guard = ENV_LOCK.lock().unwrap();
801        let tmp = tempfile::tempdir().expect("create tempdir");
802        let custom = tmp.path().to_path_buf();
803        let result = ZLayerDirs::default_log_dir_for(&custom);
804        assert_eq!(result, custom.join("logs"));
805        // Sanity: must NOT be the system default path on Linux/macOS/Windows.
806        assert_ne!(result, ZLayerDirs::default_log_dir());
807    }
808
809    #[test]
810    fn default_run_dir_for_returns_system_path_when_data_dir_is_default() {
811        let _env_guard = ENV_LOCK.lock().unwrap();
812        let system_default = ZLayerDirs::default_data_dir();
813        let result = ZLayerDirs::default_run_dir_for(&system_default);
814        assert_eq!(result, ZLayerDirs::default_run_dir());
815    }
816
817    #[test]
818    fn default_run_dir_for_returns_data_subdir_when_data_dir_overridden() {
819        let _env_guard = ENV_LOCK.lock().unwrap();
820        let tmp = tempfile::tempdir().expect("create tempdir");
821        let custom = tmp.path().to_path_buf();
822        let result = ZLayerDirs::default_run_dir_for(&custom);
823        assert_eq!(result, custom.join("run"));
824        assert_ne!(result, ZLayerDirs::default_run_dir());
825    }
826
827    #[test]
828    fn default_socket_path_for_returns_system_path_when_data_dir_is_default() {
829        let _env_guard = ENV_LOCK.lock().unwrap();
830        let system_default = ZLayerDirs::default_data_dir();
831        let result = ZLayerDirs::default_socket_path_for(&system_default);
832        assert_eq!(result, ZLayerDirs::default_socket_path());
833    }
834
835    #[cfg(not(target_os = "windows"))]
836    #[test]
837    fn default_socket_path_for_returns_data_subdir_when_data_dir_overridden() {
838        let _env_guard = ENV_LOCK.lock().unwrap();
839        let tmp = tempfile::tempdir().expect("create tempdir");
840        let custom = tmp.path().to_path_buf();
841        let result = ZLayerDirs::default_socket_path_for(&custom);
842        let expected = custom
843            .join("run")
844            .join("zlayer.sock")
845            .to_string_lossy()
846            .into_owned();
847        assert_eq!(result, expected);
848        assert_ne!(result, ZLayerDirs::default_socket_path());
849    }
850
851    #[cfg(target_os = "windows")]
852    #[test]
853    fn default_socket_path_for_always_tcp_on_windows() {
854        let _env_guard = ENV_LOCK.lock().unwrap();
855        let tmp = tempfile::tempdir().expect("create tempdir");
856        let custom = tmp.path().to_path_buf();
857        // On Windows the daemon listens on TCP loopback regardless of data_dir.
858        assert_eq!(
859            ZLayerDirs::default_socket_path_for(&custom),
860            "tcp://127.0.0.1:3669"
861        );
862    }
863
864    #[cfg(not(target_os = "windows"))]
865    #[test]
866    fn default_socket_path_uses_data_subdir_when_env_var_overrides() {
867        let _env_guard = ENV_LOCK.lock().unwrap();
868        let prev = std::env::var_os("ZLAYER_DATA_DIR");
869        let tmp = tempfile::tempdir().expect("create tempdir");
870        let custom = tmp.path().to_path_buf();
871        std::env::set_var("ZLAYER_DATA_DIR", &custom);
872
873        let result = ZLayerDirs::default_socket_path();
874        let expected = custom
875            .join("run")
876            .join("zlayer.sock")
877            .to_string_lossy()
878            .into_owned();
879        assert_eq!(result, expected);
880
881        match prev {
882            Some(v) => std::env::set_var("ZLAYER_DATA_DIR", v),
883            None => std::env::remove_var("ZLAYER_DATA_DIR"),
884        }
885    }
886
887    #[cfg(target_os = "windows")]
888    #[test]
889    fn default_socket_path_uses_data_subdir_when_env_var_overrides() {
890        let _env_guard = ENV_LOCK.lock().unwrap();
891        // On Windows the socket is always TCP loopback regardless of
892        // `ZLAYER_DATA_DIR`.
893        let prev = std::env::var_os("ZLAYER_DATA_DIR");
894        let tmp = tempfile::tempdir().expect("create tempdir");
895        let custom = tmp.path().to_path_buf();
896        std::env::set_var("ZLAYER_DATA_DIR", &custom);
897
898        assert_eq!(ZLayerDirs::default_socket_path(), "tcp://127.0.0.1:3669");
899
900        match prev {
901            Some(v) => std::env::set_var("ZLAYER_DATA_DIR", v),
902            None => std::env::remove_var("ZLAYER_DATA_DIR"),
903        }
904    }
905
906    #[cfg(target_os = "windows")]
907    #[test]
908    fn windows_default_data_dir_fallback_when_env_missing() {
909        let _env_guard = ENV_LOCK.lock().unwrap();
910        let prev = std::env::var_os("PROGRAMDATA");
911        std::env::remove_var("PROGRAMDATA");
912
913        let data = ZLayerDirs::default_data_dir();
914        assert_eq!(data, PathBuf::from(r"C:\ProgramData\ZLayer"));
915
916        if let Some(v) = prev {
917            std::env::set_var("PROGRAMDATA", v);
918        }
919    }
920
921    #[test]
922    fn default_docker_socket_path_not_empty() {
923        let result = ZLayerDirs::default_docker_socket_path();
924        assert!(!result.is_empty());
925    }
926
927    #[cfg(target_os = "windows")]
928    #[test]
929    fn default_docker_socket_path_platform_shape() {
930        let result = ZLayerDirs::default_docker_socket_path();
931        assert!(result.starts_with(r"\\.\pipe"));
932    }
933
934    #[cfg(target_os = "macos")]
935    #[test]
936    fn default_docker_socket_path_platform_shape() {
937        let result = ZLayerDirs::default_docker_socket_path();
938        assert!(result.ends_with("/docker.sock"));
939    }
940
941    #[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
942    #[test]
943    fn default_docker_socket_path_platform_shape() {
944        let result = ZLayerDirs::default_docker_socket_path();
945        assert!(result.ends_with("/docker.sock"));
946    }
947
948    #[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
949    #[test]
950    fn wireguard_returns_fhs_path_on_default_data_dir() {
951        let dirs = ZLayerDirs::system_default();
952        assert_eq!(dirs.wireguard(), PathBuf::from("/var/run/wireguard"));
953    }
954
955    #[test]
956    fn wireguard_returns_data_subdir_when_overridden() {
957        let tmp = tempfile::tempdir().expect("create tempdir");
958        let custom = tmp.path().to_path_buf();
959        let dirs = ZLayerDirs::new(&custom);
960        let result = dirs.wireguard();
961        assert_eq!(result, custom.join("run").join("wireguard"));
962        // Sanity: must NOT be the FHS path on Linux when the data dir is custom.
963        #[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
964        assert_ne!(result, PathBuf::from("/var/run/wireguard"));
965    }
966
967    #[cfg(target_os = "macos")]
968    #[test]
969    fn wireguard_always_returns_data_subdir_on_macos() {
970        // On macOS there is no FHS convention; even the system-default data
971        // dir gets `{data_dir}/run/wireguard`.
972        let dirs = ZLayerDirs::system_default();
973        let expected = ZLayerDirs::default_data_dir().join("run").join("wireguard");
974        assert_eq!(dirs.wireguard(), expected);
975    }
976
977    #[test]
978    fn scratch_dir_under_data_tmp() {
979        let parent = tempfile::tempdir().expect("parent");
980        let dirs = ZLayerDirs::new(parent.path());
981        let s = dirs.scratch_dir("zlayer-test-").expect("scratch_dir");
982        assert!(s.path().starts_with(dirs.tmp()));
983        assert!(s.path().is_dir());
984        let kept = s.path().to_path_buf();
985        drop(s);
986        assert!(!kept.exists());
987    }
988
989    #[test]
990    fn scratch_file_under_data_tmp() {
991        let parent = tempfile::tempdir().expect("parent");
992        let dirs = ZLayerDirs::new(parent.path());
993        let f = dirs.scratch_file("zlayer-test-").expect("scratch_file");
994        assert!(f.path().starts_with(dirs.tmp()));
995        assert!(f.path().is_file());
996        let kept = f.path().to_path_buf();
997        drop(f);
998        assert!(!kept.exists());
999    }
1000
1001    #[cfg(not(target_os = "windows"))]
1002    #[test]
1003    fn default_socket_path_for_falls_back_when_path_too_long() {
1004        let deep = PathBuf::from(
1005            "/var/lib/forgejo-runner/workdir/9dbc274201705d7d/hostexecutor/target/zlayer-e2e/cluster_3node/node1/data",
1006        );
1007        let result = ZLayerDirs::default_socket_path_for(&deep);
1008        assert!(
1009            result.len() <= SUN_PATH_MAX,
1010            "fallback path overflows sun_path: len={} path={}",
1011            result.len(),
1012            result,
1013        );
1014        // Determinism.
1015        assert_eq!(result, ZLayerDirs::default_socket_path_for(&deep));
1016        // Distinct from another data_dir.
1017        let other = deep.parent().unwrap().to_path_buf();
1018        assert_ne!(result, ZLayerDirs::default_socket_path_for(&other));
1019    }
1020
1021    #[cfg(not(target_os = "windows"))]
1022    #[test]
1023    fn default_socket_path_for_keeps_natural_path_when_short() {
1024        let tmp = tempfile::tempdir().expect("create tempdir");
1025        let short = tmp.path().to_path_buf();
1026        assert!(short.to_string_lossy().len() < 80);
1027        let result = ZLayerDirs::default_socket_path_for(&short);
1028        assert!(result.ends_with("/run/zlayer.sock"));
1029        assert!(result.starts_with(&*short.to_string_lossy()));
1030    }
1031
1032    #[cfg(not(any(target_os = "macos", target_os = "windows")))]
1033    #[test]
1034    fn wireguard_dir_falls_back_when_path_too_long() {
1035        let deep = PathBuf::from(
1036            "/var/lib/forgejo-runner/workdir/9dbc274201705d7d/hostexecutor/target/zlayer-e2e/cluster_3node/node1/data",
1037        );
1038        let dirs = ZLayerDirs::new(&deep);
1039        let wg = dirs.wireguard();
1040        // 21 = "/" + IFNAMSIZ(15) + ".sock"(5)
1041        assert!(
1042            wg.to_string_lossy().len() + 21 <= SUN_PATH_MAX,
1043            "wireguard dir + ifname overflows: dir={}",
1044            wg.display(),
1045        );
1046    }
1047}