Skip to main content

commonware_storage/qmdb/keyless/operation/
fixed.rs

1use crate::qmdb::{
2    any::{value::FixedEncoding, FixedValue},
3    keyless::operation::{Codec, Operation, APPEND_CONTEXT, COMMIT_CONTEXT},
4};
5use commonware_codec::{
6    util::{at_least, ensure_zeros},
7    Error as CodecError, FixedSize, ReadExt as _, Write,
8};
9use commonware_runtime::{Buf, BufMut};
10
11/// Total padded operation size: context byte + option-tag byte + value.
12/// Append: 1 (context) + V::SIZE + 1 (padding for the option tag).
13/// Commit: 1 (context) + 1 (option tag) + V::SIZE.
14/// Both are 2 + V::SIZE.
15const fn op_size<V: FixedSize>() -> usize {
16    2 + V::SIZE
17}
18
19impl<V: FixedValue> Codec for FixedEncoding<V> {
20    type ReadCfg = ();
21
22    fn write_operation(op: &Operation<Self>, buf: &mut impl BufMut) {
23        let total = op_size::<V>();
24        match op {
25            Operation::Append(value) => {
26                APPEND_CONTEXT.write(buf);
27                value.write(buf);
28                // Pad to uniform size (1 byte for the option-tag gap).
29                buf.put_bytes(0, total - 1 - V::SIZE);
30            }
31            Operation::Commit(metadata) => {
32                COMMIT_CONTEXT.write(buf);
33                if let Some(metadata) = metadata {
34                    true.write(buf);
35                    metadata.write(buf);
36                } else {
37                    buf.put_bytes(0, 1 + V::SIZE);
38                }
39            }
40        }
41    }
42
43    fn read_operation(
44        buf: &mut impl Buf,
45        _cfg: &Self::ReadCfg,
46    ) -> Result<Operation<Self>, CodecError> {
47        let total = op_size::<V>();
48        at_least(buf, total)?;
49
50        match u8::read(buf)? {
51            APPEND_CONTEXT => {
52                let value = V::read(buf)?;
53                ensure_zeros(buf, total - 1 - V::SIZE)?;
54                Ok(Operation::Append(value))
55            }
56            COMMIT_CONTEXT => {
57                let is_some = bool::read(buf)?;
58                let metadata = if is_some {
59                    Some(V::read(buf)?)
60                } else {
61                    ensure_zeros(buf, V::SIZE)?;
62                    None
63                };
64                Ok(Operation::Commit(metadata))
65            }
66            e => Err(CodecError::InvalidEnum(e)),
67        }
68    }
69}
70
71impl<V: FixedValue> FixedSize for Operation<FixedEncoding<V>> {
72    const SIZE: usize = op_size::<V>();
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78    use commonware_codec::{DecodeExt, Encode, FixedSize};
79    use commonware_utils::sequence::U64;
80
81    type Op = Operation<FixedEncoding<U64>>;
82
83    #[test]
84    fn all_variants_have_same_encoded_size() {
85        let append = Op::Append(U64::new(42));
86        let commit_some = Op::Commit(Some(U64::new(99)));
87        let commit_none = Op::Commit(None);
88
89        let a = append.encode();
90        let b = commit_some.encode();
91        let c = commit_none.encode();
92
93        assert_eq!(a.len(), Op::SIZE);
94        assert_eq!(b.len(), Op::SIZE);
95        assert_eq!(c.len(), Op::SIZE);
96        assert_eq!(Op::SIZE, 2 + U64::SIZE);
97    }
98
99    #[test]
100    fn append_roundtrip() {
101        let op = Op::Append(U64::new(12345));
102        let decoded = Op::decode(op.encode()).unwrap();
103        assert_eq!(op, decoded);
104    }
105
106    #[test]
107    fn commit_some_roundtrip() {
108        let op = Op::Commit(Some(U64::new(999)));
109        let decoded = Op::decode(op.encode()).unwrap();
110        assert_eq!(op, decoded);
111    }
112
113    #[test]
114    fn commit_none_roundtrip() {
115        let op = Op::Commit(None);
116        let decoded = Op::decode(op.encode()).unwrap();
117        assert_eq!(op, decoded);
118    }
119
120    #[test]
121    fn invalid_context_byte_rejected() {
122        let mut buf = vec![0u8; Op::SIZE];
123        buf[0] = 0xFF;
124        assert!(matches!(
125            Op::decode(buf.as_ref()).unwrap_err(),
126            CodecError::InvalidEnum(0xFF)
127        ));
128    }
129
130    #[test]
131    fn non_zero_padding_rejected() {
132        // Encode an Append, then corrupt the padding byte.
133        let op = Op::Append(U64::new(1));
134        let mut buf: Vec<u8> = op.encode().to_vec();
135        // Padding is the last byte (option-tag gap).
136        *buf.last_mut().unwrap() = 0x01;
137        assert!(Op::decode(buf.as_ref()).is_err());
138    }
139
140    #[test]
141    fn truncated_input_rejected() {
142        let op = Op::Append(U64::new(1));
143        let buf = op.encode();
144        // One byte short.
145        assert!(Op::decode(&buf[..buf.len() - 1]).is_err());
146    }
147
148    #[test]
149    fn commit_none_has_zero_value_bytes() {
150        let op = Op::Commit(None);
151        let buf: Vec<u8> = op.encode().to_vec();
152        // After context byte (0) and option-tag byte (0), all value bytes should be zero.
153        assert!(buf[2..].iter().all(|&b| b == 0));
154    }
155}