Skip to main content

nodedb_types/backup_envelope/
read.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! `parse` — decode and fully validate a plaintext backup envelope.
4
5use 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
12/// Parse and fully validate a plaintext backup envelope.
13///
14/// Rejects bytes that do not carry [`VERSION`] in the header.
15/// Use [`crate::backup_envelope::parse_encrypted`] for encrypted envelopes.
16pub 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    // Header.
25    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
37/// Inner parse shared by the plain and encrypted paths.
38/// Caller has already verified magic, version, and (for encrypted path)
39/// the crypto header. `bytes` is the full envelope; `header_bytes` is `&bytes[..HEADER_LEN]`.
40pub(super) fn parse_validated_body(
41    bytes: &[u8],
42    header_bytes: &[u8],
43) -> Result<Envelope, EnvelopeError> {
44    // Validate header CRC.
45    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    // Trailer position: tail 4 bytes.
60    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
114/// Bounds a section container by the minimum encoded size of each section.
115/// This runs before allocation so an untrusted header count cannot reserve
116/// more entries than the bytes remaining in the frame can describe.
117pub(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// ── shared tests for plaintext path ─────────────────────────────────────────
138
139#[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        // Recompute trailer to isolate body-CRC enforcement.
257        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    /// Asserts `NDBB` magic at [0..4], VERSION == 3 at [4], and that the
312    /// header CRC at [48..52] covers header bytes [0..48].
313    #[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        // Magic at [0..4].
320        assert_eq!(&bytes[0..4], MAGIC.as_slice(), "magic mismatch");
321
322        // VERSION == 3 at [4].
323        assert_eq!(bytes[4], VERSION, "version mismatch");
324        assert_eq!(bytes[4], 3u8, "expected VERSION == 3");
325
326        // Header CRC at [48..52] covers [0..48].
327        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}