use std::io;
use std::rc::Rc;
use std::sync::Arc;
use harn_vm::VmValue;
use crate::error::HostlibError;
use crate::registry::{BuiltinRegistry, HostlibCapability, RegisteredBuiltin, SyncHandler};
use crate::tools::args::{build_dict, dict_arg, require_string, str_value};
mod file;
#[cfg(any(target_os = "macos", target_os = "ios"))]
mod keychain;
#[cfg(target_os = "windows")]
mod wincred;
const GET_BUILTIN: &str = "hostlib_secret_store_get";
const SET_BUILTIN: &str = "hostlib_secret_store_set";
const DELETE_BUILTIN: &str = "hostlib_secret_store_delete";
const LIST_BUILTIN: &str = "hostlib_secret_store_list";
const BACKEND_ENV: &str = "HARN_SECRET_STORE_BACKEND";
trait Backend {
fn name(&self) -> &'static str;
fn get(&self, account: &str, key: &str) -> io::Result<Option<String>>;
fn set(&self, account: &str, key: &str, value: &str) -> io::Result<()>;
fn delete(&self, account: &str, key: &str) -> io::Result<bool>;
fn list(&self, account: &str) -> io::Result<Vec<String>>;
}
#[derive(Default)]
pub struct SecretStoreCapability;
impl HostlibCapability for SecretStoreCapability {
fn module_name(&self) -> &'static str {
"secret_store"
}
fn register_builtins(&self, registry: &mut BuiltinRegistry) {
register(registry, GET_BUILTIN, "get", handle_get);
register(registry, SET_BUILTIN, "set", handle_set);
register(registry, DELETE_BUILTIN, "delete", handle_delete);
register(registry, LIST_BUILTIN, "list", handle_list);
}
}
fn register(
registry: &mut BuiltinRegistry,
name: &'static str,
method: &'static str,
runner: fn(&[VmValue]) -> Result<VmValue, HostlibError>,
) {
let handler: SyncHandler = Arc::new(runner);
registry.register(RegisteredBuiltin {
name,
module: "secret_store",
method,
handler,
});
}
fn handle_get(args: &[VmValue]) -> Result<VmValue, HostlibError> {
let dict = dict_arg(GET_BUILTIN, args)?;
let account = require_nonempty_string(GET_BUILTIN, &dict, "account")?;
let key = require_nonempty_string(GET_BUILTIN, &dict, "key")?;
let backend = select_backend();
let value = backend
.get(&account, &key)
.map_err(|err| backend_err(GET_BUILTIN, err))?;
Ok(build_dict([
("account".to_string(), str_value(&account)),
("key".to_string(), str_value(&key)),
(
"value".to_string(),
value.as_deref().map(str_value).unwrap_or(VmValue::Nil),
),
("backend".to_string(), str_value(backend.name())),
]))
}
fn handle_set(args: &[VmValue]) -> Result<VmValue, HostlibError> {
let dict = dict_arg(SET_BUILTIN, args)?;
let account = require_nonempty_string(SET_BUILTIN, &dict, "account")?;
let key = require_nonempty_string(SET_BUILTIN, &dict, "key")?;
let value = require_string(SET_BUILTIN, &dict, "value")?;
let backend = select_backend();
backend
.set(&account, &key, &value)
.map_err(|err| backend_err(SET_BUILTIN, err))?;
Ok(build_dict([
("account".to_string(), str_value(&account)),
("key".to_string(), str_value(&key)),
("backend".to_string(), str_value(backend.name())),
]))
}
fn handle_delete(args: &[VmValue]) -> Result<VmValue, HostlibError> {
let dict = dict_arg(DELETE_BUILTIN, args)?;
let account = require_nonempty_string(DELETE_BUILTIN, &dict, "account")?;
let key = require_nonempty_string(DELETE_BUILTIN, &dict, "key")?;
let backend = select_backend();
let deleted = backend
.delete(&account, &key)
.map_err(|err| backend_err(DELETE_BUILTIN, err))?;
Ok(build_dict([
("account".to_string(), str_value(&account)),
("key".to_string(), str_value(&key)),
("deleted".to_string(), VmValue::Bool(deleted)),
("backend".to_string(), str_value(backend.name())),
]))
}
fn handle_list(args: &[VmValue]) -> Result<VmValue, HostlibError> {
let dict = dict_arg(LIST_BUILTIN, args)?;
let account = require_nonempty_string(LIST_BUILTIN, &dict, "account")?;
let backend = select_backend();
let keys = backend
.list(&account)
.map_err(|err| backend_err(LIST_BUILTIN, err))?;
let items: Vec<VmValue> = keys.into_iter().map(|k| str_value(&k)).collect();
Ok(build_dict([
("account".to_string(), str_value(&account)),
("keys".to_string(), VmValue::List(Rc::new(items))),
("backend".to_string(), str_value(backend.name())),
]))
}
fn require_nonempty_string(
builtin: &'static str,
dict: &std::collections::BTreeMap<String, VmValue>,
key: &'static str,
) -> Result<String, HostlibError> {
let value = require_string(builtin, dict, key)?;
if value.is_empty() {
Err(HostlibError::InvalidParameter {
builtin,
param: key,
message: "must be a non-empty string".to_string(),
})
} else {
Ok(value)
}
}
fn backend_err(builtin: &'static str, err: io::Error) -> HostlibError {
HostlibError::Backend {
builtin,
message: err.to_string(),
}
}
fn select_backend() -> Box<dyn Backend> {
if matches!(std::env::var(BACKEND_ENV).as_deref(), Ok("file")) {
return Box::new(file::FileStore::new());
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
{
Box::new(keychain::KeychainStore::new())
}
#[cfg(target_os = "windows")]
{
Box::new(wincred::WinCredStore::new())
}
#[cfg(not(any(target_os = "macos", target_os = "ios", target_os = "windows")))]
{
Box::new(file::FileStore::new())
}
}