use std::path::PathBuf;
pub const ENV_LOG_DIR: &str = "DIG_LOG_DIR";
const SID_USERS: &str = "S-1-5-32-545";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogDirSource {
Override,
MachineRoot,
DevFallback,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedLogDir {
pub path: PathBuf,
pub source: LogDirSource,
}
pub fn resolve_log_dir<G, C>(service: &str, get: G, can_create: C) -> PathBuf
where
G: Fn(&str) -> Option<String>,
C: Fn(&std::path::Path) -> bool,
{
resolve_log_dir_detailed(service, get, can_create).path
}
pub fn resolve_log_dir_detailed<G, C>(service: &str, get: G, can_create: C) -> ResolvedLogDir
where
G: Fn(&str) -> Option<String>,
C: Fn(&std::path::Path) -> bool,
{
let read = |key: &str| {
get(key)
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
};
if let Some(root) = read(ENV_LOG_DIR) {
return ResolvedLogDir {
path: PathBuf::from(root).join(service),
source: LogDirSource::Override,
};
}
let machine = machine_root(&read).join(service);
if can_create(&machine) {
return ResolvedLogDir {
path: machine,
source: LogDirSource::MachineRoot,
};
}
ResolvedLogDir {
path: dev_root(&read).join(service),
source: LogDirSource::DevFallback,
}
}
pub fn log_dir(service: &str) -> PathBuf {
let resolved = resolve_log_dir_detailed(
service,
|key| std::env::var(key).ok(),
|path| std::fs::create_dir_all(path).is_ok(),
);
#[cfg(windows)]
if resolved.source == LogDirSource::MachineRoot {
grant_operator_read(&resolved.path);
}
resolved.path
}
pub fn windows_operator_read_args(dir: &str) -> Vec<String> {
vec![
dir.to_string(),
"/grant:r".to_string(),
format!("*{SID_USERS}:(OI)(CI)RX"),
"/T".to_string(),
"/C".to_string(),
"/Q".to_string(),
]
}
#[cfg(windows)]
fn grant_operator_read(dir: &std::path::Path) {
let Some(dir) = dir.to_str() else { return };
let _ = std::process::Command::new("icacls")
.args(windows_operator_read_args(dir))
.output();
}
#[cfg(windows)]
fn machine_root<R: Fn(&str) -> Option<String>>(read: &R) -> PathBuf {
let base = read("ProgramData").unwrap_or_else(|| r"C:\ProgramData".to_string());
PathBuf::from(base).join("DigNetwork").join("logs")
}
#[cfg(target_os = "macos")]
fn machine_root<R: Fn(&str) -> Option<String>>(_read: &R) -> PathBuf {
PathBuf::from("/Library/Logs/DigNetwork")
}
#[cfg(all(unix, not(target_os = "macos")))]
fn machine_root<R: Fn(&str) -> Option<String>>(_read: &R) -> PathBuf {
PathBuf::from("/var/log/dig")
}
#[cfg(windows)]
fn dev_root<R: Fn(&str) -> Option<String>>(read: &R) -> PathBuf {
let base = read("LOCALAPPDATA")
.or_else(|| read("ProgramData"))
.unwrap_or_else(|| r"C:\ProgramData".to_string());
PathBuf::from(base).join("DigNetwork").join("logs")
}
#[cfg(target_os = "macos")]
fn dev_root<R: Fn(&str) -> Option<String>>(read: &R) -> PathBuf {
let home = read("HOME").unwrap_or_else(|| "/tmp".to_string());
PathBuf::from(home)
.join("Library")
.join("Logs")
.join("DigNetwork")
}
#[cfg(all(unix, not(target_os = "macos")))]
fn dev_root<R: Fn(&str) -> Option<String>>(read: &R) -> PathBuf {
if let Some(state) = read("XDG_STATE_HOME") {
return PathBuf::from(state).join("dig").join("logs");
}
let home = read("HOME").unwrap_or_else(|| "/tmp".to_string());
PathBuf::from(home)
.join(".local")
.join("state")
.join("dig")
.join("logs")
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
use std::path::Path;
fn env(pairs: &[(&str, &str)]) -> impl Fn(&str) -> Option<String> {
let map: HashMap<String, String> = pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
move |key| map.get(key).cloned()
}
#[test]
fn override_wins_and_joins_service() {
let dir = resolve_log_dir("dig-node", env(&[(ENV_LOG_DIR, "/custom/root")]), |_| true);
assert_eq!(dir, Path::new("/custom/root").join("dig-node"));
}
#[test]
fn blank_override_is_ignored() {
let dir = resolve_log_dir("dig-dns", env(&[(ENV_LOG_DIR, " ")]), |_| true);
assert!(dir.ends_with(Path::new("dig-dns")));
assert!(!dir.starts_with("/custom"));
}
#[test]
fn machine_root_used_when_creatable() {
let dir = resolve_log_dir("dig-updater", env(&[]), |_| true);
assert!(dir.ends_with(Path::new("dig-updater")));
#[cfg(all(unix, not(target_os = "macos")))]
assert_eq!(dir, Path::new("/var/log/dig/dig-updater"));
#[cfg(target_os = "macos")]
assert_eq!(dir, Path::new("/Library/Logs/DigNetwork/dig-updater"));
}
#[test]
fn dev_fallback_when_machine_root_not_creatable() {
let dir = resolve_log_dir(
"dig-node",
env(&[
("HOME", "/home/dev"),
("XDG_STATE_HOME", "/home/dev/.state"),
("LOCALAPPDATA", r"C:\Users\dev\AppData\Local"),
]),
|path: &Path| path.to_string_lossy().contains("dev"),
);
assert!(dir.ends_with(Path::new("dig-node")));
#[cfg(all(unix, not(target_os = "macos")))]
assert_eq!(dir, Path::new("/home/dev/.state/dig/logs/dig-node"));
}
#[cfg(all(unix, not(target_os = "macos")))]
#[test]
fn linux_dev_fallback_without_xdg_uses_local_state() {
let dir = resolve_log_dir("dig-dns", env(&[("HOME", "/home/dev")]), |_| false);
assert_eq!(dir, Path::new("/home/dev/.local/state/dig/logs/dig-dns"));
}
#[test]
fn override_reports_override_source() {
let resolved =
resolve_log_dir_detailed("dig-node", env(&[(ENV_LOG_DIR, "/custom")]), |_| true);
assert_eq!(resolved.source, LogDirSource::Override);
}
#[test]
fn creatable_machine_root_reports_machine_source() {
let resolved = resolve_log_dir_detailed("dig-node", env(&[]), |_| true);
assert_eq!(resolved.source, LogDirSource::MachineRoot);
}
#[test]
fn uncreatable_machine_root_reports_dev_fallback_source() {
let resolved =
resolve_log_dir_detailed("dig-node", env(&[("HOME", "/home/dev")]), |_| false);
assert_eq!(resolved.source, LogDirSource::DevFallback);
}
#[test]
fn operator_read_grant_targets_users_sid_read_execute_inheritable() {
let args = windows_operator_read_args(r"C:\ProgramData\DigNetwork\logs\dig-node");
assert_eq!(args[0], r"C:\ProgramData\DigNetwork\logs\dig-node");
assert!(args.iter().any(|a| a == "/grant:r"));
assert!(args.iter().any(|a| a == "*S-1-5-32-545:(OI)(CI)RX"));
assert!(!args.iter().any(|a| a == "/inheritance:r" || a == "/reset"));
}
}