use boxlite_shared::errors::{BoxliteError, BoxliteResult};
use std::path::{Path, PathBuf};
const MAX_SUN_PATH: usize = 104;
const SYMLINK_PREFIX: &str = "bl_";
#[derive(Debug)]
pub struct SocketShortener {
symlink_path: PathBuf,
real_dir: PathBuf,
}
impl SocketShortener {
pub fn new(short_id: &str, sockets_dir: &Path) -> BoxliteResult<Option<Self>> {
let longest_real = sockets_dir.join("ready.sock");
if longest_real.as_os_str().len() < MAX_SUN_PATH {
return Ok(None);
}
let symlink_path = std::env::temp_dir().join(format!("{SYMLINK_PREFIX}{short_id}"));
let longest_short = symlink_path.join("ready.sock");
if longest_short.as_os_str().len() >= MAX_SUN_PATH {
return Err(BoxliteError::Internal(format!(
"Socket path '{}' ({} bytes) exceeds sun_path limit ({} bytes) \
even with symlink shortening. Use a shorter temp directory.",
longest_short.display(),
longest_short.as_os_str().len(),
MAX_SUN_PATH,
)));
}
match std::fs::symlink_metadata(&symlink_path) {
Ok(meta) if meta.file_type().is_symlink() => {
let _ = std::fs::remove_file(&symlink_path);
}
Ok(_) => {
return Err(BoxliteError::Internal(format!(
"{} exists but is not a symlink — refusing to overwrite",
symlink_path.display(),
)));
}
Err(_) => {
}
}
std::os::unix::fs::symlink(sockets_dir, &symlink_path).map_err(|e| {
BoxliteError::Storage(format!(
"Failed to create socket symlink {} → {}: {}",
symlink_path.display(),
sockets_dir.display(),
e,
))
})?;
tracing::debug!(
symlink = %symlink_path.display(),
target = %sockets_dir.display(),
"Created socket path shortener symlink"
);
Ok(Some(Self {
symlink_path,
real_dir: sockets_dir.to_path_buf(),
}))
}
pub fn short_path(&self, socket_name: &str) -> PathBuf {
self.symlink_path.join(socket_name)
}
pub fn symlink_dir(&self) -> &Path {
&self.symlink_path
}
pub fn real_dir(&self) -> &Path {
&self.real_dir
}
#[allow(clippy::collapsible_if)]
pub fn cleanup(&self) {
if let Err(e) = std::fs::remove_file(&self.symlink_path) {
if e.kind() != std::io::ErrorKind::NotFound {
tracing::warn!(
path = %self.symlink_path.display(),
error = %e,
"Failed to remove socket shortener symlink"
);
}
}
}
}
impl Drop for SocketShortener {
fn drop(&mut self) {
self.cleanup();
}
}
pub fn resolve_socket_path(
shortener: Option<&SocketShortener>,
real_path: &Path,
socket_name: &str,
) -> PathBuf {
match shortener {
Some(s) => s.short_path(socket_name),
None => real_path.to_path_buf(),
}
}
pub fn cleanup_stale_symlinks() {
let tmp_dir = std::env::temp_dir();
let Ok(entries) = std::fs::read_dir(&tmp_dir) else {
return;
};
for entry in entries.flatten() {
let name = entry.file_name();
let Some(name_str) = name.to_str() else {
continue;
};
if !name_str.starts_with(SYMLINK_PREFIX) {
continue;
}
let path = entry.path();
if let Ok(meta) = std::fs::symlink_metadata(&path) {
if meta.file_type().is_symlink() && !path.exists() {
tracing::debug!(
path = %path.display(),
"Removing stale socket shortener symlink"
);
let _ = std::fs::remove_file(&path);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::os::unix::fs::symlink;
fn create_deep_sockets_dir(base: &Path) -> PathBuf {
let deep = base
.join("very_long_directory_name_that_keeps_going")
.join("and_another_long_segment_here_too")
.join("sockets");
std::fs::create_dir_all(&deep).unwrap();
assert!(
deep.join("ready.sock").as_os_str().len() >= MAX_SUN_PATH,
"Test setup: deep path {} ({} bytes) must exceed {} bytes",
deep.join("ready.sock").display(),
deep.join("ready.sock").as_os_str().len(),
MAX_SUN_PATH,
);
deep
}
#[test]
fn new_returns_none_when_path_fits() {
let tmp = tempfile::TempDir::new().unwrap();
let sockets_dir = tmp.path().join("sockets");
std::fs::create_dir_all(&sockets_dir).unwrap();
let result = SocketShortener::new("abcd1234", &sockets_dir).unwrap();
assert!(
result.is_none(),
"Should not create symlink for short paths"
);
}
#[test]
fn new_creates_symlink_when_path_too_long() {
let tmp = tempfile::TempDir::new().unwrap();
let deep = create_deep_sockets_dir(tmp.path());
let shortener = SocketShortener::new("long1234", &deep)
.unwrap()
.expect("Should create symlink for long paths");
let meta = std::fs::symlink_metadata(shortener.symlink_dir()).unwrap();
assert!(meta.file_type().is_symlink());
let target = std::fs::read_link(shortener.symlink_dir()).unwrap();
assert_eq!(target, deep);
}
#[test]
fn new_short_path_within_limit_for_all_sockets() {
let tmp = tempfile::TempDir::new().unwrap();
let deep = create_deep_sockets_dir(tmp.path());
if let Some(shortener) = SocketShortener::new("limit123", &deep).unwrap() {
for name in ["box.sock", "ready.sock", "net.sock"] {
let short = shortener.short_path(name);
assert!(
short.as_os_str().len() < MAX_SUN_PATH,
"Short path '{}' ({} bytes) must be < {} bytes",
short.display(),
short.as_os_str().len(),
MAX_SUN_PATH,
);
}
}
}
#[test]
fn new_replaces_stale_symlink() {
let tmp = tempfile::TempDir::new().unwrap();
let deep = create_deep_sockets_dir(tmp.path());
let s1 = SocketShortener::new("stale123", &deep).unwrap().unwrap();
let symlink_path = s1.symlink_dir().to_path_buf();
drop(s1);
symlink(Path::new("/nonexistent/stale/path"), &symlink_path).unwrap();
assert!(
std::fs::symlink_metadata(&symlink_path)
.unwrap()
.file_type()
.is_symlink()
);
let s2 = SocketShortener::new("stale123", &deep).unwrap().unwrap();
let target = std::fs::read_link(s2.symlink_dir()).unwrap();
assert_eq!(
target, deep,
"Should point to the new target, not the stale one"
);
}
#[test]
fn new_refuses_to_overwrite_regular_file() {
let tmp = tempfile::TempDir::new().unwrap();
let deep = create_deep_sockets_dir(tmp.path());
let blocker_path = std::env::temp_dir().join(format!("{SYMLINK_PREFIX}block123"));
std::fs::write(&blocker_path, "I am not a symlink").unwrap();
let result = SocketShortener::new("block123", &deep);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("not a symlink"),
"Error should mention 'not a symlink', got: {err}"
);
let _ = std::fs::remove_file(&blocker_path);
}
#[test]
fn new_refuses_to_overwrite_directory() {
let tmp = tempfile::TempDir::new().unwrap();
let deep = create_deep_sockets_dir(tmp.path());
let dir_path = std::env::temp_dir().join(format!("{SYMLINK_PREFIX}dir_1234"));
let _ = std::fs::remove_dir_all(&dir_path);
std::fs::create_dir_all(&dir_path).unwrap();
let result = SocketShortener::new("dir_1234", &deep);
assert!(result.is_err());
let _ = std::fs::remove_dir_all(&dir_path);
}
#[test]
fn cleanup_removes_symlink() {
let tmp = tempfile::TempDir::new().unwrap();
let deep = create_deep_sockets_dir(tmp.path());
let shortener = SocketShortener::new("cln_1234", &deep).unwrap().unwrap();
let path = shortener.symlink_dir().to_path_buf();
assert!(std::fs::symlink_metadata(&path).is_ok());
shortener.cleanup();
assert!(
std::fs::symlink_metadata(&path).is_err(),
"Symlink should be removed after cleanup"
);
}
#[test]
fn cleanup_is_idempotent() {
let tmp = tempfile::TempDir::new().unwrap();
let deep = create_deep_sockets_dir(tmp.path());
let shortener = SocketShortener::new("idem1234", &deep).unwrap().unwrap();
shortener.cleanup();
shortener.cleanup(); }
#[test]
fn drop_removes_symlink() {
let tmp = tempfile::TempDir::new().unwrap();
let deep = create_deep_sockets_dir(tmp.path());
let shortener = SocketShortener::new("drp_1234", &deep).unwrap().unwrap();
let path = shortener.symlink_dir().to_path_buf();
drop(shortener);
assert!(
std::fs::symlink_metadata(&path).is_err(),
"Symlink should be removed on Drop"
);
}
#[test]
fn real_dir_returns_original_path() {
let tmp = tempfile::TempDir::new().unwrap();
let deep = create_deep_sockets_dir(tmp.path());
let shortener = SocketShortener::new("real1234", &deep).unwrap().unwrap();
assert_eq!(shortener.real_dir(), deep);
}
#[test]
fn symlink_dir_is_in_temp() {
let tmp = tempfile::TempDir::new().unwrap();
let deep = create_deep_sockets_dir(tmp.path());
let shortener = SocketShortener::new("temp1234", &deep).unwrap().unwrap();
assert!(shortener.symlink_dir().starts_with(std::env::temp_dir()));
}
#[test]
fn resolve_returns_short_path_with_shortener() {
let tmp = tempfile::TempDir::new().unwrap();
let deep = create_deep_sockets_dir(tmp.path());
let shortener = SocketShortener::new("res_1234", &deep).unwrap().unwrap();
let real_path = deep.join("box.sock");
let resolved = resolve_socket_path(Some(&shortener), &real_path, "box.sock");
assert!(resolved.starts_with(std::env::temp_dir()));
assert!(resolved.ends_with("box.sock"));
assert!(resolved.as_os_str().len() < MAX_SUN_PATH);
}
#[test]
fn resolve_returns_real_path_without_shortener() {
let real = PathBuf::from("/some/long/path/sockets/box.sock");
let resolved = resolve_socket_path(None, &real, "box.sock");
assert_eq!(resolved, real);
}
#[test]
fn stale_cleanup_removes_dead_symlinks() {
let dead_link = std::env::temp_dir().join(format!("{SYMLINK_PREFIX}dead_test"));
let _ = std::fs::remove_file(&dead_link);
symlink(Path::new("/nonexistent/target/for/test"), &dead_link).unwrap();
cleanup_stale_symlinks();
assert!(
std::fs::symlink_metadata(&dead_link).is_err(),
"Dead symlink should be removed"
);
}
#[test]
fn stale_cleanup_keeps_live_symlinks() {
let tmp = tempfile::TempDir::new().unwrap();
let live_target = tmp.path().join("live_target");
std::fs::create_dir_all(&live_target).unwrap();
let live_link = std::env::temp_dir().join(format!("{SYMLINK_PREFIX}live_test"));
let _ = std::fs::remove_file(&live_link);
symlink(&live_target, &live_link).unwrap();
cleanup_stale_symlinks();
assert!(
std::fs::symlink_metadata(&live_link).is_ok(),
"Live symlink should be kept"
);
let _ = std::fs::remove_file(&live_link);
}
#[test]
fn stale_cleanup_ignores_non_prefixed_entries() {
let dead_link = std::env::temp_dir().join("not_bl_prefixed_test_link");
let _ = std::fs::remove_file(&dead_link);
symlink(Path::new("/nonexistent/unrelated"), &dead_link).unwrap();
cleanup_stale_symlinks();
assert!(
std::fs::symlink_metadata(&dead_link).is_ok(),
"Non-prefixed symlink should NOT be removed"
);
let _ = std::fs::remove_file(&dead_link);
}
#[test]
fn bind_and_connect_through_symlink_works() {
let tmp = tempfile::TempDir::new().unwrap();
let real_dir = tmp.path().join("real_sockets");
std::fs::create_dir_all(&real_dir).unwrap();
let short_link = tmp.path().join("s");
symlink(&real_dir, &short_link).unwrap();
let sock_path = short_link.join("test.sock");
let listener = std::os::unix::net::UnixListener::bind(&sock_path).unwrap();
assert!(
real_dir.join("test.sock").exists(),
"Socket file should exist in real directory, not just via symlink"
);
let _stream = std::os::unix::net::UnixStream::connect(&sock_path).unwrap();
drop(listener);
}
#[test]
fn bind_through_symlink_with_long_real_path() {
let tmp = tempfile::TempDir::new().unwrap();
let deep = create_deep_sockets_dir(tmp.path());
let short_link = tmp.path().join("s");
symlink(&deep, &short_link).unwrap();
let short_path = short_link.join("kernel_test.sock");
assert!(
short_path.as_os_str().len() < MAX_SUN_PATH,
"Short path should be within sun_path limit"
);
let listener = std::os::unix::net::UnixListener::bind(&short_path).unwrap();
let _stream = std::os::unix::net::UnixStream::connect(&short_path).unwrap();
assert!(deep.join("kernel_test.sock").exists());
drop(listener);
}
}