mod feature;
pub mod token;
pub mod key;
pub mod opts;
use std::collections::HashMap;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::Path;
use uuid::Uuid;
use crate::feature::{featured, SupportedDb};
use crate::key::KeyFile;
use crate::opts::Opt;
const CMD_FILE: &str = "file";
const CMD_DB: &str = "db";
const CMD_TBL: &str = "config";
const CMD_TOKEN: &str = "token";
const CMD_TTL: &str = "ttl";
const CMD_KEY: &str = "key";
const CMD_SECRET: &str = "secret";
const CMD_PASS: &str = "PASSPHRASE";
const CMD_UUID: &str = "uuid";
#[derive(Debug, Clone)]
pub struct ArgConfig {
pub uuid: Uuid,
pub uuid_gen: bool,
pub db_url: String,
pub table: String,
pub cfg: HashMap<String, String>,
pub token: token::Token,
pub pk: Option<KeyFile>,
pub secret: Option<String>,
pub verbose: Opt,
pub quiet: Opt,
pub help: Opt,
}
impl ArgConfig {
pub fn from_args() -> Result<ArgConfig, String> {
ArgConfig::from_args2(env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"))
}
pub fn from_args2(app_name: &str, app_version: &str) -> Result<ArgConfig, String> {
let user = match std::env::var_os("USER") {
Some(a) => a.to_str().unwrap_or("postgres").to_string(),
_ => "postgres".to_string(),
};
let f = featured();
let pwd = if !f.env_pwd().is_empty() {
match std::env::var_os(f.env_pwd().as_str()).map(|v| v) {
Some(a) => a.to_str().map(|v| v.to_string()),
_ => None,
}
} else {
None
};
let input: Vec<String> = std::env::args_os().map(|e| e.to_string_lossy().to_string()).collect();
for arg in input.iter().skip(1) {
if arg == "--version" || arg == "-V" {
println!("{app_name} {app_version}");
std::process::exit(0);
}
}
ArgConfig::new(input, f, user, pwd)
}
fn new(input: Vec<String>, feature: SupportedDb, user: String, pwd: Option<String>) -> Result<Self, String> {
let mut cfg = HashMap::new();
let mut db: Option<String> = None;
let mut tbl: Option<String> = None;
let mut token: Option<String> = None;
let mut ttl: Option<String> = None;
let mut pk: Option<String> = None;
let mut secret: Option<String> = None;
let mut uuid: Option<Uuid> = None;
let mut ignore_next = false;
let mut verbose = Opt::new("verbose", "debug", "v", "false");
let mut quiet = Opt::new("quiet", "batch", "q", "false");
let mut help = Opt::new("help", "info", "h", "false");
for i in 1..input.len() {
if ignore_next { ignore_next = false; continue }
ignore_next = false;
if verbose.apply(&input[i]) | quiet.apply(&input[i]) | help.apply(&input[i]) {
continue;
}
if input[i].starts_with("--") {
if i < input.len() - 1 {
let v = &input[i].as_str()[2..];
if v == CMD_FILE {
let _ = load(v, &mut cfg)?;
ignore_next = true;
} else if v == CMD_DB {
db = Some(input[i + 1].to_string());
ignore_next = true;
} else if v == CMD_TBL {
tbl = Some(input[i + 1].to_string());
ignore_next = true;
} else if v == CMD_TOKEN {
token = Some(input[i + 1].to_string());
ignore_next = true;
} else if v == CMD_TTL {
ttl = Some(input[i + 1].to_string());
ignore_next = true;
} else if v == CMD_KEY {
let file = input[i + 1].to_string();
if key::is_key_file(&file) {
pk = Some(file);
ignore_next = true;
}
} else if v == CMD_UUID {
uuid = Uuid::parse_str(input[i + 1].as_str()).ok();
ignore_next = true;
} else if v == CMD_SECRET {
secret = Some(input[i + 1].to_string());
ignore_next = true;
}
}
} else {
}
}
for i in &input {
if i.starts_with("-") {
continue
}
if db.is_none() && feature.is_valid_url(i) {
db = Some(i.to_string());
continue
}
if tbl.is_none() && is_sound_schema_table(i) {
tbl = Some(i.to_string());
continue
}
if uuid.is_none() {
uuid = Uuid::parse_str(i).ok();
if uuid.is_some() {
continue
}
};
if token.is_none() && i.len() > 1 {
token = Some(i.to_string());
continue
}
if ttl.is_none() && i.parse::<u16>().is_ok() {
ttl = Some(i.to_string());
}
}
if uuid.is_none() {
uuid = Uuid::parse_str(get_env_or_cfg(CMD_UUID, &cfg, "").as_str()).ok();
}
if let Some(a) = std::env::var_os(CMD_SECRET.to_uppercase()).map(|v| v) {
secret = Some(a.to_str().map(|v| v.to_string()).unwrap_or("".to_string()));
#[cfg(not(feature="keep_env_secret"))]
unsafe {
std::env::remove_var(CMD_SECRET);
}
}
Ok(ArgConfig {
uuid_gen: uuid.is_none(),
uuid: uuid.unwrap_or(Uuid::new_v4()),
db_url: link_db_user(db.unwrap_or(get_env_or_cfg(CMD_DB, &cfg, feature.default_url(&user).as_str())), user),
table: tbl.unwrap_or(get_env_or_cfg(CMD_TBL, &cfg, get_exec_name("public.", input[0].as_str()).as_str())),
token: token::Token::new(
token.unwrap_or(get_env_or_cfg(CMD_TOKEN, &cfg, "")),
ttl.unwrap_or(get_env_or_cfg(CMD_TTL, &cfg, "1")),
pwd
)?,
pk: KeyFile::new(
pk.unwrap_or(get_env_or_cfg(CMD_KEY, &cfg, &"")),
std::env::var_os(CMD_PASS).map(|p| p.to_string_lossy().to_string()),
)?,
cfg,
secret,
verbose,
quiet,
help,
})
}
pub fn db_url(&self) -> String {
let url = self.db_url.clone();
if let Some(i) = url.find(":$P") {
if let Some(y) = url.find("@") {
let pwd = url.as_str()[i+1..y].to_owned();
return url.replace(
pwd.as_str(),
self.token.value.clone().unwrap_or("".into()).as_str()).to_string();
}
}
url
}
}
#[inline]
fn link_db_user(url: String, user: String) -> String {
url.replace("$USER", user.as_str())
}
#[inline]
fn get_env_or_cfg(input: &str, cfg: &HashMap<String, String>, def: &str) -> String {
match std::env::var_os(input).map(|v| v) {
Some(a) => a.to_str().map(|v| v.to_string()).unwrap_or(def.to_string()),
None => cfg.get(input).unwrap_or(&def.to_string()).into()
}
}
#[inline]
fn get_exec_name(schema: &str, input: &str) -> String {
if input.is_empty() {
return "".to_string();
}
let e = Path::new(input);
let name = e.file_name().map(|f|f.to_str().unwrap_or("")).unwrap_or("").to_string();
#[cfg(windows)]
let name = name.replace(".exe", "");
let name = if let Some(i) = name.rfind(std::path::MAIN_SEPARATOR_STR) {
name[i+1..].to_string()
} else {
name
};
format!("{}{}", schema, name)
}
#[inline]
fn is_sound_schema_table(input: &str) -> bool {
let y = input.contains(".");
#[cfg(windows)]
let y = y && !input.ends_with(".exe");
y
}
#[inline]
fn load(file: &str, cfg: &mut HashMap<String, String>) -> Result<(), String> {
let f = File::open(file).map_err(|e| e.to_string())?;
let reader = BufReader::new(f);
for line in reader.lines() {
if let Ok(l) = line {
if let Some(i) = l.chars().position(|c| c == '=' || c == ':' || c == '#' || c == ';' || c == '/' || c == '[') {
if l.as_bytes()[i] != b'#' && l.as_bytes()[i] != b';' && l.as_bytes()[i] != b'/' && l.as_bytes()[i] != b'[' {
let key = l[..i].trim().to_lowercase();
let value = l[i + 1..].trim().to_string();
cfg.insert(key, value);
}
}
}
}
Ok(())
}
#[cfg(test)]
mod tests;