use std::io;
use std::path::{Path, PathBuf};
use std::process::Command;
use dirs::{config_dir, home_dir};
use shellexpand::tilde;
use thiserror::Error;
mod tomlcfg;
mod scfg;
const DEFAULT_MAILDIR: &str = "~/.maildir";
#[derive(Error, Debug)]
pub enum ConfigError {
#[error("error getting default config file location")]
DirError,
#[error("error reading config file: {0}")]
IOError(#[from] io::Error),
#[error("error executing pass-cmd: {0}")]
PassExecError(String),
#[error("UTF-8 error: {0}")]
UTF8Error(#[from] std::string::FromUtf8Error),
#[error("error parsing config file: {0}")]
TOMLError(#[from] toml::de::Error),
#[error("error parsing config file: {0}")]
ScfgError(#[from] derpscfg::Error),
#[error("config error: {0}")]
Error(&'static str),
}
enum AccountImpl<'a> {
Toml(tomlcfg::TomlAccount<'a>),
Scfg(&'a scfg::ScfgAccount),
}
pub struct Account<'a> {
name: String,
local: String,
acc: AccountImpl<'a>,
}
enum ConfigImpl {
Toml(tomlcfg::TomlConfig),
Scfg(scfg::ScfgConfig),
}
pub struct Config {
cfg: ConfigImpl,
}
impl Config {
pub fn for_account(&self, name: Option<&str>) -> Option<Account<'_>> {
match &self.cfg {
ConfigImpl::Toml(t) => t.for_account(name).map(|a| {
let local = a.local().to_string();
Account {
name: a.name().clone(),
acc: AccountImpl::Toml(a),
local,
}
}),
ConfigImpl::Scfg(s) => s.for_account(name).map(|(n, a)| {
let l = a.local.as_deref();
let local = tilde(l.unwrap_or(DEFAULT_MAILDIR)).to_string();
Account {
name: n.clone(),
acc: AccountImpl::Scfg(a),
local,
}
}),
}
}
}
impl Account<'_> {
pub fn name(&self) -> &str {
&self.name
}
pub fn local(&self) -> &str {
&self.local
}
pub fn remote(&self) -> Option<&str> {
match &self.acc {
AccountImpl::Toml(t) => t.remote(),
AccountImpl::Scfg(s) => s.remote.as_deref(),
}
}
pub fn user(&self) -> Option<&str> {
match &self.acc {
AccountImpl::Toml(t) => t.user(),
AccountImpl::Scfg(s) => s.user.as_deref(),
}
}
pub fn send_from(&self) -> Option<&str> {
match &self.acc {
AccountImpl::Toml(t) => t.send_from(),
AccountImpl::Scfg(s) => s
.send
.as_ref()
.and_then(|s| s.from.as_ref())
.map(|f| f.as_str())
.or_else(|| self.send_user()),
}
}
pub fn send_user(&self) -> Option<&str> {
match &self.acc {
AccountImpl::Toml(t) => t.send_user(),
AccountImpl::Scfg(s) => s
.send
.as_ref()
.and_then(|s| s.user.as_ref())
.map(|u| u.as_str())
.or_else(|| self.user()),
}
}
pub fn send_remote(&self) -> Option<&str> {
match &self.acc {
AccountImpl::Toml(t) => t.send_remote(),
AccountImpl::Scfg(s) => s
.send
.as_ref()
.and_then(|s| s.remote.as_ref())
.map(|r| r.as_str())
.or_else(|| self.remote()),
}
}
fn pass_cmd(cmd: &str) -> Result<Option<String>, ConfigError> {
let out = Command::new("sh").arg("-c").arg(cmd).output();
match out {
Ok(mut output) => {
let newline: u8 = 10;
if Some(&newline) == output.stdout.last() {
_ = output.stdout.pop(); }
Ok(Some(String::from_utf8(output.stdout)?))
}
Err(e) => Err(ConfigError::PassExecError(e.to_string())),
}
}
pub fn password(&self) -> Result<Option<String>, ConfigError> {
match &self.acc {
AccountImpl::Toml(t) => t.password(),
AccountImpl::Scfg(s) => {
if let Some(p) = s.password.as_ref() {
Ok(Some(String::from(p)))
} else if let Some(cmd) = s.pass_cmd.as_ref() {
Account::pass_cmd(cmd)
} else {
Ok(None)
}
}
}
}
pub fn send_password(&self) -> Result<Option<String>, ConfigError> {
match &self.acc {
AccountImpl::Toml(t) => t.send_password(),
AccountImpl::Scfg(s) => {
let pass = s.send.as_ref().and_then(|s| s.password.as_ref());
let pcmd = s.send.as_ref().and_then(|s| s.pass_cmd.as_ref());
if let Some(p) = pass {
Ok(Some(String::from(p)))
} else if let Some(cmd) = pcmd {
Account::pass_cmd(cmd)
} else {
self.password()
}
}
}
}
}
pub fn default_path() -> Result<PathBuf, ConfigError> {
if let Some(mut path) = config_dir() {
path.push("vomit");
path.push("config.scfg");
return Ok(path);
};
if let Some(mut path) = home_dir() {
path.push(".vomitrc");
return Ok(path);
}
Err(ConfigError::DirError)
}
fn default_path_toml() -> Result<PathBuf, ConfigError> {
if let Some(mut path) = config_dir() {
path.push("vomit");
path.push("config.toml");
return Ok(path);
};
if let Some(mut path) = home_dir() {
path.push(".vomitrc");
return Ok(path);
}
Err(ConfigError::DirError)
}
pub fn load<P: AsRef<Path>>(path: Option<P>) -> Result<Config, ConfigError> {
let ps = default_path()?;
let pt = default_path_toml()?;
let p = path.map(|p| p.as_ref().to_path_buf()).unwrap_or_else(|| {
if ps.exists() {
ps
} else if pt.exists() {
pt
} else {
ps
}
});
if let Some("scfg") = p.extension().and_then(|e| e.to_str()) {
Ok(Config {
cfg: ConfigImpl::Scfg(scfg::load_scfg_file(&p)?),
})
} else if let Some("toml") = p.extension().and_then(|e| e.to_str()) {
eprintln!("WARNING: loading deprecated TOML config, switch to scfg: https://docs.rs/vomit-config/");
Ok(Config {
cfg: ConfigImpl::Toml(tomlcfg::load_toml(&p)?),
})
} else {
let r = tomlcfg::load_toml(&p);
if let Ok(t) = r {
eprintln!("WARNING: loading deprecated TOML config, switch to scfg: https://docs.rs/vomit-config/");
Ok(Config {
cfg: ConfigImpl::Toml(t),
})
} else {
Ok(Config {
cfg: ConfigImpl::Scfg(scfg::load_scfg_file(&p)?),
})
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_load() {
let d = PathBuf::from_iter([env!("CARGO_MANIFEST_DIR"), "testdata"]);
let ext_scfg = PathBuf::from_iter([&d, &PathBuf::from("config.scfg")]);
let r = load(Some(&ext_scfg));
assert!(matches!(
r,
Ok(Config {
cfg: ConfigImpl::Scfg(_)
})
));
let ext_toml = PathBuf::from_iter([&d, &PathBuf::from("config.toml")]);
let r = load(Some(&ext_toml));
assert!(matches!(
r,
Ok(Config {
cfg: ConfigImpl::Toml(_)
})
));
let noext_scfg = PathBuf::from_iter([&d, &PathBuf::from("scfg.vomitrc")]);
let r = load(Some(&noext_scfg));
assert!(matches!(
r,
Ok(Config {
cfg: ConfigImpl::Scfg(_)
})
));
let noext_toml = PathBuf::from_iter([&d, &PathBuf::from("toml.vomitrc")]);
let r = load(Some(&noext_toml));
assert!(matches!(
r,
Ok(Config {
cfg: ConfigImpl::Toml(_)
})
));
}
}