1use std::collections::BTreeMap;
2
3use atuin_client::record::sqlite_store::SqliteStore;
4use atuin_common::record::{DecryptedData, Host, HostId};
9use atuin_common::utils::unquote;
10use eyre::{bail, ensure, eyre, Result};
11
12use atuin_client::record::encryption::PASETO_V4;
13use atuin_client::record::store::Store;
14
15use crate::shell::Alias;
16
17const CONFIG_SHELL_ALIAS_VERSION: &str = "v0";
18const CONFIG_SHELL_ALIAS_TAG: &str = "config-shell-alias";
19const CONFIG_SHELL_ALIAS_FIELD_MAX_LEN: usize = 20000; mod alias;
22pub mod var;
23
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum AliasRecord {
26 Create(Alias), Delete(String), }
29
30impl AliasRecord {
31 pub fn serialize(&self) -> Result<DecryptedData> {
32 use rmp::encode;
33
34 let mut output = vec![];
35
36 match self {
37 AliasRecord::Create(alias) => {
38 encode::write_u8(&mut output, 0)?; encode::write_array_len(&mut output, 2)?; encode::write_str(&mut output, alias.name.as_str())?;
42 encode::write_str(&mut output, alias.value.as_str())?;
43 }
44 AliasRecord::Delete(name) => {
45 encode::write_u8(&mut output, 1)?; encode::write_array_len(&mut output, 1)?; encode::write_str(&mut output, name.as_str())?;
49 }
50 }
51
52 Ok(DecryptedData(output))
53 }
54
55 pub fn deserialize(data: &DecryptedData, version: &str) -> Result<Self> {
56 use rmp::decode;
57
58 fn error_report<E: std::fmt::Debug>(err: E) -> eyre::Report {
59 eyre!("{err:?}")
60 }
61
62 match version {
63 CONFIG_SHELL_ALIAS_VERSION => {
64 let mut bytes = decode::Bytes::new(&data.0);
65
66 let record_type = decode::read_u8(&mut bytes).map_err(error_report)?;
67
68 match record_type {
69 0 => {
71 let nfields = decode::read_array_len(&mut bytes).map_err(error_report)?;
72 ensure!(
73 nfields == 2,
74 "too many entries in v0 shell alias create record"
75 );
76
77 let bytes = bytes.remaining_slice();
78
79 let (key, bytes) =
80 decode::read_str_from_slice(bytes).map_err(error_report)?;
81 let (value, bytes) =
82 decode::read_str_from_slice(bytes).map_err(error_report)?;
83
84 if !bytes.is_empty() {
85 bail!("trailing bytes in encoded shell alias record. malformed")
86 }
87
88 Ok(AliasRecord::Create(Alias {
89 name: key.to_owned(),
90 value: value.to_owned(),
91 }))
92 }
93
94 1 => {
96 let nfields = decode::read_array_len(&mut bytes).map_err(error_report)?;
97 ensure!(
98 nfields == 1,
99 "too many entries in v0 shell alias delete record"
100 );
101
102 let bytes = bytes.remaining_slice();
103
104 let (key, bytes) =
105 decode::read_str_from_slice(bytes).map_err(error_report)?;
106
107 if !bytes.is_empty() {
108 bail!("trailing bytes in encoded shell alias record. malformed")
109 }
110
111 Ok(AliasRecord::Delete(key.to_owned()))
112 }
113
114 n => {
115 bail!("unknown AliasRecord type {n}")
116 }
117 }
118 }
119 _ => {
120 bail!("unknown version {version:?}")
121 }
122 }
123 }
124}
125
126#[derive(Debug, Clone)]
127pub struct AliasStore {
128 pub store: SqliteStore,
129 pub host_id: HostId,
130 pub encryption_key: [u8; 32],
131}
132
133impl AliasStore {
134 pub fn new(store: SqliteStore, host_id: HostId, encryption_key: [u8; 32]) -> AliasStore {
136 AliasStore {
137 store,
138 host_id,
139 encryption_key,
140 }
141 }
142
143 pub async fn posix(&self) -> Result<String> {
144 let aliases = self.aliases().await?;
145
146 let mut config = String::new();
147
148 for alias in aliases {
149 let value = unquote(alias.value.as_str()).unwrap_or(alias.value.clone());
151
152 config.push_str(&format!("alias {}='{}'\n", alias.name, value));
154 }
155
156 Ok(config)
157 }
158
159 pub async fn xonsh(&self) -> Result<String> {
160 let aliases = self.aliases().await?;
161
162 let mut config = String::new();
163
164 for alias in aliases {
165 config.push_str(&format!("aliases['{}'] ='{}'\n", alias.name, alias.value));
166 }
167
168 Ok(config)
169 }
170
171 pub async fn build(&self) -> Result<()> {
172 let dir = atuin_common::utils::dotfiles_cache_dir();
173 tokio::fs::create_dir_all(dir.clone()).await?;
174
175 let posix = self.posix().await?;
177 let xonsh = self.xonsh().await?;
178
179 let zsh = dir.join("aliases.zsh");
183 let bash = dir.join("aliases.bash");
184 let fish = dir.join("aliases.fish");
185 let xsh = dir.join("aliases.xsh");
186
187 tokio::fs::write(zsh, &posix).await?;
188 tokio::fs::write(bash, &posix).await?;
189 tokio::fs::write(fish, &posix).await?;
190 tokio::fs::write(xsh, &xonsh).await?;
191
192 Ok(())
193 }
194
195 pub async fn set(&self, name: &str, value: &str) -> Result<()> {
196 if name.len() + value.len() > CONFIG_SHELL_ALIAS_FIELD_MAX_LEN {
197 return Err(eyre!(
198 "alias record too large: max len {} bytes",
199 CONFIG_SHELL_ALIAS_FIELD_MAX_LEN
200 ));
201 }
202
203 let record = AliasRecord::Create(Alias {
204 name: name.to_string(),
205 value: value.to_string(),
206 });
207
208 let bytes = record.serialize()?;
209
210 let idx = self
211 .store
212 .last(self.host_id, CONFIG_SHELL_ALIAS_TAG)
213 .await?
214 .map_or(0, |entry| entry.idx + 1);
215
216 let record = atuin_common::record::Record::builder()
217 .host(Host::new(self.host_id))
218 .version(CONFIG_SHELL_ALIAS_VERSION.to_string())
219 .tag(CONFIG_SHELL_ALIAS_TAG.to_string())
220 .idx(idx)
221 .data(bytes)
222 .build();
223
224 self.store
225 .push(&record.encrypt::<PASETO_V4>(&self.encryption_key))
226 .await?;
227
228 self.build().await?;
230
231 Ok(())
232 }
233
234 pub async fn delete(&self, name: &str) -> Result<()> {
235 if name.len() > CONFIG_SHELL_ALIAS_FIELD_MAX_LEN {
236 return Err(eyre!(
237 "alias record too large: max len {} bytes",
238 CONFIG_SHELL_ALIAS_FIELD_MAX_LEN
239 ));
240 }
241
242 let record = AliasRecord::Delete(name.to_string());
243
244 let bytes = record.serialize()?;
245
246 let idx = self
247 .store
248 .last(self.host_id, CONFIG_SHELL_ALIAS_TAG)
249 .await?
250 .map_or(0, |entry| entry.idx + 1);
251
252 let record = atuin_common::record::Record::builder()
253 .host(Host::new(self.host_id))
254 .version(CONFIG_SHELL_ALIAS_VERSION.to_string())
255 .tag(CONFIG_SHELL_ALIAS_TAG.to_string())
256 .idx(idx)
257 .data(bytes)
258 .build();
259
260 self.store
261 .push(&record.encrypt::<PASETO_V4>(&self.encryption_key))
262 .await?;
263
264 self.build().await?;
266
267 Ok(())
268 }
269
270 pub async fn aliases(&self) -> Result<Vec<Alias>> {
271 let mut build = BTreeMap::new();
272
273 let tagged = self.store.all_tagged(CONFIG_SHELL_ALIAS_TAG).await?;
275
276 for record in tagged {
277 let version = record.version.clone();
278
279 let decrypted = match version.as_str() {
280 CONFIG_SHELL_ALIAS_VERSION => record.decrypt::<PASETO_V4>(&self.encryption_key)?,
281 version => bail!("unknown version {version:?}"),
282 };
283
284 let ar = AliasRecord::deserialize(&decrypted.data, version.as_str())?;
285
286 match ar {
287 AliasRecord::Create(a) => {
288 build.insert(a.name.clone(), a);
289 }
290 AliasRecord::Delete(d) => {
291 build.remove(&d);
292 }
293 }
294 }
295
296 Ok(build.into_values().collect())
297 }
298}
299
300#[cfg(test)]
301pub(crate) fn test_local_timeout() -> f64 {
302 std::env::var("ATUIN_TEST_LOCAL_TIMEOUT")
303 .ok()
304 .and_then(|x| x.parse().ok())
305 .unwrap_or(2.0)
308}
309
310#[cfg(test)]
311mod tests {
312 use rand::rngs::OsRng;
313
314 use atuin_client::record::sqlite_store::SqliteStore;
315
316 use crate::shell::Alias;
317
318 use super::{test_local_timeout, AliasRecord, AliasStore, CONFIG_SHELL_ALIAS_VERSION};
319 use crypto_secretbox::{KeyInit, XSalsa20Poly1305};
320
321 #[test]
322 fn encode_decode() {
323 let record = Alias {
324 name: "k".to_owned(),
325 value: "kubectl".to_owned(),
326 };
327 let record = AliasRecord::Create(record);
328
329 let snapshot = [204, 0, 146, 161, 107, 167, 107, 117, 98, 101, 99, 116, 108];
330
331 let encoded = record.serialize().unwrap();
332 let decoded = AliasRecord::deserialize(&encoded, CONFIG_SHELL_ALIAS_VERSION).unwrap();
333
334 assert_eq!(encoded.0, &snapshot);
335 assert_eq!(decoded, record);
336 }
337
338 #[tokio::test]
339 async fn build_aliases() {
340 let store = SqliteStore::new(":memory:", test_local_timeout())
341 .await
342 .unwrap();
343 let key: [u8; 32] = XSalsa20Poly1305::generate_key(&mut OsRng).into();
344 let host_id = atuin_common::record::HostId(atuin_common::utils::uuid_v7());
345
346 let alias = AliasStore::new(store, host_id, key);
347
348 alias.set("k", "kubectl").await.unwrap();
349 alias.set("gp", "git push").await.unwrap();
350 alias
351 .set("kgap", "'kubectl get pods --all-namespaces'")
352 .await
353 .unwrap();
354
355 let mut aliases = alias.aliases().await.unwrap();
356
357 aliases.sort_by_key(|a| a.name.clone());
358
359 assert_eq!(aliases.len(), 3);
360
361 assert_eq!(
362 aliases[0],
363 Alias {
364 name: String::from("gp"),
365 value: String::from("git push")
366 }
367 );
368
369 assert_eq!(
370 aliases[1],
371 Alias {
372 name: String::from("k"),
373 value: String::from("kubectl")
374 }
375 );
376
377 assert_eq!(
378 aliases[2],
379 Alias {
380 name: String::from("kgap"),
381 value: String::from("'kubectl get pods --all-namespaces'")
382 }
383 );
384
385 let build = alias.posix().await.expect("failed to build aliases");
386
387 assert_eq!(
388 build,
389 "alias gp='git push'
390alias k='kubectl'
391alias kgap='kubectl get pods --all-namespaces'
392"
393 )
394 }
395}