commonware_storage/qmdb/keyless/operation/
variable.rs1use crate::{
2 merkle::Family,
3 qmdb::{
4 any::{VariableValue, value::VariableEncoding},
5 keyless::operation::{APPEND_CONTEXT, COMMIT_CONTEXT, Codec, Operation},
6 operation::{commit_variable_payload_size, read_commit_variable, write_commit_variable},
7 },
8};
9use commonware_codec::{EncodeSize, Error as CodecError, Read, ReadExt as _, Write};
10use commonware_runtime::{Buf, BufMut};
11
12impl<V: VariableValue> Codec for VariableEncoding<V> {
13 type ReadCfg = <V as Read>::Cfg;
14
15 fn write_operation<F: Family>(op: &Operation<F, Self>, buf: &mut impl BufMut) {
16 match op {
17 Operation::Append(value) => {
18 APPEND_CONTEXT.write(buf);
19 value.write(buf);
20 }
21 Operation::Commit(metadata, floor) => {
22 COMMIT_CONTEXT.write(buf);
23 write_commit_variable(metadata, *floor, buf);
24 }
25 }
26 }
27
28 fn read_operation<F: Family>(
29 buf: &mut impl Buf,
30 cfg: &Self::ReadCfg,
31 ) -> Result<Operation<F, Self>, CodecError> {
32 match u8::read(buf)? {
33 APPEND_CONTEXT => Ok(Operation::Append(V::read_cfg(buf, cfg)?)),
34 COMMIT_CONTEXT => {
35 let (metadata, floor) = read_commit_variable(buf, cfg)?;
36 Ok(Operation::Commit(metadata, floor))
37 }
38 e => Err(CodecError::InvalidEnum(e)),
39 }
40 }
41}
42
43impl<F: Family, V: VariableValue> EncodeSize for Operation<F, VariableEncoding<V>> {
44 fn encode_size(&self) -> usize {
45 1 + match self {
46 Self::Append(v) => v.encode_size(),
47 Self::Commit(v, floor) => commit_variable_payload_size(v, *floor),
48 }
49 }
50}
51
52#[cfg(test)]
53mod tests {
54 use super::*;
55 use crate::merkle::{Location, mmr};
56 use commonware_codec::{DecodeExt, Encode, EncodeSize};
57 use commonware_utils::sequence::U64;
58
59 type Op = Operation<mmr::Family, VariableEncoding<U64>>;
61
62 #[test]
63 fn append_roundtrip() {
64 let op = Op::Append(U64::new(12345));
65 let decoded = Op::decode(op.encode()).unwrap();
66 assert_eq!(op, decoded);
67 }
68
69 #[test]
70 fn commit_some_roundtrip() {
71 let op = Op::Commit(Some(U64::new(999)), Location::new(77));
72 let decoded = Op::decode(op.encode()).unwrap();
73 assert_eq!(op, decoded);
74 }
75
76 #[test]
77 fn commit_none_roundtrip() {
78 let op = Op::Commit(None, Location::new(42));
79 let decoded = Op::decode(op.encode()).unwrap();
80 assert_eq!(op, decoded);
81 }
82
83 #[test]
84 fn encode_size_matches_encoded_len() {
85 let cases: Vec<Op> = vec![
86 Op::Append(U64::new(0)),
87 Op::Append(U64::new(u64::MAX)),
88 Op::Commit(None, Location::new(0)),
89 Op::Commit(Some(U64::new(42)), Location::new(1234)),
90 ];
91 for op in cases {
92 assert_eq!(op.encode_size(), op.encode().len(), "mismatch for {op:?}");
93 }
94 }
95
96 #[test]
97 fn invalid_context_byte_rejected() {
98 let op = Op::Append(U64::new(1));
99 let mut buf: Vec<u8> = op.encode().to_vec();
100 buf[0] = 0xFF;
101 assert!(matches!(
102 Op::decode(buf.as_ref()).unwrap_err(),
103 CodecError::InvalidEnum(0xFF)
104 ));
105 }
106
107 #[test]
108 fn empty_input_rejected() {
109 assert!(Op::decode(&[] as &[u8]).is_err());
110 }
111
112 #[test]
113 fn append_and_commit_have_different_encodings() {
114 let append = Op::Append(U64::new(1));
115 let commit = Op::Commit(Some(U64::new(1)), Location::new(0));
116 assert_ne!(append.encode().as_ref(), commit.encode().as_ref());
117 }
118
119 #[test]
120 fn context_byte_is_first() {
121 let append = Op::Append(U64::new(0));
122 let commit = Op::Commit(None, Location::new(0));
123 assert_eq!(append.encode()[0], APPEND_CONTEXT);
124 assert_eq!(commit.encode()[0], COMMIT_CONTEXT);
125 }
126
127 #[test]
128 fn commit_floor_overflow_rejected() {
129 use commonware_codec::{Write, varint::UInt};
131 let mut buf = Vec::new();
132 COMMIT_CONTEXT.write(&mut buf);
133 Option::<U64>::None.write(&mut buf);
134 UInt(u64::MAX).write(&mut buf);
135 assert!(matches!(
136 Op::decode(buf.as_ref()).unwrap_err(),
137 CodecError::Invalid(_, _)
138 ));
139 }
140}