Skip to main content

ipfrs_network/
message_codec.rs

1//! Length-delimited message encoding/decoding for peer communication.
2//!
3//! Provides `PeerMessageCodec` which encodes messages with a 4-byte big-endian
4//! length prefix and an optional CRC32 checksum for integrity verification.
5//!
6//! # Wire Format
7//!
8//! ```text
9//! +-------------------+-------------------+--------------------+
10//! | Length (4 bytes)   | Payload (N bytes) | CRC32 (4 bytes)    |
11//! | big-endian u32     |                   | (optional)         |
12//! +-------------------+-------------------+--------------------+
13//! ```
14//!
15//! The length prefix encodes the payload size only (not including itself or the
16//! checksum). When checksums are enabled, the CRC32 is computed over the payload
17//! bytes and appended after the payload.
18
19use std::fmt;
20
21// ---------------------------------------------------------------------------
22// CRC32 lookup table (IEEE polynomial 0xEDB88320, reflected)
23// ---------------------------------------------------------------------------
24
25const CRC32_TABLE: [u32; 256] = {
26    let mut table = [0u32; 256];
27    let mut i = 0u32;
28    while i < 256 {
29        let mut crc = i;
30        let mut j = 0;
31        while j < 8 {
32            if crc & 1 != 0 {
33                crc = (crc >> 1) ^ 0xEDB8_8320;
34            } else {
35                crc >>= 1;
36            }
37            j += 1;
38        }
39        table[i as usize] = crc;
40        i += 1;
41    }
42    table
43};
44
45// ---------------------------------------------------------------------------
46// Error type
47// ---------------------------------------------------------------------------
48
49/// Errors returned by [`PeerMessageCodec`] encode/decode operations.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub enum CodecError {
52    /// The supplied buffer is too small to contain a valid message.
53    BufferTooSmall,
54    /// The payload exceeds the configured maximum message size.
55    MessageTooLarge,
56    /// The length prefix contains an invalid or inconsistent value.
57    InvalidLength,
58    /// The CRC32 checksum of the payload does not match the stored checksum.
59    ChecksumMismatch,
60}
61
62impl fmt::Display for CodecError {
63    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64        match self {
65            Self::BufferTooSmall => write!(f, "buffer too small"),
66            Self::MessageTooLarge => write!(f, "message too large"),
67            Self::InvalidLength => write!(f, "invalid length prefix"),
68            Self::ChecksumMismatch => write!(f, "checksum mismatch"),
69        }
70    }
71}
72
73impl std::error::Error for CodecError {}
74
75// ---------------------------------------------------------------------------
76// Configuration
77// ---------------------------------------------------------------------------
78
79/// Configuration for [`PeerMessageCodec`].
80#[derive(Debug, Clone)]
81pub struct CodecConfig {
82    /// Maximum allowed payload size in bytes (default: 16 MiB).
83    pub max_message_size: usize,
84    /// Whether to append/verify a CRC32 checksum (default: `true`).
85    pub use_checksum: bool,
86    /// Number of bytes used for the length prefix (always 4).
87    pub length_prefix_bytes: usize,
88}
89
90impl Default for CodecConfig {
91    fn default() -> Self {
92        Self {
93            max_message_size: 16_777_216, // 16 MiB
94            use_checksum: true,
95            length_prefix_bytes: 4,
96        }
97    }
98}
99
100// ---------------------------------------------------------------------------
101// Encoded message metadata
102// ---------------------------------------------------------------------------
103
104/// Metadata about an encoded message.
105#[derive(Debug, Clone)]
106pub struct EncodedMessage {
107    /// The full wire-format bytes (length prefix + payload + optional checksum).
108    pub data: Vec<u8>,
109    /// The size of the original payload before encoding.
110    pub original_size: usize,
111    /// The CRC32 checksum of the payload, if checksums are enabled.
112    pub checksum: Option<u32>,
113}
114
115// ---------------------------------------------------------------------------
116// Statistics
117// ---------------------------------------------------------------------------
118
119/// Cumulative codec statistics.
120#[derive(Debug, Clone)]
121pub struct CodecStats {
122    /// Total number of messages successfully encoded.
123    pub messages_encoded: u64,
124    /// Total number of messages successfully decoded.
125    pub messages_decoded: u64,
126    /// Total payload bytes encoded (before framing overhead).
127    pub bytes_encoded: u64,
128    /// Total payload bytes decoded.
129    pub bytes_decoded: u64,
130}
131
132// ---------------------------------------------------------------------------
133// PeerMessageCodec
134// ---------------------------------------------------------------------------
135
136/// Length-delimited message codec with optional CRC32 integrity checking.
137///
138/// # Example
139///
140/// ```rust
141/// use ipfrs_network::message_codec::{PeerMessageCodec, CodecConfig};
142///
143/// let codec = PeerMessageCodec::new(CodecConfig::default());
144/// let wire = codec.encode(b"hello").expect("encode");
145/// let payload = codec.decode(&wire).expect("decode");
146/// assert_eq!(payload, b"hello");
147/// ```
148pub struct PeerMessageCodec {
149    config: CodecConfig,
150    messages_encoded: std::cell::Cell<u64>,
151    messages_decoded: std::cell::Cell<u64>,
152    bytes_encoded: std::cell::Cell<u64>,
153    bytes_decoded: std::cell::Cell<u64>,
154}
155
156impl fmt::Debug for PeerMessageCodec {
157    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158        f.debug_struct("PeerMessageCodec")
159            .field("config", &self.config)
160            .field("messages_encoded", &self.messages_encoded.get())
161            .field("messages_decoded", &self.messages_decoded.get())
162            .finish()
163    }
164}
165
166impl PeerMessageCodec {
167    /// Create a new codec with the given configuration.
168    pub fn new(config: CodecConfig) -> Self {
169        Self {
170            config,
171            messages_encoded: std::cell::Cell::new(0),
172            messages_decoded: std::cell::Cell::new(0),
173            bytes_encoded: std::cell::Cell::new(0),
174            bytes_decoded: std::cell::Cell::new(0),
175        }
176    }
177
178    /// Encode a payload into wire format.
179    ///
180    /// Returns the complete frame: `[4-byte length BE][payload][optional 4-byte CRC32]`.
181    ///
182    /// # Errors
183    ///
184    /// Returns [`CodecError::MessageTooLarge`] if `payload.len()` exceeds
185    /// `config.max_message_size`.
186    pub fn encode(&self, payload: &[u8]) -> Result<Vec<u8>, CodecError> {
187        if payload.len() > self.config.max_message_size {
188            return Err(CodecError::MessageTooLarge);
189        }
190
191        let total = self.estimate_encoded_size(payload.len());
192        let mut buf = Vec::with_capacity(total);
193
194        // Length prefix (payload size only, big-endian u32).
195        let len_u32 = payload.len() as u32;
196        buf.extend_from_slice(&len_u32.to_be_bytes());
197
198        // Payload.
199        buf.extend_from_slice(payload);
200
201        // Optional CRC32 checksum.
202        if self.config.use_checksum {
203            let crc = Self::crc32(payload);
204            buf.extend_from_slice(&crc.to_be_bytes());
205        }
206
207        // Update stats.
208        self.messages_encoded.set(self.messages_encoded.get() + 1);
209        self.bytes_encoded
210            .set(self.bytes_encoded.get() + payload.len() as u64);
211
212        Ok(buf)
213    }
214
215    /// Encode a payload and return an [`EncodedMessage`] with metadata.
216    pub fn encode_with_metadata(&self, payload: &[u8]) -> Result<EncodedMessage, CodecError> {
217        if payload.len() > self.config.max_message_size {
218            return Err(CodecError::MessageTooLarge);
219        }
220
221        let checksum = if self.config.use_checksum {
222            Some(Self::crc32(payload))
223        } else {
224            None
225        };
226
227        let data = self.encode(payload)?;
228        // `encode` already updated stats, but we called it internally so
229        // we need to undo the double-count. Alternatively, we could inline
230        // the logic, but reusing `encode` is cleaner. Undo stats bump:
231        self.messages_encoded.set(self.messages_encoded.get() - 1);
232        self.bytes_encoded
233            .set(self.bytes_encoded.get() - payload.len() as u64);
234
235        // Now bump once for this call.
236        self.messages_encoded.set(self.messages_encoded.get() + 1);
237        self.bytes_encoded
238            .set(self.bytes_encoded.get() + payload.len() as u64);
239
240        Ok(EncodedMessage {
241            data,
242            original_size: payload.len(),
243            checksum,
244        })
245    }
246
247    /// Decode a wire-format frame and return the payload bytes.
248    ///
249    /// # Errors
250    ///
251    /// - [`CodecError::BufferTooSmall`] if `data` is shorter than the length prefix.
252    /// - [`CodecError::InvalidLength`] if the frame is truncated or the length
253    ///   prefix is inconsistent with the buffer size.
254    /// - [`CodecError::MessageTooLarge`] if the decoded length exceeds the
255    ///   configured maximum.
256    /// - [`CodecError::ChecksumMismatch`] if checksums are enabled and the
257    ///   stored checksum doesn't match the computed one.
258    pub fn decode(&self, data: &[u8]) -> Result<Vec<u8>, CodecError> {
259        if data.len() < 4 {
260            return Err(CodecError::BufferTooSmall);
261        }
262
263        let payload_len = self.decode_length(data)?;
264
265        if payload_len > self.config.max_message_size {
266            return Err(CodecError::MessageTooLarge);
267        }
268
269        let checksum_size = if self.config.use_checksum { 4 } else { 0 };
270        let expected_total = 4 + payload_len + checksum_size;
271
272        if data.len() < expected_total {
273            return Err(CodecError::InvalidLength);
274        }
275
276        let payload = &data[4..4 + payload_len];
277
278        if self.config.use_checksum {
279            let stored_bytes: [u8; 4] = data[4 + payload_len..4 + payload_len + 4]
280                .try_into()
281                .map_err(|_| CodecError::InvalidLength)?;
282            let stored_crc = u32::from_be_bytes(stored_bytes);
283            let computed_crc = Self::crc32(payload);
284            if stored_crc != computed_crc {
285                return Err(CodecError::ChecksumMismatch);
286            }
287        }
288
289        self.messages_decoded.set(self.messages_decoded.get() + 1);
290        self.bytes_decoded
291            .set(self.bytes_decoded.get() + payload_len as u64);
292
293        Ok(payload.to_vec())
294    }
295
296    /// Compute a CRC32 checksum (IEEE / ISO 3309) of the given data.
297    pub fn crc32(data: &[u8]) -> u32 {
298        let mut crc: u32 = 0xFFFF_FFFF;
299        for &byte in data {
300            let index = ((crc ^ u32::from(byte)) & 0xFF) as usize;
301            crc = (crc >> 8) ^ CRC32_TABLE[index];
302        }
303        crc ^ 0xFFFF_FFFF
304    }
305
306    /// Estimate the total encoded size for a payload of the given length.
307    ///
308    /// Returns `4 + payload_size + (4 if checksum enabled)`.
309    pub fn estimate_encoded_size(&self, payload_size: usize) -> usize {
310        let checksum_overhead = if self.config.use_checksum { 4 } else { 0 };
311        4 + payload_size + checksum_overhead
312    }
313
314    /// Peek at the length prefix without performing a full decode.
315    ///
316    /// Returns the payload length encoded in the first 4 bytes.
317    ///
318    /// # Errors
319    ///
320    /// - [`CodecError::BufferTooSmall`] if `data` has fewer than 4 bytes.
321    /// - [`CodecError::InvalidLength`] if the decoded length is zero but
322    ///   additional bytes suggest corruption (currently not enforced —
323    ///   zero-length payloads are valid).
324    pub fn decode_length(&self, data: &[u8]) -> Result<usize, CodecError> {
325        if data.len() < 4 {
326            return Err(CodecError::BufferTooSmall);
327        }
328        let bytes: [u8; 4] = data[..4]
329            .try_into()
330            .map_err(|_| CodecError::BufferTooSmall)?;
331        Ok(u32::from_be_bytes(bytes) as usize)
332    }
333
334    /// Return cumulative codec statistics.
335    pub fn stats(&self) -> CodecStats {
336        CodecStats {
337            messages_encoded: self.messages_encoded.get(),
338            messages_decoded: self.messages_decoded.get(),
339            bytes_encoded: self.bytes_encoded.get(),
340            bytes_decoded: self.bytes_decoded.get(),
341        }
342    }
343
344    /// Return a reference to the current configuration.
345    pub fn config(&self) -> &CodecConfig {
346        &self.config
347    }
348}
349
350// ---------------------------------------------------------------------------
351// Tests
352// ---------------------------------------------------------------------------
353
354#[cfg(test)]
355mod tests {
356    use super::*;
357
358    fn default_codec() -> PeerMessageCodec {
359        PeerMessageCodec::new(CodecConfig::default())
360    }
361
362    fn no_checksum_codec() -> PeerMessageCodec {
363        PeerMessageCodec::new(CodecConfig {
364            use_checksum: false,
365            ..Default::default()
366        })
367    }
368
369    // ---- roundtrip tests ----
370
371    #[test]
372    fn roundtrip_basic() {
373        let codec = default_codec();
374        let payload = b"hello, ipfrs";
375        let wire = codec.encode(payload).expect("encode failed");
376        let decoded = codec.decode(&wire).expect("decode failed");
377        assert_eq!(decoded, payload);
378    }
379
380    #[test]
381    fn roundtrip_empty_payload() {
382        let codec = default_codec();
383        let wire = codec.encode(b"").expect("encode empty");
384        let decoded = codec.decode(&wire).expect("decode empty");
385        assert!(decoded.is_empty());
386    }
387
388    #[test]
389    fn roundtrip_no_checksum() {
390        let codec = no_checksum_codec();
391        let payload = b"no checksum";
392        let wire = codec.encode(payload).expect("encode");
393        let decoded = codec.decode(&wire).expect("decode");
394        assert_eq!(decoded, payload);
395    }
396
397    #[test]
398    fn roundtrip_large_payload() {
399        let codec = default_codec();
400        let payload = vec![0xAB_u8; 65_536];
401        let wire = codec.encode(&payload).expect("encode large");
402        let decoded = codec.decode(&wire).expect("decode large");
403        assert_eq!(decoded, payload);
404    }
405
406    #[test]
407    fn roundtrip_binary_data() {
408        let codec = default_codec();
409        let payload: Vec<u8> = (0..=255).collect();
410        let wire = codec.encode(&payload).expect("encode binary");
411        let decoded = codec.decode(&wire).expect("decode binary");
412        assert_eq!(decoded, payload);
413    }
414
415    #[test]
416    fn roundtrip_single_byte() {
417        let codec = default_codec();
418        let wire = codec.encode(&[0x42]).expect("encode");
419        let decoded = codec.decode(&wire).expect("decode");
420        assert_eq!(decoded, vec![0x42]);
421    }
422
423    // ---- checksum tests ----
424
425    #[test]
426    fn crc32_known_values() {
427        // "123456789" should produce 0xCBF43926 (IEEE CRC32).
428        let crc = PeerMessageCodec::crc32(b"123456789");
429        assert_eq!(crc, 0xCBF4_3926);
430    }
431
432    #[test]
433    fn crc32_empty() {
434        let crc = PeerMessageCodec::crc32(b"");
435        assert_eq!(crc, 0x0000_0000);
436    }
437
438    #[test]
439    fn corrupted_payload_detected() {
440        let codec = default_codec();
441        let mut wire = codec.encode(b"integrity").expect("encode");
442        // Corrupt one byte in the payload region.
443        wire[5] ^= 0xFF;
444        let result = codec.decode(&wire);
445        assert_eq!(result, Err(CodecError::ChecksumMismatch));
446    }
447
448    #[test]
449    fn corrupted_checksum_detected() {
450        let codec = default_codec();
451        let mut wire = codec.encode(b"test").expect("encode");
452        // Corrupt the last byte (part of the checksum).
453        let last = wire.len() - 1;
454        wire[last] ^= 0x01;
455        let result = codec.decode(&wire);
456        assert_eq!(result, Err(CodecError::ChecksumMismatch));
457    }
458
459    #[test]
460    fn no_checksum_no_verification() {
461        let codec = no_checksum_codec();
462        let mut wire = codec.encode(b"test").expect("encode");
463        // Corrupt the payload — should still decode since checksum is off.
464        wire[5] ^= 0xFF;
465        let result = codec.decode(&wire);
466        assert!(result.is_ok());
467    }
468
469    // ---- size enforcement ----
470
471    #[test]
472    fn message_too_large() {
473        let codec = PeerMessageCodec::new(CodecConfig {
474            max_message_size: 100,
475            ..Default::default()
476        });
477        let payload = vec![0u8; 101];
478        assert_eq!(codec.encode(&payload), Err(CodecError::MessageTooLarge));
479    }
480
481    #[test]
482    fn message_exactly_max_size() {
483        let codec = PeerMessageCodec::new(CodecConfig {
484            max_message_size: 100,
485            ..Default::default()
486        });
487        let payload = vec![0u8; 100];
488        let wire = codec.encode(&payload).expect("encode at limit");
489        let decoded = codec.decode(&wire).expect("decode at limit");
490        assert_eq!(decoded, payload);
491    }
492
493    #[test]
494    fn decode_rejects_oversized_length() {
495        let codec = PeerMessageCodec::new(CodecConfig {
496            max_message_size: 10,
497            ..Default::default()
498        });
499        // Craft a frame claiming payload length of 100.
500        let mut wire = vec![0, 0, 0, 100];
501        wire.extend_from_slice(&[0u8; 100]);
502        wire.extend_from_slice(&[0u8; 4]); // dummy checksum
503        assert_eq!(codec.decode(&wire), Err(CodecError::MessageTooLarge));
504    }
505
506    // ---- buffer too small ----
507
508    #[test]
509    fn decode_empty_buffer() {
510        let codec = default_codec();
511        assert_eq!(codec.decode(&[]), Err(CodecError::BufferTooSmall));
512    }
513
514    #[test]
515    fn decode_short_buffer() {
516        let codec = default_codec();
517        assert_eq!(codec.decode(&[0, 0, 1]), Err(CodecError::BufferTooSmall));
518    }
519
520    #[test]
521    fn decode_truncated_payload() {
522        let codec = default_codec();
523        // Length says 10 bytes, but only 2 bytes of payload present.
524        let wire = vec![0, 0, 0, 10, 0xAA, 0xBB];
525        assert_eq!(codec.decode(&wire), Err(CodecError::InvalidLength));
526    }
527
528    #[test]
529    fn decode_truncated_checksum() {
530        let codec = default_codec();
531        // Correct length for 4-byte payload, but missing checksum bytes.
532        let mut wire = vec![0, 0, 0, 4];
533        wire.extend_from_slice(&[1, 2, 3, 4]);
534        // Only 2 of 4 checksum bytes.
535        wire.extend_from_slice(&[0, 0]);
536        assert_eq!(codec.decode(&wire), Err(CodecError::InvalidLength));
537    }
538
539    // ---- length prefix ----
540
541    #[test]
542    fn length_prefix_correctness() {
543        let codec = default_codec();
544        let payload = vec![0u8; 300];
545        let wire = codec.encode(&payload).expect("encode");
546        // First 4 bytes should be 300 in big-endian.
547        assert_eq!(&wire[..4], &[0, 0, 1, 44]); // 300 = 0x012C
548    }
549
550    #[test]
551    fn decode_length_peek() {
552        let codec = default_codec();
553        let payload = b"peek test";
554        let wire = codec.encode(payload).expect("encode");
555        let peeked = codec.decode_length(&wire).expect("peek");
556        assert_eq!(peeked, payload.len());
557    }
558
559    #[test]
560    fn decode_length_too_short() {
561        let codec = default_codec();
562        assert_eq!(
563            codec.decode_length(&[0, 1]),
564            Err(CodecError::BufferTooSmall)
565        );
566    }
567
568    // ---- estimate_encoded_size ----
569
570    #[test]
571    fn estimate_with_checksum() {
572        let codec = default_codec();
573        assert_eq!(codec.estimate_encoded_size(100), 4 + 100 + 4);
574    }
575
576    #[test]
577    fn estimate_without_checksum() {
578        let codec = no_checksum_codec();
579        assert_eq!(codec.estimate_encoded_size(100), 4 + 100);
580    }
581
582    #[test]
583    fn estimate_matches_actual() {
584        let codec = default_codec();
585        let payload = b"size check";
586        let wire = codec.encode(payload).expect("encode");
587        assert_eq!(wire.len(), codec.estimate_encoded_size(payload.len()));
588    }
589
590    #[test]
591    fn estimate_zero_payload() {
592        let codec = default_codec();
593        let wire = codec.encode(b"").expect("encode empty");
594        assert_eq!(wire.len(), codec.estimate_encoded_size(0));
595    }
596
597    // ---- stats tracking ----
598
599    #[test]
600    fn stats_initial_zero() {
601        let codec = default_codec();
602        let s = codec.stats();
603        assert_eq!(s.messages_encoded, 0);
604        assert_eq!(s.messages_decoded, 0);
605        assert_eq!(s.bytes_encoded, 0);
606        assert_eq!(s.bytes_decoded, 0);
607    }
608
609    #[test]
610    fn stats_encode_tracked() {
611        let codec = default_codec();
612        let _ = codec.encode(b"abc");
613        let _ = codec.encode(b"defgh");
614        let s = codec.stats();
615        assert_eq!(s.messages_encoded, 2);
616        assert_eq!(s.bytes_encoded, 8); // 3 + 5
617    }
618
619    #[test]
620    fn stats_decode_tracked() {
621        let codec = default_codec();
622        let w1 = codec.encode(b"one").expect("e1");
623        let w2 = codec.encode(b"two").expect("e2");
624        let _ = codec.decode(&w1);
625        let _ = codec.decode(&w2);
626        let s = codec.stats();
627        assert_eq!(s.messages_decoded, 2);
628        assert_eq!(s.bytes_decoded, 6);
629    }
630
631    #[test]
632    fn stats_failed_encode_not_counted() {
633        let codec = PeerMessageCodec::new(CodecConfig {
634            max_message_size: 5,
635            ..Default::default()
636        });
637        let _ = codec.encode(&[0u8; 10]); // should fail
638        assert_eq!(codec.stats().messages_encoded, 0);
639    }
640
641    #[test]
642    fn stats_failed_decode_not_counted() {
643        let codec = default_codec();
644        let _ = codec.decode(&[]); // should fail
645        assert_eq!(codec.stats().messages_decoded, 0);
646    }
647
648    // ---- encode_with_metadata ----
649
650    #[test]
651    fn encode_with_metadata_checksum_present() {
652        let codec = default_codec();
653        let em = codec.encode_with_metadata(b"meta").expect("meta encode");
654        assert_eq!(em.original_size, 4);
655        assert!(em.checksum.is_some());
656        // Should round-trip.
657        let decoded = codec.decode(&em.data).expect("meta decode");
658        assert_eq!(decoded, b"meta");
659    }
660
661    #[test]
662    fn encode_with_metadata_no_checksum() {
663        let codec = no_checksum_codec();
664        let em = codec.encode_with_metadata(b"no_crc").expect("meta encode");
665        assert_eq!(em.original_size, 6);
666        assert!(em.checksum.is_none());
667    }
668
669    // ---- misc ----
670
671    #[test]
672    fn codec_error_display() {
673        assert_eq!(
674            format!("{}", CodecError::BufferTooSmall),
675            "buffer too small"
676        );
677        assert_eq!(
678            format!("{}", CodecError::MessageTooLarge),
679            "message too large"
680        );
681        assert_eq!(
682            format!("{}", CodecError::InvalidLength),
683            "invalid length prefix"
684        );
685        assert_eq!(
686            format!("{}", CodecError::ChecksumMismatch),
687            "checksum mismatch"
688        );
689    }
690
691    #[test]
692    fn codec_debug_impl() {
693        let codec = default_codec();
694        let dbg = format!("{:?}", codec);
695        assert!(dbg.contains("PeerMessageCodec"));
696    }
697
698    #[test]
699    fn config_default_values() {
700        let cfg = CodecConfig::default();
701        assert_eq!(cfg.max_message_size, 16_777_216);
702        assert!(cfg.use_checksum);
703        assert_eq!(cfg.length_prefix_bytes, 4);
704    }
705}