1use serde::{Serialize, de::DeserializeOwned};
7use snafu::{ResultExt, Snafu};
8
9#[derive(Debug, Snafu)]
11pub enum CodecError {
12 #[snafu(display("Encoding failed: {source}"))]
14 Encode {
15 source: postcard::Error,
17 #[snafu(implicit)]
19 location: snafu::Location,
20 },
21
22 #[snafu(display("Decoding failed: {source}"))]
24 Decode {
25 source: postcard::Error,
27 #[snafu(implicit)]
29 location: snafu::Location,
30 },
31}
32
33pub fn encode<T: Serialize>(value: &T) -> Result<Vec<u8>, CodecError> {
39 postcard::to_allocvec(value).context(EncodeSnafu)
40}
41
42pub fn decode<T: DeserializeOwned>(bytes: &[u8]) -> Result<T, CodecError> {
48 postcard::from_bytes(bytes).context(DecodeSnafu)
49}
50
51#[cfg(test)]
52#[allow(clippy::unwrap_used, clippy::expect_used, clippy::disallowed_methods)]
53mod tests {
54 use serde::Deserialize;
55 use snafu::ResultExt;
56
57 use super::*;
58
59 #[test]
61 fn test_roundtrip_primitive_u64() {
62 let original: u64 = 42;
63 let bytes = encode(&original).expect("encode u64");
64 let decoded: u64 = decode(&bytes).expect("decode u64");
65 assert_eq!(original, decoded);
66 }
67
68 #[test]
69 fn test_roundtrip_primitive_string() {
70 let original = "hello world".to_string();
71 let bytes = encode(&original).expect("encode string");
72 let decoded: String = decode(&bytes).expect("decode string");
73 assert_eq!(original, decoded);
74 }
75
76 #[test]
77 fn test_roundtrip_primitive_bool() {
78 for original in [true, false] {
79 let bytes = encode(&original).expect("encode bool");
80 let decoded: bool = decode(&bytes).expect("decode bool");
81 assert_eq!(original, decoded);
82 }
83 }
84
85 #[test]
86 fn test_roundtrip_primitive_vec() {
87 let original: Vec<u32> = vec![1, 2, 3, 4, 5];
88 let bytes = encode(&original).expect("encode vec");
89 let decoded: Vec<u32> = decode(&bytes).expect("decode vec");
90 assert_eq!(original, decoded);
91 }
92
93 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
95 struct ComplexStruct {
96 id: u64,
97 name: String,
98 data: Vec<u8>,
99 nested: Option<NestedStruct>,
100 }
101
102 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
103 struct NestedStruct {
104 value: i32,
105 flag: bool,
106 }
107
108 #[test]
109 fn test_roundtrip_complex_struct() {
110 let original = ComplexStruct {
111 id: 12345,
112 name: "test entity".to_string(),
113 data: vec![0xDE, 0xAD, 0xBE, 0xEF],
114 nested: Some(NestedStruct { value: -42, flag: true }),
115 };
116 let bytes = encode(&original).expect("encode complex struct");
117 let decoded: ComplexStruct = decode(&bytes).expect("decode complex struct");
118 assert_eq!(original, decoded);
119 }
120
121 #[test]
122 fn test_roundtrip_complex_struct_with_none() {
123 let original = ComplexStruct { id: 0, name: String::new(), data: vec![], nested: None };
124 let bytes = encode(&original).expect("encode complex struct with None");
125 let decoded: ComplexStruct = decode(&bytes).expect("decode complex struct with None");
126 assert_eq!(original, decoded);
127 }
128
129 #[test]
131 fn test_decode_malformed_input() {
132 let malformed_bytes = [0xFF, 0xFF, 0xFF, 0xFF];
133 let result: Result<ComplexStruct, _> = decode(&malformed_bytes);
134 assert!(result.is_err());
135 let err = result.unwrap_err();
136 assert!(matches!(err, CodecError::Decode { .. }));
137 let display = err.to_string();
139 assert!(display.contains("Decoding failed"));
140 }
141
142 #[test]
143 fn test_decode_truncated_data() {
144 let original = ComplexStruct {
146 id: 12345,
147 name: "test".to_string(),
148 data: vec![1, 2, 3],
149 nested: None,
150 };
151 let bytes = encode(&original).expect("encode");
152 let truncated = &bytes[..2.min(bytes.len())];
154 let result: Result<ComplexStruct, _> = decode(truncated);
155 assert!(result.is_err());
156 }
157
158 #[test]
160 fn test_decode_empty_input() {
161 let empty: &[u8] = &[];
162 let result: Result<u64, _> = decode(empty);
163 assert!(result.is_err());
164 let err = result.unwrap_err();
165 assert!(matches!(err, CodecError::Decode { .. }));
166 }
167
168 #[test]
169 fn test_encode_empty_vec() {
170 let empty_vec: Vec<u8> = vec![];
171 let bytes = encode(&empty_vec).expect("encode empty vec");
172 let decoded: Vec<u8> = decode(&bytes).expect("decode empty vec");
173 assert_eq!(empty_vec, decoded);
174 }
175
176 #[test]
177 fn test_encode_empty_string() {
178 let empty_string = String::new();
179 let bytes = encode(&empty_string).expect("encode empty string");
180 let decoded: String = decode(&bytes).expect("decode empty string");
181 assert_eq!(empty_string, decoded);
182 }
183
184 #[test]
186 fn test_encode_error_display() {
187 let malformed: &[u8] = &[0xFF];
190 let result: Result<String, _> = decode(malformed);
191 let err = result.unwrap_err();
192 let display = err.to_string();
193 assert!(display.starts_with("Decoding failed:"));
194 }
195
196 #[test]
198 fn test_roundtrip_max_u64() {
199 let original: u64 = u64::MAX;
200 let bytes = encode(&original).expect("encode max u64");
201 let decoded: u64 = decode(&bytes).expect("decode max u64");
202 assert_eq!(original, decoded);
203 }
204
205 #[test]
206 fn test_roundtrip_unicode_string() {
207 let original = "Hello 世界 🦀 émoji".to_string();
208 let bytes = encode(&original).expect("encode unicode");
209 let decoded: String = decode(&bytes).expect("decode unicode");
210 assert_eq!(original, decoded);
211 }
212
213 #[test]
217 fn test_codec_error_encode_display() {
218 let malformed: &[u8] = &[0xFF, 0xFF, 0xFF, 0xFF];
220 let result: Result<u64, _> = decode(malformed);
221 let err = result.unwrap_err();
222
223 let display = format!("{err}");
225 assert!(
226 display.starts_with("Decoding failed:"),
227 "Expected 'Decoding failed:', got: {display}"
228 );
229 }
230
231 #[test]
233 fn test_codec_error_decode_display() {
234 let empty: &[u8] = &[];
235 let result: Result<String, _> = decode(empty);
236 let err = result.unwrap_err();
237
238 let display = format!("{err}");
239 assert!(
240 display.starts_with("Decoding failed:"),
241 "Expected 'Decoding failed:', got: {display}"
242 );
243 }
244
245 #[test]
247 fn test_codec_error_source_chain() {
248 use std::error::Error;
249
250 let malformed: &[u8] = &[0xFF];
251 let result: Result<String, _> = decode(malformed);
252 let err = result.unwrap_err();
253
254 assert!(err.source().is_some(), "CodecError should have a source");
256
257 let source = err.source().unwrap();
259 let source_display = format!("{source}");
261 assert!(!source_display.is_empty(), "Source should have non-empty display");
262 }
263
264 mod proptest_roundtrip {
269 use proptest::prelude::*;
270
271 use crate::{
272 codec::{decode, encode},
273 types::{
274 BlockHeader, ChainCommitment, Entity, Operation, RegionBlock, Relationship,
275 SetCondition, Transaction, VaultBlock, VaultEntry,
276 },
277 };
278
279 fn arb_set_condition() -> impl Strategy<Value = SetCondition> {
281 prop_oneof![
282 Just(SetCondition::MustNotExist),
283 Just(SetCondition::MustExist),
284 any::<u64>().prop_map(SetCondition::VersionEquals),
285 proptest::collection::vec(any::<u8>(), 0..32).prop_map(SetCondition::ValueEquals),
286 ]
287 }
288
289 fn arb_operation() -> impl Strategy<Value = Operation> {
291 prop_oneof![
292 (
293 "[a-z]{1,16}",
294 proptest::collection::vec(any::<u8>(), 0..32),
295 proptest::option::of(arb_set_condition()),
296 proptest::option::of(any::<u64>()),
297 )
298 .prop_map(|(key, value, condition, expires_at)| {
299 Operation::SetEntity { key, value, condition, expires_at }
300 }),
301 "[a-z]{1,16}".prop_map(|key| Operation::DeleteEntity { key }),
302 ("[a-z]{1,16}", any::<u64>())
303 .prop_map(|(key, expired_at)| Operation::ExpireEntity { key, expired_at }),
304 ("[a-z]{1,16}", "[a-z]{1,8}", "[a-z]{1,16}").prop_map(
305 |(resource, relation, subject)| {
306 Operation::CreateRelationship { resource, relation, subject }
307 }
308 ),
309 ("[a-z]{1,16}", "[a-z]{1,8}", "[a-z]{1,16}").prop_map(
310 |(resource, relation, subject)| {
311 Operation::DeleteRelationship { resource, relation, subject }
312 }
313 ),
314 ]
315 }
316
317 fn arb_hash() -> impl Strategy<Value = [u8; 32]> {
319 proptest::array::uniform32(any::<u8>())
320 }
321
322 fn arb_timestamp() -> impl Strategy<Value = chrono::DateTime<chrono::Utc>> {
324 use chrono::{TimeZone, Utc};
325 (1_577_836_800i64..1_893_456_000i64).prop_map(|secs| {
326 Utc.timestamp_opt(secs, 0)
327 .single()
328 .unwrap_or_else(|| chrono::DateTime::<Utc>::from(std::time::UNIX_EPOCH))
329 })
330 }
331
332 fn arb_transaction() -> impl Strategy<Value = Transaction> {
334 (
335 proptest::array::uniform16(any::<u8>()),
336 "[a-z]{3,10}",
337 1u64..100_000,
338 proptest::collection::vec(arb_operation(), 1..5),
339 arb_timestamp(),
340 )
341 .prop_map(|(id, client_id, sequence, operations, timestamp)| {
342 Transaction { id, client_id: client_id.into(), sequence, operations, timestamp }
343 })
344 }
345
346 fn arb_block_header() -> impl Strategy<Value = BlockHeader> {
348 (
349 any::<u64>(),
350 (1i64..10_000).prop_map(crate::types::OrganizationId::new),
351 (1i64..10_000).prop_map(crate::types::VaultId::new),
352 arb_hash(),
353 arb_hash(),
354 arb_hash(),
355 arb_timestamp(),
356 any::<u64>(),
357 any::<u64>(),
358 )
359 .prop_map(
360 |(
361 height,
362 organization,
363 vault,
364 previous_hash,
365 tx_merkle_root,
366 state_root,
367 timestamp,
368 term,
369 committed_index,
370 )| {
371 BlockHeader {
372 height,
373 organization,
374 vault,
375 previous_hash,
376 tx_merkle_root,
377 state_root,
378 timestamp,
379 term,
380 committed_index,
381 }
382 },
383 )
384 }
385
386 proptest! {
387 #[test]
389 fn prop_operation_roundtrip(op in arb_operation()) {
390 let bytes = encode(&op).expect("encode operation");
391 let decoded: Operation = decode(&bytes).expect("decode operation");
392 prop_assert_eq!(op, decoded);
393 }
394
395 #[test]
397 fn prop_set_condition_roundtrip(cond in arb_set_condition()) {
398 let bytes = encode(&cond).expect("encode condition");
399 let decoded: SetCondition = decode(&bytes).expect("decode condition");
400 prop_assert_eq!(cond, decoded);
401 }
402
403 #[test]
405 fn prop_entity_roundtrip(
406 key in proptest::collection::vec(any::<u8>(), 0..64),
407 value in proptest::collection::vec(any::<u8>(), 0..64),
408 expires_at in any::<u64>(),
409 version in any::<u64>(),
410 ) {
411 let entity = Entity { key, value, expires_at, version };
412 let bytes = encode(&entity).expect("encode entity");
413 let decoded: Entity = decode(&bytes).expect("decode entity");
414 prop_assert_eq!(entity, decoded);
415 }
416
417 #[test]
419 fn prop_relationship_roundtrip(
420 resource in "[a-z]{1,16}",
421 relation in "[a-z]{1,8}",
422 subject in "[a-z]{1,16}",
423 ) {
424 let rel = Relationship { resource, relation, subject };
425 let bytes = encode(&rel).expect("encode relationship");
426 let decoded: Relationship = decode(&bytes).expect("decode relationship");
427 prop_assert_eq!(rel, decoded);
428 }
429
430 #[test]
432 fn prop_transaction_roundtrip(tx in arb_transaction()) {
433 let bytes = encode(&tx).expect("encode transaction");
434 let decoded: Transaction = decode(&bytes).expect("decode transaction");
435 prop_assert_eq!(tx, decoded);
436 }
437
438 #[test]
440 fn prop_block_header_roundtrip(header in arb_block_header()) {
441 let bytes = encode(&header).expect("encode block header");
442 let decoded: BlockHeader = decode(&bytes).expect("decode block header");
443 prop_assert_eq!(header, decoded);
444 }
445
446 #[test]
448 fn prop_vault_block_roundtrip(
449 header in arb_block_header(),
450 transactions in proptest::collection::vec(arb_transaction(), 0..3),
451 ) {
452 let block = VaultBlock { header, transactions };
453 let bytes = encode(&block).expect("encode vault block");
454 let decoded: VaultBlock = decode(&bytes).expect("decode vault block");
455 prop_assert_eq!(block, decoded);
456 }
457
458 #[test]
460 fn prop_vault_entry_roundtrip(
461 ns_id in (1i64..10_000).prop_map(crate::types::OrganizationId::new),
462 vault_id in (1i64..10_000).prop_map(crate::types::VaultId::new),
463 vault_height in any::<u64>(),
464 previous_vault_hash in arb_hash(),
465 transactions in proptest::collection::vec(arb_transaction(), 0..2),
466 tx_merkle_root in arb_hash(),
467 state_root in arb_hash(),
468 ) {
469 let entry = VaultEntry {
470 organization: ns_id,
471 vault: vault_id,
472 vault_height,
473 previous_vault_hash,
474 transactions,
475 tx_merkle_root,
476 state_root,
477 };
478 let bytes = encode(&entry).expect("encode vault entry");
479 let decoded: VaultEntry = decode(&bytes).expect("decode vault entry");
480 prop_assert_eq!(entry, decoded);
481 }
482
483 #[test]
485 fn prop_region_block_roundtrip(
486 region in (0usize..crate::types::ALL_REGIONS.len()).prop_map(|i| crate::types::ALL_REGIONS[i]),
487 region_height in any::<u64>(),
488 previous_region_hash in arb_hash(),
489 timestamp in arb_timestamp(),
490 leader_id in "[a-z]{3,10}",
491 term in any::<u64>(),
492 committed_index in any::<u64>(),
493 ) {
494 let block = RegionBlock {
495 region,
496 region_height,
497 previous_region_hash,
498 vault_entries: vec![],
499 timestamp,
500 leader_id: leader_id.into(),
501 term,
502 committed_index,
503 };
504 let bytes = encode(&block).expect("encode region block");
505 let decoded: RegionBlock = decode(&bytes).expect("decode region block");
506 prop_assert_eq!(block, decoded);
507 }
508
509 #[test]
511 fn prop_chain_commitment_roundtrip(
512 accumulated in arb_hash(),
513 state_root_acc in arb_hash(),
514 from in any::<u64>(),
515 to in any::<u64>(),
516 ) {
517 let commitment = ChainCommitment {
518 accumulated_header_hash: accumulated,
519 state_root_accumulator: state_root_acc,
520 from_height: from.min(to),
521 to_height: from.max(to),
522 };
523 let bytes = encode(&commitment).expect("encode chain commitment");
524 let decoded: ChainCommitment =
525 decode(&bytes).expect("decode chain commitment");
526 prop_assert_eq!(commitment, decoded);
527 }
528 }
529 }
530
531 #[test]
533 fn test_codec_error_debug() {
534 let malformed: &[u8] = &[0xFF, 0xFF];
535 let result: Result<u64, _> = decode(malformed);
536 let err = result.unwrap_err();
537
538 let debug = format!("{err:?}");
539 assert!(debug.contains("Decode"), "Debug should contain 'Decode' variant name");
541 assert!(debug.contains("source"), "Debug should contain 'source' field: {debug}");
543 }
544
545 #[test]
547 fn test_codec_error_variants() {
548 let decode_result: Result<u64, CodecError> =
550 postcard::from_bytes::<u64>(&[0xFF, 0xFF, 0xFF]).context(super::DecodeSnafu);
551 let decode_err = decode_result.expect_err("should fail");
552
553 assert!(matches!(decode_err, CodecError::Decode { .. }));
555
556 assert!(!matches!(decode_err, CodecError::Encode { .. }));
560 }
561}