Skip to main content

commonware_storage/qmdb/keyless/operation/
variable.rs

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