nodedb_types/backup_envelope/
read.rs1use std::mem::size_of;
6
7use crate::decode_bounds::checked_decode_capacity;
8
9use super::types::{Envelope, EnvelopeError, EnvelopeMeta, Section, read2, read4, read8};
10use super::types::{HEADER_LEN, MAGIC, SECTION_OVERHEAD, TRAILER_LEN, VERSION};
11
12pub fn parse(bytes: &[u8], max_total: u64) -> Result<Envelope, EnvelopeError> {
17 if bytes.len() as u64 > max_total {
18 return Err(EnvelopeError::OverSizeTotal { cap: max_total });
19 }
20 if bytes.len() < HEADER_LEN + TRAILER_LEN {
21 return Err(EnvelopeError::Truncated);
22 }
23
24 let header_bytes = &bytes[..HEADER_LEN];
26 if &header_bytes[0..4] != MAGIC {
27 return Err(EnvelopeError::BadMagic);
28 }
29 let version = header_bytes[4];
30 if version != VERSION {
31 return Err(EnvelopeError::UnsupportedVersion(version));
32 }
33
34 parse_validated_body(bytes, header_bytes)
35}
36
37pub(super) fn parse_validated_body(
41 bytes: &[u8],
42 header_bytes: &[u8],
43) -> Result<Envelope, EnvelopeError> {
44 let claimed_header_crc = u32::from_le_bytes(read4(&header_bytes[48..52]));
46 let actual_header_crc = crc32c::crc32c(&header_bytes[..48]);
47 if claimed_header_crc != actual_header_crc {
48 return Err(EnvelopeError::HeaderCrcMismatch);
49 }
50
51 let meta = EnvelopeMeta {
52 tenant_id: u64::from_le_bytes(read8(&header_bytes[8..16])),
53 source_vshard_count: u16::from_le_bytes(read2(&header_bytes[16..18])),
54 hash_seed: u64::from_le_bytes(read8(&header_bytes[24..32])),
55 snapshot_watermark: u64::from_le_bytes(read8(&header_bytes[32..40])),
56 };
57 let section_count = u16::from_le_bytes(read2(&header_bytes[40..42]));
58
59 let trailer_start = bytes.len() - TRAILER_LEN;
61 let claimed_trailer_crc = u32::from_le_bytes(read4(&bytes[trailer_start..]));
62 let actual_trailer_crc = crc32c::crc32c(&bytes[..trailer_start]);
63 if claimed_trailer_crc != actual_trailer_crc {
64 return Err(EnvelopeError::TrailerCrcMismatch);
65 }
66
67 parse_sections(bytes, section_count, HEADER_LEN, trailer_start, meta)
68}
69
70pub(super) fn parse_sections(
71 bytes: &[u8],
72 section_count: u16,
73 body_start: usize,
74 trailer_start: usize,
75 meta: EnvelopeMeta,
76) -> Result<Envelope, EnvelopeError> {
77 let section_capacity = checked_section_capacity(
78 section_count,
79 trailer_start.saturating_sub(body_start),
80 SECTION_OVERHEAD,
81 )?;
82 let mut cursor = body_start;
83 let mut sections = Vec::with_capacity(section_capacity);
84 for _ in 0..section_count {
85 if cursor + SECTION_OVERHEAD > trailer_start {
86 return Err(EnvelopeError::Truncated);
87 }
88 let origin_node_id = u64::from_le_bytes(read8(&bytes[cursor..cursor + 8]));
89 let body_len = u32::from_le_bytes(read4(&bytes[cursor + 8..cursor + 12])) as usize;
90 let body_start_inner = cursor + 12;
91 let body_end = body_start_inner + body_len;
92 let crc_end = body_end + 4;
93 if crc_end > trailer_start {
94 return Err(EnvelopeError::Truncated);
95 }
96 let body = bytes[body_start_inner..body_end].to_vec();
97 let claimed_body_crc = u32::from_le_bytes(read4(&bytes[body_end..crc_end]));
98 if crc32c::crc32c(&body) != claimed_body_crc {
99 return Err(EnvelopeError::BodyCrcMismatch);
100 }
101 sections.push(Section {
102 origin_node_id,
103 body,
104 });
105 cursor = crc_end;
106 }
107 if cursor != trailer_start {
108 return Err(EnvelopeError::Truncated);
109 }
110
111 Ok(Envelope { meta, sections })
112}
113
114pub(super) fn checked_section_capacity(
118 section_count: u16,
119 remaining_bytes: usize,
120 minimum_section_bytes: usize,
121) -> Result<usize, EnvelopeError> {
122 if minimum_section_bytes == 0 {
123 return Err(EnvelopeError::Truncated);
124 }
125 let count = usize::from(section_count);
126 checked_decode_capacity(
127 count,
128 size_of::<Section>(),
129 remaining_bytes,
130 minimum_section_bytes,
131 usize::from(u16::MAX),
132 usize::MAX,
133 )
134 .ok_or(EnvelopeError::Truncated)
135}
136
137#[cfg(test)]
140mod tests {
141 use super::*;
142 use crate::backup_envelope::types::{DEFAULT_MAX_SECTION_BYTES, DEFAULT_MAX_TOTAL_BYTES};
143 use crate::backup_envelope::write::EnvelopeWriter;
144
145 fn meta() -> EnvelopeMeta {
146 EnvelopeMeta {
147 tenant_id: 42_u64,
148 source_vshard_count: 1024,
149 hash_seed: 0,
150 snapshot_watermark: 12345,
151 }
152 }
153
154 #[test]
155 fn rejects_huge_section_count_with_tiny_body_before_allocation() {
156 let mut bytes = EnvelopeWriter::new(meta()).finalize();
157 bytes[40..42].copy_from_slice(&u16::MAX.to_le_bytes());
158 let header_crc = crc32c::crc32c(&bytes[..48]);
159 bytes[48..52].copy_from_slice(&header_crc.to_le_bytes());
160 let trailer_start = bytes.len() - TRAILER_LEN;
161 let trailer_crc = crc32c::crc32c(&bytes[..trailer_start]);
162 bytes[trailer_start..].copy_from_slice(&trailer_crc.to_le_bytes());
163 assert_eq!(
164 parse(&bytes, DEFAULT_MAX_TOTAL_BYTES),
165 Err(EnvelopeError::Truncated)
166 );
167 }
168
169 #[test]
170 fn empty_envelope_roundtrips() {
171 let bytes = EnvelopeWriter::new(meta()).finalize();
172 let env = parse(&bytes, DEFAULT_MAX_TOTAL_BYTES).unwrap();
173 assert_eq!(env.meta, meta());
174 assert!(env.sections.is_empty());
175 }
176
177 #[test]
178 fn multi_section_roundtrips() {
179 let mut w = EnvelopeWriter::new(meta());
180 w.push_section(1, b"one".to_vec()).unwrap();
181 w.push_section(2, b"two-payload".to_vec()).unwrap();
182 w.push_section(3, vec![]).unwrap();
183 let bytes = w.finalize();
184
185 let env = parse(&bytes, DEFAULT_MAX_TOTAL_BYTES).unwrap();
186 assert_eq!(env.sections.len(), 3);
187 assert_eq!(env.sections[0].origin_node_id, 1);
188 assert_eq!(env.sections[0].body, b"one");
189 assert_eq!(env.sections[1].origin_node_id, 2);
190 assert_eq!(env.sections[1].body, b"two-payload");
191 assert_eq!(env.sections[2].body, b"");
192 }
193
194 #[test]
195 fn rejects_short_input() {
196 assert_eq!(
197 parse(b"NDBB", DEFAULT_MAX_TOTAL_BYTES),
198 Err(EnvelopeError::Truncated)
199 );
200 }
201
202 #[test]
203 fn rejects_bad_magic() {
204 let mut bytes = EnvelopeWriter::new(meta()).finalize();
205 bytes[0] = b'X';
206 match parse(&bytes, DEFAULT_MAX_TOTAL_BYTES).unwrap_err() {
207 EnvelopeError::BadMagic => {}
208 other => panic!("expected BadMagic, got {other:?}"),
209 }
210 }
211
212 #[test]
213 fn rejects_unsupported_version() {
214 let mut bytes = EnvelopeWriter::new(meta()).finalize();
215 bytes[4] = 99;
216 match parse(&bytes, DEFAULT_MAX_TOTAL_BYTES).unwrap_err() {
217 EnvelopeError::UnsupportedVersion(99) => {}
218 other => panic!("expected UnsupportedVersion(99), got {other:?}"),
219 }
220 }
221
222 #[test]
223 fn u64_tenant_id_roundtrips() {
224 let large_meta = EnvelopeMeta {
225 tenant_id: u32::MAX as u64 + 1,
226 source_vshard_count: 512,
227 hash_seed: 0xDEAD_BEEF_CAFE_1234,
228 snapshot_watermark: 9_999_999_999,
229 };
230 let mut w = EnvelopeWriter::new(large_meta);
231 w.push_section(42, b"payload".to_vec()).unwrap();
232 let bytes = w.finalize();
233 let env = parse(&bytes, DEFAULT_MAX_TOTAL_BYTES).unwrap();
234 assert_eq!(env.meta, large_meta);
235 assert_eq!(env.sections.len(), 1);
236 assert_eq!(env.sections[0].body, b"payload");
237 }
238
239 #[test]
240 fn rejects_header_crc_corruption() {
241 let mut bytes = EnvelopeWriter::new(meta()).finalize();
242 bytes[8] ^= 0xFF;
243 assert_eq!(
244 parse(&bytes, DEFAULT_MAX_TOTAL_BYTES),
245 Err(EnvelopeError::HeaderCrcMismatch)
246 );
247 }
248
249 #[test]
250 fn rejects_body_crc_corruption() {
251 let mut w = EnvelopeWriter::new(meta());
252 w.push_section(7, b"hello".to_vec()).unwrap();
253 let mut bytes = w.finalize();
254 let body_off = HEADER_LEN + 12;
255 bytes[body_off] ^= 0xFF;
256 let trailer_off = bytes.len() - TRAILER_LEN;
258 let new_trailer = crc32c::crc32c(&bytes[..trailer_off]);
259 bytes[trailer_off..].copy_from_slice(&new_trailer.to_le_bytes());
260 assert_eq!(
261 parse(&bytes, DEFAULT_MAX_TOTAL_BYTES),
262 Err(EnvelopeError::BodyCrcMismatch)
263 );
264 }
265
266 #[test]
267 fn rejects_trailer_crc_corruption() {
268 let mut w = EnvelopeWriter::new(meta());
269 w.push_section(7, b"x".to_vec()).unwrap();
270 let mut bytes = w.finalize();
271 let last = bytes.len() - 1;
272 bytes[last] ^= 0xFF;
273 assert_eq!(
274 parse(&bytes, DEFAULT_MAX_TOTAL_BYTES),
275 Err(EnvelopeError::TrailerCrcMismatch)
276 );
277 }
278
279 #[test]
280 fn rejects_oversized_total() {
281 let mut w = EnvelopeWriter::with_caps(meta(), 64, DEFAULT_MAX_SECTION_BYTES);
282 let err = w.push_section(1, vec![0u8; 1024]).unwrap_err();
283 assert!(matches!(err, EnvelopeError::OverSizeTotal { .. }));
284 }
285
286 #[test]
287 fn rejects_oversized_section_at_write() {
288 let mut w = EnvelopeWriter::with_caps(meta(), DEFAULT_MAX_TOTAL_BYTES, 8);
289 let err = w.push_section(1, vec![0u8; 9]).unwrap_err();
290 assert!(matches!(err, EnvelopeError::OverSizeSection { .. }));
291 }
292
293 #[test]
294 fn rejects_oversized_total_at_parse() {
295 let bytes = EnvelopeWriter::new(meta()).finalize();
296 assert!(matches!(
297 parse(&bytes, 4),
298 Err(EnvelopeError::OverSizeTotal { .. })
299 ));
300 }
301
302 #[test]
303 fn truncated_section_body() {
304 let mut w = EnvelopeWriter::new(meta());
305 w.push_section(1, b"hello world".to_vec()).unwrap();
306 let bytes = w.finalize();
307 let truncated = &bytes[..bytes.len() - 8];
308 assert!(parse(truncated, DEFAULT_MAX_TOTAL_BYTES).is_err());
309 }
310
311 #[test]
314 fn golden_backup_envelope_format() {
315 use crate::backup_envelope::types::{HEADER_LEN, MAGIC, VERSION};
316
317 let bytes = EnvelopeWriter::new(meta()).finalize();
318
319 assert_eq!(&bytes[0..4], MAGIC.as_slice(), "magic mismatch");
321
322 assert_eq!(bytes[4], VERSION, "version mismatch");
324 assert_eq!(bytes[4], 3u8, "expected VERSION == 3");
325
326 assert!(bytes.len() >= HEADER_LEN, "envelope too short for header");
328 let stored_crc = u32::from_le_bytes([bytes[48], bytes[49], bytes[50], bytes[51]]);
329 let recomputed = crc32c::crc32c(&bytes[..48]);
330 assert_eq!(stored_crc, recomputed, "header CRC mismatch");
331 }
332}