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