use std::path::{Path, PathBuf};
use anyhow::{bail, Context, Result};
pub const MAX_SOCKET_PATH_LEN: usize = 104;
pub fn runtime_dir() -> Result<PathBuf> {
let base = dirs::data_dir().context("could not determine the user data directory")?;
Ok(base.join("omni-dev"))
}
pub fn socket_path() -> Result<PathBuf> {
Ok(runtime_dir()?.join("daemon.sock"))
}
pub fn token_path() -> Result<PathBuf> {
Ok(runtime_dir()?.join("bridge.token"))
}
pub fn token_path_for_socket(socket: &Path) -> PathBuf {
socket.parent().map_or_else(
|| PathBuf::from("bridge.token"),
|dir| dir.join("bridge.token"),
)
}
pub fn log_path_for_socket(socket: &Path) -> PathBuf {
socket
.parent()
.map_or_else(|| PathBuf::from("daemon.log"), |dir| dir.join("daemon.log"))
}
pub fn ensure_dir_0700(dir: &Path) -> Result<()> {
let mut builder = std::fs::DirBuilder::new();
builder.recursive(true);
#[cfg(unix)]
{
use std::os::unix::fs::DirBuilderExt;
builder.mode(0o700);
}
builder
.create(dir)
.with_context(|| format!("failed to create directory {}", dir.display()))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700))
.with_context(|| format!("failed to set 0700 on {}", dir.display()))?;
}
Ok(())
}
pub fn write_file_0600(path: &Path, contents: &[u8]) -> Result<()> {
use std::io::Write;
let mut options = std::fs::OpenOptions::new();
options.write(true).create(true).truncate(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
let mut file = options
.open(path)
.with_context(|| format!("failed to create file {}", path.display()))?;
ensure_handle_0600(&file)
.with_context(|| format!("failed to set 0600 on {}", path.display()))?;
file.write_all(contents)
.with_context(|| format!("failed to write file {}", path.display()))?;
Ok(())
}
pub fn ensure_handle_0600(file: &std::fs::File) -> Result<()> {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = file
.metadata()
.context("failed to read file metadata")?
.permissions()
.mode();
if mode & 0o077 != 0 {
file.set_permissions(std::fs::Permissions::from_mode(0o600))
.context("failed to set 0600 on open file")?;
}
}
#[cfg(not(unix))]
{
let _ = file;
}
Ok(())
}
pub fn set_file_0600(path: &Path) -> Result<()> {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
.with_context(|| format!("failed to set 0600 on {}", path.display()))?;
}
#[cfg(not(unix))]
{
let _ = path;
}
Ok(())
}
pub fn check_socket_path_len(path: &Path) -> Result<()> {
let len = path.as_os_str().len();
if len >= MAX_SOCKET_PATH_LEN {
bail!(
"socket path is {len} bytes, exceeding the {MAX_SOCKET_PATH_LEN}-byte limit: {} — \
pass a shorter --socket path",
path.display()
);
}
Ok(())
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn default_paths_sit_under_the_runtime_dir() {
let rt = runtime_dir().unwrap();
assert!(socket_path().unwrap().starts_with(&rt));
assert!(token_path().unwrap().starts_with(&rt));
}
#[test]
fn token_sits_beside_its_socket() {
assert_eq!(
token_path_for_socket(Path::new("/tmp/x/daemon.sock")),
Path::new("/tmp/x/bridge.token")
);
assert_eq!(
token_path_for_socket(Path::new("daemon.sock")),
Path::new("bridge.token")
);
}
#[test]
fn log_sits_beside_its_socket() {
assert_eq!(
log_path_for_socket(Path::new("/tmp/x/daemon.sock")),
Path::new("/tmp/x/daemon.log")
);
assert_eq!(
log_path_for_socket(Path::new("daemon.sock")),
Path::new("daemon.log")
);
}
#[test]
fn socket_path_length_guard() {
check_socket_path_len(Path::new("/tmp/short.sock")).unwrap();
let too_long = PathBuf::from(format!("/{}", "a".repeat(MAX_SOCKET_PATH_LEN)));
assert!(check_socket_path_len(&too_long).is_err());
}
#[test]
fn write_file_0600_creates_owner_only() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("fresh.token");
write_file_0600(&file, b"secret").unwrap();
assert_eq!(std::fs::read(&file).unwrap(), b"secret");
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
assert_eq!(
std::fs::metadata(&file).unwrap().permissions().mode() & 0o777,
0o600
);
}
}
#[cfg(unix)]
#[test]
fn write_file_0600_retightens_preexisting_loose_file() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("stale.token");
std::fs::write(&file, "old-secret-with-longer-content").unwrap();
std::fs::set_permissions(&file, std::fs::Permissions::from_mode(0o644)).unwrap();
write_file_0600(&file, b"new").unwrap();
assert_eq!(std::fs::read(&file).unwrap(), b"new");
assert_eq!(
std::fs::metadata(&file).unwrap().permissions().mode() & 0o777,
0o600
);
}
#[test]
fn ensure_dir_and_file_perms() {
let dir = tempfile::tempdir().unwrap();
let sub = dir.path().join("run");
ensure_dir_0700(&sub).unwrap();
assert!(sub.is_dir());
let file = sub.join("k");
std::fs::write(&file, "x").unwrap();
set_file_0600(&file).unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
assert_eq!(
std::fs::metadata(&sub).unwrap().permissions().mode() & 0o777,
0o700
);
assert_eq!(
std::fs::metadata(&file).unwrap().permissions().mode() & 0o777,
0o600
);
}
}
}