1use std::fmt;
59
60#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct BdatCommand {
63 pub chunk_size: usize,
65 pub last: bool,
67}
68
69impl BdatCommand {
70 pub fn new(chunk_size: usize, last: bool) -> Self {
72 Self { chunk_size, last }
73 }
74
75 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 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#[derive(Debug, Clone)]
116pub struct BdatState {
117 chunks: Vec<u8>,
119 total_size: usize,
121 max_size: usize,
123 complete: bool,
125}
126
127impl BdatState {
128 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 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 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 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 pub fn is_complete(&self) -> bool {
183 self.complete
184 }
185
186 pub fn total_size(&self) -> usize {
188 self.total_size
189 }
190
191 pub fn max_size(&self) -> usize {
193 self.max_size
194 }
195
196 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 pub fn data(&self) -> &[u8] {
206 &self.chunks
207 }
208
209 pub fn reset(&mut self) {
211 self.chunks.clear();
212 self.total_size = 0;
213 self.complete = false;
214 }
215}
216
217#[derive(Debug, Clone, PartialEq, Eq)]
219pub enum BdatError {
220 MissingChunkSize,
222 InvalidChunkSize(String),
224 ZeroChunkSize,
226 InvalidSyntax,
228 MessageTooLarge { current: usize, max: usize },
230 AlreadyComplete,
232 Incomplete,
234 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 #[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"); 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 assert!(matches!(
368 BdatCommand::parse("100 INVALID"),
369 Err(BdatError::InvalidSyntax)
370 ));
371
372 assert!(matches!(
374 BdatCommand::parse("100 LAST EXTRA"),
375 Err(BdatError::InvalidSyntax)
376 ));
377 }
378
379 #[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 #[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 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 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 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 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 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 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 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 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 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 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 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 #[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 #[test]
637 fn test_bdat_workflow_complete() {
638 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 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 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 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 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 state.reset();
747 assert_eq!(state.total_size(), 0);
748 assert!(!state.is_complete());
749 assert_eq!(state.data().len(), 0);
750
751 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); 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 let chunk_size = 10 * 1024; 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 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}