harn_hostlib/secret_store/
mod.rs1use std::io;
24use std::sync::Arc;
25
26use harn_vm::VmValue;
27
28use crate::error::HostlibError;
29use crate::registry::{BuiltinRegistry, HostlibCapability};
30use crate::tools::args::{build_dict, dict_arg, require_string, str_value};
31
32mod file;
33#[cfg(any(
34 target_os = "macos",
35 target_os = "ios",
36 target_os = "windows",
37 all(
38 unix,
39 not(any(target_os = "macos", target_os = "ios", target_os = "android"))
40 )
41))]
42mod native;
43
44const GET_BUILTIN: &str = "hostlib_secret_store_get";
45const SET_BUILTIN: &str = "hostlib_secret_store_set";
46const DELETE_BUILTIN: &str = "hostlib_secret_store_delete";
47const LIST_BUILTIN: &str = "hostlib_secret_store_list";
48
49const BACKEND_ENV: &str = "HARN_SECRET_STORE_BACKEND";
52
53trait Backend {
55 fn name(&self) -> &'static str;
56 fn get(&self, account: &str, key: &str) -> io::Result<Option<String>>;
57 fn set(&self, account: &str, key: &str, value: &str) -> io::Result<()>;
58 fn delete(&self, account: &str, key: &str) -> io::Result<bool>;
59 fn list(&self, account: &str) -> io::Result<Vec<String>>;
60}
61
62#[derive(Default)]
66pub struct SecretStoreCapability;
67
68impl HostlibCapability for SecretStoreCapability {
69 fn module_name(&self) -> &'static str {
70 "secret_store"
71 }
72
73 fn register_builtins(&self, registry: &mut BuiltinRegistry) {
74 registry.register_fn("secret_store", GET_BUILTIN, "get", handle_get);
75 registry.register_fn("secret_store", SET_BUILTIN, "set", handle_set);
76 registry.register_fn("secret_store", DELETE_BUILTIN, "delete", handle_delete);
77 registry.register_fn("secret_store", LIST_BUILTIN, "list", handle_list);
78 }
79}
80
81fn handle_get(args: &[VmValue]) -> Result<VmValue, HostlibError> {
82 let dict = dict_arg(GET_BUILTIN, args)?;
83 let account = require_nonempty_string(GET_BUILTIN, &dict, "account")?;
84 let key = require_nonempty_string(GET_BUILTIN, &dict, "key")?;
85
86 let backend = select_backend();
87 let value = backend
88 .get(&account, &key)
89 .map_err(|err| backend_err(GET_BUILTIN, err))?;
90
91 Ok(build_dict([
92 ("account".to_string(), str_value(&account)),
93 ("key".to_string(), str_value(&key)),
94 (
95 "value".to_string(),
96 value.as_deref().map(str_value).unwrap_or(VmValue::Nil),
97 ),
98 ("backend".to_string(), str_value(backend.name())),
99 ]))
100}
101
102fn handle_set(args: &[VmValue]) -> Result<VmValue, HostlibError> {
103 let dict = dict_arg(SET_BUILTIN, args)?;
104 let account = require_nonempty_string(SET_BUILTIN, &dict, "account")?;
105 let key = require_nonempty_string(SET_BUILTIN, &dict, "key")?;
106 let value = require_string(SET_BUILTIN, &dict, "value")?;
107
108 let backend = select_backend();
109 backend
110 .set(&account, &key, &value)
111 .map_err(|err| backend_err(SET_BUILTIN, err))?;
112
113 Ok(build_dict([
114 ("account".to_string(), str_value(&account)),
115 ("key".to_string(), str_value(&key)),
116 ("backend".to_string(), str_value(backend.name())),
117 ]))
118}
119
120fn handle_delete(args: &[VmValue]) -> Result<VmValue, HostlibError> {
121 let dict = dict_arg(DELETE_BUILTIN, args)?;
122 let account = require_nonempty_string(DELETE_BUILTIN, &dict, "account")?;
123 let key = require_nonempty_string(DELETE_BUILTIN, &dict, "key")?;
124
125 let backend = select_backend();
126 let deleted = backend
127 .delete(&account, &key)
128 .map_err(|err| backend_err(DELETE_BUILTIN, err))?;
129
130 Ok(build_dict([
131 ("account".to_string(), str_value(&account)),
132 ("key".to_string(), str_value(&key)),
133 ("deleted".to_string(), VmValue::Bool(deleted)),
134 ("backend".to_string(), str_value(backend.name())),
135 ]))
136}
137
138fn handle_list(args: &[VmValue]) -> Result<VmValue, HostlibError> {
139 let dict = dict_arg(LIST_BUILTIN, args)?;
140 let account = require_nonempty_string(LIST_BUILTIN, &dict, "account")?;
141
142 let backend = select_backend();
143 let keys = backend
144 .list(&account)
145 .map_err(|err| backend_err(LIST_BUILTIN, err))?;
146
147 let items: Vec<VmValue> = keys.into_iter().map(|k| str_value(&k)).collect();
148 Ok(build_dict([
149 ("account".to_string(), str_value(&account)),
150 ("keys".to_string(), VmValue::List(Arc::new(items))),
151 ("backend".to_string(), str_value(backend.name())),
152 ]))
153}
154
155fn require_nonempty_string(
156 builtin: &'static str,
157 dict: &harn_vm::value::DictMap,
158 key: &'static str,
159) -> Result<String, HostlibError> {
160 let value = require_string(builtin, dict, key)?;
161 if value.is_empty() {
162 Err(HostlibError::InvalidParameter {
163 builtin,
164 param: key,
165 message: "must be a non-empty string".to_string(),
166 })
167 } else {
168 Ok(value)
169 }
170}
171
172fn backend_err(builtin: &'static str, err: io::Error) -> HostlibError {
173 HostlibError::Backend {
174 builtin,
175 message: err.to_string(),
176 }
177}
178
179fn select_backend() -> Box<dyn Backend> {
180 if matches!(std::env::var(BACKEND_ENV).as_deref(), Ok("file")) {
181 return Box::new(file::FileStore::new());
182 }
183
184 #[cfg(any(
185 target_os = "macos",
186 target_os = "ios",
187 target_os = "windows",
188 all(
189 unix,
190 not(any(target_os = "macos", target_os = "ios", target_os = "android"))
191 )
192 ))]
193 {
194 Box::new(native::NativeStore::new())
195 }
196 #[cfg(not(any(
197 target_os = "macos",
198 target_os = "ios",
199 target_os = "windows",
200 all(
201 unix,
202 not(any(target_os = "macos", target_os = "ios", target_os = "android"))
203 )
204 )))]
205 {
206 Box::new(file::FileStore::new())
207 }
208}