char/avril
(wip) why'd you have to go and make things so complicaaaaated?
git clone https://git.t4t.associates/char/avril
5e8730e
main
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 { 27Check { 28keyspace : String , 29key : Vec < u8 >, 30expected : Option < String >, 31}, 32Set { 33keyspace : String , 34key : Vec < u8 >, 35value : Vec < u8 >, 36}, 37Delete { 38keyspace : String , 39key : Vec < u8 >, 40}, 41} 42 43impl BatchOp { 44pub fn keyspace ( & self ) ->& str { 45match self { 46BatchOp :: 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 > { 58bytes : &' a [ u8 ], 59} 60 61impl < ' a > Cursor < ' a > { 62fn take ( & mut self , len : usize ) -> io:: Result < &' a [ u8 ]> { 63if self . bytes . len () < len{ 64return Err ( invalid ( "Truncated atomic batch" )); 65} 66let ( head, tail) =self . bytes . split_at ( len); 67self . bytes = tail; 68Ok ( head) 69} 70 71fn u8 ( & mut self ) -> io:: Result < u8 > { 72Ok ( self . take ( 1 ) ?[ 0 ]) 73} 74 75fn u16 ( & mut self ) -> io:: Result < usize > { 76let bytes =self . take ( 2 ) ?; 77Ok ( u16:: from_le_bytes ([ bytes[ 0 ], bytes[ 1 ]]) as usize ) 78} 79 80fn u32 ( & mut self ) -> io:: Result < usize > { 81let bytes =self . take ( 4 ) ?; 82Ok ( u32:: from_le_bytes ([ bytes[ 0 ], bytes[ 1 ], bytes[ 2 ], bytes[ 3 ]]) as usize ) 83} 84 85fn keyspace ( & mut self ) -> io:: Result < String > { 86let len =self . u16 () ?; 87let bytes =self . take ( len) ?; 88let name = std:: str:: from_utf8 ( bytes). map_err ( |_|invalid ( "Keyspace name is not UTF-8" )) ?; 89super :: store:: check_keyspace_name ( name) ?; 90Ok ( name. to_owned ()) 91} 92 93fn key ( & mut self ) -> io:: Result < Vec < u8 >> { 94let len =self . u16 () ?; 95Ok ( self . take ( len) ?. to_vec ()) 96} 97} 98 99pub fn decode ( bytes : & [ u8 ]) -> io:: Result < Vec < BatchOp >> { 100if bytes. len () >MAX_BATCH_BYTES { 101return Err ( invalid ( "Atomic batch exceeds size limit" )); 102} 103let mut cursor =Cursor { bytes}; 104let mut ops =Vec :: new (); 105while !cursor. bytes . is_empty () { 106if ops. len () >=MAX_BATCH_OPS { 107return Err ( invalid ( "Atomic batch exceeds operation limit" )); 108} 109let keyspace; 110let key; 111match cursor. u8 () ?{ 112TAG_CHECK =>{ 113 keyspace = cursor. keyspace () ?; 114 key = cursor. key () ?; 115let expected =match cursor. u8 () ?{ 1160 =>None , 1171 =>{ 118let len = cursor. u16 () ?; 119let stamp = cursor. take ( len) ?; 120let stamp = std:: str:: from_utf8 ( stamp) 121. map_err ( |_|invalid ( "Versionstamp is not UTF-8" )) ?; 122Some ( stamp. to_owned ()) 123} 124 _ =>return Err ( invalid ( "Invalid check expectation flag" )), 125}; 126 ops. push ( BatchOp :: Check { keyspace, key, expected}); 127} 128TAG_SET =>{ 129 keyspace = cursor. keyspace () ?; 130 key = cursor. key () ?; 131let len = cursor. u32 () ?; 132let value = cursor. take ( len) ?. to_vec (); 133 ops. push ( BatchOp :: Set { keyspace, key, value}); 134} 135TAG_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} 143Ok ( ops) 144} 145 146# [ cfg ( test )] 147mod tests{ 148use super :: * ; 149 150fn encode ( parts : & [ & [ u8 ]]) ->Vec < u8 > { 151 parts. concat () 152} 153 154# [ test ] 155fn round_trip () { 156let batch =encode ( & [ 157& [ TAG_CHECK ], 158& [ 2 , 0 ], 159b"ks" , 160& [ 1 , 0 ], 161b"k" , 162& [ 1 ], 163& [ 4 , 0 ], 164b"0123" , 165& [ TAG_SET ], 166& [ 2 , 0 ], 167b"ks" , 168& [ 1 , 0 ], 169b"k" , 170& [ 3 , 0 , 0 , 0 ], 171b"val" , 172& [ TAG_DELETE ], 173& [ 1 , 0 ], 174b"q" , 175& [ 0 , 0 ], 176]); 177assert_eq! ( 178decode ( & batch). unwrap (), 179vec! [ 180BatchOp :: Check { 181keyspace : "ks" . into (), 182key : b"k" . to_vec (), 183expected : Some ( "0123" . into ()), 184}, 185BatchOp :: Set { 186keyspace : "ks" . into (), 187key : b"k" . into (), 188value : b"val" . to_vec (), 189}, 190BatchOp :: Delete { 191keyspace : "q" . into (), 192key : vec! [], 193}, 194] 195); 196} 197 198# [ test ] 199fn rejects_truncation_and_unknown_tags () { 200assert! ( decode ( & [ TAG_SET , 2 , 0 ]). is_err ()); 201assert! ( decode ( & [ 9 ]). is_err ()); 202assert! ( decode ( & []). unwrap (). is_empty ()); 203} 204}