atuin_scripts/store/
script.rs

1use atuin_common::record::DecryptedData;
2use eyre::{Result, bail, ensure};
3use uuid::Uuid;
4
5use rmp::{
6    decode::{self, Bytes},
7    encode,
8};
9use typed_builder::TypedBuilder;
10
11pub const SCRIPT_VERSION: &str = "v0";
12pub const SCRIPT_TAG: &str = "script";
13pub const SCRIPT_LEN: usize = 20000; // 20kb max total len
14
15#[derive(Debug, Clone, PartialEq, Eq, TypedBuilder)]
16/// A script is a set of commands that can be run, with the specified shebang
17pub struct Script {
18    /// The id of the script
19    #[builder(default = uuid::Uuid::new_v4())]
20    pub id: Uuid,
21
22    /// The name of the script
23    pub name: String,
24
25    /// The description of the script
26    #[builder(default = String::new())]
27    pub description: String,
28
29    /// The interpreter of the script
30    #[builder(default = String::new())]
31    pub shebang: String,
32
33    /// The tags of the script
34    #[builder(default = Vec::new())]
35    pub tags: Vec<String>,
36
37    /// The script content
38    pub script: String,
39}
40
41impl Script {
42    pub fn serialize(&self) -> Result<DecryptedData> {
43        // sort the tags first, to ensure consistent ordering
44        let mut tags = self.tags.clone();
45        tags.sort();
46
47        let mut output = vec![];
48
49        encode::write_array_len(&mut output, 6)?;
50        encode::write_str(&mut output, &self.id.to_string())?;
51        encode::write_str(&mut output, &self.name)?;
52        encode::write_str(&mut output, &self.description)?;
53        encode::write_str(&mut output, &self.shebang)?;
54        encode::write_array_len(&mut output, self.tags.len() as u32)?;
55
56        for tag in &tags {
57            encode::write_str(&mut output, tag)?;
58        }
59
60        encode::write_str(&mut output, &self.script)?;
61
62        Ok(DecryptedData(output))
63    }
64
65    pub fn deserialize(bytes: &[u8]) -> Result<Self> {
66        let mut bytes = decode::Bytes::new(bytes);
67        let nfields = decode::read_array_len(&mut bytes).unwrap();
68
69        ensure!(nfields == 6, "too many entries in v0 script record");
70
71        let bytes = bytes.remaining_slice();
72
73        let (id, bytes) = decode::read_str_from_slice(bytes).unwrap();
74        let (name, bytes) = decode::read_str_from_slice(bytes).unwrap();
75        let (description, bytes) = decode::read_str_from_slice(bytes).unwrap();
76        let (shebang, bytes) = decode::read_str_from_slice(bytes).unwrap();
77
78        let mut bytes = Bytes::new(bytes);
79        let tags_len = decode::read_array_len(&mut bytes).unwrap();
80
81        let mut bytes = bytes.remaining_slice();
82
83        let mut tags = Vec::new();
84        for _ in 0..tags_len {
85            let (tag, remaining) = decode::read_str_from_slice(bytes).unwrap();
86            tags.push(tag.to_owned());
87            bytes = remaining;
88        }
89
90        let (script, bytes) = decode::read_str_from_slice(bytes).unwrap();
91
92        if !bytes.is_empty() {
93            bail!("trailing bytes in encoded script record. malformed")
94        }
95
96        Ok(Script {
97            id: Uuid::parse_str(id).unwrap(),
98            name: name.to_owned(),
99            description: description.to_owned(),
100            shebang: shebang.to_owned(),
101            tags,
102            script: script.to_owned(),
103        })
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn test_serialize() {
113        let script = Script {
114            id: uuid::Uuid::parse_str("0195c825a35f7982bdb016168881cbc6").unwrap(),
115            name: "test".to_string(),
116            description: "test".to_string(),
117            shebang: "test".to_string(),
118            tags: vec!["test".to_string()],
119            script: "test".to_string(),
120        };
121
122        let serialized = script.serialize().unwrap();
123        assert_eq!(
124            serialized.0,
125            vec![
126                150, 217, 36, 48, 49, 57, 53, 99, 56, 50, 53, 45, 97, 51, 53, 102, 45, 55, 57, 56,
127                50, 45, 98, 100, 98, 48, 45, 49, 54, 49, 54, 56, 56, 56, 49, 99, 98, 99, 54, 164,
128                116, 101, 115, 116, 164, 116, 101, 115, 116, 164, 116, 101, 115, 116, 145, 164,
129                116, 101, 115, 116, 164, 116, 101, 115, 116
130            ]
131        );
132    }
133
134    #[test]
135    fn test_serialize_deserialize() {
136        let script = Script {
137            id: uuid::Uuid::new_v4(),
138            name: "test".to_string(),
139            description: "test".to_string(),
140            shebang: "test".to_string(),
141            tags: vec!["test".to_string()],
142            script: "test".to_string(),
143        };
144
145        let serialized = script.serialize().unwrap();
146
147        let deserialized = Script::deserialize(&serialized.0).unwrap();
148
149        assert_eq!(script, deserialized);
150    }
151}