char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Som(break) add lib/kv (avril-kv crate) and make Web Storage use it5e8730e

main
5.8 KiB204 linesraw
1use std::io;
2
3// Wire format for one atomic batch, encoded incrementally by JS via
4// op_kv_atomic_push and committed by op_kv_atomic_commit. All integers are
5// little-endian; keyspace names are UTF-8 bytes, keys/values are opaque.
6//
7//   batch := op*
8//   op    := CHECK ks key expected | SET ks key value | DELETE ks key
9//   CHECK  := 0x01 u16 ks_len ks u16 key_len key u8 has_expected [u16 stamp_len stamp]
10//   SET    := 0x02 u16 ks_len ks u16 key_len key u32 value_len value
11//   DELETE := 0x03 u16 ks_len ks u16 key_len key
12//
13// has_expected = 0 means "must not exist" (null check); 1 carries the exact
14// versionstamp the entry must currently hold.
15
16pub const TAG_CHECK: u8 = 1;
17pub const TAG_SET: u8 = 2;
18pub const TAG_DELETE: u8 = 3;
19
20// Sane staging limits so a hostile or buggy guest cannot grow a batch without
21// bound before commit parsing rejects it.
22pub const MAX_BATCH_OPS: usize = 1_000;
23pub const MAX_BATCH_BYTES: usize = 4 << 20;
24
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum BatchOp {
27    Check {
28        keyspace: String,
29        key: Vec<u8>,
30        expected: Option<String>,
31    },
32    Set {
33        keyspace: String,
34        key: Vec<u8>,
35        value: Vec<u8>,
36    },
37    Delete {
38        keyspace: String,
39        key: Vec<u8>,
40    },
41}
42
43impl BatchOp {
44    pub fn keyspace(&self) -> &str {
45        match self {
46            BatchOp::Check { keyspace, .. }
47            | BatchOp::Set { keyspace, .. }
48            | BatchOp::Delete { keyspace, .. } => keyspace,
49        }
50    }
51}
52
53fn invalid(message: impl Into<String>) -> io::Error {
54    io::Error::new(io::ErrorKind::InvalidInput, message.into())
55}
56
57struct Cursor<'a> {
58    bytes: &'a [u8],
59}
60
61impl<'a> Cursor<'a> {
62    fn take(&mut self, len: usize) -> io::Result<&'a [u8]> {
63        if self.bytes.len() < len {
64            return Err(invalid("Truncated atomic batch"));
65        }
66        let (head, tail) = self.bytes.split_at(len);
67        self.bytes = tail;
68        Ok(head)
69    }
70
71    fn u8(&mut self) -> io::Result<u8> {
72        Ok(self.take(1)?[0])
73    }
74
75    fn u16(&mut self) -> io::Result<usize> {
76        let bytes = self.take(2)?;
77        Ok(u16::from_le_bytes([bytes[0], bytes[1]]) as usize)
78    }
79
80    fn u32(&mut self) -> io::Result<usize> {
81        let bytes = self.take(4)?;
82        Ok(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as usize)
83    }
84
85    fn keyspace(&mut self) -> io::Result<String> {
86        let len = self.u16()?;
87        let bytes = self.take(len)?;
88        let name = std::str::from_utf8(bytes).map_err(|_| invalid("Keyspace name is not UTF-8"))?;
89        super::store::check_keyspace_name(name)?;
90        Ok(name.to_owned())
91    }
92
93    fn key(&mut self) -> io::Result<Vec<u8>> {
94        let len = self.u16()?;
95        Ok(self.take(len)?.to_vec())
96    }
97}
98
99pub fn decode(bytes: &[u8]) -> io::Result<Vec<BatchOp>> {
100    if bytes.len() > MAX_BATCH_BYTES {
101        return Err(invalid("Atomic batch exceeds size limit"));
102    }
103    let mut cursor = Cursor { bytes };
104    let mut ops = Vec::new();
105    while !cursor.bytes.is_empty() {
106        if ops.len() >= MAX_BATCH_OPS {
107            return Err(invalid("Atomic batch exceeds operation limit"));
108        }
109        let keyspace;
110        let key;
111        match cursor.u8()? {
112            TAG_CHECK => {
113                keyspace = cursor.keyspace()?;
114                key = cursor.key()?;
115                let expected = match cursor.u8()? {
116                    0 => None,
117                    1 => {
118                        let len = cursor.u16()?;
119                        let stamp = cursor.take(len)?;
120                        let stamp = std::str::from_utf8(stamp)
121                            .map_err(|_| invalid("Versionstamp is not UTF-8"))?;
122                        Some(stamp.to_owned())
123                    }
124                    _ => return Err(invalid("Invalid check expectation flag")),
125                };
126                ops.push(BatchOp::Check { keyspace, key, expected });
127            }
128            TAG_SET => {
129                keyspace = cursor.keyspace()?;
130                key = cursor.key()?;
131                let len = cursor.u32()?;
132                let value = cursor.take(len)?.to_vec();
133                ops.push(BatchOp::Set { keyspace, key, value });
134            }
135            TAG_DELETE => {
136                keyspace = cursor.keyspace()?;
137                key = cursor.key()?;
138                ops.push(BatchOp::Delete { keyspace, key });
139            }
140            tag => return Err(invalid(format!("Unknown atomic batch tag {tag}"))),
141        }
142    }
143    Ok(ops)
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    fn encode(parts: &[&[u8]]) -> Vec<u8> {
151        parts.concat()
152    }
153
154    #[test]
155    fn round_trip() {
156        let batch = encode(&[
157            &[TAG_CHECK],
158            &[2, 0],
159            b"ks",
160            &[1, 0],
161            b"k",
162            &[1],
163            &[4, 0],
164            b"0123",
165            &[TAG_SET],
166            &[2, 0],
167            b"ks",
168            &[1, 0],
169            b"k",
170            &[3, 0, 0, 0],
171            b"val",
172            &[TAG_DELETE],
173            &[1, 0],
174            b"q",
175            &[0, 0],
176        ]);
177        assert_eq!(
178            decode(&batch).unwrap(),
179            vec![
180                BatchOp::Check {
181                    keyspace: "ks".into(),
182                    key: b"k".to_vec(),
183                    expected: Some("0123".into()),
184                },
185                BatchOp::Set {
186                    keyspace: "ks".into(),
187                    key: b"k".into(),
188                    value: b"val".to_vec(),
189                },
190                BatchOp::Delete {
191                    keyspace: "q".into(),
192                    key: vec![],
193                },
194            ]
195        );
196    }
197
198    #[test]
199    fn rejects_truncation_and_unknown_tags() {
200        assert!(decode(&[TAG_SET, 2, 0]).is_err());
201        assert!(decode(&[9]).is_err());
202        assert!(decode(&[]).unwrap().is_empty());
203    }
204}