commonware_storage/qmdb/keyless/operation/
fixed.rs1use crate::{
2 merkle::Family,
3 qmdb::{
4 any::{FixedValue, value::FixedEncoding},
5 keyless::operation::{APPEND_CONTEXT, COMMIT_CONTEXT, Codec, Operation},
6 operation::{commit_fixed_operation_size, read_commit_fixed, write_commit_fixed},
7 },
8};
9use commonware_codec::{
10 Error as CodecError, FixedSize, ReadExt as _, Write,
11 util::{at_least, ensure_zeros},
12};
13use commonware_runtime::{Buf, BufMut};
14
15const fn op_size<V: FixedSize>() -> usize {
18 commit_fixed_operation_size::<V>()
19}
20
21impl<V: FixedValue> Codec for FixedEncoding<V> {
22 type ReadCfg = ();
23
24 fn write_operation<F: Family>(op: &Operation<F, Self>, buf: &mut impl BufMut) {
25 let total = op_size::<V>();
26 match op {
27 Operation::Append(value) => {
28 APPEND_CONTEXT.write(buf);
29 value.write(buf);
30 buf.put_bytes(0, total - 1 - V::SIZE);
32 }
33 Operation::Commit(metadata, floor) => {
34 COMMIT_CONTEXT.write(buf);
35 write_commit_fixed(metadata, *floor, buf);
36 }
37 }
38 }
39
40 fn read_operation<F: Family>(
41 buf: &mut impl Buf,
42 _cfg: &Self::ReadCfg,
43 ) -> Result<Operation<F, Self>, CodecError> {
44 let total = op_size::<V>();
45 at_least(buf, total)?;
46
47 match u8::read(buf)? {
48 APPEND_CONTEXT => {
49 let value = V::read(buf)?;
50 ensure_zeros(buf, total - 1 - V::SIZE)?;
51 Ok(Operation::Append(value))
52 }
53 COMMIT_CONTEXT => {
54 let (metadata, floor) = read_commit_fixed(buf)?;
55 Ok(Operation::Commit(metadata, floor))
56 }
57 e => Err(CodecError::InvalidEnum(e)),
58 }
59 }
60}
61
62impl<F: Family, V: FixedValue> FixedSize for Operation<F, FixedEncoding<V>> {
63 const SIZE: usize = op_size::<V>();
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69 use crate::merkle::{Location, mmr};
70 use commonware_codec::{DecodeExt, Encode, FixedSize};
71 use commonware_utils::sequence::U64;
72
73 type Op = Operation<mmr::Family, FixedEncoding<U64>>;
74
75 #[test]
76 fn all_variants_have_same_encoded_size() {
77 let append = Op::Append(U64::new(42));
78 let commit_some = Op::Commit(Some(U64::new(99)), Location::new(5));
79 let commit_none = Op::Commit(None, Location::new(0));
80
81 let a = append.encode();
82 let b = commit_some.encode();
83 let c = commit_none.encode();
84
85 assert_eq!(a.len(), Op::SIZE);
86 assert_eq!(b.len(), Op::SIZE);
87 assert_eq!(c.len(), Op::SIZE);
88 assert_eq!(Op::SIZE, 2 + U64::SIZE + u64::SIZE);
89 }
90
91 #[test]
92 fn append_roundtrip() {
93 let op = Op::Append(U64::new(12345));
94 let decoded = Op::decode(op.encode()).unwrap();
95 assert_eq!(op, decoded);
96 }
97
98 #[test]
99 fn commit_some_roundtrip() {
100 let op = Op::Commit(Some(U64::new(999)), Location::new(77));
101 let decoded = Op::decode(op.encode()).unwrap();
102 assert_eq!(op, decoded);
103 }
104
105 #[test]
106 fn commit_none_roundtrip() {
107 let op = Op::Commit(None, Location::new(42));
108 let decoded = Op::decode(op.encode()).unwrap();
109 assert_eq!(op, decoded);
110 }
111
112 #[test]
113 fn invalid_context_byte_rejected() {
114 let mut buf = vec![0u8; Op::SIZE];
115 buf[0] = 0xFF;
116 assert!(matches!(
117 Op::decode(buf.as_ref()).unwrap_err(),
118 CodecError::InvalidEnum(0xFF)
119 ));
120 }
121
122 #[test]
123 fn non_zero_padding_rejected() {
124 let op = Op::Append(U64::new(1));
126 let mut buf: Vec<u8> = op.encode().to_vec();
127 *buf.last_mut().unwrap() = 0x01;
129 assert!(Op::decode(buf.as_ref()).is_err());
130 }
131
132 #[test]
133 fn truncated_input_rejected() {
134 let op = Op::Append(U64::new(1));
135 let buf = op.encode();
136 assert!(Op::decode(&buf[..buf.len() - 1]).is_err());
138 }
139
140 #[test]
141 fn commit_none_has_zero_value_bytes() {
142 let op = Op::Commit(None, Location::new(0));
143 let buf: Vec<u8> = op.encode().to_vec();
144 assert!(buf[2..].iter().all(|&b| b == 0));
147 }
148
149 #[test]
150 fn commit_floor_overflow_rejected() {
151 let mut buf = vec![0u8; Op::SIZE];
153 buf[0] = COMMIT_CONTEXT;
154 let floor_bytes = u64::MAX.to_be_bytes();
157 let floor_offset = Op::SIZE - u64::SIZE;
158 buf[floor_offset..].copy_from_slice(&floor_bytes);
159 assert!(matches!(
160 Op::decode(buf.as_ref()).unwrap_err(),
161 CodecError::Invalid(_, _)
162 ));
163 }
164
165 #[test]
166 fn commit_nonzero_metadata_bytes_rejected() {
167 let mut buf = vec![0u8; Op::SIZE];
170 buf[0] = COMMIT_CONTEXT;
171 buf[2] = 0x01;
172 assert!(matches!(
173 Op::decode(buf.as_ref()).unwrap_err(),
174 CodecError::Invalid(_, _)
175 ));
176 }
177}