Skip to main content

rvf_types/
delta.rs

1//! DELTA_SEG (0x23) types for the RVF computational container.
2//!
3//! Defines the 64-byte `DeltaHeader` and associated enums per ADR-031.
4//! The DELTA_SEG stores sparse delta patches between clusters,
5//! enabling efficient incremental updates without full cluster rewrites.
6
7use crate::error::RvfError;
8
9/// Magic number for `DeltaHeader`: "RVDL" in big-endian.
10pub const DELTA_MAGIC: u32 = 0x5256_444C;
11
12/// Delta encoding strategy.
13#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15#[repr(u8)]
16pub enum DeltaEncoding {
17    /// Sparse row patches (individual vector updates).
18    SparseRows = 0,
19    /// Low-rank approximation of the delta.
20    LowRank = 1,
21    /// Full cluster patch (complete replacement).
22    FullPatch = 2,
23}
24
25impl TryFrom<u8> for DeltaEncoding {
26    type Error = RvfError;
27
28    fn try_from(value: u8) -> Result<Self, Self::Error> {
29        match value {
30            0 => Ok(Self::SparseRows),
31            1 => Ok(Self::LowRank),
32            2 => Ok(Self::FullPatch),
33            _ => Err(RvfError::InvalidEnumValue {
34                type_name: "DeltaEncoding",
35                value: value as u64,
36            }),
37        }
38    }
39}
40
41/// 64-byte header for DELTA_SEG payloads.
42///
43/// Follows the standard 64-byte `SegmentHeader`. All multi-byte fields are
44/// little-endian on the wire.
45#[derive(Clone, Copy, Debug)]
46#[repr(C)]
47pub struct DeltaHeader {
48    /// Magic: `DELTA_MAGIC` (0x5256444C, "RVDL").
49    pub magic: u32,
50    /// DeltaHeader format version (currently 1).
51    pub version: u16,
52    /// Delta encoding strategy (see `DeltaEncoding`).
53    pub encoding: u8,
54    /// Padding (must be zero).
55    pub _pad: u8,
56    /// Cluster ID that this delta applies to.
57    pub base_cluster_id: u32,
58    /// Number of vectors affected by this delta.
59    pub affected_count: u32,
60    /// Size of the delta payload in bytes.
61    pub delta_size: u64,
62    /// SHAKE-256-256 hash of the delta payload.
63    pub delta_hash: [u8; 32],
64    /// Reserved (must be zero).
65    pub _reserved: [u8; 8],
66}
67
68// Compile-time assertion: DeltaHeader must be exactly 64 bytes.
69const _: () = assert!(core::mem::size_of::<DeltaHeader>() == 64);
70
71impl DeltaHeader {
72    /// Serialize the header to a 64-byte little-endian array.
73    pub fn to_bytes(&self) -> [u8; 64] {
74        let mut buf = [0u8; 64];
75        buf[0x00..0x04].copy_from_slice(&self.magic.to_le_bytes());
76        buf[0x04..0x06].copy_from_slice(&self.version.to_le_bytes());
77        buf[0x06] = self.encoding;
78        buf[0x07] = self._pad;
79        buf[0x08..0x0C].copy_from_slice(&self.base_cluster_id.to_le_bytes());
80        buf[0x0C..0x10].copy_from_slice(&self.affected_count.to_le_bytes());
81        buf[0x10..0x18].copy_from_slice(&self.delta_size.to_le_bytes());
82        buf[0x18..0x38].copy_from_slice(&self.delta_hash);
83        buf[0x38..0x40].copy_from_slice(&self._reserved);
84        buf
85    }
86
87    /// Deserialize a `DeltaHeader` from a 64-byte slice.
88    pub fn from_bytes(data: &[u8; 64]) -> Result<Self, RvfError> {
89        let magic = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
90        if magic != DELTA_MAGIC {
91            return Err(RvfError::BadMagic {
92                expected: DELTA_MAGIC,
93                got: magic,
94            });
95        }
96
97        Ok(Self {
98            magic,
99            version: u16::from_le_bytes([data[0x04], data[0x05]]),
100            encoding: data[0x06],
101            _pad: data[0x07],
102            base_cluster_id: u32::from_le_bytes([data[0x08], data[0x09], data[0x0A], data[0x0B]]),
103            affected_count: u32::from_le_bytes([data[0x0C], data[0x0D], data[0x0E], data[0x0F]]),
104            delta_size: u64::from_le_bytes([
105                data[0x10], data[0x11], data[0x12], data[0x13],
106                data[0x14], data[0x15], data[0x16], data[0x17],
107            ]),
108            delta_hash: {
109                let mut h = [0u8; 32];
110                h.copy_from_slice(&data[0x18..0x38]);
111                h
112            },
113            _reserved: {
114                let mut r = [0u8; 8];
115                r.copy_from_slice(&data[0x38..0x40]);
116                r
117            },
118        })
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    fn sample_header() -> DeltaHeader {
127        DeltaHeader {
128            magic: DELTA_MAGIC,
129            version: 1,
130            encoding: DeltaEncoding::SparseRows as u8,
131            _pad: 0,
132            base_cluster_id: 42,
133            affected_count: 10,
134            delta_size: 2048,
135            delta_hash: [0xDD; 32],
136            _reserved: [0; 8],
137        }
138    }
139
140    #[test]
141    fn header_size_is_64() {
142        assert_eq!(core::mem::size_of::<DeltaHeader>(), 64);
143    }
144
145    #[test]
146    fn magic_bytes_match_ascii() {
147        let bytes_be = DELTA_MAGIC.to_be_bytes();
148        assert_eq!(&bytes_be, b"RVDL");
149    }
150
151    #[test]
152    fn round_trip_serialization() {
153        let original = sample_header();
154        let bytes = original.to_bytes();
155        let decoded = DeltaHeader::from_bytes(&bytes).expect("from_bytes should succeed");
156
157        assert_eq!(decoded.magic, DELTA_MAGIC);
158        assert_eq!(decoded.version, 1);
159        assert_eq!(decoded.encoding, DeltaEncoding::SparseRows as u8);
160        assert_eq!(decoded._pad, 0);
161        assert_eq!(decoded.base_cluster_id, 42);
162        assert_eq!(decoded.affected_count, 10);
163        assert_eq!(decoded.delta_size, 2048);
164        assert_eq!(decoded.delta_hash, [0xDD; 32]);
165        assert_eq!(decoded._reserved, [0; 8]);
166    }
167
168    #[test]
169    fn bad_magic_returns_error() {
170        let mut bytes = sample_header().to_bytes();
171        bytes[0] = 0x00; // corrupt magic
172        let err = DeltaHeader::from_bytes(&bytes).unwrap_err();
173        match err {
174            RvfError::BadMagic { expected, .. } => assert_eq!(expected, DELTA_MAGIC),
175            other => panic!("expected BadMagic, got {other:?}"),
176        }
177    }
178
179    #[test]
180    fn field_offsets() {
181        let h = sample_header();
182        let base = &h as *const _ as usize;
183
184        assert_eq!(&h.magic as *const _ as usize - base, 0x00);
185        assert_eq!(&h.version as *const _ as usize - base, 0x04);
186        assert_eq!(&h.encoding as *const _ as usize - base, 0x06);
187        assert_eq!(&h._pad as *const _ as usize - base, 0x07);
188        assert_eq!(&h.base_cluster_id as *const _ as usize - base, 0x08);
189        assert_eq!(&h.affected_count as *const _ as usize - base, 0x0C);
190        assert_eq!(&h.delta_size as *const _ as usize - base, 0x10);
191        assert_eq!(&h.delta_hash as *const _ as usize - base, 0x18);
192        assert_eq!(&h._reserved as *const _ as usize - base, 0x38);
193    }
194
195    #[test]
196    fn delta_encoding_try_from() {
197        assert_eq!(DeltaEncoding::try_from(0), Ok(DeltaEncoding::SparseRows));
198        assert_eq!(DeltaEncoding::try_from(1), Ok(DeltaEncoding::LowRank));
199        assert_eq!(DeltaEncoding::try_from(2), Ok(DeltaEncoding::FullPatch));
200        assert!(DeltaEncoding::try_from(3).is_err());
201        assert!(DeltaEncoding::try_from(0xFF).is_err());
202    }
203}