1#![doc = include_str!("../README.md")]
14
15use std::io;
16
17use serde::{Serialize, de::DeserializeOwned};
18
19#[derive(Debug, thiserror::Error)]
26pub enum CodecError {
27 #[error("payload empty")]
29 Empty,
30 #[error("version mismatch: expected {expected}, got {actual}")]
34 Version { expected: u8, actual: u8 },
35 #[error("encode failed: {0}")]
37 Encode(#[source] postcard::Error),
38 #[error("decode failed: {0}")]
40 Decode(#[source] postcard::Error),
41 #[error("trailing bytes: {extra} unconsumed after a valid body")]
45 TrailingBytes { extra: usize },
46}
47
48pub fn encode<T: Serialize>(version: u8, value: &T) -> Result<Vec<u8>, CodecError> {
55 let body = postcard::to_stdvec(value).map_err(CodecError::Encode)?;
56 let mut out = Vec::with_capacity(1 + body.len());
57 out.push(version);
58 out.extend_from_slice(&body);
59 Ok(out)
60}
61
62pub fn decode<T: DeserializeOwned>(expected_version: u8, bytes: &[u8]) -> Result<T, CodecError> {
71 let (first, rest) = bytes.split_first().ok_or(CodecError::Empty)?;
72 if *first != expected_version {
73 return Err(CodecError::Version {
74 expected: expected_version,
75 actual: *first,
76 });
77 }
78 let (value, remainder) = postcard::take_from_bytes(rest).map_err(CodecError::Decode)?;
79 if !remainder.is_empty() {
80 return Err(CodecError::TrailingBytes {
81 extra: remainder.len(),
82 });
83 }
84 Ok(value)
85}
86
87pub fn codec_io_error(context: &str, err: CodecError) -> io::Error {
102 match err {
103 version_mismatch @ CodecError::Version { .. } => io::Error::new(
104 io::ErrorKind::InvalidData,
105 format!("{context}: {version_mismatch}"),
106 ),
107 other => io::Error::other(format!("{context}: {other}")),
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114 use serde::{Deserialize, Serialize};
115
116 #[derive(Debug, PartialEq, Serialize, Deserialize)]
117 struct Sample {
118 idx: u64,
119 name: String,
120 }
121
122 #[test]
123 fn encode_decode_roundtrip() {
124 let original = Sample {
125 idx: 42,
126 name: "tsoracle".into(),
127 };
128 let bytes = encode(1, &original).expect("encode");
129 assert_eq!(bytes[0], 1);
130 let decoded: Sample = decode(1, &bytes).expect("decode");
131 assert_eq!(original, decoded);
132 }
133
134 #[test]
135 fn decode_rejects_wrong_version() {
136 let bytes = encode(
137 2,
138 &Sample {
139 idx: 1,
140 name: "x".into(),
141 },
142 )
143 .expect("encode");
144 let err = decode::<Sample>(1, &bytes).expect_err("must reject");
145 assert!(matches!(
146 err,
147 CodecError::Version {
148 expected: 1,
149 actual: 2
150 }
151 ));
152 }
153
154 #[test]
155 fn decode_rejects_empty() {
156 let err = decode::<Sample>(1, &[]).expect_err("must reject");
157 assert!(matches!(err, CodecError::Empty));
158 }
159
160 #[test]
161 fn decode_rejects_truncated_input() {
162 let original = Sample {
163 idx: u64::MAX,
164 name: "hello-world-storage-roundtrip".into(),
165 };
166 let bytes = encode(1, &original).expect("encode");
167 assert!(bytes.len() >= 16, "payload should be non-trivial");
168 let truncated = &bytes[..bytes.len() / 2];
169 assert!(matches!(
170 decode::<Sample>(1, truncated),
171 Err(CodecError::Decode(_))
172 ));
173 }
174
175 #[test]
176 fn decode_rejects_trailing_bytes() {
177 let original = Sample {
178 idx: 7,
179 name: "trailing".into(),
180 };
181 let mut bytes = encode(1, &original).expect("encode");
182 bytes.extend_from_slice(&[0xAB, 0xCD, 0xEF]);
185 assert!(matches!(
186 decode::<Sample>(1, &bytes),
187 Err(CodecError::TrailingBytes { extra: 3 })
188 ));
189 }
190
191 #[test]
192 fn codec_io_error_maps_version_mismatch_to_invalid_data() {
193 let v2_bytes = encode(
196 2,
197 &Sample {
198 idx: 1,
199 name: "x".into(),
200 },
201 )
202 .unwrap();
203 let err = decode::<Sample>(1, &v2_bytes).expect_err("must reject");
204 assert!(matches!(err, CodecError::Version { .. }));
205 let io_err = codec_io_error("vote decode", err);
206 assert_eq!(io_err.kind(), io::ErrorKind::InvalidData);
207 assert!(
208 io_err.to_string().starts_with("vote decode: "),
209 "context must prefix the message, got {io_err}"
210 );
211 }
212
213 #[test]
214 fn codec_io_error_maps_other_variants_to_other_kind() {
215 let io_err = codec_io_error("vote decode", CodecError::Empty);
218 assert_ne!(io_err.kind(), io::ErrorKind::InvalidData);
219 assert!(io_err.to_string().starts_with("vote decode: "));
220 }
221
222 use proptest::prelude::*;
223
224 proptest! {
225 #[test]
228 fn encode_decode_roundtrip_any(
229 version in any::<u8>(),
230 idx in any::<u64>(),
231 name in any::<String>(),
232 ) {
233 let s = Sample { idx, name };
234 let bytes = encode(version, &s).unwrap();
235 prop_assert_eq!(bytes[0], version);
236 let back: Sample = decode(version, &bytes).unwrap();
237 prop_assert_eq!(s, back);
238 }
239
240 #[test]
244 fn decode_rejects_any_version_mismatch(
245 encoded in any::<u8>(),
246 expected in any::<u8>(),
247 idx in any::<u64>(),
248 name in any::<String>(),
249 ) {
250 prop_assume!(encoded != expected);
251 let bytes = encode(encoded, &Sample { idx, name }).unwrap();
252 match decode::<Sample>(expected, &bytes) {
253 Err(CodecError::Version { expected: e, actual: a }) => {
254 prop_assert_eq!(e, expected);
255 prop_assert_eq!(a, encoded);
256 }
257 other => prop_assert!(false, "expected Version mismatch; got {other:?}"),
258 }
259 }
260 }
261}