use std::env;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Layout {
pub system: Option<PathBuf>,
pub user: Option<PathBuf>,
pub project: Option<PathBuf>,
pub project_local: Option<PathBuf>,
}
impl Layout {
pub fn ordered_paths(&self) -> Vec<PathBuf> {
[
self.system.as_ref(),
self.user.as_ref(),
self.project.as_ref(),
self.project_local.as_ref(),
]
.into_iter()
.flatten()
.cloned()
.collect()
}
}
pub trait EnvLookup {
fn var_os(&self, key: &str) -> Option<OsString>;
}
pub struct SystemEnv;
impl EnvLookup for SystemEnv {
fn var_os(&self, key: &str) -> Option<OsString> {
env::var_os(key)
}
}
pub fn default_layout(repo_root: Option<&Path>) -> Layout {
layout_for(repo_root, &SystemEnv)
}
pub fn layout_for(repo_root: Option<&Path>, env: &dyn EnvLookup) -> Layout {
Layout {
system: system_config_path(env),
user: user_config_path(env),
project: repo_root.map(|r| r.join(".ptuf.yaml")),
project_local: repo_root.map(|r| r.join(".ptuf.local.yaml")),
}
}
fn system_config_path(env: &dyn EnvLookup) -> Option<PathBuf> {
if let Some(dir) = env.var_os("PTUF_ETC_DIR") {
return Some(PathBuf::from(dir).join("policy.yaml"));
}
Some(PathBuf::from("/etc/ptuf/policy.yaml"))
}
fn user_config_path(env: &dyn EnvLookup) -> Option<PathBuf> {
if let Some(dir) = env.var_os("PTUF_CONFIG_DIR") {
return Some(PathBuf::from(dir).join("config.yaml"));
}
if let Some(xdg) = env.var_os("XDG_CONFIG_HOME") {
return Some(PathBuf::from(xdg).join("ptuf/config.yaml"));
}
let home = env.var_os("HOME")?;
Some(PathBuf::from(home).join(".config/ptuf/config.yaml"))
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
use std::path::PathBuf;
struct MapEnv {
vars: HashMap<String, OsString>,
}
impl MapEnv {
fn new(pairs: &[(&str, &str)]) -> Self {
let mut vars = HashMap::new();
for (k, v) in pairs {
vars.insert((*k).to_string(), OsString::from(*v));
}
MapEnv { vars }
}
}
impl EnvLookup for MapEnv {
fn var_os(&self, key: &str) -> Option<OsString> {
self.vars.get(key).cloned()
}
}
#[test]
fn ordered_paths_skips_none_entries() {
let layout = Layout {
system: Some(PathBuf::from("/etc/ptuf/policy.yaml")),
user: None,
project: Some(PathBuf::from("/repo/.ptuf.yaml")),
project_local: None,
};
assert_eq!(
layout.ordered_paths(),
vec![
PathBuf::from("/etc/ptuf/policy.yaml"),
PathBuf::from("/repo/.ptuf.yaml"),
]
);
}
#[test]
fn ordered_paths_preserves_priority_order() {
let layout = Layout {
system: Some(PathBuf::from("a")),
user: Some(PathBuf::from("b")),
project: Some(PathBuf::from("c")),
project_local: Some(PathBuf::from("d")),
};
assert_eq!(
layout.ordered_paths(),
vec![
PathBuf::from("a"),
PathBuf::from("b"),
PathBuf::from("c"),
PathBuf::from("d"),
]
);
}
#[test]
fn ptuf_etc_dir_overrides_system_path() {
let env = MapEnv::new(&[("PTUF_ETC_DIR", "/custom/etc")]);
let layout = layout_for(Some(Path::new("/repo")), &env);
assert_eq!(
layout.system,
Some(PathBuf::from("/custom/etc/policy.yaml"))
);
}
#[test]
fn ptuf_config_dir_overrides_user_path() {
let env = MapEnv::new(&[("PTUF_CONFIG_DIR", "/custom/config")]);
let layout = layout_for(None, &env);
assert_eq!(
layout.user,
Some(PathBuf::from("/custom/config/config.yaml"))
);
}
#[test]
fn falls_back_to_home_when_no_xdg() {
let env = MapEnv::new(&[("HOME", "/home/example")]);
let layout = layout_for(None, &env);
assert_eq!(
layout.user,
Some(PathBuf::from("/home/example/.config/ptuf/config.yaml"))
);
}
#[test]
fn xdg_config_home_takes_precedence_over_home() {
let env = MapEnv::new(&[
("HOME", "/home/example"),
("XDG_CONFIG_HOME", "/home/example/.config-xdg"),
]);
let layout = layout_for(None, &env);
assert_eq!(
layout.user,
Some(PathBuf::from("/home/example/.config-xdg/ptuf/config.yaml"))
);
}
#[test]
fn ptuf_config_dir_takes_precedence_over_xdg_and_home() {
let env = MapEnv::new(&[
("HOME", "/home/example"),
("XDG_CONFIG_HOME", "/home/example/.config-xdg"),
("PTUF_CONFIG_DIR", "/custom/config"),
]);
let layout = layout_for(None, &env);
assert_eq!(
layout.user,
Some(PathBuf::from("/custom/config/config.yaml"))
);
}
#[test]
fn user_path_is_none_when_no_home_or_overrides() {
let env = MapEnv::new(&[]);
let layout = layout_for(None, &env);
assert!(layout.user.is_none());
}
#[test]
fn project_paths_are_set_only_when_repo_root_is_provided() {
let env = MapEnv::new(&[("HOME", "/h")]);
let layout = layout_for(Some(Path::new("/repo")), &env);
assert_eq!(layout.project, Some(PathBuf::from("/repo/.ptuf.yaml")));
assert_eq!(
layout.project_local,
Some(PathBuf::from("/repo/.ptuf.local.yaml"))
);
}
#[test]
fn project_paths_are_none_when_no_repo_root() {
let env = MapEnv::new(&[("HOME", "/h")]);
let layout = layout_for(None, &env);
assert!(layout.project.is_none());
assert!(layout.project_local.is_none());
}
#[test]
fn system_env_var_os_reads_from_real_process_env() {
let env = SystemEnv;
let _path = env.var_os("PATH");
let absent = env.var_os("PTUF_DEFINITELY_NOT_SET_ANYWHERE_xyz123");
assert!(absent.is_none());
}
#[test]
fn default_layout_uses_system_env_lookup() {
let layout = default_layout(None);
assert!(layout.system.is_some());
assert!(layout.project.is_none());
assert!(layout.project_local.is_none());
}
}