commonware_storage/qmdb/immutable/operation/
fixed.rs1use super::{Operation, COMMIT_CONTEXT, SET_CONTEXT};
2use crate::qmdb::any::{value::FixedEncoding, FixedValue};
3use commonware_codec::{
4 util::{at_least, ensure_zeros},
5 Error as CodecError, FixedSize, Read, ReadExt as _, Write,
6};
7use commonware_runtime::{Buf, BufMut};
8use commonware_utils::Array;
9
10const fn const_max(a: usize, b: usize) -> usize {
12 if a > b {
13 a
14 } else {
15 b
16 }
17}
18
19const fn set_op_size<K: Array, V: FixedSize>() -> usize {
20 1 + K::SIZE + V::SIZE
21}
22
23const fn commit_op_size<V: FixedSize>() -> usize {
24 1 + 1 + V::SIZE
25}
26
27const fn total_op_size<K: Array, V: FixedSize>() -> usize {
28 const_max(set_op_size::<K, V>(), commit_op_size::<V>())
29}
30
31impl<K: Array, V: FixedValue> FixedSize for Operation<K, FixedEncoding<V>> {
32 const SIZE: usize = total_op_size::<K, V>();
33}
34
35impl<K: Array, V: FixedValue> Write for Operation<K, FixedEncoding<V>> {
36 fn write(&self, buf: &mut impl BufMut) {
37 let total = total_op_size::<K, V>();
38 match &self {
39 Self::Set(k, v) => {
40 SET_CONTEXT.write(buf);
41 k.write(buf);
42 v.write(buf);
43 buf.put_bytes(0, total - set_op_size::<K, V>());
44 }
45 Self::Commit(v) => {
46 COMMIT_CONTEXT.write(buf);
47 if let Some(v) = v {
48 true.write(buf);
49 v.write(buf);
50 } else {
51 buf.put_bytes(0, 1 + V::SIZE);
52 }
53 buf.put_bytes(0, total - commit_op_size::<V>());
54 }
55 }
56 }
57}
58
59impl<K: Array, V: FixedValue> Read for Operation<K, FixedEncoding<V>> {
60 type Cfg = ();
61
62 fn read_cfg(buf: &mut impl Buf, _: &Self::Cfg) -> Result<Self, CodecError> {
63 let total = total_op_size::<K, V>();
64 at_least(buf, total)?;
65
66 match u8::read(buf)? {
67 SET_CONTEXT => {
68 let key = K::read(buf)?;
69 let value = V::read(buf)?;
70 ensure_zeros(buf, total - set_op_size::<K, V>())?;
71 Ok(Self::Set(key, value))
72 }
73 COMMIT_CONTEXT => {
74 let is_some = bool::read(buf)?;
75 let value = if is_some {
76 Some(V::read(buf)?)
77 } else {
78 ensure_zeros(buf, V::SIZE)?;
79 None
80 };
81 ensure_zeros(buf, total - commit_op_size::<V>())?;
82 Ok(Self::Commit(value))
83 }
84 e => Err(CodecError::InvalidEnum(e)),
85 }
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92 use commonware_codec::{DecodeExt, Encode};
93 use commonware_utils::sequence::U64;
94
95 type FixedOp = Operation<U64, FixedEncoding<U64>>;
96
97 #[test]
98 fn test_fixed_size() {
99 assert_eq!(FixedOp::SIZE, 17);
103 }
104
105 #[test]
106 fn test_uniform_encoding_size() {
107 let set_op = FixedOp::Set(U64::new(1), U64::new(2));
108 let commit_some = FixedOp::Commit(Some(U64::new(3)));
109 let commit_none = FixedOp::Commit(None);
110
111 assert_eq!(set_op.encode().len(), FixedOp::SIZE);
112 assert_eq!(commit_some.encode().len(), FixedOp::SIZE);
113 assert_eq!(commit_none.encode().len(), FixedOp::SIZE);
114 }
115
116 #[test]
117 fn test_roundtrip() {
118 let operations: Vec<FixedOp> = vec![
119 FixedOp::Set(U64::new(1234), U64::new(56789)),
120 FixedOp::Commit(Some(U64::new(42))),
121 FixedOp::Commit(None),
122 ];
123
124 for op in operations {
125 let encoded = op.encode();
126 assert_eq!(encoded.len(), FixedOp::SIZE);
127 let decoded = FixedOp::decode(encoded).unwrap();
128 assert_eq!(op, decoded, "Failed to roundtrip: {op:?}");
129 }
130 }
131
132 #[test]
133 fn test_invalid_context() {
134 let mut invalid = vec![0xFF];
135 invalid.resize(FixedOp::SIZE, 0);
136 let decoded = FixedOp::decode(invalid.as_ref());
137 assert!(matches!(
138 decoded.unwrap_err(),
139 CodecError::InvalidEnum(0xFF)
140 ));
141 }
142
143 #[test]
144 fn test_insufficient_buffer() {
145 let invalid = vec![SET_CONTEXT];
146 let decoded = FixedOp::decode(invalid.as_ref());
147 assert!(matches!(decoded.unwrap_err(), CodecError::EndOfBuffer));
148 }
149
150 #[test]
151 fn test_nonzero_padding_rejected() {
152 let op = FixedOp::Set(U64::new(1), U64::new(2));
153 let mut encoded: Vec<u8> = op.encode().to_vec();
154 if set_op_size::<U64, U64>() < total_op_size::<U64, U64>() {
156 let last = encoded.len() - 1;
157 encoded[last] = 0xFF;
158 let decoded = FixedOp::decode(encoded.as_ref());
159 assert!(decoded.is_err());
160 }
161 }
162
163 #[cfg(feature = "arbitrary")]
164 mod conformance {
165 use super::*;
166 use commonware_codec::conformance::CodecConformance;
167
168 commonware_conformance::conformance_tests! {
169 CodecConformance<FixedOp>
170 }
171 }
172}