1use crate::error::RvfError;
8
9pub const COWMAP_MAGIC: u32 = 0x5256_434D;
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15#[repr(u8)]
16pub enum MapFormat {
17 FlatArray = 0,
19 ArtTree = 1,
21 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
43#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
44pub enum CowMapEntry {
45 LocalOffset(u64),
47 ParentRef,
49 Unallocated,
51}
52
53#[derive(Clone, Copy, Debug)]
58#[repr(C)]
59pub struct CowMapHeader {
60 pub magic: u32,
62 pub version: u16,
64 pub map_format: u8,
66 pub compression_policy: u8,
68 pub cluster_size_bytes: u32,
70 pub vectors_per_cluster: u32,
72 pub base_file_id: [u8; 16],
74 pub base_file_hash: [u8; 32],
76}
77
78const _: () = assert!(core::mem::size_of::<CowMapHeader>() == 64);
80
81impl CowMapHeader {
82 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 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 let _ = MapFormat::try_from(map_format)?;
113
114 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 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; 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}