Skip to main content

commonware_storage/qmdb/immutable/operation/
fixed.rs

1use super::{Operation, COMMIT_CONTEXT, SET_CONTEXT};
2use crate::{
3    merkle::{Family, Location},
4    qmdb::any::{value::FixedEncoding, FixedValue},
5};
6use commonware_codec::{
7    util::{at_least, ensure_zeros},
8    Error as CodecError, FixedSize, Read, ReadExt as _, Write,
9};
10use commonware_runtime::{Buf, BufMut};
11use commonware_utils::Array;
12
13/// `max(a, b)` in a const context.
14const fn const_max(a: usize, b: usize) -> usize {
15    if a > b {
16        a
17    } else {
18        b
19    }
20}
21
22const fn set_op_size<K: Array, V: FixedSize>() -> usize {
23    1 + K::SIZE + V::SIZE
24}
25
26const fn commit_op_size<V: FixedSize>() -> usize {
27    1 + 1 + V::SIZE + u64::SIZE
28}
29
30const fn total_op_size<K: Array, V: FixedSize>() -> usize {
31    const_max(set_op_size::<K, V>(), commit_op_size::<V>())
32}
33
34impl<F: Family, K: Array, V: FixedValue> FixedSize for Operation<F, K, FixedEncoding<V>> {
35    const SIZE: usize = total_op_size::<K, V>();
36}
37
38impl<F: Family, K: Array, V: FixedValue> Write for Operation<F, K, FixedEncoding<V>> {
39    fn write(&self, buf: &mut impl BufMut) {
40        let total = total_op_size::<K, V>();
41        match &self {
42            Self::Set(k, v) => {
43                SET_CONTEXT.write(buf);
44                k.write(buf);
45                v.write(buf);
46                buf.put_bytes(0, total - set_op_size::<K, V>());
47            }
48            Self::Commit(v, floor_loc) => {
49                COMMIT_CONTEXT.write(buf);
50                if let Some(v) = v {
51                    true.write(buf);
52                    v.write(buf);
53                } else {
54                    buf.put_bytes(0, 1 + V::SIZE);
55                }
56                buf.put_slice(&floor_loc.to_be_bytes());
57                buf.put_bytes(0, total - commit_op_size::<V>());
58            }
59        }
60    }
61}
62
63impl<F: Family, K: Array, V: FixedValue> Read for Operation<F, K, FixedEncoding<V>> {
64    type Cfg = ();
65
66    fn read_cfg(buf: &mut impl Buf, _: &Self::Cfg) -> Result<Self, CodecError> {
67        let total = total_op_size::<K, V>();
68        at_least(buf, total)?;
69
70        match u8::read(buf)? {
71            SET_CONTEXT => {
72                let key = K::read(buf)?;
73                let value = V::read(buf)?;
74                ensure_zeros(buf, total - set_op_size::<K, V>())?;
75                Ok(Self::Set(key, value))
76            }
77            COMMIT_CONTEXT => {
78                let is_some = bool::read(buf)?;
79                let value = if is_some {
80                    Some(V::read(buf)?)
81                } else {
82                    ensure_zeros(buf, V::SIZE)?;
83                    None
84                };
85                let floor_loc = Location::new(u64::read(buf)?);
86                if !floor_loc.is_valid() {
87                    return Err(CodecError::Invalid(
88                        "storage::qmdb::immutable::operation::fixed::Operation",
89                        "commit floor location overflow",
90                    ));
91                }
92                ensure_zeros(buf, total - commit_op_size::<V>())?;
93                Ok(Self::Commit(value, floor_loc))
94            }
95            e => Err(CodecError::InvalidEnum(e)),
96        }
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use crate::merkle::mmr;
104    use commonware_codec::{DecodeExt, Encode};
105    use commonware_utils::sequence::U64;
106
107    type FixedOp = Operation<mmr::Family, U64, FixedEncoding<U64>>;
108
109    #[test]
110    fn test_fixed_size() {
111        // Set: 1 + 8 + 8 = 17
112        // Commit: 1 + 1 + 8 + 8 = 18
113        // Max = 18
114        assert_eq!(FixedOp::SIZE, 18);
115    }
116
117    #[test]
118    fn test_uniform_encoding_size() {
119        let set_op = FixedOp::Set(U64::new(1), U64::new(2));
120        let commit_some = FixedOp::Commit(Some(U64::new(3)), Location::new(10));
121        let commit_none = FixedOp::Commit(None, Location::new(0));
122
123        assert_eq!(set_op.encode().len(), FixedOp::SIZE);
124        assert_eq!(commit_some.encode().len(), FixedOp::SIZE);
125        assert_eq!(commit_none.encode().len(), FixedOp::SIZE);
126    }
127
128    #[test]
129    fn test_roundtrip() {
130        let operations: Vec<FixedOp> = vec![
131            FixedOp::Set(U64::new(1234), U64::new(56789)),
132            FixedOp::Commit(Some(U64::new(42)), Location::new(100)),
133            FixedOp::Commit(None, Location::new(0)),
134        ];
135
136        for op in operations {
137            let encoded = op.encode();
138            assert_eq!(encoded.len(), FixedOp::SIZE);
139            let decoded = FixedOp::decode(encoded).unwrap();
140            assert_eq!(op, decoded, "Failed to roundtrip: {op:?}");
141        }
142    }
143
144    #[test]
145    fn test_invalid_context() {
146        let mut invalid = vec![0xFF];
147        invalid.resize(FixedOp::SIZE, 0);
148        let decoded = FixedOp::decode(invalid.as_ref());
149        assert!(matches!(
150            decoded.unwrap_err(),
151            CodecError::InvalidEnum(0xFF)
152        ));
153    }
154
155    #[test]
156    fn test_insufficient_buffer() {
157        let invalid = vec![SET_CONTEXT];
158        let decoded = FixedOp::decode(invalid.as_ref());
159        assert!(matches!(decoded.unwrap_err(), CodecError::EndOfBuffer));
160    }
161
162    #[test]
163    fn test_nonzero_padding_rejected() {
164        let op = FixedOp::Set(U64::new(1), U64::new(2));
165        let mut encoded: Vec<u8> = op.encode().to_vec();
166        // Corrupt padding byte (only if there is padding)
167        if set_op_size::<U64, U64>() < total_op_size::<U64, U64>() {
168            let last = encoded.len() - 1;
169            encoded[last] = 0xFF;
170            let decoded = FixedOp::decode(encoded.as_ref());
171            assert!(decoded.is_err());
172        }
173    }
174
175    #[cfg(feature = "arbitrary")]
176    mod conformance {
177        use super::*;
178        use commonware_codec::conformance::CodecConformance;
179
180        commonware_conformance::conformance_tests! {
181            CodecConformance<FixedOp>
182        }
183    }
184}