Skip to main content

rvf_types/
cow_map.rs

1//! COW_MAP_SEG (0x20) types for the RVF computational container.
2//!
3//! Defines the 64-byte `CowMapHeader` and associated enums per ADR-031.
4//! The COW_MAP_SEG tracks copy-on-write cluster mappings, enabling
5//! branching and snapshotting of vector data without full duplication.
6
7use crate::error::RvfError;
8
9/// Magic number for `CowMapHeader`: "RVCM" in big-endian.
10pub const COWMAP_MAGIC: u32 = 0x5256_434D;
11
12/// Cluster map storage format.
13#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15#[repr(u8)]
16pub enum MapFormat {
17    /// Simple flat array of cluster entries.
18    FlatArray = 0,
19    /// Adaptive Radix Tree for sparse mappings.
20    ArtTree = 1,
21    /// Extent list for contiguous ranges.
22    ExtentList = 2,
23}
24
25impl TryFrom<u8> for MapFormat {
26    type Error = RvfError;
27
28    fn try_from(value: u8) -> Result<Self, Self::Error> {
29        match value {
30            0 => Ok(Self::FlatArray),
31            1 => Ok(Self::ArtTree),
32            2 => Ok(Self::ExtentList),
33            _ => Err(RvfError::InvalidEnumValue {
34                type_name: "MapFormat",
35                value: value as u64,
36            }),
37        }
38    }
39}
40
41/// Entry in the COW cluster map.
42#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
43#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
44pub enum CowMapEntry {
45    /// Cluster has been written locally at the given offset.
46    LocalOffset(u64),
47    /// Cluster data lives in the parent file.
48    ParentRef,
49    /// Cluster has not been allocated.
50    Unallocated,
51}
52
53/// 64-byte header for COW_MAP_SEG payloads.
54///
55/// Follows the standard 64-byte `SegmentHeader`. All multi-byte fields are
56/// little-endian on the wire.
57#[derive(Clone, Copy, Debug)]
58#[repr(C)]
59pub struct CowMapHeader {
60    /// Magic: `COWMAP_MAGIC` (0x5256434D, "RVCM").
61    pub magic: u32,
62    /// CowMapHeader format version (currently 1).
63    pub version: u16,
64    /// Map storage format (see `MapFormat`).
65    pub map_format: u8,
66    /// Compression policy for COW clusters.
67    pub compression_policy: u8,
68    /// Cluster size in bytes (must be power of 2, SIMD aligned).
69    pub cluster_size_bytes: u32,
70    /// Number of vectors per cluster.
71    pub vectors_per_cluster: u32,
72    /// UUID of the base (parent) file.
73    pub base_file_id: [u8; 16],
74    /// SHAKE-256-256 hash of the base file.
75    pub base_file_hash: [u8; 32],
76}
77
78// Compile-time assertion: CowMapHeader must be exactly 64 bytes.
79const _: () = assert!(core::mem::size_of::<CowMapHeader>() == 64);
80
81impl CowMapHeader {
82    /// Serialize the header to a 64-byte little-endian array.
83    pub fn to_bytes(&self) -> [u8; 64] {
84        let mut buf = [0u8; 64];
85        buf[0x00..0x04].copy_from_slice(&self.magic.to_le_bytes());
86        buf[0x04..0x06].copy_from_slice(&self.version.to_le_bytes());
87        buf[0x06] = self.map_format;
88        buf[0x07] = self.compression_policy;
89        buf[0x08..0x0C].copy_from_slice(&self.cluster_size_bytes.to_le_bytes());
90        buf[0x0C..0x10].copy_from_slice(&self.vectors_per_cluster.to_le_bytes());
91        buf[0x10..0x20].copy_from_slice(&self.base_file_id);
92        buf[0x20..0x40].copy_from_slice(&self.base_file_hash);
93        buf
94    }
95
96    /// Deserialize a `CowMapHeader` from a 64-byte slice.
97    pub fn from_bytes(data: &[u8; 64]) -> Result<Self, RvfError> {
98        let magic = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
99        if magic != COWMAP_MAGIC {
100            return Err(RvfError::BadMagic {
101                expected: COWMAP_MAGIC,
102                got: magic,
103            });
104        }
105
106        let version = u16::from_le_bytes([data[0x04], data[0x05]]);
107        let map_format = data[0x06];
108        let cluster_size_bytes = u32::from_le_bytes([data[0x08], data[0x09], data[0x0A], data[0x0B]]);
109        let vectors_per_cluster = u32::from_le_bytes([data[0x0C], data[0x0D], data[0x0E], data[0x0F]]);
110
111        // Validate map_format is a known enum value
112        let _ = MapFormat::try_from(map_format)?;
113
114        // Validate cluster_size_bytes is a power of 2 and non-zero
115        if cluster_size_bytes == 0 || !cluster_size_bytes.is_power_of_two() {
116            return Err(RvfError::InvalidEnumValue {
117                type_name: "CowMapHeader::cluster_size_bytes",
118                value: cluster_size_bytes as u64,
119            });
120        }
121
122        // Validate vectors_per_cluster is non-zero (prevents division by zero)
123        if vectors_per_cluster == 0 {
124            return Err(RvfError::InvalidEnumValue {
125                type_name: "CowMapHeader::vectors_per_cluster",
126                value: 0,
127            });
128        }
129
130        Ok(Self {
131            magic,
132            version,
133            map_format,
134            compression_policy: data[0x07],
135            cluster_size_bytes,
136            vectors_per_cluster,
137            base_file_id: {
138                let mut id = [0u8; 16];
139                id.copy_from_slice(&data[0x10..0x20]);
140                id
141            },
142            base_file_hash: {
143                let mut h = [0u8; 32];
144                h.copy_from_slice(&data[0x20..0x40]);
145                h
146            },
147        })
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    fn sample_header() -> CowMapHeader {
156        CowMapHeader {
157            magic: COWMAP_MAGIC,
158            version: 1,
159            map_format: MapFormat::FlatArray as u8,
160            compression_policy: 0,
161            cluster_size_bytes: 4096,
162            vectors_per_cluster: 64,
163            base_file_id: [0xAA; 16],
164            base_file_hash: [0xBB; 32],
165        }
166    }
167
168    #[test]
169    fn header_size_is_64() {
170        assert_eq!(core::mem::size_of::<CowMapHeader>(), 64);
171    }
172
173    #[test]
174    fn magic_bytes_match_ascii() {
175        let bytes_be = COWMAP_MAGIC.to_be_bytes();
176        assert_eq!(&bytes_be, b"RVCM");
177    }
178
179    #[test]
180    fn round_trip_serialization() {
181        let original = sample_header();
182        let bytes = original.to_bytes();
183        let decoded = CowMapHeader::from_bytes(&bytes).expect("from_bytes should succeed");
184
185        assert_eq!(decoded.magic, COWMAP_MAGIC);
186        assert_eq!(decoded.version, 1);
187        assert_eq!(decoded.map_format, MapFormat::FlatArray as u8);
188        assert_eq!(decoded.compression_policy, 0);
189        assert_eq!(decoded.cluster_size_bytes, 4096);
190        assert_eq!(decoded.vectors_per_cluster, 64);
191        assert_eq!(decoded.base_file_id, [0xAA; 16]);
192        assert_eq!(decoded.base_file_hash, [0xBB; 32]);
193    }
194
195    #[test]
196    fn bad_magic_returns_error() {
197        let mut bytes = sample_header().to_bytes();
198        bytes[0] = 0x00; // corrupt magic
199        let err = CowMapHeader::from_bytes(&bytes).unwrap_err();
200        match err {
201            RvfError::BadMagic { expected, .. } => assert_eq!(expected, COWMAP_MAGIC),
202            other => panic!("expected BadMagic, got {other:?}"),
203        }
204    }
205
206    #[test]
207    fn field_offsets() {
208        let h = sample_header();
209        let base = &h as *const _ as usize;
210
211        assert_eq!(&h.magic as *const _ as usize - base, 0x00);
212        assert_eq!(&h.version as *const _ as usize - base, 0x04);
213        assert_eq!(&h.map_format as *const _ as usize - base, 0x06);
214        assert_eq!(&h.compression_policy as *const _ as usize - base, 0x07);
215        assert_eq!(&h.cluster_size_bytes as *const _ as usize - base, 0x08);
216        assert_eq!(&h.vectors_per_cluster as *const _ as usize - base, 0x0C);
217        assert_eq!(&h.base_file_id as *const _ as usize - base, 0x10);
218        assert_eq!(&h.base_file_hash as *const _ as usize - base, 0x20);
219    }
220
221    #[test]
222    fn map_format_try_from() {
223        assert_eq!(MapFormat::try_from(0), Ok(MapFormat::FlatArray));
224        assert_eq!(MapFormat::try_from(1), Ok(MapFormat::ArtTree));
225        assert_eq!(MapFormat::try_from(2), Ok(MapFormat::ExtentList));
226        assert!(MapFormat::try_from(3).is_err());
227        assert!(MapFormat::try_from(0xFF).is_err());
228    }
229
230    #[test]
231    fn cow_map_entry_variants() {
232        let local = CowMapEntry::LocalOffset(0x1000);
233        let parent = CowMapEntry::ParentRef;
234        let unalloc = CowMapEntry::Unallocated;
235
236        assert_eq!(local, CowMapEntry::LocalOffset(0x1000));
237        assert_eq!(parent, CowMapEntry::ParentRef);
238        assert_eq!(unalloc, CowMapEntry::Unallocated);
239        assert_ne!(local, parent);
240    }
241}