nodedb_array/segment/format/
footer.rs1use serde::{Deserialize, Serialize};
19
20use super::framing::{BlockFraming, FRAMING_OVERHEAD};
21use super::tile_entry::TileEntry;
22use crate::error::{ArrayError, ArrayResult};
23
24pub const FOOTER_MAGIC: [u8; 4] = *b"NDFT";
26
27pub const TRAILER_SIZE: usize = 16;
28
29#[derive(
31 Debug,
32 Clone,
33 PartialEq,
34 Serialize,
35 Deserialize,
36 zerompk::ToMessagePack,
37 zerompk::FromMessagePack,
38)]
39pub struct SegmentFooter {
40 pub schema_hash: u64,
41 pub tiles: Vec<TileEntry>,
42}
43
44impl SegmentFooter {
45 pub fn new(schema_hash: u64, tiles: Vec<TileEntry>) -> Self {
46 Self { schema_hash, tiles }
47 }
48
49 pub fn encode_to(&self, out: &mut Vec<u8>) -> ArrayResult<(u64, u32)> {
52 let body = zerompk::to_msgpack_vec(self).map_err(|e| ArrayError::SegmentCorruption {
53 detail: format!("footer encode failed: {e}"),
54 })?;
55 let footer_off = out.len() as u64;
56 let framed_len = BlockFraming::encode(&body, out);
57 out.extend_from_slice(&footer_off.to_le_bytes());
59 out.extend_from_slice(&(framed_len as u32).to_le_bytes());
60 out.extend_from_slice(&FOOTER_MAGIC);
61 Ok((footer_off, framed_len as u32))
62 }
63
64 pub fn decode(segment: &[u8]) -> ArrayResult<Self> {
66 if segment.len() < TRAILER_SIZE + FRAMING_OVERHEAD {
67 return Err(ArrayError::SegmentCorruption {
68 detail: format!("segment too small for footer: {}", segment.len()),
69 });
70 }
71 let trailer = &segment[segment.len() - TRAILER_SIZE..];
72 if trailer[12..16] != FOOTER_MAGIC {
73 return Err(ArrayError::SegmentCorruption {
74 detail: "footer trailer magic mismatch (not NDFT)".into(),
75 });
76 }
77 let mut u64_buf = [0u8; 8];
78 u64_buf.copy_from_slice(&trailer[..8]);
79 let footer_off = u64::from_le_bytes(u64_buf) as usize;
80 let mut u32_buf = [0u8; 4];
81 u32_buf.copy_from_slice(&trailer[8..12]);
82 let footer_len = u32::from_le_bytes(u32_buf) as usize;
83 if footer_off + footer_len > segment.len() - TRAILER_SIZE {
84 return Err(ArrayError::SegmentCorruption {
85 detail: format!(
86 "footer offset/length out of bounds: off={footer_off} len={footer_len} \
87 file_size={}",
88 segment.len()
89 ),
90 });
91 }
92 let (body, _) = BlockFraming::decode(&segment[footer_off..footer_off + footer_len])?;
93 zerompk::from_msgpack(body).map_err(|e| ArrayError::SegmentCorruption {
94 detail: format!("footer body decode failed: {e}"),
95 })
96 }
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102 use crate::segment::format::tile_entry::TileKind;
103 use crate::tile::mbr::TileMBR;
104 use crate::types::TileId;
105
106 #[test]
107 fn footer_round_trip() {
108 let f = SegmentFooter::new(
109 0xABCDEF,
110 vec![TileEntry::new(
111 TileId::snapshot(7),
112 TileKind::Sparse,
113 32,
114 64,
115 TileMBR::new(0, 0),
116 )],
117 );
118 let mut buf = vec![0u8; 32];
120 buf.extend_from_slice(&[0u8; 64]);
121 f.encode_to(&mut buf).unwrap();
122 let d = SegmentFooter::decode(&buf).unwrap();
123 assert_eq!(d, f);
124 }
125
126 #[test]
127 fn footer_rejects_bad_trailer_magic() {
128 let f = SegmentFooter::new(0, vec![]);
129 let mut buf = Vec::new();
130 f.encode_to(&mut buf).unwrap();
131 let last = buf.len() - 1;
132 buf[last] ^= 0xFF;
133 assert!(SegmentFooter::decode(&buf).is_err());
134 }
135
136 #[test]
139 fn golden_array_segment_footer_format() {
140 let f = SegmentFooter::new(0xCAFE_BABE_1234_5678, vec![]);
141 let mut buf = vec![0u8; 32]; f.encode_to(&mut buf).unwrap();
143
144 let n = buf.len();
145 assert!(n >= TRAILER_SIZE, "buffer too small for trailer");
146 let trailer = &buf[n - TRAILER_SIZE..];
147
148 assert_eq!(&trailer[12..16], b"NDFT", "NDFT magic not at EOF trailer");
150
151 let mut u64_buf = [0u8; 8];
153 u64_buf.copy_from_slice(&trailer[..8]);
154 let footer_off = u64::from_le_bytes(u64_buf) as usize;
155 assert!(footer_off < n - TRAILER_SIZE, "footer_off out of range");
156
157 let mut u32_buf = [0u8; 4];
159 u32_buf.copy_from_slice(&trailer[8..12]);
160 let footer_len = u32::from_le_bytes(u32_buf) as usize;
161 assert!(footer_len > 0, "footer_length must be > 0");
162
163 let decoded = SegmentFooter::decode(&buf).unwrap();
165 assert_eq!(decoded.schema_hash, 0xCAFE_BABE_1234_5678);
166 }
167}