1use crate::core::{FieldId, LuciError, Result, SegmentId};
2
3use crate::storage::block::{BLOCK_SIZE, BlockId, Extent};
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub struct SegmentEntry {
14 pub segment_id: SegmentId,
16 pub extent: Extent,
18 pub generation: u64,
21 pub data_len: u64,
24}
25
26impl SegmentEntry {
27 pub const fn new(
28 segment_id: SegmentId,
29 extent: Extent,
30 generation: u64,
31 data_len: u64,
32 ) -> Self {
33 Self {
34 segment_id,
35 extent,
36 generation,
37 data_len,
38 }
39 }
40}
41
42#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51pub struct VectorIndexEntry {
52 pub field_id: FieldId,
54 pub extent: Extent,
56 pub data_len: u64,
58}
59
60impl VectorIndexEntry {
61 pub const fn new(field_id: FieldId, extent: Extent, data_len: u64) -> Self {
62 Self {
63 field_id,
64 extent,
65 data_len,
66 }
67 }
68}
69
70#[derive(Clone, Debug, PartialEq, Eq)]
79pub struct MetadataSnapshot {
80 pub segments: Vec<SegmentEntry>,
82 pub vector_indexes: Vec<VectorIndexEntry>,
86 pub total_blocks: u64,
88 pub free_list: Vec<Extent>,
90 pub user_metadata: Vec<u8>,
94}
95
96const HEADER_BYTES: usize = 20;
122const SEGMENT_ENTRY_BYTES: usize = 36;
123const FREE_EXTENT_BYTES: usize = 12;
124const VECTOR_INDEX_ENTRY_BYTES: usize = 22;
125
126impl MetadataSnapshot {
127 pub fn empty() -> Self {
129 Self {
130 segments: Vec::new(),
131 vector_indexes: Vec::new(),
132 total_blocks: 0,
133 free_list: Vec::new(),
134 user_metadata: Vec::new(),
135 }
136 }
137
138 pub fn serialized_size(&self) -> usize {
140 HEADER_BYTES
141 + self.segments.len() * SEGMENT_ENTRY_BYTES
142 + self.free_list.len() * FREE_EXTENT_BYTES
143 + self.vector_indexes.len() * VECTOR_INDEX_ENTRY_BYTES
144 + 4 + self.user_metadata.len()
146 }
147
148 pub fn to_bytes(&self) -> Vec<u8> {
155 let size = self.serialized_size();
156 let mut buf = Vec::with_capacity(size);
157
158 buf.extend_from_slice(&self.total_blocks.to_le_bytes());
160 buf.extend_from_slice(&(self.segments.len() as u32).to_le_bytes());
161 buf.extend_from_slice(&(self.free_list.len() as u32).to_le_bytes());
162 buf.extend_from_slice(&(self.vector_indexes.len() as u32).to_le_bytes());
163
164 for entry in &self.segments {
166 buf.extend_from_slice(&entry.segment_id.as_u64().to_le_bytes());
167 buf.extend_from_slice(&entry.extent.start.as_u64().to_le_bytes());
168 buf.extend_from_slice(&entry.extent.count.to_le_bytes());
169 buf.extend_from_slice(&entry.generation.to_le_bytes());
170 buf.extend_from_slice(&entry.data_len.to_le_bytes());
171 }
172
173 for extent in &self.free_list {
175 buf.extend_from_slice(&extent.start.as_u64().to_le_bytes());
176 buf.extend_from_slice(&extent.count.to_le_bytes());
177 }
178
179 for entry in &self.vector_indexes {
181 buf.extend_from_slice(&entry.field_id.as_u16().to_le_bytes());
182 buf.extend_from_slice(&entry.extent.start.as_u64().to_le_bytes());
183 buf.extend_from_slice(&entry.extent.count.to_le_bytes());
184 buf.extend_from_slice(&entry.data_len.to_le_bytes());
185 }
186
187 buf.extend_from_slice(&(self.user_metadata.len() as u32).to_le_bytes());
189 buf.extend_from_slice(&self.user_metadata);
190
191 debug_assert_eq!(buf.len(), size);
192 buf
193 }
194
195 pub fn from_bytes(data: &[u8]) -> Result<Self> {
202 if data.len() < HEADER_BYTES {
203 return Err(LuciError::IndexCorrupted(
204 "metadata block too small for header".into(),
205 ));
206 }
207
208 let total_blocks = u64::from_le_bytes(data[0..8].try_into().unwrap());
209 let segment_count = u32::from_le_bytes(data[8..12].try_into().unwrap()) as usize;
210 let free_extent_count = u32::from_le_bytes(data[12..16].try_into().unwrap()) as usize;
211 let vector_index_count = u32::from_le_bytes(data[16..20].try_into().unwrap()) as usize;
212
213 let expected_size = HEADER_BYTES
214 + segment_count * SEGMENT_ENTRY_BYTES
215 + free_extent_count * FREE_EXTENT_BYTES
216 + vector_index_count * VECTOR_INDEX_ENTRY_BYTES;
217 if data.len() < expected_size {
218 return Err(LuciError::IndexCorrupted(format!(
219 "metadata block truncated: need {expected_size} bytes, got {}",
220 data.len()
221 )));
222 }
223
224 let mut offset = HEADER_BYTES;
225
226 let mut segments = Vec::with_capacity(segment_count);
227 for _ in 0..segment_count {
228 let segment_id = SegmentId::new(u64::from_le_bytes(
229 data[offset..offset + 8].try_into().unwrap(),
230 ));
231 offset += 8;
232 let extent_start = BlockId::new(u64::from_le_bytes(
233 data[offset..offset + 8].try_into().unwrap(),
234 ));
235 offset += 8;
236 let extent_count = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap());
237 offset += 4;
238 let generation = u64::from_le_bytes(data[offset..offset + 8].try_into().unwrap());
239 offset += 8;
240 let data_len = u64::from_le_bytes(data[offset..offset + 8].try_into().unwrap());
241 offset += 8;
242
243 segments.push(SegmentEntry::new(
244 segment_id,
245 Extent::new(extent_start, extent_count),
246 generation,
247 data_len,
248 ));
249 }
250
251 let mut free_list = Vec::with_capacity(free_extent_count);
252 for _ in 0..free_extent_count {
253 let start = BlockId::new(u64::from_le_bytes(
254 data[offset..offset + 8].try_into().unwrap(),
255 ));
256 offset += 8;
257 let count = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap());
258 offset += 4;
259
260 free_list.push(Extent::new(start, count));
261 }
262
263 let mut vector_indexes = Vec::with_capacity(vector_index_count);
264 for _ in 0..vector_index_count {
265 let field_id = FieldId::new(u16::from_le_bytes(
266 data[offset..offset + 2].try_into().unwrap(),
267 ));
268 offset += 2;
269 let extent_start = BlockId::new(u64::from_le_bytes(
270 data[offset..offset + 8].try_into().unwrap(),
271 ));
272 offset += 8;
273 let extent_count = u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap());
274 offset += 4;
275 let data_len = u64::from_le_bytes(data[offset..offset + 8].try_into().unwrap());
276 offset += 8;
277
278 vector_indexes.push(VectorIndexEntry::new(
279 field_id,
280 Extent::new(extent_start, extent_count),
281 data_len,
282 ));
283 }
284
285 let user_metadata = if offset + 4 <= data.len() {
286 let meta_len =
287 u32::from_le_bytes(data[offset..offset + 4].try_into().unwrap()) as usize;
288 offset += 4;
289 if offset + meta_len <= data.len() {
290 data[offset..offset + meta_len].to_vec()
291 } else {
292 Vec::new()
293 }
294 } else {
295 Vec::new()
296 };
297
298 Ok(Self {
299 segments,
300 vector_indexes,
301 total_blocks,
302 free_list,
303 user_metadata,
304 })
305 }
306
307 pub fn fits_in_single_block(&self) -> bool {
312 self.serialized_size() <= BLOCK_SIZE as usize
313 }
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319
320 fn sample_snapshot() -> MetadataSnapshot {
321 MetadataSnapshot {
322 segments: vec![
323 SegmentEntry::new(SegmentId::new(1), Extent::new(BlockId(0), 4), 1, 900_000),
324 SegmentEntry::new(SegmentId::new(2), Extent::new(BlockId(4), 2), 1, 400_000),
325 SegmentEntry::new(SegmentId::new(3), Extent::new(BlockId(8), 6), 2, 1_500_000),
326 ],
327 vector_indexes: Vec::new(),
328 total_blocks: 14,
329 free_list: vec![Extent::new(BlockId(6), 2)],
330 user_metadata: Vec::new(),
331 }
332 }
333
334 #[test]
335 fn empty_snapshot() {
336 let snap = MetadataSnapshot::empty();
337 assert!(snap.segments.is_empty());
338 assert_eq!(snap.total_blocks, 0);
339 assert!(snap.free_list.is_empty());
340 }
341
342 #[test]
343 fn round_trip_empty() {
344 let snap = MetadataSnapshot::empty();
345 let bytes = snap.to_bytes();
346 let snap2 = MetadataSnapshot::from_bytes(&bytes).unwrap();
347 assert_eq!(snap, snap2);
348 }
349
350 #[test]
351 fn round_trip_populated() {
352 let snap = sample_snapshot();
353 let bytes = snap.to_bytes();
354 let snap2 = MetadataSnapshot::from_bytes(&bytes).unwrap();
355 assert_eq!(snap, snap2);
356 }
357
358 #[test]
359 fn serialized_size_matches_output() {
360 let snap = sample_snapshot();
361 let bytes = snap.to_bytes();
362 assert_eq!(bytes.len(), snap.serialized_size());
363 assert_eq!(bytes.len(), 144);
366 }
367
368 #[test]
369 fn round_trip_with_vector_indexes() {
370 let mut snap = sample_snapshot();
371 snap.vector_indexes = vec![
372 VectorIndexEntry::new(FieldId::new(7), Extent::new(BlockId(100), 3), 600_000),
373 VectorIndexEntry::new(FieldId::new(11), Extent::new(BlockId(103), 5), 1_200_000),
374 ];
375 let bytes = snap.to_bytes();
376 let snap2 = MetadataSnapshot::from_bytes(&bytes).unwrap();
377 assert_eq!(snap, snap2);
378 }
379
380 #[test]
381 fn truncated_header_is_rejected() {
382 let err = MetadataSnapshot::from_bytes(&[0u8; 10]).unwrap_err();
383 assert!(format!("{err}").contains("too small"));
384 }
385
386 #[test]
387 fn truncated_body_is_rejected() {
388 let snap = sample_snapshot();
389 let bytes = snap.to_bytes();
390 let err = MetadataSnapshot::from_bytes(&bytes[..bytes.len() - 5]).unwrap_err();
391 assert!(format!("{err}").contains("truncated"));
392 }
393
394 #[test]
395 fn fits_in_single_block() {
396 let snap = sample_snapshot();
397 assert!(snap.fits_in_single_block());
398 }
399
400 #[test]
401 fn from_bytes_tolerates_trailing_data() {
402 let snap = sample_snapshot();
403 let mut bytes = snap.to_bytes();
404 bytes.extend_from_slice(&[0u8; 1024]);
405 let snap2 = MetadataSnapshot::from_bytes(&bytes).unwrap();
406 assert_eq!(snap, snap2);
407 }
408
409 #[test]
410 fn segment_entry_fields() {
411 let entry = SegmentEntry::new(SegmentId::new(42), Extent::new(BlockId(10), 3), 7, 768_000);
412 assert_eq!(entry.segment_id, SegmentId::new(42));
413 assert_eq!(entry.extent, Extent::new(BlockId(10), 3));
414 assert_eq!(entry.generation, 7);
415 assert_eq!(entry.data_len, 768_000);
416 }
417
418 #[test]
419 fn round_trip_large_snapshot() {
420 let segments: Vec<_> = (0..500)
421 .map(|i| {
422 SegmentEntry::new(
423 SegmentId::new(i),
424 Extent::new(BlockId(i * 10), 5),
425 i / 10,
426 256_000,
427 )
428 })
429 .collect();
430 let free_list: Vec<_> = (0..200)
431 .map(|i| Extent::new(BlockId(5000 + i * 3), 2))
432 .collect();
433 let snap = MetadataSnapshot {
434 segments,
435 vector_indexes: Vec::new(),
436 total_blocks: 10000,
437 free_list,
438 user_metadata: Vec::new(),
439 };
440
441 assert!(snap.fits_in_single_block());
442 let bytes = snap.to_bytes();
443 let snap2 = MetadataSnapshot::from_bytes(&bytes).unwrap();
444 assert_eq!(snap, snap2);
445 }
446
447 #[test]
448 fn checksum_integration() {
449 let snap = sample_snapshot();
450 let bytes = snap.to_bytes();
451 let checksum = crate::storage::xxh3_checksum(&bytes);
452 assert_eq!(checksum, crate::storage::xxh3_checksum(&bytes));
453 let snap2 = MetadataSnapshot::empty();
454 let bytes2 = snap2.to_bytes();
455 assert_ne!(checksum, crate::storage::xxh3_checksum(&bytes2));
456 }
457}