1#![doc = include_str!("../README.md")]
14
15use serde::{Serialize, de::DeserializeOwned};
16
17#[derive(Debug, thiserror::Error)]
24pub enum CodecError {
25 #[error("payload empty")]
27 Empty,
28 #[error("version mismatch: expected {expected}, got {actual}")]
32 Version { expected: u8, actual: u8 },
33 #[error("encode failed: {0}")]
35 Encode(#[source] postcard::Error),
36 #[error("decode failed: {0}")]
38 Decode(#[source] postcard::Error),
39}
40
41pub fn encode<T: Serialize>(version: u8, value: &T) -> Result<Vec<u8>, CodecError> {
48 let body = postcard::to_stdvec(value).map_err(CodecError::Encode)?;
49 let mut out = Vec::with_capacity(1 + body.len());
50 out.push(version);
51 out.extend_from_slice(&body);
52 Ok(out)
53}
54
55pub fn decode<T: DeserializeOwned>(expected_version: u8, bytes: &[u8]) -> Result<T, CodecError> {
61 let (first, rest) = bytes.split_first().ok_or(CodecError::Empty)?;
62 if *first != expected_version {
63 return Err(CodecError::Version {
64 expected: expected_version,
65 actual: *first,
66 });
67 }
68 postcard::from_bytes(rest).map_err(CodecError::Decode)
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74 use serde::{Deserialize, Serialize};
75
76 #[derive(Debug, PartialEq, Serialize, Deserialize)]
77 struct Sample {
78 idx: u64,
79 name: String,
80 }
81
82 #[test]
83 fn encode_decode_roundtrip() {
84 let original = Sample {
85 idx: 42,
86 name: "tsoracle".into(),
87 };
88 let bytes = encode(1, &original).expect("encode");
89 assert_eq!(bytes[0], 1);
90 let decoded: Sample = decode(1, &bytes).expect("decode");
91 assert_eq!(original, decoded);
92 }
93
94 #[test]
95 fn decode_rejects_wrong_version() {
96 let bytes = encode(
97 2,
98 &Sample {
99 idx: 1,
100 name: "x".into(),
101 },
102 )
103 .expect("encode");
104 let err = decode::<Sample>(1, &bytes).expect_err("must reject");
105 assert!(matches!(
106 err,
107 CodecError::Version {
108 expected: 1,
109 actual: 2
110 }
111 ));
112 }
113
114 #[test]
115 fn decode_rejects_empty() {
116 let err = decode::<Sample>(1, &[]).expect_err("must reject");
117 assert!(matches!(err, CodecError::Empty));
118 }
119
120 #[test]
121 fn decode_rejects_truncated_input() {
122 let original = Sample {
123 idx: u64::MAX,
124 name: "hello-world-storage-roundtrip".into(),
125 };
126 let bytes = encode(1, &original).expect("encode");
127 assert!(bytes.len() >= 16, "payload should be non-trivial");
128 let truncated = &bytes[..bytes.len() / 2];
129 assert!(matches!(
130 decode::<Sample>(1, truncated),
131 Err(CodecError::Decode(_))
132 ));
133 }
134
135 use proptest::prelude::*;
136
137 proptest! {
138 #[test]
141 fn encode_decode_roundtrip_any(
142 version in any::<u8>(),
143 idx in any::<u64>(),
144 name in any::<String>(),
145 ) {
146 let s = Sample { idx, name };
147 let bytes = encode(version, &s).unwrap();
148 prop_assert_eq!(bytes[0], version);
149 let back: Sample = decode(version, &bytes).unwrap();
150 prop_assert_eq!(s, back);
151 }
152
153 #[test]
157 fn decode_rejects_any_version_mismatch(
158 encoded in any::<u8>(),
159 expected in any::<u8>(),
160 idx in any::<u64>(),
161 name in any::<String>(),
162 ) {
163 prop_assume!(encoded != expected);
164 let bytes = encode(encoded, &Sample { idx, name }).unwrap();
165 match decode::<Sample>(expected, &bytes) {
166 Err(CodecError::Version { expected: e, actual: a }) => {
167 prop_assert_eq!(e, expected);
168 prop_assert_eq!(a, encoded);
169 }
170 other => prop_assert!(false, "expected Version mismatch; got {other:?}"),
171 }
172 }
173 }
174}