use std::path::{Path, PathBuf};
use log::debug;
use serde::{Deserialize, Serialize};
use crate::{sop::Sop, DEFAULT_CERT_FILENAME, DEFAULT_KEY_FILENAME};
const DEFAULT_SOP: &str = "rsop";
const CONFIG_FILE_BASENAME: &str = "sopass";
#[derive(Default, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
struct ConfigFile {
store: Option<PathBuf>,
sop: Option<PathBuf>,
sop_decrypt: Option<PathBuf>,
}
impl ConfigFile {
fn load_path(filename: &Path) -> Result<Self, ConfigError> {
if filename.exists() {
debug!("load configuration file {}", filename.display());
confy::load_path(filename).map_err(|err| ConfigError::Load(filename.into(), err))
} else {
debug!(
"requested configuration file {} does not exist",
filename.display()
);
Err(ConfigError::NoSuchFile(filename.into()))
}
}
fn load(app: &'static str) -> Result<Self, ConfigError> {
let filename = confy::get_configuration_file_path(app, CONFIG_FILE_BASENAME)
.map_err(ConfigError::Filename)?;
if filename.exists() {
debug!("load default configuration file {}", filename.display());
confy::load(app, CONFIG_FILE_BASENAME).map_err(|err| ConfigError::Load(filename, err))
} else {
debug!(
"configuration file {} does not exist, using defaults",
filename.display()
);
Ok(Self::default())
}
}
fn store(&self, default: &Path) -> Result<PathBuf, ConfigError> {
self.get("store", self.store.as_deref(), default)
}
fn sop(&self, default: &Path) -> Result<PathBuf, ConfigError> {
self.get("sop", self.sop.as_deref(), default)
}
fn sop_decrypt(&self, default: &Path) -> Result<PathBuf, ConfigError> {
self.get("sop_decrypt", self.sop_decrypt.as_deref(), default)
}
fn get(
&self,
name: &'static str,
path: Option<&Path>,
default: &Path,
) -> Result<PathBuf, ConfigError> {
let result = if path.is_none() {
Ok(default.into())
} else {
path.ok_or(ConfigError::Missing(name))
.map(|path| path.into())
};
debug!(
"get: name={} path={:?} default={} => {:?}",
name,
path,
default.display(),
result
);
result
}
}
#[derive(Default, Debug)]
pub struct ConfigBuilder {
app: &'static str,
default_store: PathBuf,
default_sop: PathBuf,
filename: Option<PathBuf>,
store: Option<PathBuf>,
sop: Option<PathBuf>,
sop_decrypt: Option<PathBuf>,
}
impl ConfigBuilder {
pub fn new(app: &'static str, default_store: &Path) -> Self {
Self {
app,
default_store: default_store.into(),
default_sop: PathBuf::from(DEFAULT_SOP),
..Default::default()
}
}
pub fn build(self) -> Result<Config, ConfigError> {
debug!("building configuration from {self:#?}");
let file = if let Some(filename) = &self.filename {
ConfigFile::load_path(filename)?
} else {
ConfigFile::load(self.app)?
};
debug!("loaded configuration {file:#?}");
let store = self.store.unwrap_or(file.store(&self.default_store)?);
let sop = self.sop.unwrap_or(file.sop(&self.default_sop)?);
debug!("sop={sop:?}");
let sop_decrypt = self.sop_decrypt.unwrap_or(file.sop_decrypt(&sop)?);
debug!("sop_decrypt={sop_decrypt:?}");
let config = Config::new(&store, &sop, &sop_decrypt);
debug!("using configuration {config:#?}");
Ok(config)
}
pub fn filename(&mut self, filename: &Path) {
self.filename = Some(filename.into());
}
pub fn store(&mut self, store: &Path) {
self.store = Some(store.into());
}
pub fn sop(&mut self, sop: &Path) {
self.sop = Some(sop.into());
}
pub fn sop_decrypt(&mut self, sop: &Path) {
self.sop_decrypt = Some(sop.into());
}
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Config {
store: PathBuf,
key_file: PathBuf,
cert_file: PathBuf,
sop: PathBuf,
sop_decrypt: PathBuf,
}
impl Config {
fn new(store: &Path, sop: &Path, sop_decrypt: &Path) -> Self {
Self {
store: store.into(),
key_file: store.join(DEFAULT_KEY_FILENAME),
cert_file: store.join(DEFAULT_CERT_FILENAME),
sop: sop.into(),
sop_decrypt: sop_decrypt.into(),
}
}
pub fn store(&self) -> &Path {
&self.store
}
pub fn sop(&self) -> Sop {
if self.sop != self.sop_decrypt {
Sop::hardware_key(&self.sop, &self.sop_decrypt, &self.cert_file)
} else {
Sop::software_key(&self.sop, &self.key_file)
}
}
pub fn pretty_json(&self) -> Result<String, ConfigError> {
serde_json::to_string_pretty(self).map_err(ConfigError::Json)
}
}
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
#[error("failed to load configuration file {0}")]
Load(PathBuf, #[source] confy::ConfyError),
#[error("requested configuration file does not exist: {0}")]
NoSuchFile(PathBuf),
#[error("failed to determine default filename for configuration file")]
Filename(#[source] confy::ConfyError),
#[error("configuration does not specify field {0}")]
Missing(&'static str),
#[error("failed to serialize configuration as JSON")]
Json(#[source] serde_json::Error),
}