use std::path::{Path, PathBuf};
pub struct ZLayerDirs {
data_dir: PathBuf,
}
impl ZLayerDirs {
pub fn new(data_dir: impl Into<PathBuf>) -> Self {
Self {
data_dir: data_dir.into(),
}
}
pub fn system_default() -> Self {
Self::new(Self::default_data_dir())
}
pub fn default_data_dir() -> PathBuf {
if let Some(env_dir) = std::env::var_os("ZLAYER_DATA_DIR") {
if !env_dir.is_empty() {
return PathBuf::from(env_dir);
}
}
platform_default_data_dir()
}
pub fn detect_data_dir() -> PathBuf {
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
if !is_root() {
let system_data = PathBuf::from("/var/lib/zlayer");
if system_data.join("daemon.json").exists() {
return system_data;
}
}
}
#[cfg(target_os = "windows")]
{
let system_data = windows_program_data_root();
if system_data.join("daemon.json").exists() {
return system_data;
}
}
Self::default_data_dir()
}
pub fn default_run_dir() -> PathBuf {
Self::default_run_dir_for(&Self::default_data_dir())
}
pub fn default_run_dir_for(data_dir: &Path) -> PathBuf {
let system_default = platform_default_data_dir();
if data_dir == system_default.as_path() {
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
return PathBuf::from("/var/run/zlayer");
}
#[cfg(any(target_os = "macos", target_os = "windows"))]
{
return system_default.join("run");
}
}
data_dir.join("run")
}
pub fn default_log_dir() -> PathBuf {
Self::default_log_dir_for(&Self::default_data_dir())
}
pub fn default_log_dir_for(data_dir: &Path) -> PathBuf {
let system_default = platform_default_data_dir();
if data_dir == system_default.as_path() {
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
return PathBuf::from("/var/log/zlayer");
}
#[cfg(any(target_os = "macos", target_os = "windows"))]
{
return system_default.join("logs");
}
}
data_dir.join("logs")
}
pub fn default_socket_path() -> String {
Self::default_socket_path_for(&Self::default_data_dir())
}
pub fn default_socket_path_for(data_dir: &Path) -> String {
#[cfg(target_os = "windows")]
{
let _ = data_dir;
"tcp://127.0.0.1:3669".to_string()
}
#[cfg(not(target_os = "windows"))]
{
let system_default = platform_default_data_dir();
if data_dir == system_default.as_path() {
#[cfg(target_os = "macos")]
{
return system_default
.join("run")
.join("zlayer.sock")
.to_string_lossy()
.into_owned();
}
#[cfg(not(target_os = "macos"))]
{
return "/var/run/zlayer.sock".to_string();
}
}
let natural = data_dir
.join("run")
.join("zlayer.sock")
.to_string_lossy()
.into_owned();
if natural.len() <= SUN_PATH_MAX {
natural
} else {
socket_safe_fallback(data_dir, "daemon")
}
}
}
pub fn default_overlayd_socket_path_for(data_dir: &Path) -> String {
#[cfg(target_os = "windows")]
{
let _ = data_dir;
r"\\.\pipe\zlayer-overlayd".to_string()
}
#[cfg(not(target_os = "windows"))]
{
let system_default = platform_default_data_dir();
if data_dir == system_default.as_path() {
#[cfg(target_os = "macos")]
{
return system_default
.join("run")
.join("zlayer-overlayd.sock")
.to_string_lossy()
.into_owned();
}
#[cfg(not(target_os = "macos"))]
{
return "/var/run/zlayer-overlayd.sock".to_string();
}
}
let natural = data_dir
.join("run")
.join("zlayer-overlayd.sock")
.to_string_lossy()
.into_owned();
if natural.len() <= SUN_PATH_MAX {
natural
} else {
socket_safe_fallback(data_dir, "overlayd")
}
}
}
pub fn default_docker_socket_path() -> String {
#[cfg(target_os = "windows")]
{
r"\\.\pipe\zlayer-docker".to_string()
}
#[cfg(not(target_os = "windows"))]
{
#[cfg(target_os = "macos")]
{
let path = Self::default_data_dir()
.join("run")
.join("docker.sock")
.to_string_lossy()
.into_owned();
if path.len() <= SUN_PATH_MAX {
path
} else {
socket_safe_fallback(&Self::default_data_dir(), "docker")
}
}
#[cfg(not(target_os = "macos"))]
{
if is_root() {
"/var/run/zlayer/docker.sock".to_string()
} else if let Some(xdg) = std::env::var_os("XDG_RUNTIME_DIR") {
let xdg_path = PathBuf::from(&xdg);
let mut p = xdg_path.clone();
p.push("zlayer");
p.push("docker.sock");
let path = p.to_string_lossy().into_owned();
if path.len() <= SUN_PATH_MAX {
path
} else {
socket_safe_fallback(&xdg_path, "docker")
}
} else {
let path = Self::default_data_dir()
.join("run")
.join("docker.sock")
.to_string_lossy()
.into_owned();
if path.len() <= SUN_PATH_MAX {
path
} else {
socket_safe_fallback(&Self::default_data_dir(), "docker")
}
}
}
}
}
pub fn default_binary_dir() -> PathBuf {
#[cfg(unix)]
{
let probe = PathBuf::from("/usr/local/bin/.zlayer_write_probe");
if std::fs::write(&probe, b"").is_ok() {
let _ = std::fs::remove_file(&probe);
return PathBuf::from("/usr/local/bin");
}
}
let dirs = Self::system_default();
let bin_dir = dirs.bin();
let _ = std::fs::create_dir_all(&bin_dir);
bin_dir
}
pub fn data_dir(&self) -> &Path {
&self.data_dir
}
pub fn containers(&self) -> PathBuf {
self.data_dir.join("containers")
}
pub fn rootfs(&self) -> PathBuf {
self.data_dir.join("rootfs")
}
pub fn bundles(&self) -> PathBuf {
self.data_dir.join("bundles")
}
pub fn cache(&self) -> PathBuf {
self.data_dir.join("cache")
}
pub fn volumes(&self) -> PathBuf {
self.data_dir.join("volumes")
}
pub fn projects(&self) -> PathBuf {
self.data_dir.join("projects")
}
pub fn wasm(&self) -> PathBuf {
self.data_dir.join("wasm")
}
pub fn wasm_compiled(&self) -> PathBuf {
self.data_dir.join("wasm").join("compiled")
}
pub fn secrets(&self) -> PathBuf {
self.data_dir.join("secrets")
}
pub fn certs(&self) -> PathBuf {
self.data_dir.join("certs")
}
pub fn raft(&self) -> PathBuf {
self.data_dir.join("raft")
}
pub fn admin_password(&self) -> PathBuf {
self.data_dir.join("admin_password")
}
#[must_use]
pub fn admin_bearer_path(&self) -> PathBuf {
self.data_dir.join("admin_bearer.token")
}
pub fn daemon_json(&self) -> PathBuf {
self.data_dir.join("daemon.json")
}
pub fn agent_ipam_state(&self) -> PathBuf {
self.data_dir.join("agent_ipam.json")
}
pub fn agent_network_state(&self) -> PathBuf {
self.data_dir.join("agent_network.json")
}
pub fn logs(&self) -> PathBuf {
self.data_dir.join("logs")
}
pub fn vms(&self) -> PathBuf {
self.data_dir.join("vms")
}
pub fn images(&self) -> PathBuf {
self.data_dir.join("images")
}
pub fn bin(&self) -> PathBuf {
self.data_dir.join("bin")
}
#[must_use]
pub fn buildd_bin(&self) -> PathBuf {
self.bin().join("zlayer-buildd")
}
#[must_use]
pub fn buildd(&self) -> PathBuf {
self.data_dir.join("buildd")
}
pub fn toolchain_cache(&self) -> PathBuf {
self.data_dir.join("toolchain-cache")
}
pub fn tmp(&self) -> PathBuf {
self.data_dir.join("tmp")
}
pub fn scratch_dir(&self, prefix: &str) -> std::io::Result<zlayer_types::Scratch> {
std::fs::create_dir_all(self.tmp())?;
let td = tempfile::Builder::new()
.prefix(prefix)
.tempdir_in(self.tmp())?;
Ok(zlayer_types::Scratch::from_tempdir(td))
}
pub fn scratch_file(&self, prefix: &str) -> std::io::Result<zlayer_types::ScratchFile> {
std::fs::create_dir_all(self.tmp())?;
let nf = tempfile::Builder::new()
.prefix(prefix)
.tempfile_in(self.tmp())?;
Ok(zlayer_types::ScratchFile::from_named(nf))
}
pub fn wireguard(&self) -> PathBuf {
#[cfg(any(target_os = "macos", target_os = "windows"))]
let natural = self.data_dir.join("run").join("wireguard");
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
let natural = if self.data_dir == Self::default_data_dir() {
PathBuf::from("/var/run/wireguard")
} else {
self.data_dir.join("run").join("wireguard")
};
const WG_DIR_MAX: usize = SUN_PATH_MAX - 21;
if natural.to_string_lossy().len() <= WG_DIR_MAX {
natural
} else {
PathBuf::from(format!(
"/tmp/zlayer-wg-{:016x}",
hash_for_socket(&self.data_dir, "wg")
))
}
}
}
#[must_use]
pub fn default_admin_bearer_path() -> PathBuf {
ZLayerDirs::system_default().admin_bearer_path()
}
pub(crate) fn platform_default_data_dir() -> PathBuf {
#[cfg(target_os = "macos")]
{
home_dir_or_fallback().join(".zlayer")
}
#[cfg(target_os = "windows")]
{
windows_program_data_root()
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
if is_root() {
PathBuf::from("/var/lib/zlayer")
} else {
home_dir_or_fallback().join(".zlayer")
}
}
}
const SUN_PATH_MAX: usize = 103;
fn hash_for_socket(data_dir: &Path, label: &str) -> u64 {
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for b in data_dir.to_string_lossy().as_bytes() {
h ^= u64::from(*b);
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
for b in label.as_bytes() {
h ^= u64::from(*b);
h = h.wrapping_mul(0x0000_0100_0000_01b3);
}
h
}
#[cfg(not(target_os = "windows"))]
fn socket_safe_fallback(data_dir: &Path, label: &str) -> String {
format!(
"/tmp/zlayer-{label}-{:016x}.sock",
hash_for_socket(data_dir, label)
)
}
#[cfg(not(target_os = "windows"))]
fn home_dir_or_fallback() -> PathBuf {
std::env::var_os("HOME")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("/var/lib/zlayer"))
}
#[cfg(target_os = "windows")]
fn windows_program_data_root() -> PathBuf {
if let Some(program_data) = std::env::var_os("PROGRAMDATA") {
let mut p = PathBuf::from(program_data);
p.push("ZLayer");
p
} else {
PathBuf::from(r"C:\ProgramData\ZLayer")
}
}
#[cfg(unix)]
#[must_use]
pub fn is_root() -> bool {
unsafe { libc::geteuid() == 0 }
}
#[cfg(windows)]
#[must_use]
pub fn is_root() -> bool {
use windows::Win32::UI::Shell::IsUserAnAdmin;
unsafe { IsUserAnAdmin().as_bool() }
}
#[cfg(not(any(unix, windows)))]
#[must_use]
pub fn is_root() -> bool {
false
}
#[cfg(test)]
mod tests {
use super::*;
static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[test]
fn subdirectories_are_relative_to_data_dir() {
let dirs = ZLayerDirs::new("/test/data");
assert_eq!(dirs.containers(), PathBuf::from("/test/data/containers"));
assert_eq!(dirs.rootfs(), PathBuf::from("/test/data/rootfs"));
assert_eq!(dirs.bundles(), PathBuf::from("/test/data/bundles"));
assert_eq!(dirs.cache(), PathBuf::from("/test/data/cache"));
assert_eq!(dirs.volumes(), PathBuf::from("/test/data/volumes"));
assert_eq!(dirs.wasm(), PathBuf::from("/test/data/wasm"));
assert_eq!(
dirs.wasm_compiled(),
PathBuf::from("/test/data/wasm/compiled")
);
assert_eq!(dirs.secrets(), PathBuf::from("/test/data/secrets"));
assert_eq!(dirs.certs(), PathBuf::from("/test/data/certs"));
assert_eq!(dirs.raft(), PathBuf::from("/test/data/raft"));
assert_eq!(
dirs.admin_password(),
PathBuf::from("/test/data/admin_password")
);
assert_eq!(dirs.daemon_json(), PathBuf::from("/test/data/daemon.json"));
assert_eq!(dirs.logs(), PathBuf::from("/test/data/logs"));
assert_eq!(dirs.vms(), PathBuf::from("/test/data/vms"));
assert_eq!(dirs.images(), PathBuf::from("/test/data/images"));
assert_eq!(dirs.bin(), PathBuf::from("/test/data/bin"));
assert_eq!(
dirs.buildd_bin(),
PathBuf::from("/test/data/bin/zlayer-buildd")
);
assert_eq!(dirs.buildd(), PathBuf::from("/test/data/buildd"));
assert_eq!(
dirs.toolchain_cache(),
PathBuf::from("/test/data/toolchain-cache")
);
assert_eq!(dirs.tmp(), PathBuf::from("/test/data/tmp"));
}
#[test]
fn system_default_uses_default_data_dir() {
let _env_guard = ENV_LOCK.lock().unwrap();
let dirs = ZLayerDirs::system_default();
assert_eq!(dirs.data_dir(), ZLayerDirs::default_data_dir().as_path());
}
#[test]
fn admin_bearer_path_is_under_data_dir() {
let dirs = ZLayerDirs::new(PathBuf::from("/var/lib/zlayer-test"));
assert_eq!(
dirs.admin_bearer_path(),
PathBuf::from("/var/lib/zlayer-test/admin_bearer.token")
);
}
#[test]
fn default_admin_bearer_path_matches_system_default() {
let _env_guard = ENV_LOCK.lock().unwrap();
assert_eq!(
default_admin_bearer_path(),
ZLayerDirs::system_default().admin_bearer_path()
);
}
#[cfg(target_os = "windows")]
#[test]
fn windows_default_data_dir_uses_program_data() {
let _env_guard = ENV_LOCK.lock().unwrap();
let prev = std::env::var_os("PROGRAMDATA");
std::env::set_var("PROGRAMDATA", r"C:\TestProgramData");
let data = ZLayerDirs::default_data_dir();
assert_eq!(data, PathBuf::from(r"C:\TestProgramData\ZLayer"));
let dirs = ZLayerDirs::system_default();
assert_eq!(dirs.certs(), data.join("certs"));
assert_eq!(dirs.secrets(), data.join("secrets"));
assert_eq!(dirs.logs(), data.join("logs"));
assert_eq!(ZLayerDirs::default_run_dir(), data.join("run"));
assert_eq!(ZLayerDirs::default_log_dir(), data.join("logs"));
assert_eq!(ZLayerDirs::default_socket_path(), "tcp://127.0.0.1:3669");
match prev {
Some(v) => std::env::set_var("PROGRAMDATA", v),
None => std::env::remove_var("PROGRAMDATA"),
}
}
#[test]
fn default_log_dir_for_returns_system_path_when_data_dir_is_default() {
let _env_guard = ENV_LOCK.lock().unwrap();
let system_default = ZLayerDirs::default_data_dir();
let result = ZLayerDirs::default_log_dir_for(&system_default);
assert_eq!(result, ZLayerDirs::default_log_dir());
}
#[test]
fn default_log_dir_for_returns_data_subdir_when_data_dir_overridden() {
let _env_guard = ENV_LOCK.lock().unwrap();
let tmp = tempfile::tempdir().expect("create tempdir");
let custom = tmp.path().to_path_buf();
let result = ZLayerDirs::default_log_dir_for(&custom);
assert_eq!(result, custom.join("logs"));
assert_ne!(result, ZLayerDirs::default_log_dir());
}
#[test]
fn default_run_dir_for_returns_system_path_when_data_dir_is_default() {
let _env_guard = ENV_LOCK.lock().unwrap();
let system_default = ZLayerDirs::default_data_dir();
let result = ZLayerDirs::default_run_dir_for(&system_default);
assert_eq!(result, ZLayerDirs::default_run_dir());
}
#[test]
fn default_run_dir_for_returns_data_subdir_when_data_dir_overridden() {
let _env_guard = ENV_LOCK.lock().unwrap();
let tmp = tempfile::tempdir().expect("create tempdir");
let custom = tmp.path().to_path_buf();
let result = ZLayerDirs::default_run_dir_for(&custom);
assert_eq!(result, custom.join("run"));
assert_ne!(result, ZLayerDirs::default_run_dir());
}
#[test]
fn default_socket_path_for_returns_system_path_when_data_dir_is_default() {
let _env_guard = ENV_LOCK.lock().unwrap();
let system_default = ZLayerDirs::default_data_dir();
let result = ZLayerDirs::default_socket_path_for(&system_default);
assert_eq!(result, ZLayerDirs::default_socket_path());
}
#[cfg(not(target_os = "windows"))]
#[test]
fn default_socket_path_for_returns_data_subdir_when_data_dir_overridden() {
let _env_guard = ENV_LOCK.lock().unwrap();
let tmp = tempfile::tempdir().expect("create tempdir");
let custom = tmp.path().to_path_buf();
let result = ZLayerDirs::default_socket_path_for(&custom);
let expected = custom
.join("run")
.join("zlayer.sock")
.to_string_lossy()
.into_owned();
assert_eq!(result, expected);
assert_ne!(result, ZLayerDirs::default_socket_path());
}
#[cfg(target_os = "windows")]
#[test]
fn default_socket_path_for_always_tcp_on_windows() {
let _env_guard = ENV_LOCK.lock().unwrap();
let tmp = tempfile::tempdir().expect("create tempdir");
let custom = tmp.path().to_path_buf();
assert_eq!(
ZLayerDirs::default_socket_path_for(&custom),
"tcp://127.0.0.1:3669"
);
}
#[cfg(not(target_os = "windows"))]
#[test]
fn default_socket_path_uses_data_subdir_when_env_var_overrides() {
let _env_guard = ENV_LOCK.lock().unwrap();
let prev = std::env::var_os("ZLAYER_DATA_DIR");
let tmp = tempfile::tempdir().expect("create tempdir");
let custom = tmp.path().to_path_buf();
std::env::set_var("ZLAYER_DATA_DIR", &custom);
let result = ZLayerDirs::default_socket_path();
let expected = custom
.join("run")
.join("zlayer.sock")
.to_string_lossy()
.into_owned();
assert_eq!(result, expected);
match prev {
Some(v) => std::env::set_var("ZLAYER_DATA_DIR", v),
None => std::env::remove_var("ZLAYER_DATA_DIR"),
}
}
#[cfg(target_os = "windows")]
#[test]
fn default_socket_path_uses_data_subdir_when_env_var_overrides() {
let _env_guard = ENV_LOCK.lock().unwrap();
let prev = std::env::var_os("ZLAYER_DATA_DIR");
let tmp = tempfile::tempdir().expect("create tempdir");
let custom = tmp.path().to_path_buf();
std::env::set_var("ZLAYER_DATA_DIR", &custom);
assert_eq!(ZLayerDirs::default_socket_path(), "tcp://127.0.0.1:3669");
match prev {
Some(v) => std::env::set_var("ZLAYER_DATA_DIR", v),
None => std::env::remove_var("ZLAYER_DATA_DIR"),
}
}
#[cfg(target_os = "windows")]
#[test]
fn windows_default_data_dir_fallback_when_env_missing() {
let _env_guard = ENV_LOCK.lock().unwrap();
let prev = std::env::var_os("PROGRAMDATA");
std::env::remove_var("PROGRAMDATA");
let data = ZLayerDirs::default_data_dir();
assert_eq!(data, PathBuf::from(r"C:\ProgramData\ZLayer"));
if let Some(v) = prev {
std::env::set_var("PROGRAMDATA", v);
}
}
#[test]
fn default_docker_socket_path_not_empty() {
let result = ZLayerDirs::default_docker_socket_path();
assert!(!result.is_empty());
}
#[cfg(target_os = "windows")]
#[test]
fn default_docker_socket_path_platform_shape() {
let result = ZLayerDirs::default_docker_socket_path();
assert!(result.starts_with(r"\\.\pipe"));
}
#[cfg(target_os = "macos")]
#[test]
fn default_docker_socket_path_platform_shape() {
let result = ZLayerDirs::default_docker_socket_path();
assert!(result.ends_with("/docker.sock"));
}
#[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
#[test]
fn default_docker_socket_path_platform_shape() {
let result = ZLayerDirs::default_docker_socket_path();
assert!(result.ends_with("/docker.sock"));
}
#[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
#[test]
fn wireguard_returns_fhs_path_on_default_data_dir() {
let dirs = ZLayerDirs::system_default();
assert_eq!(dirs.wireguard(), PathBuf::from("/var/run/wireguard"));
}
#[test]
fn wireguard_returns_data_subdir_when_overridden() {
let tmp = tempfile::tempdir().expect("create tempdir");
let custom = tmp.path().to_path_buf();
let dirs = ZLayerDirs::new(&custom);
let result = dirs.wireguard();
assert_eq!(result, custom.join("run").join("wireguard"));
#[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
assert_ne!(result, PathBuf::from("/var/run/wireguard"));
}
#[cfg(target_os = "macos")]
#[test]
fn wireguard_always_returns_data_subdir_on_macos() {
let dirs = ZLayerDirs::system_default();
let expected = ZLayerDirs::default_data_dir().join("run").join("wireguard");
assert_eq!(dirs.wireguard(), expected);
}
#[test]
fn scratch_dir_under_data_tmp() {
let parent = tempfile::tempdir().expect("parent");
let dirs = ZLayerDirs::new(parent.path());
let s = dirs.scratch_dir("zlayer-test-").expect("scratch_dir");
assert!(s.path().starts_with(dirs.tmp()));
assert!(s.path().is_dir());
let kept = s.path().to_path_buf();
drop(s);
assert!(!kept.exists());
}
#[test]
fn scratch_file_under_data_tmp() {
let parent = tempfile::tempdir().expect("parent");
let dirs = ZLayerDirs::new(parent.path());
let f = dirs.scratch_file("zlayer-test-").expect("scratch_file");
assert!(f.path().starts_with(dirs.tmp()));
assert!(f.path().is_file());
let kept = f.path().to_path_buf();
drop(f);
assert!(!kept.exists());
}
#[cfg(not(target_os = "windows"))]
#[test]
fn default_socket_path_for_falls_back_when_path_too_long() {
let deep = PathBuf::from(
"/var/lib/forgejo-runner/workdir/9dbc274201705d7d/hostexecutor/target/zlayer-e2e/cluster_3node/node1/data",
);
let result = ZLayerDirs::default_socket_path_for(&deep);
assert!(
result.len() <= SUN_PATH_MAX,
"fallback path overflows sun_path: len={} path={}",
result.len(),
result,
);
assert_eq!(result, ZLayerDirs::default_socket_path_for(&deep));
let other = deep.parent().unwrap().to_path_buf();
assert_ne!(result, ZLayerDirs::default_socket_path_for(&other));
}
#[cfg(not(target_os = "windows"))]
#[test]
fn default_socket_path_for_keeps_natural_path_when_short() {
let tmp = tempfile::tempdir().expect("create tempdir");
let short = tmp.path().to_path_buf();
assert!(short.to_string_lossy().len() < 80);
let result = ZLayerDirs::default_socket_path_for(&short);
assert!(result.ends_with("/run/zlayer.sock"));
assert!(result.starts_with(&*short.to_string_lossy()));
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
#[test]
fn wireguard_dir_falls_back_when_path_too_long() {
let deep = PathBuf::from(
"/var/lib/forgejo-runner/workdir/9dbc274201705d7d/hostexecutor/target/zlayer-e2e/cluster_3node/node1/data",
);
let dirs = ZLayerDirs::new(&deep);
let wg = dirs.wireguard();
assert!(
wg.to_string_lossy().len() + 21 <= SUN_PATH_MAX,
"wireguard dir + ifname overflows: dir={}",
wg.display(),
);
}
}