Skip to main content

rusmes_smtp/
bdat.rs

1//! SMTP CHUNKING/BDAT Extension - RFC 3030
2//!
3//! This module implements the BDAT command for binary data transfer,
4//! which is more efficient than DATA for large messages.
5//!
6//! # Overview
7//!
8//! The CHUNKING extension allows clients to send message data in chunks without
9//! the need for dot-stuffing (transparency) that is required by the DATA command.
10//! This is especially useful for:
11//! - Binary data transfer (no need to escape lines starting with '.')
12//! - Large messages that can be sent in multiple chunks
13//! - More efficient transfer of already-encoded MIME messages
14//!
15//! # RFC 3030 Compliance
16//!
17//! This implementation follows RFC 3030 and provides:
18//! - `BdatCommand`: Parser for BDAT commands with chunk size and LAST flag
19//! - `BdatState`: State machine for accumulating chunks and validating message size
20//! - `BdatError`: Comprehensive error handling for all edge cases
21//!
22//! # Usage Example
23//!
24//! ```rust
25//! use rusmes_smtp::{BdatCommand, BdatState};
26//!
27//! // Parse BDAT command
28//! let cmd = BdatCommand::parse("1024 LAST").expect("valid BDAT parse");
29//! assert_eq!(cmd.chunk_size, 1024);
30//! assert!(cmd.last);
31//!
32//! // Accumulate message chunks
33//! let mut state = BdatState::new(10 * 1024 * 1024); // 10MB max
34//! state.add_chunk(b"First chunk".to_vec(), false).expect("chunk add");
35//! state.add_chunk(b" Second chunk".to_vec(), true).expect("last chunk add");
36//!
37//! // Get complete message
38//! let message = state.into_message().expect("complete message");
39//! assert_eq!(message, b"First chunk Second chunk");
40//! ```
41//!
42//! # Key Features
43//!
44//! - **No Dot-Stuffing**: Binary data can be transferred without transparency
45//! - **Chunk Validation**: Validates that received chunk size matches declared size
46//! - **Size Limits**: Enforces maximum message size across all chunks
47//! - **Error Handling**: Comprehensive errors for all failure modes
48//! - **LAST Flag**: Proper handling of the final chunk marker
49//!
50//! # Integration with SMTP Server
51//!
52//! The SMTP server advertises CHUNKING in EHLO and handles BDAT commands:
53//! 1. Client sends: `BDAT 1024`
54//! 2. Server reads exactly 1024 bytes
55//! 3. Server responds: `250 1024 octets received`
56//! 4. Repeat for more chunks or send `BDAT 512 LAST` for final chunk
57
58use std::fmt;
59
60/// BDAT command for chunked message transfer
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct BdatCommand {
63    /// Size of this chunk in octets
64    pub chunk_size: usize,
65    /// Whether this is the last chunk
66    pub last: bool,
67}
68
69impl BdatCommand {
70    /// Create a new BDAT command
71    pub fn new(chunk_size: usize, last: bool) -> Self {
72        Self { chunk_size, last }
73    }
74
75    /// Parse BDAT command from arguments
76    ///
77    /// Format: BDAT `<chunk-size>` \[LAST\]
78    pub fn parse(args: &str) -> Result<Self, BdatError> {
79        let parts: Vec<&str> = args.split_whitespace().collect();
80
81        if parts.is_empty() {
82            return Err(BdatError::MissingChunkSize);
83        }
84
85        let chunk_size = parts[0]
86            .parse::<usize>()
87            .map_err(|_| BdatError::InvalidChunkSize(parts[0].to_string()))?;
88
89        if chunk_size == 0 {
90            return Err(BdatError::ZeroChunkSize);
91        }
92
93        let last = parts.get(1).is_some_and(|s| s.eq_ignore_ascii_case("LAST"));
94
95        // Check for invalid extra arguments
96        if parts.len() > 2 || (parts.len() == 2 && !last) {
97            return Err(BdatError::InvalidSyntax);
98        }
99
100        Ok(Self::new(chunk_size, last))
101    }
102}
103
104impl fmt::Display for BdatCommand {
105    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106        if self.last {
107            write!(f, "BDAT {} LAST", self.chunk_size)
108        } else {
109            write!(f, "BDAT {}", self.chunk_size)
110        }
111    }
112}
113
114/// State machine for BDAT message accumulation
115#[derive(Debug, Clone)]
116pub struct BdatState {
117    /// Accumulated message data
118    chunks: Vec<u8>,
119    /// Total size accumulated so far
120    total_size: usize,
121    /// Maximum allowed message size
122    max_size: usize,
123    /// Whether we've received the LAST chunk
124    complete: bool,
125}
126
127impl BdatState {
128    /// Create a new BDAT state
129    pub fn new(max_size: usize) -> Self {
130        Self {
131            chunks: Vec::new(),
132            total_size: 0,
133            max_size,
134            complete: false,
135        }
136    }
137
138    /// Add a chunk of data
139    pub fn add_chunk(&mut self, data: Vec<u8>, last: bool) -> Result<(), BdatError> {
140        if self.complete {
141            return Err(BdatError::AlreadyComplete);
142        }
143
144        let chunk_size = data.len();
145
146        // Check size limit
147        if self.total_size + chunk_size > self.max_size {
148            return Err(BdatError::MessageTooLarge {
149                current: self.total_size + chunk_size,
150                max: self.max_size,
151            });
152        }
153
154        self.chunks.extend(data);
155        self.total_size += chunk_size;
156        self.complete = last;
157
158        Ok(())
159    }
160
161    /// Add a chunk with size validation
162    ///
163    /// This validates that the actual data size matches the expected size from BDAT command
164    pub fn add_chunk_with_validation(
165        &mut self,
166        data: Vec<u8>,
167        expected_size: usize,
168        last: bool,
169    ) -> Result<(), BdatError> {
170        let actual_size = data.len();
171        if actual_size != expected_size {
172            return Err(BdatError::ChunkSizeMismatch {
173                expected: expected_size,
174                actual: actual_size,
175            });
176        }
177
178        self.add_chunk(data, last)
179    }
180
181    /// Check if the message is complete
182    pub fn is_complete(&self) -> bool {
183        self.complete
184    }
185
186    /// Get the total size accumulated
187    pub fn total_size(&self) -> usize {
188        self.total_size
189    }
190
191    /// Get the maximum size allowed
192    pub fn max_size(&self) -> usize {
193        self.max_size
194    }
195
196    /// Consume the state and return the complete message
197    pub fn into_message(self) -> Result<Vec<u8>, BdatError> {
198        if !self.complete {
199            return Err(BdatError::Incomplete);
200        }
201        Ok(self.chunks)
202    }
203
204    /// Get a reference to the accumulated data (for inspection)
205    pub fn data(&self) -> &[u8] {
206        &self.chunks
207    }
208
209    /// Reset the state to accept a new message
210    pub fn reset(&mut self) {
211        self.chunks.clear();
212        self.total_size = 0;
213        self.complete = false;
214    }
215}
216
217/// BDAT-related errors
218#[derive(Debug, Clone, PartialEq, Eq)]
219pub enum BdatError {
220    /// Missing chunk size argument
221    MissingChunkSize,
222    /// Invalid chunk size format
223    InvalidChunkSize(String),
224    /// Chunk size is zero
225    ZeroChunkSize,
226    /// Invalid BDAT syntax
227    InvalidSyntax,
228    /// Message size exceeds limit
229    MessageTooLarge { current: usize, max: usize },
230    /// Already received LAST chunk
231    AlreadyComplete,
232    /// Message not complete (no LAST chunk yet)
233    Incomplete,
234    /// Received chunk size doesn't match actual data size
235    ChunkSizeMismatch { expected: usize, actual: usize },
236}
237
238impl fmt::Display for BdatError {
239    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
240        match self {
241            BdatError::MissingChunkSize => write!(f, "Missing chunk size"),
242            BdatError::InvalidChunkSize(s) => write!(f, "Invalid chunk size: {}", s),
243            BdatError::ZeroChunkSize => write!(f, "Chunk size cannot be zero"),
244            BdatError::InvalidSyntax => write!(f, "Invalid BDAT syntax"),
245            BdatError::MessageTooLarge { current, max } => {
246                write!(
247                    f,
248                    "Message too large: {} bytes exceeds {} bytes",
249                    current, max
250                )
251            }
252            BdatError::AlreadyComplete => write!(f, "Message already complete"),
253            BdatError::Incomplete => write!(f, "Message incomplete (no LAST chunk)"),
254            BdatError::ChunkSizeMismatch { expected, actual } => {
255                write!(
256                    f,
257                    "Chunk size mismatch: expected {} bytes, got {}",
258                    expected, actual
259                )
260            }
261        }
262    }
263}
264
265impl std::error::Error for BdatError {}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270
271    // ===== BdatCommand Parsing Tests =====
272
273    #[test]
274    fn test_bdat_parse_basic() {
275        let cmd = BdatCommand::parse("1024").expect("valid BDAT parse without LAST");
276        assert_eq!(cmd.chunk_size, 1024);
277        assert!(!cmd.last);
278    }
279
280    #[test]
281    fn test_bdat_parse_with_last() {
282        let cmd_last = BdatCommand::parse("512 LAST").expect("valid BDAT parse with LAST");
283        assert_eq!(cmd_last.chunk_size, 512);
284        assert!(cmd_last.last);
285    }
286
287    #[test]
288    fn test_bdat_parse_last_case_insensitive() {
289        let cmd_upper =
290            BdatCommand::parse("256 LAST").expect("valid BDAT parse with uppercase LAST");
291        assert_eq!(cmd_upper.chunk_size, 256);
292        assert!(cmd_upper.last);
293
294        let cmd_lower =
295            BdatCommand::parse("128 last").expect("valid BDAT parse with lowercase last");
296        assert_eq!(cmd_lower.chunk_size, 128);
297        assert!(cmd_lower.last);
298
299        let cmd_mixed =
300            BdatCommand::parse("64 LaSt").expect("valid BDAT parse with mixed-case LaSt");
301        assert_eq!(cmd_mixed.chunk_size, 64);
302        assert!(cmd_mixed.last);
303    }
304
305    #[test]
306    fn test_bdat_parse_large_chunk() {
307        let cmd = BdatCommand::parse("1073741824").expect("valid BDAT parse for 1GB chunk"); // 1GB
308        assert_eq!(cmd.chunk_size, 1073741824);
309        assert!(!cmd.last);
310    }
311
312    #[test]
313    fn test_bdat_parse_with_extra_whitespace() {
314        let cmd =
315            BdatCommand::parse("  512   LAST  ").expect("valid BDAT parse with extra whitespace");
316        assert_eq!(cmd.chunk_size, 512);
317        assert!(cmd.last);
318    }
319
320    #[test]
321    fn test_bdat_parse_missing_chunk_size() {
322        assert!(matches!(
323            BdatCommand::parse(""),
324            Err(BdatError::MissingChunkSize)
325        ));
326
327        assert!(matches!(
328            BdatCommand::parse("   "),
329            Err(BdatError::MissingChunkSize)
330        ));
331    }
332
333    #[test]
334    fn test_bdat_parse_invalid_chunk_size() {
335        assert!(matches!(
336            BdatCommand::parse("abc"),
337            Err(BdatError::InvalidChunkSize(_))
338        ));
339
340        assert!(matches!(
341            BdatCommand::parse("12.34"),
342            Err(BdatError::InvalidChunkSize(_))
343        ));
344
345        assert!(matches!(
346            BdatCommand::parse("-100"),
347            Err(BdatError::InvalidChunkSize(_))
348        ));
349    }
350
351    #[test]
352    fn test_bdat_parse_zero_chunk_size() {
353        assert!(matches!(
354            BdatCommand::parse("0"),
355            Err(BdatError::ZeroChunkSize)
356        ));
357
358        assert!(matches!(
359            BdatCommand::parse("0 LAST"),
360            Err(BdatError::ZeroChunkSize)
361        ));
362    }
363
364    #[test]
365    fn test_bdat_parse_invalid_syntax() {
366        // Invalid second argument
367        assert!(matches!(
368            BdatCommand::parse("100 INVALID"),
369            Err(BdatError::InvalidSyntax)
370        ));
371
372        // Too many arguments
373        assert!(matches!(
374            BdatCommand::parse("100 LAST EXTRA"),
375            Err(BdatError::InvalidSyntax)
376        ));
377    }
378
379    // ===== BdatCommand Display Tests =====
380
381    #[test]
382    fn test_bdat_display_without_last() {
383        let cmd = BdatCommand::new(1024, false);
384        assert_eq!(cmd.to_string(), "BDAT 1024");
385    }
386
387    #[test]
388    fn test_bdat_display_with_last() {
389        let cmd_last = BdatCommand::new(512, true);
390        assert_eq!(cmd_last.to_string(), "BDAT 512 LAST");
391    }
392
393    // ===== BdatState Tests =====
394
395    #[test]
396    fn test_bdat_state_new() {
397        let state = BdatState::new(1024);
398        assert_eq!(state.total_size(), 0);
399        assert!(!state.is_complete());
400        assert_eq!(state.data().len(), 0);
401    }
402
403    #[test]
404    fn test_bdat_state_single_chunk() {
405        let mut state = BdatState::new(1024);
406        state
407            .add_chunk(b"Hello World".to_vec(), true)
408            .expect("single chunk add should succeed");
409
410        assert!(state.is_complete());
411        assert_eq!(state.total_size(), 11);
412        assert_eq!(state.data(), b"Hello World");
413
414        let message = state.into_message().expect("complete message extraction");
415        assert_eq!(message, b"Hello World");
416    }
417
418    #[test]
419    fn test_bdat_state_multiple_chunks() {
420        let mut state = BdatState::new(1024);
421
422        // Add first chunk
423        state
424            .add_chunk(b"Hello ".to_vec(), false)
425            .expect("first chunk add should succeed");
426        assert!(!state.is_complete());
427        assert_eq!(state.total_size(), 6);
428
429        // Add second chunk
430        state
431            .add_chunk(b"World".to_vec(), false)
432            .expect("second chunk add should succeed");
433        assert!(!state.is_complete());
434        assert_eq!(state.total_size(), 11);
435
436        // Add final chunk
437        state
438            .add_chunk(b"!".to_vec(), true)
439            .expect("final chunk add should succeed");
440        assert!(state.is_complete());
441        assert_eq!(state.total_size(), 12);
442
443        // Get complete message
444        let message = state.into_message().expect("complete message extraction");
445        assert_eq!(message, b"Hello World!");
446    }
447
448    #[test]
449    fn test_bdat_state_empty_last_chunk() {
450        let mut state = BdatState::new(1024);
451
452        state
453            .add_chunk(b"Data".to_vec(), false)
454            .expect("data chunk add should succeed");
455        assert!(!state.is_complete());
456
457        // Empty LAST chunk is valid
458        state
459            .add_chunk(Vec::new(), true)
460            .expect("empty LAST chunk should be valid");
461        assert!(state.is_complete());
462        assert_eq!(state.total_size(), 4);
463    }
464
465    #[test]
466    fn test_bdat_state_binary_data() {
467        let mut state = BdatState::new(1024);
468
469        // Binary data including null bytes
470        let binary_data = vec![0x00, 0xFF, 0x01, 0x02, 0x03, 0x00, 0xFE];
471        state
472            .add_chunk(binary_data.clone(), true)
473            .expect("binary chunk add should succeed");
474
475        assert!(state.is_complete());
476        assert_eq!(state.total_size(), 7);
477
478        let message = state.into_message().expect("complete message extraction");
479        assert_eq!(message, binary_data);
480    }
481
482    #[test]
483    fn test_bdat_state_size_limit_exact() {
484        let mut state = BdatState::new(10);
485
486        // Exactly at the limit
487        state
488            .add_chunk(b"1234567890".to_vec(), true)
489            .expect("chunk at exact size limit should succeed");
490        assert_eq!(state.total_size(), 10);
491        assert!(state.is_complete());
492    }
493
494    #[test]
495    fn test_bdat_state_size_limit_exceeded() {
496        let mut state = BdatState::new(10);
497
498        // This should exceed the limit
499        let result = state.add_chunk(b"12345678901".to_vec(), true);
500        assert!(matches!(
501            result,
502            Err(BdatError::MessageTooLarge {
503                current: 11,
504                max: 10
505            })
506        ));
507    }
508
509    #[test]
510    fn test_bdat_state_size_limit_multiple_chunks() {
511        let mut state = BdatState::new(20);
512
513        state
514            .add_chunk(b"1234567890".to_vec(), false)
515            .expect("first chunk within limit should succeed");
516        assert_eq!(state.total_size(), 10);
517
518        // This chunk would exceed the limit
519        let result = state.add_chunk(b"12345678901".to_vec(), false);
520        assert!(matches!(
521            result,
522            Err(BdatError::MessageTooLarge {
523                current: 21,
524                max: 20
525            })
526        ));
527    }
528
529    #[test]
530    fn test_bdat_state_already_complete() {
531        let mut state = BdatState::new(1024);
532
533        state
534            .add_chunk(b"Data".to_vec(), true)
535            .expect("LAST chunk add should succeed");
536        assert!(state.is_complete());
537
538        // Try to add another chunk after LAST
539        let result = state.add_chunk(b"More".to_vec(), false);
540        assert!(matches!(result, Err(BdatError::AlreadyComplete)));
541    }
542
543    #[test]
544    fn test_bdat_state_incomplete() {
545        let mut state = BdatState::new(1024);
546
547        state
548            .add_chunk(b"Partial".to_vec(), false)
549            .expect("partial chunk add should succeed");
550        assert!(!state.is_complete());
551
552        // Try to get message before LAST
553        let result = state.into_message();
554        assert!(matches!(result, Err(BdatError::Incomplete)));
555    }
556
557    #[test]
558    fn test_bdat_state_data_reference() {
559        let mut state = BdatState::new(1024);
560
561        state
562            .add_chunk(b"Test".to_vec(), false)
563            .expect("first chunk add should succeed");
564        assert_eq!(state.data(), b"Test");
565
566        state
567            .add_chunk(b" Data".to_vec(), false)
568            .expect("second chunk add should succeed");
569        assert_eq!(state.data(), b"Test Data");
570    }
571
572    // ===== BdatError Display Tests =====
573
574    #[test]
575    fn test_bdat_error_display_missing_chunk_size() {
576        let err = BdatError::MissingChunkSize;
577        assert_eq!(err.to_string(), "Missing chunk size");
578    }
579
580    #[test]
581    fn test_bdat_error_display_invalid_chunk_size() {
582        let err = BdatError::InvalidChunkSize("abc".to_string());
583        assert_eq!(err.to_string(), "Invalid chunk size: abc");
584    }
585
586    #[test]
587    fn test_bdat_error_display_zero_chunk_size() {
588        let err = BdatError::ZeroChunkSize;
589        assert_eq!(err.to_string(), "Chunk size cannot be zero");
590    }
591
592    #[test]
593    fn test_bdat_error_display_invalid_syntax() {
594        let err = BdatError::InvalidSyntax;
595        assert_eq!(err.to_string(), "Invalid BDAT syntax");
596    }
597
598    #[test]
599    fn test_bdat_error_display_message_too_large() {
600        let err = BdatError::MessageTooLarge {
601            current: 1000,
602            max: 500,
603        };
604        assert_eq!(
605            err.to_string(),
606            "Message too large: 1000 bytes exceeds 500 bytes"
607        );
608    }
609
610    #[test]
611    fn test_bdat_error_display_already_complete() {
612        let err = BdatError::AlreadyComplete;
613        assert_eq!(err.to_string(), "Message already complete");
614    }
615
616    #[test]
617    fn test_bdat_error_display_incomplete() {
618        let err = BdatError::Incomplete;
619        assert_eq!(err.to_string(), "Message incomplete (no LAST chunk)");
620    }
621
622    #[test]
623    fn test_bdat_error_display_chunk_size_mismatch() {
624        let err = BdatError::ChunkSizeMismatch {
625            expected: 100,
626            actual: 95,
627        };
628        assert_eq!(
629            err.to_string(),
630            "Chunk size mismatch: expected 100 bytes, got 95"
631        );
632    }
633
634    // ===== Integration Tests =====
635
636    #[test]
637    fn test_bdat_workflow_complete() {
638        // Simulate complete BDAT workflow
639        let cmd1 = BdatCommand::parse("11").expect("valid BDAT parse for 11-byte chunk");
640        assert_eq!(cmd1.chunk_size, 11);
641        assert!(!cmd1.last);
642
643        let cmd2 = BdatCommand::parse("13 LAST").expect("valid BDAT parse for 13-byte LAST chunk");
644        assert_eq!(cmd2.chunk_size, 13);
645        assert!(cmd2.last);
646
647        let mut state = BdatState::new(1024);
648
649        state
650            .add_chunk(b"First chunk".to_vec(), false)
651            .expect("first chunk add should succeed");
652        assert_eq!(state.total_size(), 11);
653
654        state
655            .add_chunk(b" second chunk".to_vec(), true)
656            .expect("second (LAST) chunk add should succeed");
657        assert_eq!(state.total_size(), 24);
658        assert!(state.is_complete());
659
660        let message = state.into_message().expect("complete message extraction");
661        assert_eq!(message, b"First chunk second chunk");
662    }
663
664    #[test]
665    fn test_bdat_clone() {
666        let cmd = BdatCommand::new(100, true);
667        let cloned = cmd.clone();
668        assert_eq!(cmd, cloned);
669
670        let mut state = BdatState::new(1024);
671        state
672            .add_chunk(b"test".to_vec(), false)
673            .expect("chunk add before clone should succeed");
674        let cloned_state = state.clone();
675        assert_eq!(cloned_state.total_size(), state.total_size());
676        assert_eq!(cloned_state.is_complete(), state.is_complete());
677    }
678
679    #[test]
680    fn test_bdat_command_equality() {
681        let cmd1 = BdatCommand::new(100, false);
682        let cmd2 = BdatCommand::new(100, false);
683        let cmd3 = BdatCommand::new(100, true);
684        let cmd4 = BdatCommand::new(200, false);
685
686        assert_eq!(cmd1, cmd2);
687        assert_ne!(cmd1, cmd3);
688        assert_ne!(cmd1, cmd4);
689    }
690
691    #[test]
692    fn test_bdat_state_add_chunk_with_validation_success() {
693        let mut state = BdatState::new(1024);
694
695        // Add chunk with correct size
696        state
697            .add_chunk_with_validation(b"Hello".to_vec(), 5, false)
698            .expect("chunk with matching size should succeed");
699        assert_eq!(state.total_size(), 5);
700        assert!(!state.is_complete());
701
702        // Add final chunk with correct size
703        state
704            .add_chunk_with_validation(b" World".to_vec(), 6, true)
705            .expect("LAST chunk with matching size should succeed");
706        assert_eq!(state.total_size(), 11);
707        assert!(state.is_complete());
708
709        let message = state.into_message().expect("complete message extraction");
710        assert_eq!(message, b"Hello World");
711    }
712
713    #[test]
714    fn test_bdat_state_add_chunk_with_validation_mismatch() {
715        let mut state = BdatState::new(1024);
716
717        // Add chunk with incorrect size
718        let result = state.add_chunk_with_validation(b"Hello".to_vec(), 10, false);
719        assert!(matches!(
720            result,
721            Err(BdatError::ChunkSizeMismatch {
722                expected: 10,
723                actual: 5
724            })
725        ));
726    }
727
728    #[test]
729    fn test_bdat_state_max_size() {
730        let state = BdatState::new(2048);
731        assert_eq!(state.max_size(), 2048);
732    }
733
734    #[test]
735    fn test_bdat_state_reset() {
736        let mut state = BdatState::new(1024);
737
738        // Add some data
739        state
740            .add_chunk(b"Test data".to_vec(), true)
741            .expect("LAST chunk add should succeed");
742        assert_eq!(state.total_size(), 9);
743        assert!(state.is_complete());
744
745        // Reset the state
746        state.reset();
747        assert_eq!(state.total_size(), 0);
748        assert!(!state.is_complete());
749        assert_eq!(state.data().len(), 0);
750
751        // Can add new data after reset
752        state
753            .add_chunk(b"New data".to_vec(), true)
754            .expect("chunk add after reset should succeed");
755        assert_eq!(state.total_size(), 8);
756        assert!(state.is_complete());
757    }
758
759    #[test]
760    fn test_bdat_large_binary_transfer() {
761        let mut state = BdatState::new(1024 * 1024); // 1MB
762
763        // Create large binary data (100KB)
764        let mut large_data = Vec::with_capacity(100 * 1024);
765        for i in 0..100 * 1024 {
766            large_data.push((i % 256) as u8);
767        }
768
769        // Transfer in chunks
770        let chunk_size = 10 * 1024; // 10KB chunks
771        for i in 0..10 {
772            let start = i * chunk_size;
773            let end = start + chunk_size;
774            let chunk = large_data[start..end].to_vec();
775            let is_last = i == 9;
776
777            state
778                .add_chunk(chunk, is_last)
779                .expect("large binary chunk add should succeed");
780        }
781
782        assert!(state.is_complete());
783        assert_eq!(state.total_size(), 100 * 1024);
784
785        let message = state
786            .into_message()
787            .expect("complete large message extraction");
788        assert_eq!(message, large_data);
789    }
790
791    #[test]
792    fn test_bdat_error_is_std_error() {
793        // Verify that BdatError implements std::error::Error
794        let err: Box<dyn std::error::Error> = Box::new(BdatError::MissingChunkSize);
795        assert_eq!(err.to_string(), "Missing chunk size");
796    }
797
798    #[test]
799    fn test_bdat_command_debug() {
800        let cmd = BdatCommand::new(1024, true);
801        let debug_str = format!("{:?}", cmd);
802        assert!(debug_str.contains("1024"));
803        assert!(debug_str.contains("true"));
804    }
805
806    #[test]
807    fn test_bdat_state_debug() {
808        let state = BdatState::new(1024);
809        let debug_str = format!("{:?}", state);
810        assert!(debug_str.contains("BdatState"));
811    }
812}