Skip to main content

gpp_core/
wire.rs

1//! On-disk / on-wire framing for stored objects.
2//!
3//! Layout (see `docs/DATA_MODEL.md` ยง Wire Format):
4//!
5//! ```text
6//! Magic:    b"GPP\0"        (4 bytes)
7//! Version:  u8              (1 byte)
8//! Type:     u8              (1 byte)   ObjectType::code()
9//! Flags:    u16 LE          (2 bytes)  Compressed | Encrypted | Signed
10//! Length:   u32 LE          (4 bytes)  payload length
11//! Payload:  [u8; Length]               zstd(encode_body())
12//! Checksum: [u8; 4]                    BLAKE3(payload) truncated to 4 bytes
13//! ```
14
15use crate::error::{Error, Result};
16use crate::object::ObjectType;
17
18pub const MAGIC: [u8; 4] = *b"GPP\0";
19pub const VERSION: u8 = 1;
20
21/// zstd compression level used for all stored objects.
22const ZSTD_LEVEL: i32 = 3;
23
24const HEADER_LEN: usize = 4 + 1 + 1 + 2 + 4;
25const CHECKSUM_LEN: usize = 4;
26
27/// Frame flag bits.
28pub mod flags {
29    pub const COMPRESSED: u16 = 1 << 0;
30    pub const ENCRYPTED: u16 = 1 << 1;
31    pub const SIGNED: u16 = 1 << 2;
32}
33
34/// A decoded frame: the object type plus its uncompressed canonical body.
35pub struct Decoded {
36    pub object_type: ObjectType,
37    pub body: Vec<u8>,
38}
39
40/// Frame `body` (the object's canonical bytes) into the stored representation.
41///
42/// Phase 0 always sets the `COMPRESSED` flag; encryption/signing are later phases.
43pub fn encode(object_type: ObjectType, body: &[u8]) -> Result<Vec<u8>> {
44    let payload =
45        zstd::encode_all(body, ZSTD_LEVEL).map_err(|e| Error::Compression(e.to_string()))?;
46    let len: u32 = payload
47        .len()
48        .try_into()
49        .map_err(|_| Error::Serialize("object payload exceeds 4 GiB".into()))?;
50
51    let mut out = Vec::with_capacity(HEADER_LEN + payload.len() + CHECKSUM_LEN);
52    out.extend_from_slice(&MAGIC);
53    out.push(VERSION);
54    out.push(object_type.code());
55    out.extend_from_slice(&flags::COMPRESSED.to_le_bytes());
56    out.extend_from_slice(&len.to_le_bytes());
57    out.extend_from_slice(&payload);
58
59    let checksum = blake3::hash(&payload);
60    out.extend_from_slice(&checksum.as_bytes()[..CHECKSUM_LEN]);
61    Ok(out)
62}
63
64/// Parse and validate a stored frame, returning the uncompressed body.
65pub fn decode(frame: &[u8]) -> Result<Decoded> {
66    if frame.len() < HEADER_LEN + CHECKSUM_LEN {
67        return Err(Error::TruncatedFrame);
68    }
69    if frame[0..4] != MAGIC {
70        return Err(Error::BadMagic);
71    }
72    let version = frame[4];
73    if version != VERSION {
74        return Err(Error::UnsupportedVersion(version));
75    }
76    let object_type = ObjectType::from_code(frame[5])?;
77    let flag_bits = u16::from_le_bytes([frame[6], frame[7]]);
78    let length = u32::from_le_bytes([frame[8], frame[9], frame[10], frame[11]]) as usize;
79
80    let payload_start = HEADER_LEN;
81    let payload_end = payload_start
82        .checked_add(length)
83        .ok_or(Error::TruncatedFrame)?;
84    if frame.len() != payload_end + CHECKSUM_LEN {
85        return Err(Error::TruncatedFrame);
86    }
87    let payload = &frame[payload_start..payload_end];
88    let stored_checksum = &frame[payload_end..payload_end + CHECKSUM_LEN];
89    if &blake3::hash(payload).as_bytes()[..CHECKSUM_LEN] != stored_checksum {
90        return Err(Error::ChecksumMismatch);
91    }
92
93    let body = if flag_bits & flags::COMPRESSED != 0 {
94        zstd::decode_all(payload).map_err(|e| Error::Compression(e.to_string()))?
95    } else {
96        payload.to_vec()
97    };
98
99    Ok(Decoded { object_type, body })
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn roundtrip() {
108        let body = b"the quick brown fox".repeat(10);
109        let frame = encode(ObjectType::Blob, &body).unwrap();
110        let decoded = decode(&frame).unwrap();
111        assert_eq!(decoded.object_type, ObjectType::Blob);
112        assert_eq!(decoded.body, body);
113    }
114
115    #[test]
116    fn detects_bad_magic() {
117        let mut frame = encode(ObjectType::Blob, b"x").unwrap();
118        frame[0] = b'X';
119        assert!(matches!(decode(&frame), Err(Error::BadMagic)));
120    }
121
122    #[test]
123    fn detects_corruption() {
124        let mut frame = encode(ObjectType::Tree, b"payload bytes here").unwrap();
125        let n = frame.len();
126        frame[n - CHECKSUM_LEN - 1] ^= 0xff; // flip a payload bit
127        assert!(matches!(decode(&frame), Err(Error::ChecksumMismatch)));
128    }
129
130    #[test]
131    fn rejects_truncated() {
132        let frame = encode(ObjectType::Blob, b"data").unwrap();
133        assert!(matches!(decode(&frame[..5]), Err(Error::TruncatedFrame)));
134    }
135
136    #[test]
137    fn rejects_unknown_version() {
138        let mut frame = encode(ObjectType::Blob, b"v").unwrap();
139        frame[4] = 99;
140        assert!(matches!(decode(&frame), Err(Error::UnsupportedVersion(99))));
141    }
142}