rushdb_types/
command_codec.rs1use crate::command::CommitBatch;
13
14const MAX_COMMAND_BYTES: usize = 1 << 30;
17
18#[derive(Debug, thiserror::Error, PartialEq, Eq)]
20#[error("malformed command: {0}")]
21pub struct CommandCodecError(String);
22
23#[must_use]
29pub fn encode(batch: &CommitBatch) -> Vec<u8> {
30 postcard::to_stdvec(batch).expect("a CommitBatch always encodes")
31}
32
33pub fn decode(bytes: &[u8]) -> Result<CommitBatch, CommandCodecError> {
40 if bytes.len() > MAX_COMMAND_BYTES {
41 return Err(CommandCodecError(format!("{} bytes", bytes.len())));
42 }
43 let (batch, rest) = postcard::take_from_bytes::<CommitBatch>(bytes)
44 .map_err(|error| CommandCodecError(error.to_string()))?;
45 if !rest.is_empty() {
46 return Err(CommandCodecError(format!("{} trailing bytes", rest.len())));
47 }
48 if batch.command_version != crate::command::COMMAND_VERSION {
49 return Err(CommandCodecError(format!(
50 "command version {} (expected {})",
51 batch.command_version,
52 crate::command::COMMAND_VERSION
53 )));
54 }
55 Ok(batch)
56}
57
58#[cfg(test)]
59mod tests {
60 use super::*;
61 use crate::command::{IsolationMode, Mutation, Precondition};
62 use crate::ids::{OwnerKind, TenantId};
63 use crate::model::LogicalType;
64 use crate::occurrence::{OccurrenceRow, OccurrenceValue};
65
66 struct Rng(u64);
68 impl Rng {
69 fn next(&mut self) -> u64 {
70 self.0 ^= self.0 << 13;
71 self.0 ^= self.0 >> 7;
72 self.0 ^= self.0 << 17;
73 self.0
74 }
75 fn bytes(&mut self, max: u64) -> Vec<u8> {
76 (0..self.next() % max)
77 .map(|_| self.next().to_le_bytes()[0])
78 .collect()
79 }
80 }
81
82 fn batch(rng: &mut Rng) -> CommitBatch {
83 let mut mutations = Vec::new();
84 for _ in 0..rng.next() % 8 {
85 mutations.push(match rng.next() % 5 {
86 0 => Mutation::CreateRecordHeader {
87 record_id: rng.next().to_le_bytes().repeat(2).try_into().unwrap(),
88 ordinal: rng.next(),
89 label: format!("L{}", rng.next() % 100),
90 },
91 1 => Mutation::PutOccurrence {
92 row: OccurrenceRow {
93 owner_kind: OwnerKind::Record,
94 owner_ord: rng.next() >> (rng.next() % 64),
95 property_id: rng.next() % 1000,
96 family_id: rng.next() % 1000,
97 elem_ordinal: rng.next().is_multiple_of(2).then(|| rng.next() % 10),
98 occurrence_value: OccurrenceValue::Stored {
99 ty: LogicalType::Utf8,
100 },
101 },
102 canonical_value_bytes: Some(rng.bytes(200)),
103 },
104 2 => Mutation::PutRawKey {
105 key: rng.bytes(64),
106 value: rng.bytes(1024),
107 },
108 3 => Mutation::SetMetaValue {
109 key: format!("k{}", rng.next()),
110 value: rng.next(),
111 },
112 _ => Mutation::IndexApplied {
113 index: u32::try_from(rng.next() % 50).unwrap(),
114 upto: rng.next(),
115 owners: (0..rng.next() % 20).map(|_| rng.next()).collect(),
116 },
117 });
118 }
119 CommitBatch {
120 command_version: crate::command::COMMAND_VERSION,
121 tenant: TenantId::from_ordinal(rng.next() % 100 + 1),
122 transaction_id: format!("tx{}", rng.next() % 7),
123 commit_key: uuid::Uuid::from_u128(
124 u128::from(rng.next()) << 64 | u128::from(rng.next()),
125 ),
126 start_index: rng.next(),
127 isolation: [
128 IsolationMode::ReadCommitted,
129 IsolationMode::Snapshot,
130 IsolationMode::Serializable,
131 ][usize::try_from(rng.next() % 3).unwrap()],
132 preconditions: (0..rng.next() % 4)
133 .map(|i| {
134 if i % 2 == 0 {
135 Precondition::absent(rng.bytes(40))
136 } else {
137 Precondition::exists(rng.bytes(40))
138 }
139 })
140 .collect(),
141 mutations,
142 }
143 }
144
145 #[test]
146 fn command_codec_roundtrip_proptest_and_malformed_fuzz() {
147 let mut rng = Rng(0x2545_f491_4f6c_dd1d);
148 for _ in 0..2_000 {
149 let original = batch(&mut rng);
150 let bytes = encode(&original);
151 assert_eq!(decode(&bytes).unwrap(), original);
152 let cut = usize::try_from(rng.next()).unwrap() % bytes.len();
154 assert!(decode(&bytes[..cut]).is_err());
155 let mut longer = bytes.clone();
157 longer.push(0);
158 assert!(decode(&longer).is_err());
159 let mut corrupt = bytes;
162 let at = usize::try_from(rng.next()).unwrap() % corrupt.len();
163 corrupt[at] = rng.next().to_le_bytes()[0];
164 let _ = decode(&corrupt);
165 }
166 for _ in 0..2_000 {
167 let noise = rng.bytes(256);
168 let _ = decode(&noise);
169 }
170 }
171
172 #[test]
173 fn foreign_command_version_is_rejected() {
174 let mut rng = Rng(7);
175 let mut stale = batch(&mut rng);
176 stale.command_version -= 1;
177 assert!(decode(&encode(&stale)).is_err());
178 }
179
180 #[test]
181 fn a_threshold_policy_round_trips_after_the_earlier_variants() {
182 use crate::security::{AuthorizationMode, DataAccessPolicy};
183 let batch = CommitBatch {
184 command_version: crate::command::COMMAND_VERSION,
185 tenant: TenantId::from_ordinal(1),
186 transaction_id: String::new(),
187 commit_key: uuid::Uuid::nil(),
188 start_index: 0,
189 isolation: IsolationMode::Snapshot,
190 preconditions: Vec::new(),
191 mutations: vec![
192 Mutation::SetSecurityPolicy {
193 policy: DataAccessPolicy::default(),
194 mode: AuthorizationMode::PolicyEnforced,
195 },
196 Mutation::SetSecurityPolicyThreshold {
197 policy: DataAccessPolicy::default(),
198 mode: AuthorizationMode::PolicyEnforced,
199 min_count: 5,
200 },
201 ],
202 };
203 let bytes = encode(&batch);
204 assert_eq!(decode(&bytes).unwrap(), batch);
207 }
208}