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 Self::default_run_dir_for(&Self::default_data_dir())
87 }
88
89 pub fn default_run_dir_for(data_dir: &Path) -> PathBuf {
96 let system_default = Self::default_data_dir();
97 if data_dir == system_default.as_path() {
98 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
99 {
100 return PathBuf::from("/var/run/zlayer");
101 }
102 #[cfg(any(target_os = "macos", target_os = "windows"))]
103 {
104 return system_default.join("run");
105 }
106 }
107 data_dir.join("run")
108 }
109
110 pub fn default_log_dir() -> PathBuf {
116 Self::default_log_dir_for(&Self::default_data_dir())
117 }
118
119 pub fn default_log_dir_for(data_dir: &Path) -> PathBuf {
126 let system_default = Self::default_data_dir();
127 if data_dir == system_default.as_path() {
128 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
129 {
130 return PathBuf::from("/var/log/zlayer");
131 }
132 #[cfg(any(target_os = "macos", target_os = "windows"))]
133 {
134 return system_default.join("logs");
135 }
136 }
137 data_dir.join("logs")
138 }
139
140 pub fn default_socket_path() -> String {
146 Self::default_socket_path_for(&Self::default_data_dir())
147 }
148
149 pub fn default_socket_path_for(data_dir: &Path) -> String {
158 #[cfg(target_os = "windows")]
159 {
160 let _ = data_dir;
161 "tcp://127.0.0.1:3669".to_string()
162 }
163 #[cfg(not(target_os = "windows"))]
164 {
165 let system_default = Self::default_data_dir();
166 if data_dir == system_default.as_path() {
167 #[cfg(target_os = "macos")]
168 {
169 return system_default
170 .join("run")
171 .join("zlayer.sock")
172 .to_string_lossy()
173 .into_owned();
174 }
175 #[cfg(not(target_os = "macos"))]
176 {
177 return "/var/run/zlayer.sock".to_string();
178 }
179 }
180 data_dir
181 .join("run")
182 .join("zlayer.sock")
183 .to_string_lossy()
184 .into_owned()
185 }
186 }
187
188 pub fn default_docker_socket_path() -> String {
196 #[cfg(target_os = "windows")]
197 {
198 r"\\.\pipe\zlayer-docker".to_string()
199 }
200 #[cfg(not(target_os = "windows"))]
201 {
202 #[cfg(target_os = "macos")]
203 {
204 Self::default_data_dir()
205 .join("run")
206 .join("docker.sock")
207 .to_string_lossy()
208 .into_owned()
209 }
210 #[cfg(not(target_os = "macos"))]
211 {
212 if is_root() {
213 "/var/run/zlayer/docker.sock".to_string()
214 } else if let Some(xdg) = std::env::var_os("XDG_RUNTIME_DIR") {
215 let mut p = PathBuf::from(xdg);
216 p.push("zlayer");
217 p.push("docker.sock");
218 p.to_string_lossy().into_owned()
219 } else {
220 Self::default_data_dir()
221 .join("run")
222 .join("docker.sock")
223 .to_string_lossy()
224 .into_owned()
225 }
226 }
227 }
228 }
229
230 pub fn default_binary_dir() -> PathBuf {
239 #[cfg(unix)]
241 {
242 let probe = PathBuf::from("/usr/local/bin/.zlayer_write_probe");
243 if std::fs::write(&probe, b"").is_ok() {
244 let _ = std::fs::remove_file(&probe);
245 return PathBuf::from("/usr/local/bin");
246 }
247 }
248 let dirs = Self::system_default();
250 let bin_dir = dirs.bin();
251 let _ = std::fs::create_dir_all(&bin_dir);
252 bin_dir
253 }
254
255 pub fn data_dir(&self) -> &Path {
259 &self.data_dir
260 }
261
262 pub fn containers(&self) -> PathBuf {
264 self.data_dir.join("containers")
265 }
266
267 pub fn rootfs(&self) -> PathBuf {
269 self.data_dir.join("rootfs")
270 }
271
272 pub fn bundles(&self) -> PathBuf {
274 self.data_dir.join("bundles")
275 }
276
277 pub fn cache(&self) -> PathBuf {
279 self.data_dir.join("cache")
280 }
281
282 pub fn volumes(&self) -> PathBuf {
284 self.data_dir.join("volumes")
285 }
286
287 pub fn wasm(&self) -> PathBuf {
289 self.data_dir.join("wasm")
290 }
291
292 pub fn wasm_compiled(&self) -> PathBuf {
294 self.data_dir.join("wasm").join("compiled")
295 }
296
297 pub fn secrets(&self) -> PathBuf {
299 self.data_dir.join("secrets")
300 }
301
302 pub fn certs(&self) -> PathBuf {
304 self.data_dir.join("certs")
305 }
306
307 pub fn raft(&self) -> PathBuf {
309 self.data_dir.join("raft")
310 }
311
312 pub fn admin_password(&self) -> PathBuf {
314 self.data_dir.join("admin_password")
315 }
316
317 #[must_use]
331 pub fn admin_bearer_path(&self) -> PathBuf {
332 self.data_dir.join("admin_bearer.token")
333 }
334
335 pub fn daemon_json(&self) -> PathBuf {
337 self.data_dir.join("daemon.json")
338 }
339
340 pub fn agent_ipam_state(&self) -> PathBuf {
342 self.data_dir.join("agent_ipam.json")
343 }
344
345 pub fn logs(&self) -> PathBuf {
348 self.data_dir.join("logs")
349 }
350
351 pub fn vms(&self) -> PathBuf {
355 self.data_dir.join("vms")
356 }
357
358 pub fn images(&self) -> PathBuf {
360 self.data_dir.join("images")
361 }
362
363 pub fn bin(&self) -> PathBuf {
365 self.data_dir.join("bin")
366 }
367
368 pub fn toolchain_cache(&self) -> PathBuf {
370 self.data_dir.join("toolchain-cache")
371 }
372
373 pub fn tmp(&self) -> PathBuf {
375 self.data_dir.join("tmp")
376 }
377
378 pub fn wireguard(&self) -> PathBuf {
389 #[cfg(any(target_os = "macos", target_os = "windows"))]
390 {
391 self.data_dir.join("run").join("wireguard")
393 }
394 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
395 {
396 if self.data_dir == Self::default_data_dir() {
397 PathBuf::from("/var/run/wireguard")
398 } else {
399 self.data_dir.join("run").join("wireguard")
400 }
401 }
402 }
403}
404
405#[must_use]
407pub fn default_admin_bearer_path() -> PathBuf {
408 ZLayerDirs::system_default().admin_bearer_path()
409}
410
411#[cfg(not(target_os = "windows"))]
414fn home_dir_or_tmp() -> PathBuf {
415 std::env::var_os("HOME")
416 .map(PathBuf::from)
417 .unwrap_or_else(|| PathBuf::from("/tmp"))
418}
419
420#[cfg(target_os = "windows")]
426fn windows_program_data_root() -> PathBuf {
427 if let Some(program_data) = std::env::var_os("PROGRAMDATA") {
428 let mut p = PathBuf::from(program_data);
429 p.push("ZLayer");
430 p
431 } else {
432 PathBuf::from(r"C:\ProgramData\ZLayer")
433 }
434}
435
436#[cfg(unix)]
444#[must_use]
445pub fn is_root() -> bool {
446 unsafe { libc::geteuid() == 0 }
448}
449
450#[cfg(windows)]
453#[must_use]
454pub fn is_root() -> bool {
455 use windows::Win32::UI::Shell::IsUserAnAdmin;
456 unsafe { IsUserAnAdmin().as_bool() }
458}
459
460#[cfg(not(any(unix, windows)))]
462#[must_use]
463pub fn is_root() -> bool {
464 false
465}
466
467#[cfg(test)]
468mod tests {
469 use super::*;
470
471 #[cfg(target_os = "windows")]
477 static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
478
479 #[test]
480 fn subdirectories_are_relative_to_data_dir() {
481 let dirs = ZLayerDirs::new("/test/data");
482 assert_eq!(dirs.containers(), PathBuf::from("/test/data/containers"));
483 assert_eq!(dirs.rootfs(), PathBuf::from("/test/data/rootfs"));
484 assert_eq!(dirs.bundles(), PathBuf::from("/test/data/bundles"));
485 assert_eq!(dirs.cache(), PathBuf::from("/test/data/cache"));
486 assert_eq!(dirs.volumes(), PathBuf::from("/test/data/volumes"));
487 assert_eq!(dirs.wasm(), PathBuf::from("/test/data/wasm"));
488 assert_eq!(
489 dirs.wasm_compiled(),
490 PathBuf::from("/test/data/wasm/compiled")
491 );
492 assert_eq!(dirs.secrets(), PathBuf::from("/test/data/secrets"));
493 assert_eq!(dirs.certs(), PathBuf::from("/test/data/certs"));
494 assert_eq!(dirs.raft(), PathBuf::from("/test/data/raft"));
495 assert_eq!(
496 dirs.admin_password(),
497 PathBuf::from("/test/data/admin_password")
498 );
499 assert_eq!(dirs.daemon_json(), PathBuf::from("/test/data/daemon.json"));
500 assert_eq!(dirs.logs(), PathBuf::from("/test/data/logs"));
501 assert_eq!(dirs.vms(), PathBuf::from("/test/data/vms"));
502 assert_eq!(dirs.images(), PathBuf::from("/test/data/images"));
503 assert_eq!(dirs.bin(), PathBuf::from("/test/data/bin"));
504 assert_eq!(
505 dirs.toolchain_cache(),
506 PathBuf::from("/test/data/toolchain-cache")
507 );
508 assert_eq!(dirs.tmp(), PathBuf::from("/test/data/tmp"));
509 }
510
511 #[test]
512 fn system_default_uses_default_data_dir() {
513 #[cfg(target_os = "windows")]
514 let _env_guard = ENV_LOCK.lock().unwrap();
515 let dirs = ZLayerDirs::system_default();
516 assert_eq!(dirs.data_dir(), ZLayerDirs::default_data_dir().as_path());
517 }
518
519 #[test]
520 fn admin_bearer_path_is_under_data_dir() {
521 let dirs = ZLayerDirs::new(PathBuf::from("/tmp/zlayer-test"));
522 assert_eq!(
523 dirs.admin_bearer_path(),
524 PathBuf::from("/tmp/zlayer-test/admin_bearer.token")
525 );
526 }
527
528 #[test]
529 fn default_admin_bearer_path_matches_system_default() {
530 #[cfg(target_os = "windows")]
531 let _env_guard = ENV_LOCK.lock().unwrap();
532 assert_eq!(
533 default_admin_bearer_path(),
534 ZLayerDirs::system_default().admin_bearer_path()
535 );
536 }
537
538 #[cfg(target_os = "windows")]
539 #[test]
540 fn windows_default_data_dir_uses_program_data() {
541 let _env_guard = ENV_LOCK.lock().unwrap();
542 let prev = std::env::var_os("PROGRAMDATA");
543 std::env::set_var("PROGRAMDATA", r"C:\TestProgramData");
544
545 let data = ZLayerDirs::default_data_dir();
546 assert_eq!(data, PathBuf::from(r"C:\TestProgramData\ZLayer"));
547
548 let dirs = ZLayerDirs::system_default();
550 assert_eq!(dirs.certs(), data.join("certs"));
551 assert_eq!(dirs.secrets(), data.join("secrets"));
552 assert_eq!(dirs.logs(), data.join("logs"));
553
554 assert_eq!(ZLayerDirs::default_run_dir(), data.join("run"));
556 assert_eq!(ZLayerDirs::default_log_dir(), data.join("logs"));
557
558 assert_eq!(ZLayerDirs::default_socket_path(), "tcp://127.0.0.1:3669");
561
562 match prev {
563 Some(v) => std::env::set_var("PROGRAMDATA", v),
564 None => std::env::remove_var("PROGRAMDATA"),
565 }
566 }
567
568 #[test]
569 fn default_log_dir_for_returns_system_path_when_data_dir_is_default() {
570 #[cfg(target_os = "windows")]
571 let _env_guard = ENV_LOCK.lock().unwrap();
572 let system_default = ZLayerDirs::default_data_dir();
573 let result = ZLayerDirs::default_log_dir_for(&system_default);
574 assert_eq!(result, ZLayerDirs::default_log_dir());
575 }
576
577 #[test]
578 fn default_log_dir_for_returns_data_subdir_when_data_dir_overridden() {
579 let tmp = tempfile::tempdir().expect("create tempdir");
580 let custom = tmp.path().to_path_buf();
581 let result = ZLayerDirs::default_log_dir_for(&custom);
582 assert_eq!(result, custom.join("logs"));
583 assert_ne!(result, ZLayerDirs::default_log_dir());
585 }
586
587 #[test]
588 fn default_run_dir_for_returns_system_path_when_data_dir_is_default() {
589 #[cfg(target_os = "windows")]
590 let _env_guard = ENV_LOCK.lock().unwrap();
591 let system_default = ZLayerDirs::default_data_dir();
592 let result = ZLayerDirs::default_run_dir_for(&system_default);
593 assert_eq!(result, ZLayerDirs::default_run_dir());
594 }
595
596 #[test]
597 fn default_run_dir_for_returns_data_subdir_when_data_dir_overridden() {
598 let tmp = tempfile::tempdir().expect("create tempdir");
599 let custom = tmp.path().to_path_buf();
600 let result = ZLayerDirs::default_run_dir_for(&custom);
601 assert_eq!(result, custom.join("run"));
602 assert_ne!(result, ZLayerDirs::default_run_dir());
603 }
604
605 #[test]
606 fn default_socket_path_for_returns_system_path_when_data_dir_is_default() {
607 #[cfg(target_os = "windows")]
608 let _env_guard = ENV_LOCK.lock().unwrap();
609 let system_default = ZLayerDirs::default_data_dir();
610 let result = ZLayerDirs::default_socket_path_for(&system_default);
611 assert_eq!(result, ZLayerDirs::default_socket_path());
612 }
613
614 #[cfg(not(target_os = "windows"))]
615 #[test]
616 fn default_socket_path_for_returns_data_subdir_when_data_dir_overridden() {
617 let tmp = tempfile::tempdir().expect("create tempdir");
618 let custom = tmp.path().to_path_buf();
619 let result = ZLayerDirs::default_socket_path_for(&custom);
620 let expected = custom
621 .join("run")
622 .join("zlayer.sock")
623 .to_string_lossy()
624 .into_owned();
625 assert_eq!(result, expected);
626 assert_ne!(result, ZLayerDirs::default_socket_path());
627 }
628
629 #[cfg(target_os = "windows")]
630 #[test]
631 fn default_socket_path_for_always_tcp_on_windows() {
632 let _env_guard = ENV_LOCK.lock().unwrap();
633 let tmp = tempfile::tempdir().expect("create tempdir");
634 let custom = tmp.path().to_path_buf();
635 assert_eq!(
637 ZLayerDirs::default_socket_path_for(&custom),
638 "tcp://127.0.0.1:3669"
639 );
640 }
641
642 #[cfg(target_os = "windows")]
643 #[test]
644 fn windows_default_data_dir_fallback_when_env_missing() {
645 let _env_guard = ENV_LOCK.lock().unwrap();
646 let prev = std::env::var_os("PROGRAMDATA");
647 std::env::remove_var("PROGRAMDATA");
648
649 let data = ZLayerDirs::default_data_dir();
650 assert_eq!(data, PathBuf::from(r"C:\ProgramData\ZLayer"));
651
652 if let Some(v) = prev {
653 std::env::set_var("PROGRAMDATA", v);
654 }
655 }
656
657 #[test]
658 fn default_docker_socket_path_not_empty() {
659 let result = ZLayerDirs::default_docker_socket_path();
660 assert!(!result.is_empty());
661 }
662
663 #[cfg(target_os = "windows")]
664 #[test]
665 fn default_docker_socket_path_platform_shape() {
666 let result = ZLayerDirs::default_docker_socket_path();
667 assert!(result.starts_with(r"\\.\pipe"));
668 }
669
670 #[cfg(target_os = "macos")]
671 #[test]
672 fn default_docker_socket_path_platform_shape() {
673 let result = ZLayerDirs::default_docker_socket_path();
674 assert!(result.ends_with("/docker.sock"));
675 }
676
677 #[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
678 #[test]
679 fn default_docker_socket_path_platform_shape() {
680 let result = ZLayerDirs::default_docker_socket_path();
681 assert!(result.ends_with("/docker.sock"));
682 }
683
684 #[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
685 #[test]
686 fn wireguard_returns_fhs_path_on_default_data_dir() {
687 let dirs = ZLayerDirs::system_default();
688 assert_eq!(dirs.wireguard(), PathBuf::from("/var/run/wireguard"));
689 }
690
691 #[test]
692 fn wireguard_returns_data_subdir_when_overridden() {
693 let tmp = tempfile::tempdir().expect("create tempdir");
694 let custom = tmp.path().to_path_buf();
695 let dirs = ZLayerDirs::new(&custom);
696 let result = dirs.wireguard();
697 assert_eq!(result, custom.join("run").join("wireguard"));
698 #[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
700 assert_ne!(result, PathBuf::from("/var/run/wireguard"));
701 }
702
703 #[cfg(target_os = "macos")]
704 #[test]
705 fn wireguard_always_returns_data_subdir_on_macos() {
706 let dirs = ZLayerDirs::system_default();
709 let expected = ZLayerDirs::default_data_dir().join("run").join("wireguard");
710 assert_eq!(dirs.wireguard(), expected);
711 }
712}