#![allow(dead_code, clippy::missing_const_for_fn)]
use std::path::{Path, PathBuf};
pub const RUNTIME_DIR_ENV: &str = "AUTUMN_RUNTIME_DIR";
const QUALIFIER: &str = "dev";
const ORGANIZATION: &str = "autumn";
const MAX_UNIX_SOCKET_PATH: usize = 100;
#[derive(Debug, thiserror::Error)]
pub enum PathsError {
#[error("could not determine a platform data directory for this user")]
NoPlatformDir,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RuntimePaths {
runtime: PathBuf,
data: PathBuf,
logs: PathBuf,
socket: PathBuf,
}
fn resolve_socket_path(runtime: &Path, project: &str) -> PathBuf {
let natural = runtime.join("serve.sock");
if natural.as_os_str().len() <= MAX_UNIX_SOCKET_PATH {
return natural;
}
short_socket_root()
.join(format!("autumn-{}", short_project_id(project)))
.join("s.sock")
}
fn short_project_id(project: &str) -> String {
use sha2::{Digest, Sha256};
hex::encode(&Sha256::digest(project.as_bytes())[..4])
}
fn short_socket_root() -> PathBuf {
std::env::var_os("XDG_RUNTIME_DIR").map_or_else(std::env::temp_dir, PathBuf::from)
}
impl RuntimePaths {
pub fn resolve(project: &str) -> Result<Self, PathsError> {
if let Some(base) = std::env::var_os(RUNTIME_DIR_ENV) {
let base = PathBuf::from(base);
let base = match (base.is_absolute(), std::env::current_dir()) {
(false, Ok(cwd)) => cwd.join(&base),
_ => base,
};
return Ok(Self::from_base(&base, project));
}
let dirs = directories::ProjectDirs::from(QUALIFIER, ORGANIZATION, project)
.ok_or(PathsError::NoPlatformDir)?;
let runtime = dirs
.runtime_dir()
.map_or_else(|| dirs.data_dir().join("run"), Path::to_path_buf);
let data = dirs.data_dir().to_path_buf();
let logs = dirs
.state_dir()
.map_or_else(|| dirs.data_dir().join("logs"), |s| s.join("logs"));
let socket = resolve_socket_path(&runtime, project);
Ok(Self {
runtime,
data,
logs,
socket,
})
}
#[must_use]
pub fn from_base(base: &Path, project: &str) -> Self {
let root = base.join(project);
let socket = resolve_socket_path(&root, project);
Self {
runtime: root.clone(),
data: root.clone(),
logs: root,
socket,
}
}
#[must_use]
pub fn pid_file(&self) -> PathBuf {
self.runtime.join("serve.pid")
}
#[must_use]
pub fn socket_file(&self) -> PathBuf {
self.socket.clone()
}
#[must_use]
pub fn addr_file(&self) -> PathBuf {
self.runtime.join("serve.addr")
}
#[must_use]
pub fn mode_file(&self) -> PathBuf {
self.runtime.join("serve.mode")
}
#[must_use]
pub fn ready_file(&self) -> PathBuf {
self.runtime.join("serve.ready")
}
#[must_use]
pub fn pg_data_dir(&self) -> PathBuf {
self.data.join("pg")
}
#[must_use]
pub fn log_file(&self) -> PathBuf {
self.logs.join("serve.log")
}
pub fn ensure_dirs(&self) -> std::io::Result<()> {
std::fs::create_dir_all(&self.runtime)?;
std::fs::create_dir_all(&self.logs)?;
std::fs::create_dir_all(&self.data)?;
#[cfg(unix)]
harden_private_dir(&self.runtime)?;
let socket_parent = self.socket.parent().unwrap_or(self.runtime.as_path());
std::fs::create_dir_all(socket_parent)?;
#[cfg(unix)]
harden_private_dir(socket_parent)?;
Ok(())
}
}
#[cfg(unix)]
fn harden_private_dir(dir: &Path) -> std::io::Result<()> {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
let meta = std::fs::symlink_metadata(dir)?;
if !meta.file_type().is_dir() {
return Err(std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
format!(
"refusing to use {} for the daemon socket: not a directory",
dir.display()
),
));
}
if meta.uid() != nix::unistd::geteuid().as_raw() {
return Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
format!(
"refusing to use {} for the daemon socket: not owned by the current user",
dir.display()
),
));
}
std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn paths_from_base_layout() {
let paths = RuntimePaths::from_base(Path::new("/var/run"), "demo");
assert_eq!(paths.pid_file(), Path::new("/var/run/demo/serve.pid"));
assert_eq!(paths.socket_file(), Path::new("/var/run/demo/serve.sock"));
assert_eq!(paths.addr_file(), Path::new("/var/run/demo/serve.addr"));
assert_eq!(paths.pg_data_dir(), Path::new("/var/run/demo/pg"));
assert_eq!(paths.log_file(), Path::new("/var/run/demo/serve.log"));
}
#[test]
fn socket_uses_natural_path_when_short() {
let paths = RuntimePaths::from_base(Path::new("/var/run"), "demo");
assert_eq!(paths.socket_file(), Path::new("/var/run/demo/serve.sock"));
assert!(paths.socket_file().as_os_str().len() <= MAX_UNIX_SOCKET_PATH);
}
#[cfg(unix)]
#[test]
fn ensure_dirs_makes_socket_parent_private() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().expect("tempdir");
let paths = RuntimePaths::from_base(dir.path(), "p");
paths.ensure_dirs().expect("ensure_dirs");
let parent = paths.socket_file().parent().unwrap().to_path_buf();
let mode = std::fs::metadata(&parent).unwrap().permissions().mode();
assert_eq!(mode & 0o777, 0o700, "socket dir must be owner-only");
}
#[cfg(unix)]
#[test]
fn harden_private_dir_rejects_symlink() {
let dir = tempfile::tempdir().expect("tempdir");
let target = dir.path().join("real");
std::fs::create_dir(&target).unwrap();
let link = dir.path().join("link");
std::os::unix::fs::symlink(&target, &link).unwrap();
assert!(harden_private_dir(&link).is_err());
}
#[test]
fn socket_falls_back_to_short_path_when_runtime_too_long() {
let long_base = format!("/{}", "longsegment".repeat(12)); temp_env::with_var("XDG_RUNTIME_DIR", None::<&str>, || {
let paths = RuntimePaths::from_base(Path::new(&long_base), "proj");
let sock = paths.socket_file();
assert!(
sock.as_os_str().len() <= MAX_UNIX_SOCKET_PATH,
"socket path still too long ({} bytes): {}",
sock.as_os_str().len(),
sock.display()
);
assert_ne!(sock, Path::new(&long_base).join("proj").join("serve.sock"));
assert!(paths.pid_file().starts_with(&long_base));
assert!(paths.addr_file().starts_with(&long_base));
});
}
#[cfg(unix)]
#[test]
fn paths_socket_absolute_under_base() {
let paths = RuntimePaths::from_base(Path::new("/tmp/xdg-base"), "svc");
let sock = paths.socket_file();
assert!(sock.is_absolute());
assert!(sock.starts_with("/tmp/xdg-base"));
}
#[test]
fn resolve_honors_env_override() {
let dir = tempfile::tempdir().expect("tempdir");
temp_env::with_var(RUNTIME_DIR_ENV, Some(dir.path()), || {
let paths = RuntimePaths::resolve("demo").expect("resolve with override");
assert_eq!(paths.pid_file(), dir.path().join("demo").join("serve.pid"));
assert!(paths.socket_file().starts_with(dir.path()));
});
}
}