1use std::path::{Path, PathBuf};
2
3pub struct ZLayerDirs {
7 data_dir: PathBuf,
8}
9
10impl ZLayerDirs {
11 pub fn new(data_dir: impl Into<PathBuf>) -> Self {
13 Self {
14 data_dir: data_dir.into(),
15 }
16 }
17
18 pub fn system_default() -> Self {
20 Self::new(Self::default_data_dir())
21 }
22
23 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 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 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 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 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 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 pub fn default_binary_dir() -> PathBuf {
190 #[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 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 pub fn data_dir(&self) -> &Path {
210 &self.data_dir
211 }
212
213 pub fn containers(&self) -> PathBuf {
215 self.data_dir.join("containers")
216 }
217
218 pub fn rootfs(&self) -> PathBuf {
220 self.data_dir.join("rootfs")
221 }
222
223 pub fn bundles(&self) -> PathBuf {
225 self.data_dir.join("bundles")
226 }
227
228 pub fn cache(&self) -> PathBuf {
230 self.data_dir.join("cache")
231 }
232
233 pub fn volumes(&self) -> PathBuf {
235 self.data_dir.join("volumes")
236 }
237
238 pub fn wasm(&self) -> PathBuf {
240 self.data_dir.join("wasm")
241 }
242
243 pub fn wasm_compiled(&self) -> PathBuf {
245 self.data_dir.join("wasm").join("compiled")
246 }
247
248 pub fn secrets(&self) -> PathBuf {
250 self.data_dir.join("secrets")
251 }
252
253 pub fn certs(&self) -> PathBuf {
255 self.data_dir.join("certs")
256 }
257
258 pub fn raft(&self) -> PathBuf {
260 self.data_dir.join("raft")
261 }
262
263 pub fn admin_password(&self) -> PathBuf {
265 self.data_dir.join("admin_password")
266 }
267
268 #[must_use]
282 pub fn admin_bearer_path(&self) -> PathBuf {
283 self.data_dir.join("admin_bearer.token")
284 }
285
286 pub fn daemon_json(&self) -> PathBuf {
288 self.data_dir.join("daemon.json")
289 }
290
291 pub fn agent_ipam_state(&self) -> PathBuf {
293 self.data_dir.join("agent_ipam.json")
294 }
295
296 pub fn logs(&self) -> PathBuf {
299 self.data_dir.join("logs")
300 }
301
302 pub fn vms(&self) -> PathBuf {
306 self.data_dir.join("vms")
307 }
308
309 pub fn images(&self) -> PathBuf {
311 self.data_dir.join("images")
312 }
313
314 pub fn bin(&self) -> PathBuf {
316 self.data_dir.join("bin")
317 }
318
319 pub fn toolchain_cache(&self) -> PathBuf {
321 self.data_dir.join("toolchain-cache")
322 }
323
324 pub fn tmp(&self) -> PathBuf {
326 self.data_dir.join("tmp")
327 }
328}
329
330#[must_use]
332pub fn default_admin_bearer_path() -> PathBuf {
333 ZLayerDirs::system_default().admin_bearer_path()
334}
335
336#[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#[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#[cfg(unix)]
369#[must_use]
370pub fn is_root() -> bool {
371 unsafe { libc::geteuid() == 0 }
373}
374
375#[cfg(windows)]
378#[must_use]
379pub fn is_root() -> bool {
380 use windows::Win32::UI::Shell::IsUserAnAdmin;
381 unsafe { IsUserAnAdmin().as_bool() }
383}
384
385#[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 #[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 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 assert_eq!(ZLayerDirs::default_run_dir(), data.join("run"));
481 assert_eq!(ZLayerDirs::default_log_dir(), data.join("logs"));
482
483 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}