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