use std::path::{Path, PathBuf};
const MAX_SOCKET_PATH: usize = 100;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DaemonPaths {
pub db_path: PathBuf,
pub socket: PathBuf,
pub spawn_lock: PathBuf,
pub pid_file: PathBuf,
pub log_file: PathBuf,
}
impl DaemonPaths {
pub fn new(run_dir: &Path, db_path: &Path) -> Self {
let db_path = canonical_db_path(db_path);
let key = format!("{:016x}", fnv1a64(db_path.to_string_lossy().as_bytes()));
Self {
socket: socket_path(run_dir, &key),
spawn_lock: run_dir.join(format!("{key}.lock")),
pid_file: run_dir.join(format!("{key}.pid")),
log_file: run_dir.join(format!("{key}.log")),
db_path,
}
}
pub fn for_db(db_path: &Path) -> Self {
Self::new(&default_run_dir(), db_path)
}
pub fn run_dir(&self) -> &Path {
self.pid_file.parent().unwrap_or(Path::new("."))
}
pub fn socket_is_external(&self) -> bool {
self.socket.parent() != Some(self.run_dir())
}
}
pub fn default_run_dir() -> PathBuf {
std::env::var_os("HOME")
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("."))
.join(".scryer")
.join("run")
}
pub fn is_local_db(db_url: &str) -> bool {
let lower = db_url.to_ascii_lowercase();
!(db_url == ":memory:"
|| lower.starts_with("turso:")
|| lower.starts_with("libsql:")
|| lower.starts_with("http:")
|| lower.starts_with("https:"))
}
fn socket_path(run_dir: &Path, key: &str) -> PathBuf {
let preferred = run_dir.join(format!("{key}.sock"));
if preferred.as_os_str().len() <= MAX_SOCKET_PATH {
return preferred;
}
let short_dir = std::env::var_os("XDG_RUNTIME_DIR")
.map(PathBuf::from)
.filter(|dir| dir.is_absolute() && dir.as_os_str().len() < 64)
.unwrap_or_else(|| PathBuf::from("/tmp"));
short_dir.join(format!("scryer-{key}.sock"))
}
fn canonical_db_path(db_path: &Path) -> PathBuf {
if let Ok(path) = dunce::canonicalize(db_path) {
return path;
}
let absolute = std::path::absolute(db_path).unwrap_or_else(|_| db_path.to_path_buf());
match (absolute.parent(), absolute.file_name()) {
(Some(parent), Some(name)) => dunce::canonicalize(parent)
.map(|p| p.join(name))
.unwrap_or(absolute),
_ => absolute,
}
}
fn fnv1a64(bytes: &[u8]) -> u64 {
bytes.iter().fold(0xcbf2_9ce4_8422_2325, |hash, byte| {
(hash ^ u64::from(*byte)).wrapping_mul(0x0100_0000_01b3)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn same_db_maps_to_same_socket() {
let dir = tempfile::tempdir().unwrap();
let db = dir.path().join("scryer.db");
let a = DaemonPaths::new(dir.path(), &db);
let b = DaemonPaths::new(dir.path(), &dir.path().join(".").join("scryer.db"));
assert_eq!(a.socket, b.socket);
assert_ne!(
a.socket,
DaemonPaths::new(dir.path(), &dir.path().join("other.db")).socket
);
}
#[test]
fn long_run_dirs_fall_back_to_short_socket_paths() {
let long_dir = PathBuf::from("/").join("d".repeat(120));
let paths = DaemonPaths::new(&long_dir, &long_dir.join("scryer.db"));
assert!(paths.socket.as_os_str().len() <= MAX_SOCKET_PATH);
assert!(paths.socket_is_external());
assert_eq!(paths.run_dir(), long_dir.as_path());
let short = DaemonPaths::new(Path::new("/home/me/.scryer/run"), Path::new("/x.db"));
assert!(!short.socket_is_external());
}
#[test]
fn remote_and_memory_urls_are_not_local() {
assert!(is_local_db("/home/me/.scryer/scryer.db"));
assert!(!is_local_db(":memory:"));
assert!(!is_local_db("libsql://db.turso.io"));
assert!(!is_local_db("https://db.turso.io"));
}
}