use std::path::{Path, PathBuf};
use anyhow::{Context, Result, anyhow};
use crate::exit::usage_error;
pub(crate) fn resolve_config_path(
flag: Option<&PathBuf>,
env_value: Option<&str>,
) -> Result<PathBuf> {
let raw = pick_path(flag, env_value, default_config_path)?;
absolutize(&raw)
}
pub(crate) fn resolve_state_path(
flag: Option<&PathBuf>,
env_value: Option<&str>,
) -> Result<PathBuf> {
let raw = pick_path(flag, env_value, default_state_path)?;
absolutize(&raw)
}
pub(crate) fn default_config_path() -> Result<PathBuf> {
Ok(aviso_config_dir()?.join("config.yaml"))
}
pub(crate) fn default_state_path() -> Result<PathBuf> {
Ok(aviso_config_dir()?.join("state.json"))
}
pub(crate) fn aviso_config_dir() -> Result<PathBuf> {
let home = directories::UserDirs::new()
.map(|u| u.home_dir().to_path_buf())
.ok_or_else(|| {
usage_error("could not resolve home directory; set HOME (Unix) or %USERPROFILE% (Windows), or pass --config <PATH> explicitly")
})?;
Ok(home.join(".config").join("aviso"))
}
fn pick_path<F>(flag: Option<&PathBuf>, env_value: Option<&str>, default_fn: F) -> Result<PathBuf>
where
F: FnOnce() -> Result<PathBuf>,
{
if let Some(p) = flag {
return Ok(p.clone());
}
if let Some(s) = env_value {
return Ok(PathBuf::from(s));
}
default_fn()
}
pub(crate) fn absolutize(path: &Path) -> Result<PathBuf> {
if path.is_absolute() {
return Ok(path.to_path_buf());
}
let cwd =
std::env::current_dir().context("resolve current working directory for relative path")?;
let joined = cwd.join(path);
let canonical = joined.canonicalize().unwrap_or_else(|_| joined.clone());
if !canonical.is_absolute() {
return Err(anyhow!(
"could not render path absolute: {}",
path.display()
));
}
Ok(canonical)
}
pub(crate) fn ensure_secure_dir(path: &Path) -> std::io::Result<()> {
#[cfg(unix)]
{
use std::os::unix::fs::DirBuilderExt;
std::fs::DirBuilder::new()
.mode(0o700)
.recursive(true)
.create(path)
}
#[cfg(not(unix))]
{
std::fs::create_dir_all(path)
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
reason = "test code: unwrap/expect on path resolution is the expected diagnostic"
)]
mod tests {
use super::*;
#[test]
fn flag_value_wins_over_env_and_default() {
let flag = PathBuf::from("/tmp/aviso-test-flag.yaml");
let got = resolve_config_path(Some(&flag), Some("/tmp/aviso-test-env.yaml")).unwrap();
assert_eq!(got, flag);
}
#[test]
fn env_value_wins_when_no_flag() {
let env = "/tmp/aviso-test-env.yaml";
let got = resolve_config_path(None, Some(env)).unwrap();
assert_eq!(got, PathBuf::from(env));
}
#[test]
fn default_used_when_neither_flag_nor_env() {
let got = resolve_config_path(None, None).expect("home_dir resolves on this platform");
assert!(
got.ends_with(".config/aviso/config.yaml"),
"default path should end with .config/aviso/config.yaml; got {}",
got.display()
);
}
#[test]
fn state_default_resolves_to_state_json_in_aviso_config_dir() {
let got = resolve_state_path(None, None).expect("home_dir resolves on this platform");
assert!(
got.ends_with(".config/aviso/state.json"),
"default state path should end with .config/aviso/state.json; got {}",
got.display()
);
}
#[test]
fn relative_path_made_absolute() {
let rel = PathBuf::from("relative/config.yaml");
let got = resolve_config_path(Some(&rel), None).unwrap();
assert!(got.is_absolute(), "got {}", got.display());
}
#[test]
fn aviso_config_dir_path_shape() {
let dir = aviso_config_dir().expect("home_dir resolves on this platform");
assert!(
dir.ends_with(".config/aviso"),
"config dir should end with .config/aviso; got {}",
dir.display()
);
}
#[test]
fn absolutize_passes_absolute_path_through_unchanged() {
let abs = PathBuf::from("/tmp/aviso-test-absolutize-passthrough");
let got = absolutize(&abs).unwrap();
assert_eq!(got, abs);
}
#[test]
fn absolutize_joins_relative_path_with_cwd() {
let rel = PathBuf::from("aviso-test-relative-listener.yaml");
let got = absolutize(&rel).unwrap();
assert!(
got.is_absolute(),
"expected absolute path, got {}",
got.display()
);
assert!(
got.ends_with("aviso-test-relative-listener.yaml"),
"expected file name to be preserved, got {}",
got.display()
);
}
#[cfg(unix)]
#[test]
fn ensure_secure_dir_creates_with_0700_perms_on_unix() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().expect("create tempdir");
let nested = dir.path().join("aviso-secure").join("subdir");
ensure_secure_dir(&nested).expect("create secure dir");
let metadata = std::fs::metadata(&nested).expect("read created dir metadata");
let mode = metadata.permissions().mode() & 0o777;
assert_eq!(
mode, 0o700,
"newly-created directory should have mode 0o700, got {mode:o}"
);
let parent_meta = std::fs::metadata(nested.parent().unwrap()).expect("parent meta");
let parent_mode = parent_meta.permissions().mode() & 0o777;
assert_eq!(
parent_mode, 0o700,
"intermediate created directory should also have mode 0o700, got {parent_mode:o}"
);
}
#[cfg(unix)]
#[test]
fn ensure_secure_dir_does_not_modify_existing_dir_perms() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().expect("create tempdir");
let pre_existing = dir.path().join("aviso-pre-existing");
std::fs::create_dir(&pre_existing).expect("create pre-existing");
std::fs::set_permissions(&pre_existing, std::fs::Permissions::from_mode(0o755))
.expect("set 0o755 on pre-existing");
ensure_secure_dir(&pre_existing).expect("call on pre-existing dir is a no-op");
let mode = std::fs::metadata(&pre_existing)
.unwrap()
.permissions()
.mode()
& 0o777;
assert_eq!(
mode, 0o755,
"pre-existing dir perms must not be modified, got {mode:o}"
);
}
}