use super::{Conn, Engine, NewConn};
use crate::ini;
use anyhow::Result;
use std::path::PathBuf;
const KNOWN: [&str; 4] = ["host", "port", "database", "user"];
pub fn store_path() -> PathBuf {
dirs::config_dir()
.unwrap_or_else(|| dirs::home_dir().unwrap_or_default().join(".config"))
.join("easysql")
.join("mssql.conf")
}
pub fn list() -> Vec<Conn> {
list_in(&store_path())
}
pub fn pass_path() -> PathBuf {
dirs::home_dir().unwrap_or_default().join(".esqlpass")
}
pub fn pass_is_private() -> bool {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::metadata(pass_path()).is_ok_and(|m| m.permissions().mode() & 0o077 == 0)
}
#[cfg(not(unix))]
true
}
pub fn password(name: &str) -> Option<String> {
if !pass_is_private() {
return None;
}
ini::read(&pass_path())
.into_iter()
.find(|s| s.name == name)
.and_then(|s| s.get("password").map(str::to_string))
.filter(|p| !p.is_empty())
}
pub fn has_password(name: &str) -> bool {
ini::read(&pass_path())
.iter()
.any(|s| s.name == name && s.get("password").is_some())
}
pub fn set_password(name: &str, password: Option<&str>) -> Result<()> {
match password {
Some(p) => ini::upsert(
&pass_path(),
None,
name,
&[("password".to_string(), p.to_string())],
),
None if has_password(name) => ini::remove(&pass_path(), name),
None => Ok(()),
}
}
pub fn rename_password(from: &str, to: &str) -> Result<()> {
match password(from) {
Some(p) => ini::upsert(&pass_path(), Some(from), to, &[("password".to_string(), p)]),
None => Ok(()),
}
}
pub fn list_in(path: &std::path::Path) -> Vec<Conn> {
ini::read(path)
.into_iter()
.map(|s| Conn {
engine: Engine::MsSql,
host: s.get("host").unwrap_or_default().to_string(),
port: s.get("port").unwrap_or_default().to_string(),
database: s.get("database").unwrap_or_default().to_string(),
user: s.get("user").unwrap_or_default().to_string(),
extra: s.rest(&KNOWN),
name: s.name,
})
.collect()
}
pub fn trusts_cert(c: &Conn) -> bool {
c.extra
.iter()
.any(|(k, v)| k.eq_ignore_ascii_case("trust_cert") && v == "yes")
}
pub fn flags(c: &Conn, db: Option<&str>) -> Vec<String> {
let host = if c.host.is_empty() {
"localhost"
} else {
&c.host
};
let mut argv = vec!["-S".to_string(), format!("{host},{}", c.port_or_default())];
let database = db.unwrap_or(&c.database);
if !database.is_empty() {
argv.push("-d".into());
argv.push(database.to_string());
}
if !c.user.is_empty() {
argv.push("-U".into());
argv.push(c.user.clone());
}
if trusts_cert(c) {
argv.push("-C".into());
}
argv
}
pub fn save(original: Option<&str>, nc: &NewConn) -> Result<()> {
let mut keys: Vec<(String, String)> = Vec::new();
for (k, v) in [
("host", &nc.host),
("port", &nc.port),
("database", &nc.database),
("user", &nc.user),
] {
let v = v.trim();
if !v.is_empty() {
keys.push((k.to_string(), v.to_string()));
}
}
keys.extend(nc.extra.iter().cloned());
ini::upsert(&store_path(), original, nc.name.trim(), &keys)
}
pub fn delete(name: &str) -> Result<()> {
ini::remove(&store_path(), name)
}
pub fn probe_argv(c: &Conn, s: &crate::settings::Settings) -> (Vec<String>, Vec<(String, String)>) {
let mut argv = c.connect_argv(s);
argv.push("-l".into());
argv.push(s.probe_timeout.to_string());
argv.push("-Q".into());
argv.push("select 1".into());
let env = c
.secret_env(s)
.into_iter()
.next()
.unwrap_or_else(|| ("SQLCMDPASSWORD".to_string(), String::new()));
(argv, vec![env])
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::{SystemTime, UNIX_EPOCH};
#[test]
fn a_saved_trust_cert_reaches_the_sqlcmd_argv() {
let stamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let path =
std::env::temp_dir().join(format!("easysql-mssql-{}-{stamp}.conf", std::process::id()));
std::fs::write(
&path,
"[selfsigned]\nhost=db.example.com\ndatabase=app\nuser=sa\ntrust_cert=yes\n\n[strict]\nhost=db.example.com\n",
)
.unwrap();
let conns = list_in(&path);
let _ = std::fs::remove_file(&path);
let argv = |name: &str| {
let c = conns.iter().find(|c| c.name == name).expect(name);
flags(c, None)
};
assert!(
argv("selfsigned").contains(&"-C".to_string()),
"a certificate the file says to trust must be trusted when read back: {:?}",
argv("selfsigned")
);
assert!(!argv("strict").contains(&"-C".to_string()));
}
}