1use crate::error::{Error, Result};
16use crate::object::ObjectType;
17
18pub const MAGIC: [u8; 4] = *b"GPP\0";
19pub const VERSION: u8 = 1;
20
21const ZSTD_LEVEL: i32 = 3;
23
24const HEADER_LEN: usize = 4 + 1 + 1 + 2 + 4;
25const CHECKSUM_LEN: usize = 4;
26
27pub 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
34pub struct Decoded {
36 pub object_type: ObjectType,
37 pub body: Vec<u8>,
38}
39
40pub 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
64pub 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; 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}