1use std::path::{Path, PathBuf};
2
3pub mod escalate;
4pub mod safe_fs;
5
6pub use escalate::{
7 applescript_quote, root_command, run_as_root, shell_quote, EscalationError, RootCommand,
8};
9
10pub struct ZLayerDirs {
14 data_dir: PathBuf,
15}
16
17impl ZLayerDirs {
18 pub fn new(data_dir: impl Into<PathBuf>) -> Self {
20 Self {
21 data_dir: data_dir.into(),
22 }
23 }
24
25 pub fn system_default() -> Self {
27 Self::new(Self::default_data_dir())
28 }
29
30 pub fn default_data_dir() -> PathBuf {
42 if let Some(env_dir) = std::env::var_os("ZLAYER_DATA_DIR") {
43 if !env_dir.is_empty() {
44 return PathBuf::from(env_dir);
45 }
46 }
47 platform_default_data_dir()
48 }
49
50 pub fn detect_data_dir() -> PathBuf {
59 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
60 {
61 if !is_root() {
62 let system_data = PathBuf::from("/var/lib/zlayer");
63 if system_data.join("daemon.json").exists() {
64 return system_data;
65 }
66 }
67 }
68 #[cfg(target_os = "windows")]
69 {
70 let system_data = windows_program_data_root();
71 if system_data.join("daemon.json").exists() {
72 return system_data;
73 }
74 }
75 Self::default_data_dir()
76 }
77
78 pub fn default_run_dir() -> PathBuf {
84 Self::default_run_dir_for(&Self::default_data_dir())
85 }
86
87 pub fn default_run_dir_for(data_dir: &Path) -> PathBuf {
94 let system_default = fhs_system_data_dir();
95 if data_dir == system_default.as_path() {
96 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
97 {
98 return PathBuf::from("/var/run/zlayer");
99 }
100 #[cfg(any(target_os = "macos", target_os = "windows"))]
101 {
102 return system_default.join("run");
103 }
104 }
105 data_dir.join("run")
106 }
107
108 pub fn default_log_dir() -> PathBuf {
114 Self::default_log_dir_for(&Self::default_data_dir())
115 }
116
117 pub fn default_log_dir_for(data_dir: &Path) -> PathBuf {
124 let system_default = fhs_system_data_dir();
125 if data_dir == system_default.as_path() {
126 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
127 {
128 return PathBuf::from("/var/log/zlayer");
129 }
130 #[cfg(any(target_os = "macos", target_os = "windows"))]
131 {
132 return system_default.join("logs");
133 }
134 }
135 data_dir.join("logs")
136 }
137
138 pub fn default_socket_path() -> String {
144 Self::default_socket_path_for(&Self::default_data_dir())
145 }
146
147 pub fn default_socket_path_for(data_dir: &Path) -> String {
156 #[cfg(target_os = "windows")]
157 {
158 let _ = data_dir;
159 "tcp://127.0.0.1:3669".to_string()
160 }
161 #[cfg(not(target_os = "windows"))]
162 {
163 let system_default = fhs_system_data_dir();
164 if data_dir == system_default.as_path() {
165 #[cfg(target_os = "macos")]
166 {
167 return system_default
168 .join("run")
169 .join("zlayer.sock")
170 .to_string_lossy()
171 .into_owned();
172 }
173 #[cfg(not(target_os = "macos"))]
174 {
175 return "/var/run/zlayer.sock".to_string();
176 }
177 }
178 let natural = data_dir
179 .join("run")
180 .join("zlayer.sock")
181 .to_string_lossy()
182 .into_owned();
183 if natural.len() <= SUN_PATH_MAX {
184 natural
185 } else {
186 socket_safe_fallback(data_dir, "daemon")
187 }
188 }
189 }
190
191 pub fn default_overlayd_socket_path_for(data_dir: &Path) -> String {
201 #[cfg(target_os = "windows")]
202 {
203 let _ = data_dir;
204 r"\\.\pipe\zlayer-overlayd".to_string()
205 }
206 #[cfg(not(target_os = "windows"))]
207 {
208 let system_default = platform_default_data_dir();
209 if data_dir == system_default.as_path() {
210 #[cfg(target_os = "macos")]
211 {
212 return system_default
213 .join("run")
214 .join("zlayer-overlayd.sock")
215 .to_string_lossy()
216 .into_owned();
217 }
218 #[cfg(not(target_os = "macos"))]
219 {
220 return "/var/run/zlayer-overlayd.sock".to_string();
221 }
222 }
223 let natural = data_dir
224 .join("run")
225 .join("zlayer-overlayd.sock")
226 .to_string_lossy()
227 .into_owned();
228 if natural.len() <= SUN_PATH_MAX {
229 natural
230 } else {
231 socket_safe_fallback(data_dir, "overlayd")
232 }
233 }
234 }
235
236 pub fn default_docker_socket_path() -> String {
244 #[cfg(target_os = "windows")]
245 {
246 r"\\.\pipe\zlayer-docker".to_string()
247 }
248 #[cfg(not(target_os = "windows"))]
249 {
250 #[cfg(target_os = "macos")]
251 {
252 let path = Self::default_data_dir()
253 .join("run")
254 .join("docker.sock")
255 .to_string_lossy()
256 .into_owned();
257 if path.len() <= SUN_PATH_MAX {
258 path
259 } else {
260 socket_safe_fallback(&Self::default_data_dir(), "docker")
261 }
262 }
263 #[cfg(not(target_os = "macos"))]
264 {
265 if is_root() {
266 "/var/run/zlayer/docker.sock".to_string()
267 } else if let Some(xdg) = std::env::var_os("XDG_RUNTIME_DIR") {
268 let xdg_path = PathBuf::from(&xdg);
269 let mut p = xdg_path.clone();
270 p.push("zlayer");
271 p.push("docker.sock");
272 let path = p.to_string_lossy().into_owned();
273 if path.len() <= SUN_PATH_MAX {
274 path
275 } else {
276 socket_safe_fallback(&xdg_path, "docker")
277 }
278 } else {
279 let path = Self::default_data_dir()
280 .join("run")
281 .join("docker.sock")
282 .to_string_lossy()
283 .into_owned();
284 if path.len() <= SUN_PATH_MAX {
285 path
286 } else {
287 socket_safe_fallback(&Self::default_data_dir(), "docker")
288 }
289 }
290 }
291 }
292 }
293
294 pub fn default_binary_dir() -> PathBuf {
303 #[cfg(unix)]
305 {
306 let probe = PathBuf::from("/usr/local/bin/.zlayer_write_probe");
307 if std::fs::write(&probe, b"").is_ok() {
308 let _ = std::fs::remove_file(&probe);
309 return PathBuf::from("/usr/local/bin");
310 }
311 }
312 let dirs = Self::system_default();
314 let bin_dir = dirs.bin();
315 let _ = std::fs::create_dir_all(&bin_dir);
316 bin_dir
317 }
318
319 pub fn data_dir(&self) -> &Path {
323 &self.data_dir
324 }
325
326 pub fn containers(&self) -> PathBuf {
328 self.data_dir.join("containers")
329 }
330
331 pub fn rootfs(&self) -> PathBuf {
333 self.data_dir.join("rootfs")
334 }
335
336 pub fn bundles(&self) -> PathBuf {
338 self.data_dir.join("bundles")
339 }
340
341 pub fn layer_store(&self) -> PathBuf {
354 self.data_dir.join("layers")
355 }
356
357 pub fn cache(&self) -> PathBuf {
359 self.data_dir.join("cache")
360 }
361
362 pub fn volumes(&self) -> PathBuf {
364 self.data_dir.join("volumes")
365 }
366
367 pub fn projects(&self) -> PathBuf {
370 self.data_dir.join("projects")
371 }
372
373 pub fn wasm(&self) -> PathBuf {
375 self.data_dir.join("wasm")
376 }
377
378 pub fn wasm_compiled(&self) -> PathBuf {
380 self.data_dir.join("wasm").join("compiled")
381 }
382
383 pub fn secrets(&self) -> PathBuf {
385 self.data_dir.join("secrets")
386 }
387
388 pub fn certs(&self) -> PathBuf {
390 self.data_dir.join("certs")
391 }
392
393 pub fn raft(&self) -> PathBuf {
395 self.data_dir.join("raft")
396 }
397
398 pub fn admin_password(&self) -> PathBuf {
400 self.data_dir.join("admin_password")
401 }
402
403 #[must_use]
417 pub fn admin_bearer_path(&self) -> PathBuf {
418 self.data_dir.join("admin_bearer.token")
419 }
420
421 pub fn daemon_json(&self) -> PathBuf {
423 self.data_dir.join("daemon.json")
424 }
425
426 pub fn agent_ipam_state(&self) -> PathBuf {
428 self.data_dir.join("agent_ipam.json")
429 }
430
431 pub fn agent_network_state(&self) -> PathBuf {
439 self.data_dir.join("agent_network.json")
440 }
441
442 pub fn logs(&self) -> PathBuf {
445 self.data_dir.join("logs")
446 }
447
448 pub fn vms(&self) -> PathBuf {
452 self.data_dir.join("vms")
453 }
454
455 pub fn images(&self) -> PathBuf {
457 self.data_dir.join("images")
458 }
459
460 pub fn bin(&self) -> PathBuf {
462 self.data_dir.join("bin")
463 }
464
465 #[must_use]
470 pub fn buildd_bin(&self) -> PathBuf {
471 self.bin().join("zlayer-buildd")
472 }
473
474 #[must_use]
478 pub fn buildd(&self) -> PathBuf {
479 self.data_dir.join("buildd")
480 }
481
482 pub fn toolchain_cache(&self) -> PathBuf {
488 self.data_dir.join("toolchain-cache")
489 }
490
491 pub fn host_toolchain_cache(&self) -> PathBuf {
501 self.data_dir.join("host-toolchains")
502 }
503
504 pub fn tmp(&self) -> PathBuf {
506 self.data_dir.join("tmp")
507 }
508
509 pub fn scratch_dir(&self, prefix: &str) -> std::io::Result<zlayer_types::Scratch> {
523 std::fs::create_dir_all(self.tmp())?;
524 let td = tempfile::Builder::new()
525 .prefix(prefix)
526 .tempdir_in(self.tmp())?;
527 Ok(zlayer_types::Scratch::from_tempdir(td))
528 }
529
530 pub fn scratch_dir_in(
544 base: &std::path::Path,
545 prefix: &str,
546 ) -> std::io::Result<zlayer_types::Scratch> {
547 std::fs::create_dir_all(base)?;
548 let td = tempfile::Builder::new().prefix(prefix).tempdir_in(base)?;
549 Ok(zlayer_types::Scratch::from_tempdir(td))
550 }
551
552 pub fn scratch_file(&self, prefix: &str) -> std::io::Result<zlayer_types::ScratchFile> {
562 std::fs::create_dir_all(self.tmp())?;
563 let nf = tempfile::Builder::new()
564 .prefix(prefix)
565 .tempfile_in(self.tmp())?;
566 Ok(zlayer_types::ScratchFile::from_named(nf))
567 }
568
569 pub fn wireguard(&self) -> PathBuf {
580 #[cfg(any(target_os = "macos", target_os = "windows"))]
581 let natural = self.data_dir.join("run").join("wireguard");
582 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
583 let natural = if self.data_dir == Self::default_data_dir() {
584 PathBuf::from("/var/run/wireguard")
585 } else {
586 self.data_dir.join("run").join("wireguard")
587 };
588 const WG_DIR_MAX: usize = SUN_PATH_MAX - 21;
590 if natural.to_string_lossy().len() <= WG_DIR_MAX {
591 natural
592 } else {
593 PathBuf::from(format!(
594 "/tmp/zlayer-wg-{:016x}",
595 hash_for_socket(&self.data_dir, "wg")
596 ))
597 }
598 }
599}
600
601#[must_use]
603pub fn default_admin_bearer_path() -> PathBuf {
604 ZLayerDirs::system_default().admin_bearer_path()
605}
606
607#[must_use]
619pub fn sanitize_digest(digest: &str) -> String {
620 digest.replace([':', '/'], "_")
621}
622
623pub(crate) fn platform_default_data_dir() -> PathBuf {
634 #[cfg(target_os = "macos")]
635 {
636 home_dir_or_fallback().join(".zlayer")
637 }
638 #[cfg(target_os = "windows")]
639 {
640 windows_program_data_root()
641 }
642 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
643 {
644 if is_root() {
645 PathBuf::from("/var/lib/zlayer")
646 } else {
647 home_dir_or_fallback().join(".zlayer")
648 }
649 }
650}
651
652fn fhs_system_data_dir() -> PathBuf {
668 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
669 {
670 PathBuf::from("/var/lib/zlayer")
671 }
672 #[cfg(any(target_os = "macos", target_os = "windows"))]
673 {
674 platform_default_data_dir()
675 }
676}
677
678const SUN_PATH_MAX: usize = 103;
683
684fn hash_for_socket(data_dir: &Path, label: &str) -> u64 {
689 let mut h: u64 = 0xcbf2_9ce4_8422_2325;
690 for b in data_dir.to_string_lossy().as_bytes() {
691 h ^= u64::from(*b);
692 h = h.wrapping_mul(0x0000_0100_0000_01b3);
693 }
694 for b in label.as_bytes() {
695 h ^= u64::from(*b);
696 h = h.wrapping_mul(0x0000_0100_0000_01b3);
697 }
698 h
699}
700
701#[cfg(not(target_os = "windows"))]
713fn socket_safe_fallback(data_dir: &Path, label: &str) -> String {
714 format!(
715 "/tmp/zlayer-{label}-{:016x}.sock",
716 hash_for_socket(data_dir, label)
717 )
718}
719
720#[cfg(not(target_os = "windows"))]
721fn home_dir_or_fallback() -> PathBuf {
722 std::env::var_os("HOME")
726 .map(PathBuf::from)
727 .unwrap_or_else(|| PathBuf::from("/var/lib/zlayer"))
728}
729
730#[cfg(target_os = "windows")]
736fn windows_program_data_root() -> PathBuf {
737 if let Some(program_data) = std::env::var_os("PROGRAMDATA") {
738 let mut p = PathBuf::from(program_data);
739 p.push("ZLayer");
740 p
741 } else {
742 PathBuf::from(r"C:\ProgramData\ZLayer")
743 }
744}
745
746#[cfg(unix)]
754#[must_use]
755pub fn is_root() -> bool {
756 unsafe { libc::geteuid() == 0 }
758}
759
760#[cfg(windows)]
776#[must_use]
777pub fn is_root() -> bool {
778 use windows::Win32::UI::Shell::IsUserAnAdmin;
779 unsafe { IsUserAnAdmin().as_bool() }
781}
782
783#[cfg(not(any(unix, windows)))]
785#[must_use]
786pub fn is_root() -> bool {
787 false
788}
789
790#[cfg(test)]
791mod tests {
792 use super::*;
793
794 static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
801
802 #[test]
803 fn layer_store_shares_data_dir_filesystem_with_bundles() {
804 let dirs = ZLayerDirs::new("/test/data");
809 assert_eq!(dirs.layer_store().parent(), dirs.bundles().parent());
810 assert_eq!(dirs.layer_store().parent(), Some(dirs.data_dir()));
811 }
812
813 #[test]
814 fn sanitize_digest_matches_blob_staging_filename_rule() {
815 assert_eq!(
818 super::sanitize_digest("sha256:abc123"),
819 "sha256_abc123",
820 "colon is replaced with underscore"
821 );
822 assert_eq!(
823 super::sanitize_digest("sha256:ab/cd"),
824 "sha256_ab_cd",
825 "both colon and slash are replaced"
826 );
827 assert_eq!(super::sanitize_digest("plainname"), "plainname");
829 let s = super::sanitize_digest("sha256:deadbeef");
831 assert_eq!(std::path::Path::new(&s).components().count(), 1);
832 }
833
834 #[test]
835 fn subdirectories_are_relative_to_data_dir() {
836 let dirs = ZLayerDirs::new("/test/data");
837 assert_eq!(dirs.containers(), PathBuf::from("/test/data/containers"));
838 assert_eq!(dirs.rootfs(), PathBuf::from("/test/data/rootfs"));
839 assert_eq!(dirs.bundles(), PathBuf::from("/test/data/bundles"));
840 assert_eq!(dirs.layer_store(), PathBuf::from("/test/data/layers"));
841 assert_eq!(dirs.cache(), PathBuf::from("/test/data/cache"));
842 assert_eq!(dirs.volumes(), PathBuf::from("/test/data/volumes"));
843 assert_eq!(dirs.wasm(), PathBuf::from("/test/data/wasm"));
844 assert_eq!(
845 dirs.wasm_compiled(),
846 PathBuf::from("/test/data/wasm/compiled")
847 );
848 assert_eq!(dirs.secrets(), PathBuf::from("/test/data/secrets"));
849 assert_eq!(dirs.certs(), PathBuf::from("/test/data/certs"));
850 assert_eq!(dirs.raft(), PathBuf::from("/test/data/raft"));
851 assert_eq!(
852 dirs.admin_password(),
853 PathBuf::from("/test/data/admin_password")
854 );
855 assert_eq!(dirs.daemon_json(), PathBuf::from("/test/data/daemon.json"));
856 assert_eq!(dirs.logs(), PathBuf::from("/test/data/logs"));
857 assert_eq!(dirs.vms(), PathBuf::from("/test/data/vms"));
858 assert_eq!(dirs.images(), PathBuf::from("/test/data/images"));
859 assert_eq!(dirs.bin(), PathBuf::from("/test/data/bin"));
860 assert_eq!(
861 dirs.buildd_bin(),
862 PathBuf::from("/test/data/bin/zlayer-buildd")
863 );
864 assert_eq!(dirs.buildd(), PathBuf::from("/test/data/buildd"));
865 assert_eq!(
866 dirs.toolchain_cache(),
867 PathBuf::from("/test/data/toolchain-cache")
868 );
869 assert_eq!(dirs.tmp(), PathBuf::from("/test/data/tmp"));
870 }
871
872 #[test]
873 fn system_default_uses_default_data_dir() {
874 let _env_guard = ENV_LOCK.lock().unwrap();
875 let dirs = ZLayerDirs::system_default();
876 assert_eq!(dirs.data_dir(), ZLayerDirs::default_data_dir().as_path());
877 }
878
879 #[test]
880 fn admin_bearer_path_is_under_data_dir() {
881 let dirs = ZLayerDirs::new(PathBuf::from("/var/lib/zlayer-test"));
882 assert_eq!(
883 dirs.admin_bearer_path(),
884 PathBuf::from("/var/lib/zlayer-test/admin_bearer.token")
885 );
886 }
887
888 #[test]
889 fn default_admin_bearer_path_matches_system_default() {
890 let _env_guard = ENV_LOCK.lock().unwrap();
891 assert_eq!(
892 default_admin_bearer_path(),
893 ZLayerDirs::system_default().admin_bearer_path()
894 );
895 }
896
897 #[cfg(target_os = "windows")]
898 #[test]
899 fn windows_default_data_dir_uses_program_data() {
900 let _env_guard = ENV_LOCK.lock().unwrap();
901 let prev = std::env::var_os("PROGRAMDATA");
902 std::env::set_var("PROGRAMDATA", r"C:\TestProgramData");
903
904 let data = ZLayerDirs::default_data_dir();
905 assert_eq!(data, PathBuf::from(r"C:\TestProgramData\ZLayer"));
906
907 let dirs = ZLayerDirs::system_default();
909 assert_eq!(dirs.certs(), data.join("certs"));
910 assert_eq!(dirs.secrets(), data.join("secrets"));
911 assert_eq!(dirs.logs(), data.join("logs"));
912
913 assert_eq!(ZLayerDirs::default_run_dir(), data.join("run"));
915 assert_eq!(ZLayerDirs::default_log_dir(), data.join("logs"));
916
917 assert_eq!(ZLayerDirs::default_socket_path(), "tcp://127.0.0.1:3669");
920
921 match prev {
922 Some(v) => std::env::set_var("PROGRAMDATA", v),
923 None => std::env::remove_var("PROGRAMDATA"),
924 }
925 }
926
927 #[test]
928 fn default_log_dir_for_returns_system_path_when_data_dir_is_default() {
929 let _env_guard = ENV_LOCK.lock().unwrap();
930 let system_default = ZLayerDirs::default_data_dir();
931 let result = ZLayerDirs::default_log_dir_for(&system_default);
932 assert_eq!(result, ZLayerDirs::default_log_dir());
933 }
934
935 #[test]
936 fn default_log_dir_for_returns_data_subdir_when_data_dir_overridden() {
937 let _env_guard = ENV_LOCK.lock().unwrap();
938 let tmp = tempfile::tempdir().expect("create tempdir");
939 let custom = tmp.path().to_path_buf();
940 let result = ZLayerDirs::default_log_dir_for(&custom);
941 assert_eq!(result, custom.join("logs"));
942 assert_ne!(result, ZLayerDirs::default_log_dir());
944 }
945
946 #[test]
947 fn default_run_dir_for_returns_system_path_when_data_dir_is_default() {
948 let _env_guard = ENV_LOCK.lock().unwrap();
949 let system_default = ZLayerDirs::default_data_dir();
950 let result = ZLayerDirs::default_run_dir_for(&system_default);
951 assert_eq!(result, ZLayerDirs::default_run_dir());
952 }
953
954 #[test]
955 fn default_run_dir_for_returns_data_subdir_when_data_dir_overridden() {
956 let _env_guard = ENV_LOCK.lock().unwrap();
957 let tmp = tempfile::tempdir().expect("create tempdir");
958 let custom = tmp.path().to_path_buf();
959 let result = ZLayerDirs::default_run_dir_for(&custom);
960 assert_eq!(result, custom.join("run"));
961 assert_ne!(result, ZLayerDirs::default_run_dir());
962 }
963
964 #[test]
965 fn default_socket_path_for_returns_system_path_when_data_dir_is_default() {
966 let _env_guard = ENV_LOCK.lock().unwrap();
967 let system_default = ZLayerDirs::default_data_dir();
968 let result = ZLayerDirs::default_socket_path_for(&system_default);
969 assert_eq!(result, ZLayerDirs::default_socket_path());
970 }
971
972 #[cfg(not(target_os = "windows"))]
973 #[test]
974 fn default_socket_path_for_returns_data_subdir_when_data_dir_overridden() {
975 let _env_guard = ENV_LOCK.lock().unwrap();
976 let tmp = tempfile::tempdir().expect("create tempdir");
977 let custom = tmp.path().to_path_buf();
978 let result = ZLayerDirs::default_socket_path_for(&custom);
979 let expected = custom
980 .join("run")
981 .join("zlayer.sock")
982 .to_string_lossy()
983 .into_owned();
984 assert_eq!(result, expected);
985 assert_ne!(result, ZLayerDirs::default_socket_path());
986 }
987
988 #[cfg(target_os = "windows")]
989 #[test]
990 fn default_socket_path_for_always_tcp_on_windows() {
991 let _env_guard = ENV_LOCK.lock().unwrap();
992 let tmp = tempfile::tempdir().expect("create tempdir");
993 let custom = tmp.path().to_path_buf();
994 assert_eq!(
996 ZLayerDirs::default_socket_path_for(&custom),
997 "tcp://127.0.0.1:3669"
998 );
999 }
1000
1001 #[cfg(not(target_os = "windows"))]
1002 #[test]
1003 fn default_socket_path_uses_data_subdir_when_env_var_overrides() {
1004 let _env_guard = ENV_LOCK.lock().unwrap();
1005 let prev = std::env::var_os("ZLAYER_DATA_DIR");
1006 let tmp = tempfile::tempdir().expect("create tempdir");
1007 let custom = tmp.path().to_path_buf();
1008 std::env::set_var("ZLAYER_DATA_DIR", &custom);
1009
1010 let result = ZLayerDirs::default_socket_path();
1011 let expected = custom
1012 .join("run")
1013 .join("zlayer.sock")
1014 .to_string_lossy()
1015 .into_owned();
1016 assert_eq!(result, expected);
1017
1018 match prev {
1019 Some(v) => std::env::set_var("ZLAYER_DATA_DIR", v),
1020 None => std::env::remove_var("ZLAYER_DATA_DIR"),
1021 }
1022 }
1023
1024 #[cfg(target_os = "windows")]
1025 #[test]
1026 fn default_socket_path_uses_data_subdir_when_env_var_overrides() {
1027 let _env_guard = ENV_LOCK.lock().unwrap();
1028 let prev = std::env::var_os("ZLAYER_DATA_DIR");
1031 let tmp = tempfile::tempdir().expect("create tempdir");
1032 let custom = tmp.path().to_path_buf();
1033 std::env::set_var("ZLAYER_DATA_DIR", &custom);
1034
1035 assert_eq!(ZLayerDirs::default_socket_path(), "tcp://127.0.0.1:3669");
1036
1037 match prev {
1038 Some(v) => std::env::set_var("ZLAYER_DATA_DIR", v),
1039 None => std::env::remove_var("ZLAYER_DATA_DIR"),
1040 }
1041 }
1042
1043 #[cfg(target_os = "windows")]
1044 #[test]
1045 fn windows_default_data_dir_fallback_when_env_missing() {
1046 let _env_guard = ENV_LOCK.lock().unwrap();
1047 let prev = std::env::var_os("PROGRAMDATA");
1048 std::env::remove_var("PROGRAMDATA");
1049
1050 let data = ZLayerDirs::default_data_dir();
1051 assert_eq!(data, PathBuf::from(r"C:\ProgramData\ZLayer"));
1052
1053 if let Some(v) = prev {
1054 std::env::set_var("PROGRAMDATA", v);
1055 }
1056 }
1057
1058 #[test]
1059 fn default_docker_socket_path_not_empty() {
1060 let result = ZLayerDirs::default_docker_socket_path();
1061 assert!(!result.is_empty());
1062 }
1063
1064 #[cfg(target_os = "windows")]
1065 #[test]
1066 fn default_docker_socket_path_platform_shape() {
1067 let result = ZLayerDirs::default_docker_socket_path();
1068 assert!(result.starts_with(r"\\.\pipe"));
1069 }
1070
1071 #[cfg(target_os = "macos")]
1072 #[test]
1073 fn default_docker_socket_path_platform_shape() {
1074 let result = ZLayerDirs::default_docker_socket_path();
1075 assert!(result.ends_with("/docker.sock"));
1076 }
1077
1078 #[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
1079 #[test]
1080 fn default_docker_socket_path_platform_shape() {
1081 let result = ZLayerDirs::default_docker_socket_path();
1082 assert!(result.ends_with("/docker.sock"));
1083 }
1084
1085 #[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
1086 #[test]
1087 fn wireguard_returns_fhs_path_on_default_data_dir() {
1088 let dirs = ZLayerDirs::system_default();
1089 assert_eq!(dirs.wireguard(), PathBuf::from("/var/run/wireguard"));
1090 }
1091
1092 #[test]
1093 fn wireguard_returns_data_subdir_when_overridden() {
1094 let tmp = tempfile::tempdir().expect("create tempdir");
1095 let custom = tmp.path().to_path_buf();
1096 let dirs = ZLayerDirs::new(&custom);
1097 let result = dirs.wireguard();
1098 assert_eq!(result, custom.join("run").join("wireguard"));
1099 #[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
1101 assert_ne!(result, PathBuf::from("/var/run/wireguard"));
1102 }
1103
1104 #[cfg(target_os = "macos")]
1105 #[test]
1106 fn wireguard_always_returns_data_subdir_on_macos() {
1107 let dirs = ZLayerDirs::system_default();
1110 let expected = ZLayerDirs::default_data_dir().join("run").join("wireguard");
1111 assert_eq!(dirs.wireguard(), expected);
1112 }
1113
1114 #[test]
1115 fn scratch_dir_under_data_tmp() {
1116 let parent = tempfile::tempdir().expect("parent");
1117 let dirs = ZLayerDirs::new(parent.path());
1118 let s = dirs.scratch_dir("zlayer-test-").expect("scratch_dir");
1119 assert!(s.path().starts_with(dirs.tmp()));
1120 assert!(s.path().is_dir());
1121 let kept = s.path().to_path_buf();
1122 drop(s);
1123 assert!(!kept.exists());
1124 }
1125
1126 #[test]
1127 fn scratch_file_under_data_tmp() {
1128 let parent = tempfile::tempdir().expect("parent");
1129 let dirs = ZLayerDirs::new(parent.path());
1130 let f = dirs.scratch_file("zlayer-test-").expect("scratch_file");
1131 assert!(f.path().starts_with(dirs.tmp()));
1132 assert!(f.path().is_file());
1133 let kept = f.path().to_path_buf();
1134 drop(f);
1135 assert!(!kept.exists());
1136 }
1137
1138 #[cfg(not(target_os = "windows"))]
1139 #[test]
1140 fn default_socket_path_for_falls_back_when_path_too_long() {
1141 let deep = PathBuf::from(
1142 "/var/lib/forgejo-runner/workdir/9dbc274201705d7d/hostexecutor/target/zlayer-e2e/cluster_3node/node1/data",
1143 );
1144 let result = ZLayerDirs::default_socket_path_for(&deep);
1145 assert!(
1146 result.len() <= SUN_PATH_MAX,
1147 "fallback path overflows sun_path: len={} path={}",
1148 result.len(),
1149 result,
1150 );
1151 assert_eq!(result, ZLayerDirs::default_socket_path_for(&deep));
1153 let other = deep.parent().unwrap().to_path_buf();
1155 assert_ne!(result, ZLayerDirs::default_socket_path_for(&other));
1156 }
1157
1158 #[cfg(not(target_os = "windows"))]
1159 #[test]
1160 fn default_socket_path_for_keeps_natural_path_when_short() {
1161 let tmp = tempfile::tempdir().expect("create tempdir");
1162 let short = tmp.path().to_path_buf();
1163 assert!(short.to_string_lossy().len() < 80);
1164 let result = ZLayerDirs::default_socket_path_for(&short);
1165 assert!(result.ends_with("/run/zlayer.sock"));
1166 assert!(result.starts_with(&*short.to_string_lossy()));
1167 }
1168
1169 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
1178 #[test]
1179 fn system_data_dir_resolves_fhs_paths_regardless_of_euid() {
1180 let system = PathBuf::from("/var/lib/zlayer");
1181 assert_eq!(
1182 ZLayerDirs::default_socket_path_for(&system),
1183 "/var/run/zlayer.sock",
1184 "non-root caller pointed at the system data dir must get the FHS socket"
1185 );
1186 assert_eq!(
1187 ZLayerDirs::default_run_dir_for(&system),
1188 PathBuf::from("/var/run/zlayer")
1189 );
1190 assert_eq!(
1191 ZLayerDirs::default_log_dir_for(&system),
1192 PathBuf::from("/var/log/zlayer")
1193 );
1194 }
1195
1196 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
1197 #[test]
1198 fn wireguard_dir_falls_back_when_path_too_long() {
1199 let deep = PathBuf::from(
1200 "/var/lib/forgejo-runner/workdir/9dbc274201705d7d/hostexecutor/target/zlayer-e2e/cluster_3node/node1/data",
1201 );
1202 let dirs = ZLayerDirs::new(&deep);
1203 let wg = dirs.wireguard();
1204 assert!(
1206 wg.to_string_lossy().len() + 21 <= SUN_PATH_MAX,
1207 "wireguard dir + ifname overflows: dir={}",
1208 wg.display(),
1209 );
1210 }
1211}