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