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        #[cfg(not(any(target_os = "macos", target_os = "windows")))]
87        {
88            PathBuf::from("/var/run/zlayer")
89        }
90        #[cfg(any(target_os = "macos", target_os = "windows"))]
91        {
92            Self::default_data_dir().join("run")
93        }
94    }
95
96    /// Default log directory.
97    ///
98    /// - Linux: `/var/log/zlayer`
99    /// - macOS: `{default_data_dir}/logs`
100    /// - Windows: `{default_data_dir}\logs` (i.e. `%ProgramData%\ZLayer\logs`)
101    pub fn default_log_dir() -> PathBuf {
102        #[cfg(not(any(target_os = "macos", target_os = "windows")))]
103        {
104            PathBuf::from("/var/log/zlayer")
105        }
106        #[cfg(any(target_os = "macos", target_os = "windows"))]
107        {
108            Self::default_data_dir().join("logs")
109        }
110    }
111
112    /// Default Unix socket path.
113    ///
114    /// - Linux: `/var/run/zlayer.sock`
115    /// - macOS: `{default_data_dir}/run/zlayer.sock`
116    /// - Windows: `tcp://127.0.0.1:3669`
117    pub fn default_socket_path() -> String {
118        #[cfg(target_os = "windows")]
119        {
120            "tcp://127.0.0.1:3669".to_string()
121        }
122        #[cfg(not(target_os = "windows"))]
123        {
124            #[cfg(target_os = "macos")]
125            {
126                Self::default_data_dir()
127                    .join("run")
128                    .join("zlayer.sock")
129                    .to_string_lossy()
130                    .into_owned()
131            }
132            #[cfg(not(target_os = "macos"))]
133            {
134                "/var/run/zlayer.sock".to_string()
135            }
136        }
137    }
138
139    /// Default Docker-compatible API socket path.
140    ///
141    /// - Linux (root): `/var/run/zlayer/docker.sock`
142    /// - Linux (user, `XDG_RUNTIME_DIR` set): `{XDG_RUNTIME_DIR}/zlayer/docker.sock`
143    /// - Linux (user, no `XDG_RUNTIME_DIR`): `{default_data_dir}/run/docker.sock`
144    /// - macOS: `{default_data_dir}/run/docker.sock`
145    /// - Windows: `\\.\pipe\zlayer-docker`
146    pub fn default_docker_socket_path() -> String {
147        #[cfg(target_os = "windows")]
148        {
149            r"\\.\pipe\zlayer-docker".to_string()
150        }
151        #[cfg(not(target_os = "windows"))]
152        {
153            #[cfg(target_os = "macos")]
154            {
155                Self::default_data_dir()
156                    .join("run")
157                    .join("docker.sock")
158                    .to_string_lossy()
159                    .into_owned()
160            }
161            #[cfg(not(target_os = "macos"))]
162            {
163                if is_root() {
164                    "/var/run/zlayer/docker.sock".to_string()
165                } else if let Some(xdg) = std::env::var_os("XDG_RUNTIME_DIR") {
166                    let mut p = PathBuf::from(xdg);
167                    p.push("zlayer");
168                    p.push("docker.sock");
169                    p.to_string_lossy().into_owned()
170                } else {
171                    Self::default_data_dir()
172                        .join("run")
173                        .join("docker.sock")
174                        .to_string_lossy()
175                        .into_owned()
176                }
177            }
178        }
179    }
180
181    /// Preferred system directory for the `zlayer` binary.
182    ///
183    /// Tries `/usr/local/bin` first (standard FHS, writable on most systems).
184    /// Falls back to `{data_dir}/bin` (`/var/lib/zlayer/bin` on Linux as root)
185    /// which is always writable since `ZLayer` owns that directory.
186    ///
187    /// On macOS and Windows, returns `/usr/local/bin` or the data-dir `bin`
188    /// subdirectory respectively.
189    pub fn default_binary_dir() -> PathBuf {
190        // Probe /usr/local/bin writability — metadata mode bits lie on overlayfs
191        #[cfg(unix)]
192        {
193            let probe = PathBuf::from("/usr/local/bin/.zlayer_write_probe");
194            if std::fs::write(&probe, b"").is_ok() {
195                let _ = std::fs::remove_file(&probe);
196                return PathBuf::from("/usr/local/bin");
197            }
198        }
199        // Fallback: our own bin dir (always writable)
200        let dirs = Self::system_default();
201        let bin_dir = dirs.bin();
202        let _ = std::fs::create_dir_all(&bin_dir);
203        bin_dir
204    }
205
206    // -- Core subdirectories -------------------------------------------------
207
208    /// Root data directory.
209    pub fn data_dir(&self) -> &Path {
210        &self.data_dir
211    }
212
213    /// Container state directory (`{data}/containers`).
214    pub fn containers(&self) -> PathBuf {
215        self.data_dir.join("containers")
216    }
217
218    /// Unpacked image rootfs directory (`{data}/rootfs`).
219    pub fn rootfs(&self) -> PathBuf {
220        self.data_dir.join("rootfs")
221    }
222
223    /// OCI bundle directory (`{data}/bundles`).
224    pub fn bundles(&self) -> PathBuf {
225        self.data_dir.join("bundles")
226    }
227
228    /// Image/blob cache directory (`{data}/cache`).
229    pub fn cache(&self) -> PathBuf {
230        self.data_dir.join("cache")
231    }
232
233    /// Named volumes directory (`{data}/volumes`).
234    pub fn volumes(&self) -> PathBuf {
235        self.data_dir.join("volumes")
236    }
237
238    /// WASM module cache directory (`{data}/wasm`).
239    pub fn wasm(&self) -> PathBuf {
240        self.data_dir.join("wasm")
241    }
242
243    /// AOT-compiled WASM cache directory (`{data}/wasm/compiled`).
244    pub fn wasm_compiled(&self) -> PathBuf {
245        self.data_dir.join("wasm").join("compiled")
246    }
247
248    /// Encrypted secrets store directory (`{data}/secrets`).
249    pub fn secrets(&self) -> PathBuf {
250        self.data_dir.join("secrets")
251    }
252
253    /// TLS certificate storage directory (`{data}/certs`).
254    pub fn certs(&self) -> PathBuf {
255        self.data_dir.join("certs")
256    }
257
258    /// Raft consensus data directory (`{data}/raft`).
259    pub fn raft(&self) -> PathBuf {
260        self.data_dir.join("raft")
261    }
262
263    /// Admin password file path (`{data}/admin_password`).
264    pub fn admin_password(&self) -> PathBuf {
265        self.data_dir.join("admin_password")
266    }
267
268    /// Path to the persisted local-admin bearer token file.
269    ///
270    /// On Linux/macOS this file is informational — the daemon's UDS middleware
271    /// already injects the bearer into UDS-originated requests. On Windows the
272    /// `DaemonClient` reads this file on connect to authenticate against the
273    /// loopback TCP listener (which has no socket-path-based local-admin
274    /// bypass).
275    ///
276    /// Default: `<data_dir>/admin_bearer.token`
277    ///
278    /// On Windows this resolves under `%ProgramData%\ZLayer` so the file
279    /// inherits the parent ACL (SYSTEM + Administrators write, Users read),
280    /// which is adequate for the local-admin bearer.
281    #[must_use]
282    pub fn admin_bearer_path(&self) -> PathBuf {
283        self.data_dir.join("admin_bearer.token")
284    }
285
286    /// Daemon metadata file path (`{data}/daemon.json`).
287    pub fn daemon_json(&self) -> PathBuf {
288        self.data_dir.join("daemon.json")
289    }
290
291    /// Path to the agent's local IPAM (per-node slice allocator) state file.
292    pub fn agent_ipam_state(&self) -> PathBuf {
293        self.data_dir.join("agent_ipam.json")
294    }
295
296    /// Logs subdirectory under data_dir (`{data}/logs`).
297    /// Used on macOS where logs live under the user data dir.
298    pub fn logs(&self) -> PathBuf {
299        self.data_dir.join("logs")
300    }
301
302    // -- macOS sandbox / builder paths ---------------------------------------
303
304    /// macOS VM state directory (`{data}/vms`).
305    pub fn vms(&self) -> PathBuf {
306        self.data_dir.join("vms")
307    }
308
309    /// OCI image storage directory (`{data}/images`).
310    pub fn images(&self) -> PathBuf {
311        self.data_dir.join("images")
312    }
313
314    /// Local binary directory (`{data}/bin`).
315    pub fn bin(&self) -> PathBuf {
316        self.data_dir.join("bin")
317    }
318
319    /// Toolchain download cache directory (`{data}/toolchain-cache`).
320    pub fn toolchain_cache(&self) -> PathBuf {
321        self.data_dir.join("toolchain-cache")
322    }
323
324    /// Temporary build directory (`{data}/tmp`).
325    pub fn tmp(&self) -> PathBuf {
326        self.data_dir.join("tmp")
327    }
328}
329
330/// Convenience: `ZLayerDirs::system_default().admin_bearer_path()`.
331#[must_use]
332pub fn default_admin_bearer_path() -> PathBuf {
333    ZLayerDirs::system_default().admin_bearer_path()
334}
335
336// -- Internal helpers --------------------------------------------------------
337
338#[cfg(not(target_os = "windows"))]
339fn home_dir_or_tmp() -> PathBuf {
340    std::env::var_os("HOME")
341        .map(PathBuf::from)
342        .unwrap_or_else(|| PathBuf::from("/tmp"))
343}
344
345/// Resolve the Windows system-wide ZLayer data root.
346///
347/// Uses `%ProgramData%` (typically `C:\ProgramData`) when present, falling
348/// back to the literal `C:\ProgramData\ZLayer` path when the env var is
349/// missing (as can happen under a stripped-down service account).
350#[cfg(target_os = "windows")]
351fn windows_program_data_root() -> PathBuf {
352    if let Some(program_data) = std::env::var_os("PROGRAMDATA") {
353        let mut p = PathBuf::from(program_data);
354        p.push("ZLayer");
355        p
356    } else {
357        PathBuf::from(r"C:\ProgramData\ZLayer")
358    }
359}
360
361/// Returns `true` when the current process is running with superuser /
362/// Administrator privileges.
363///
364/// - Unix: true when the effective UID is `0`.
365/// - Windows: true when the current process token is a member of the
366///   built-in Administrators group (checked via `IsUserAnAdmin`).
367/// - Other targets: always returns `false`.
368#[cfg(unix)]
369#[must_use]
370pub fn is_root() -> bool {
371    // SAFETY: `geteuid` is always safe to call and is thread-safe.
372    unsafe { libc::geteuid() == 0 }
373}
374
375/// Returns `true` when the current process is running with superuser /
376/// Administrator privileges.
377#[cfg(windows)]
378#[must_use]
379pub fn is_root() -> bool {
380    use windows::Win32::UI::Shell::IsUserAnAdmin;
381    // SAFETY: `IsUserAnAdmin` has no preconditions and returns a BOOL.
382    unsafe { IsUserAnAdmin().as_bool() }
383}
384
385/// Fallback for non-unix, non-windows targets.
386#[cfg(not(any(unix, windows)))]
387#[must_use]
388pub fn is_root() -> bool {
389    false
390}
391
392#[cfg(test)]
393mod tests {
394    use super::*;
395
396    // Windows tests below mutate the `PROGRAMDATA` env var to exercise
397    // platform-default path resolution. Cargo runs tests concurrently,
398    // so readers (`system_default`, `default_admin_bearer_path`) must
399    // serialize against the mutators or they race and observe a mix of
400    // pre- and post-mutation env state.
401    #[cfg(target_os = "windows")]
402    static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
403
404    #[test]
405    fn subdirectories_are_relative_to_data_dir() {
406        let dirs = ZLayerDirs::new("/test/data");
407        assert_eq!(dirs.containers(), PathBuf::from("/test/data/containers"));
408        assert_eq!(dirs.rootfs(), PathBuf::from("/test/data/rootfs"));
409        assert_eq!(dirs.bundles(), PathBuf::from("/test/data/bundles"));
410        assert_eq!(dirs.cache(), PathBuf::from("/test/data/cache"));
411        assert_eq!(dirs.volumes(), PathBuf::from("/test/data/volumes"));
412        assert_eq!(dirs.wasm(), PathBuf::from("/test/data/wasm"));
413        assert_eq!(
414            dirs.wasm_compiled(),
415            PathBuf::from("/test/data/wasm/compiled")
416        );
417        assert_eq!(dirs.secrets(), PathBuf::from("/test/data/secrets"));
418        assert_eq!(dirs.certs(), PathBuf::from("/test/data/certs"));
419        assert_eq!(dirs.raft(), PathBuf::from("/test/data/raft"));
420        assert_eq!(
421            dirs.admin_password(),
422            PathBuf::from("/test/data/admin_password")
423        );
424        assert_eq!(dirs.daemon_json(), PathBuf::from("/test/data/daemon.json"));
425        assert_eq!(dirs.logs(), PathBuf::from("/test/data/logs"));
426        assert_eq!(dirs.vms(), PathBuf::from("/test/data/vms"));
427        assert_eq!(dirs.images(), PathBuf::from("/test/data/images"));
428        assert_eq!(dirs.bin(), PathBuf::from("/test/data/bin"));
429        assert_eq!(
430            dirs.toolchain_cache(),
431            PathBuf::from("/test/data/toolchain-cache")
432        );
433        assert_eq!(dirs.tmp(), PathBuf::from("/test/data/tmp"));
434    }
435
436    #[test]
437    fn system_default_uses_default_data_dir() {
438        #[cfg(target_os = "windows")]
439        let _env_guard = ENV_LOCK.lock().unwrap();
440        let dirs = ZLayerDirs::system_default();
441        assert_eq!(dirs.data_dir(), ZLayerDirs::default_data_dir().as_path());
442    }
443
444    #[test]
445    fn admin_bearer_path_is_under_data_dir() {
446        let dirs = ZLayerDirs::new(PathBuf::from("/tmp/zlayer-test"));
447        assert_eq!(
448            dirs.admin_bearer_path(),
449            PathBuf::from("/tmp/zlayer-test/admin_bearer.token")
450        );
451    }
452
453    #[test]
454    fn default_admin_bearer_path_matches_system_default() {
455        #[cfg(target_os = "windows")]
456        let _env_guard = ENV_LOCK.lock().unwrap();
457        assert_eq!(
458            default_admin_bearer_path(),
459            ZLayerDirs::system_default().admin_bearer_path()
460        );
461    }
462
463    #[cfg(target_os = "windows")]
464    #[test]
465    fn windows_default_data_dir_uses_program_data() {
466        let _env_guard = ENV_LOCK.lock().unwrap();
467        let prev = std::env::var_os("PROGRAMDATA");
468        std::env::set_var("PROGRAMDATA", r"C:\TestProgramData");
469
470        let data = ZLayerDirs::default_data_dir();
471        assert_eq!(data, PathBuf::from(r"C:\TestProgramData\ZLayer"));
472
473        // Sub-paths should live under the ProgramData root.
474        let dirs = ZLayerDirs::system_default();
475        assert_eq!(dirs.certs(), data.join("certs"));
476        assert_eq!(dirs.secrets(), data.join("secrets"));
477        assert_eq!(dirs.logs(), data.join("logs"));
478
479        // Run/log helpers should also honour the ProgramData root.
480        assert_eq!(ZLayerDirs::default_run_dir(), data.join("run"));
481        assert_eq!(ZLayerDirs::default_log_dir(), data.join("logs"));
482
483        // Socket path on Windows is a TCP loopback endpoint, not a filesystem
484        // path.
485        assert_eq!(ZLayerDirs::default_socket_path(), "tcp://127.0.0.1:3669");
486
487        match prev {
488            Some(v) => std::env::set_var("PROGRAMDATA", v),
489            None => std::env::remove_var("PROGRAMDATA"),
490        }
491    }
492
493    #[cfg(target_os = "windows")]
494    #[test]
495    fn windows_default_data_dir_fallback_when_env_missing() {
496        let _env_guard = ENV_LOCK.lock().unwrap();
497        let prev = std::env::var_os("PROGRAMDATA");
498        std::env::remove_var("PROGRAMDATA");
499
500        let data = ZLayerDirs::default_data_dir();
501        assert_eq!(data, PathBuf::from(r"C:\ProgramData\ZLayer"));
502
503        if let Some(v) = prev {
504            std::env::set_var("PROGRAMDATA", v);
505        }
506    }
507
508    #[test]
509    fn default_docker_socket_path_not_empty() {
510        let result = ZLayerDirs::default_docker_socket_path();
511        assert!(!result.is_empty());
512    }
513
514    #[cfg(target_os = "windows")]
515    #[test]
516    fn default_docker_socket_path_platform_shape() {
517        let result = ZLayerDirs::default_docker_socket_path();
518        assert!(result.starts_with(r"\\.\pipe"));
519    }
520
521    #[cfg(target_os = "macos")]
522    #[test]
523    fn default_docker_socket_path_platform_shape() {
524        let result = ZLayerDirs::default_docker_socket_path();
525        assert!(result.ends_with("/docker.sock"));
526    }
527
528    #[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
529    #[test]
530    fn default_docker_socket_path_platform_shape() {
531        let result = ZLayerDirs::default_docker_socket_path();
532        assert!(result.ends_with("/docker.sock"));
533    }
534}