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: `%LOCALAPPDATA%\ZLayer` or `C:\ProgramData\ZLayer`
31    pub fn default_data_dir() -> PathBuf {
32        #[cfg(target_os = "macos")]
33        {
34            home_dir_or_tmp().join(".zlayer")
35        }
36        #[cfg(target_os = "windows")]
37        {
38            if let Some(local_app_data) = std::env::var_os("LOCALAPPDATA") {
39                PathBuf::from(local_app_data).join("ZLayer")
40            } else {
41                PathBuf::from(r"C:\ProgramData\ZLayer")
42            }
43        }
44        #[cfg(not(any(target_os = "macos", target_os = "windows")))]
45        {
46            if is_root() {
47                PathBuf::from("/var/lib/zlayer")
48            } else {
49                home_dir_or_tmp().join(".zlayer")
50            }
51        }
52    }
53
54    /// Default runtime directory.
55    ///
56    /// - Linux: `/var/run/zlayer`
57    /// - macOS: `{default_data_dir}/run`
58    /// - Windows: `{default_data_dir}\run`
59    pub fn default_run_dir() -> PathBuf {
60        #[cfg(not(any(target_os = "macos", target_os = "windows")))]
61        {
62            PathBuf::from("/var/run/zlayer")
63        }
64        #[cfg(any(target_os = "macos", target_os = "windows"))]
65        {
66            Self::default_data_dir().join("run")
67        }
68    }
69
70    /// Default log directory.
71    ///
72    /// - Linux: `/var/log/zlayer`
73    /// - macOS: `{default_data_dir}/logs`
74    /// - Windows: `{default_data_dir}\logs`
75    pub fn default_log_dir() -> PathBuf {
76        #[cfg(not(any(target_os = "macos", target_os = "windows")))]
77        {
78            PathBuf::from("/var/log/zlayer")
79        }
80        #[cfg(any(target_os = "macos", target_os = "windows"))]
81        {
82            Self::default_data_dir().join("logs")
83        }
84    }
85
86    /// Default Unix socket path.
87    ///
88    /// - Linux: `/var/run/zlayer.sock`
89    /// - macOS: `{default_data_dir}/run/zlayer.sock`
90    /// - Windows: `tcp://127.0.0.1:3669`
91    pub fn default_socket_path() -> String {
92        #[cfg(target_os = "windows")]
93        {
94            "tcp://127.0.0.1:3669".to_string()
95        }
96        #[cfg(not(target_os = "windows"))]
97        {
98            #[cfg(target_os = "macos")]
99            {
100                Self::default_data_dir()
101                    .join("run")
102                    .join("zlayer.sock")
103                    .to_string_lossy()
104                    .into_owned()
105            }
106            #[cfg(not(target_os = "macos"))]
107            {
108                "/var/run/zlayer.sock".to_string()
109            }
110        }
111    }
112
113    /// Preferred system directory for the `zlayer` binary.
114    ///
115    /// Tries `/usr/local/bin` first (standard FHS, writable on most systems).
116    /// Falls back to `{data_dir}/bin` (`/var/lib/zlayer/bin` on Linux as root)
117    /// which is always writable since `ZLayer` owns that directory.
118    ///
119    /// On macOS and Windows, returns `/usr/local/bin` or the data-dir `bin`
120    /// subdirectory respectively.
121    pub fn default_binary_dir() -> PathBuf {
122        // Probe /usr/local/bin writability — metadata mode bits lie on overlayfs
123        #[cfg(unix)]
124        {
125            let probe = PathBuf::from("/usr/local/bin/.zlayer_write_probe");
126            if std::fs::write(&probe, b"").is_ok() {
127                let _ = std::fs::remove_file(&probe);
128                return PathBuf::from("/usr/local/bin");
129            }
130        }
131        // Fallback: our own bin dir (always writable)
132        let dirs = Self::system_default();
133        let bin_dir = dirs.bin();
134        let _ = std::fs::create_dir_all(&bin_dir);
135        bin_dir
136    }
137
138    // -- Core subdirectories -------------------------------------------------
139
140    /// Root data directory.
141    pub fn data_dir(&self) -> &Path {
142        &self.data_dir
143    }
144
145    /// Container state directory (`{data}/containers`).
146    pub fn containers(&self) -> PathBuf {
147        self.data_dir.join("containers")
148    }
149
150    /// Unpacked image rootfs directory (`{data}/rootfs`).
151    pub fn rootfs(&self) -> PathBuf {
152        self.data_dir.join("rootfs")
153    }
154
155    /// OCI bundle directory (`{data}/bundles`).
156    pub fn bundles(&self) -> PathBuf {
157        self.data_dir.join("bundles")
158    }
159
160    /// Image/blob cache directory (`{data}/cache`).
161    pub fn cache(&self) -> PathBuf {
162        self.data_dir.join("cache")
163    }
164
165    /// Named volumes directory (`{data}/volumes`).
166    pub fn volumes(&self) -> PathBuf {
167        self.data_dir.join("volumes")
168    }
169
170    /// WASM module cache directory (`{data}/wasm`).
171    pub fn wasm(&self) -> PathBuf {
172        self.data_dir.join("wasm")
173    }
174
175    /// AOT-compiled WASM cache directory (`{data}/wasm/compiled`).
176    pub fn wasm_compiled(&self) -> PathBuf {
177        self.data_dir.join("wasm").join("compiled")
178    }
179
180    /// Encrypted secrets store directory (`{data}/secrets`).
181    pub fn secrets(&self) -> PathBuf {
182        self.data_dir.join("secrets")
183    }
184
185    /// TLS certificate storage directory (`{data}/certs`).
186    pub fn certs(&self) -> PathBuf {
187        self.data_dir.join("certs")
188    }
189
190    /// Raft consensus data directory (`{data}/raft`).
191    pub fn raft(&self) -> PathBuf {
192        self.data_dir.join("raft")
193    }
194
195    /// Admin password file path (`{data}/admin_password`).
196    pub fn admin_password(&self) -> PathBuf {
197        self.data_dir.join("admin_password")
198    }
199
200    /// Daemon metadata file path (`{data}/daemon.json`).
201    pub fn daemon_json(&self) -> PathBuf {
202        self.data_dir.join("daemon.json")
203    }
204
205    /// Logs subdirectory under data_dir (`{data}/logs`).
206    /// Used on macOS where logs live under the user data dir.
207    pub fn logs(&self) -> PathBuf {
208        self.data_dir.join("logs")
209    }
210
211    // -- macOS sandbox / builder paths ---------------------------------------
212
213    /// macOS VM state directory (`{data}/vms`).
214    pub fn vms(&self) -> PathBuf {
215        self.data_dir.join("vms")
216    }
217
218    /// OCI image storage directory (`{data}/images`).
219    pub fn images(&self) -> PathBuf {
220        self.data_dir.join("images")
221    }
222
223    /// Local binary directory (`{data}/bin`).
224    pub fn bin(&self) -> PathBuf {
225        self.data_dir.join("bin")
226    }
227
228    /// Toolchain download cache directory (`{data}/toolchain-cache`).
229    pub fn toolchain_cache(&self) -> PathBuf {
230        self.data_dir.join("toolchain-cache")
231    }
232
233    /// Temporary build directory (`{data}/tmp`).
234    pub fn tmp(&self) -> PathBuf {
235        self.data_dir.join("tmp")
236    }
237}
238
239// -- Internal helpers --------------------------------------------------------
240
241fn home_dir_or_tmp() -> PathBuf {
242    std::env::var_os("HOME")
243        .map(PathBuf::from)
244        .unwrap_or_else(|| PathBuf::from("/tmp"))
245}
246
247#[cfg(not(any(target_os = "macos", target_os = "windows")))]
248fn is_root() -> bool {
249    #[cfg(unix)]
250    {
251        nix::unistd::geteuid().is_root()
252    }
253    #[cfg(not(unix))]
254    {
255        false
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262
263    #[test]
264    fn subdirectories_are_relative_to_data_dir() {
265        let dirs = ZLayerDirs::new("/test/data");
266        assert_eq!(dirs.containers(), PathBuf::from("/test/data/containers"));
267        assert_eq!(dirs.rootfs(), PathBuf::from("/test/data/rootfs"));
268        assert_eq!(dirs.bundles(), PathBuf::from("/test/data/bundles"));
269        assert_eq!(dirs.cache(), PathBuf::from("/test/data/cache"));
270        assert_eq!(dirs.volumes(), PathBuf::from("/test/data/volumes"));
271        assert_eq!(dirs.wasm(), PathBuf::from("/test/data/wasm"));
272        assert_eq!(
273            dirs.wasm_compiled(),
274            PathBuf::from("/test/data/wasm/compiled")
275        );
276        assert_eq!(dirs.secrets(), PathBuf::from("/test/data/secrets"));
277        assert_eq!(dirs.certs(), PathBuf::from("/test/data/certs"));
278        assert_eq!(dirs.raft(), PathBuf::from("/test/data/raft"));
279        assert_eq!(
280            dirs.admin_password(),
281            PathBuf::from("/test/data/admin_password")
282        );
283        assert_eq!(dirs.daemon_json(), PathBuf::from("/test/data/daemon.json"));
284        assert_eq!(dirs.logs(), PathBuf::from("/test/data/logs"));
285        assert_eq!(dirs.vms(), PathBuf::from("/test/data/vms"));
286        assert_eq!(dirs.images(), PathBuf::from("/test/data/images"));
287        assert_eq!(dirs.bin(), PathBuf::from("/test/data/bin"));
288        assert_eq!(
289            dirs.toolchain_cache(),
290            PathBuf::from("/test/data/toolchain-cache")
291        );
292        assert_eq!(dirs.tmp(), PathBuf::from("/test/data/tmp"));
293    }
294
295    #[test]
296    fn system_default_uses_default_data_dir() {
297        let dirs = ZLayerDirs::system_default();
298        assert_eq!(dirs.data_dir(), ZLayerDirs::default_data_dir().as_path());
299    }
300}