1use crate::domain::errors::{DeniableError, StegoError};
4use crate::domain::ports::{DeniableEmbedder, EmbedTechnique, ExtractTechnique};
5use crate::domain::types::{
6 Capacity, CoverMedia, CoverMediaKind, DeniableKeySet, DeniablePayloadPair, Payload,
7 StegoTechnique,
8};
9use rand::seq::SliceRandom;
10use rand_chacha::ChaCha20Rng;
11use rand_core::SeedableRng;
12use sha2::{Digest, Sha256};
13
14#[derive(Debug, Default)]
19pub struct LsbImage;
20
21impl LsbImage {
22 #[must_use]
24 pub const fn new() -> Self {
25 Self
26 }
27}
28
29impl EmbedTechnique for LsbImage {
30 fn technique(&self) -> StegoTechnique {
31 StegoTechnique::LsbImage
32 }
33
34 fn capacity(&self, cover: &CoverMedia) -> Result<Capacity, StegoError> {
35 match cover.kind {
37 CoverMediaKind::PngImage | CoverMediaKind::BmpImage => {}
38 _ => {
39 return Err(StegoError::UnsupportedCoverType {
40 reason: format!("LSB image requires PNG or BMP, got {:?}", cover.kind),
41 });
42 }
43 }
44
45 let width: u32 = cover
47 .metadata
48 .get("width")
49 .ok_or_else(|| StegoError::MalformedCoverData {
50 reason: "missing width metadata".to_string(),
51 })?
52 .parse()
53 .map_err(
54 |e: std::num::ParseIntError| StegoError::MalformedCoverData {
55 reason: format!("invalid width: {e}"),
56 },
57 )?;
58
59 let height: u32 = cover
60 .metadata
61 .get("height")
62 .ok_or_else(|| StegoError::MalformedCoverData {
63 reason: "missing height metadata".to_string(),
64 })?
65 .parse()
66 .map_err(
67 |e: std::num::ParseIntError| StegoError::MalformedCoverData {
68 reason: format!("invalid height: {e}"),
69 },
70 )?;
71
72 let pixel_count =
73 width
74 .checked_mul(height)
75 .ok_or_else(|| StegoError::MalformedCoverData {
76 reason: "pixel count overflow".to_string(),
77 })?;
78
79 let bits = pixel_count
82 .checked_mul(3)
83 .and_then(|b| b.checked_sub(32))
84 .ok_or_else(|| StegoError::MalformedCoverData {
85 reason: "capacity calculation overflow".to_string(),
86 })?;
87
88 let bytes = u64::from(bits / 8);
89
90 Ok(Capacity {
91 bytes,
92 technique: StegoTechnique::LsbImage,
93 })
94 }
95
96 fn embed(&self, mut cover: CoverMedia, payload: &Payload) -> Result<CoverMedia, StegoError> {
97 match cover.kind {
99 CoverMediaKind::PngImage | CoverMediaKind::BmpImage => {}
100 _ => {
101 return Err(StegoError::UnsupportedCoverType {
102 reason: format!("LSB image requires PNG or BMP, got {:?}", cover.kind),
103 });
104 }
105 }
106
107 let cap = self.capacity(&cover)?;
109 let payload_len = payload.as_bytes().len() as u64;
110 if payload_len > cap.bytes {
111 return Err(StegoError::PayloadTooLarge {
112 needed: payload_len,
113 available: cap.bytes,
114 });
115 }
116
117 if payload_len > u64::from(u32::MAX) {
119 return Err(StegoError::PayloadTooLarge {
120 needed: payload_len,
121 available: u64::from(u32::MAX),
122 });
123 }
124
125 let data = cover.data.to_vec();
127 let mut pixels = data;
128
129 #[expect(
131 clippy::cast_possible_truncation,
132 reason = "checked above: payload_len <= u32::MAX"
133 )]
134 let len_bytes = (payload_len as u32).to_be_bytes();
135 for (byte_idx, byte) in len_bytes.iter().enumerate() {
136 for bit_idx in 0..8 {
137 let bit = (byte >> (7 - bit_idx)) & 1;
138 let pixel_idx = byte_idx * 8 + bit_idx;
139
140 let channel_idx = pixel_idx / 3;
142 let rgb_offset = pixel_idx % 3;
143 let byte_pos = channel_idx * 4 + rgb_offset;
144
145 let pixel =
146 pixels
147 .get_mut(byte_pos)
148 .ok_or_else(|| StegoError::MalformedCoverData {
149 reason: "pixel index out of bounds".to_string(),
150 })?;
151 *pixel = (*pixel & 0xFE) | bit;
152 }
153 }
154
155 let payload_bytes = payload.as_bytes();
157 for (byte_idx, byte) in payload_bytes.iter().enumerate() {
158 for bit_idx in 0..8 {
159 let bit = (byte >> (7 - bit_idx)) & 1;
160 let pixel_idx = 32 + byte_idx * 8 + bit_idx;
161
162 let channel_idx = pixel_idx / 3;
164 let rgb_offset = pixel_idx % 3;
165 let byte_pos = channel_idx * 4 + rgb_offset;
166
167 let pixel =
168 pixels
169 .get_mut(byte_pos)
170 .ok_or_else(|| StegoError::MalformedCoverData {
171 reason: "pixel index out of bounds".to_string(),
172 })?;
173 *pixel = (*pixel & 0xFE) | bit;
174 }
175 }
176
177 cover.data = pixels.into();
178 Ok(cover)
179 }
180}
181
182impl ExtractTechnique for LsbImage {
183 fn technique(&self) -> StegoTechnique {
184 StegoTechnique::LsbImage
185 }
186
187 fn extract(&self, cover: &CoverMedia) -> Result<Payload, StegoError> {
188 match cover.kind {
190 CoverMediaKind::PngImage | CoverMediaKind::BmpImage => {}
191 _ => {
192 return Err(StegoError::UnsupportedCoverType {
193 reason: format!("LSB image requires PNG or BMP, got {:?}", cover.kind),
194 });
195 }
196 }
197
198 let pixels = cover.data.as_ref();
199
200 let mut len_bytes = [0u8; 4];
202 for (byte_idx, len_byte) in len_bytes.iter_mut().enumerate() {
203 for bit_idx in 0..8 {
204 let pixel_idx = byte_idx * 8 + bit_idx;
205
206 let channel_idx = pixel_idx / 3;
208 let rgb_offset = pixel_idx % 3;
209 let byte_pos = channel_idx * 4 + rgb_offset;
210
211 let bit = pixels
212 .get(byte_pos)
213 .ok_or_else(|| StegoError::MalformedCoverData {
214 reason: "pixel index out of bounds".to_string(),
215 })?
216 & 1;
217 *len_byte |= bit << (7 - bit_idx);
218 }
219 }
220
221 let payload_len = u32::from_be_bytes(len_bytes) as usize;
222
223 let mut payload_bytes = vec![0u8; payload_len];
225 for (byte_idx, payload_byte) in payload_bytes.iter_mut().enumerate() {
226 for bit_idx in 0..8 {
227 let pixel_idx = 32 + byte_idx * 8 + bit_idx;
228
229 let channel_idx = pixel_idx / 3;
231 let rgb_offset = pixel_idx % 3;
232 let byte_pos = channel_idx * 4 + rgb_offset;
233
234 let bit = pixels
235 .get(byte_pos)
236 .ok_or_else(|| StegoError::MalformedCoverData {
237 reason: "pixel index out of bounds".to_string(),
238 })?
239 & 1;
240 *payload_byte |= bit << (7 - bit_idx);
241 }
242 }
243
244 Ok(Payload::from_bytes(payload_bytes))
245 }
246}
247
248#[derive(Debug, Default)]
261pub struct DctJpeg;
262
263impl DctJpeg {
264 #[must_use]
266 pub const fn new() -> Self {
267 Self
268 }
269}
270
271impl EmbedTechnique for DctJpeg {
272 fn technique(&self) -> StegoTechnique {
273 StegoTechnique::DctJpeg
274 }
275
276 fn capacity(&self, _cover: &CoverMedia) -> Result<Capacity, StegoError> {
277 Err(StegoError::UnsupportedCoverType {
278 reason: "DCT JPEG steganography not yet implemented (requires DCT coefficient access)"
279 .to_string(),
280 })
281 }
282
283 fn embed(&self, _cover: CoverMedia, _payload: &Payload) -> Result<CoverMedia, StegoError> {
284 Err(StegoError::UnsupportedCoverType {
285 reason: "DCT JPEG steganography not yet implemented (requires DCT coefficient access)"
286 .to_string(),
287 })
288 }
289}
290
291impl ExtractTechnique for DctJpeg {
292 fn technique(&self) -> StegoTechnique {
293 StegoTechnique::DctJpeg
294 }
295
296 fn extract(&self, _cover: &CoverMedia) -> Result<Payload, StegoError> {
297 Err(StegoError::UnsupportedCoverType {
298 reason: "DCT JPEG steganography not yet implemented (requires DCT coefficient access)"
299 .to_string(),
300 })
301 }
302}
303
304#[derive(Debug, Default)]
317pub struct PaletteStego;
318
319impl PaletteStego {
320 #[must_use]
322 pub const fn new() -> Self {
323 Self
324 }
325}
326
327impl EmbedTechnique for PaletteStego {
328 fn technique(&self) -> StegoTechnique {
329 StegoTechnique::Palette
330 }
331
332 fn capacity(&self, _cover: &CoverMedia) -> Result<Capacity, StegoError> {
333 Err(StegoError::UnsupportedCoverType {
334 reason: "Palette steganography not yet implemented (requires palette extraction)"
335 .to_string(),
336 })
337 }
338
339 fn embed(&self, _cover: CoverMedia, _payload: &Payload) -> Result<CoverMedia, StegoError> {
340 Err(StegoError::UnsupportedCoverType {
341 reason: "Palette steganography not yet implemented (requires palette extraction)"
342 .to_string(),
343 })
344 }
345}
346
347impl ExtractTechnique for PaletteStego {
348 fn technique(&self) -> StegoTechnique {
349 StegoTechnique::Palette
350 }
351
352 fn extract(&self, _cover: &CoverMedia) -> Result<Payload, StegoError> {
353 Err(StegoError::UnsupportedCoverType {
354 reason: "Palette steganography not yet implemented (requires palette extraction)"
355 .to_string(),
356 })
357 }
358}
359
360#[derive(Debug, Default)]
365pub struct LsbAudio;
366
367impl LsbAudio {
368 #[must_use]
370 pub const fn new() -> Self {
371 Self
372 }
373}
374
375impl EmbedTechnique for LsbAudio {
376 fn technique(&self) -> StegoTechnique {
377 StegoTechnique::LsbAudio
378 }
379
380 fn capacity(&self, cover: &CoverMedia) -> Result<Capacity, StegoError> {
381 if cover.kind != CoverMediaKind::WavAudio {
383 return Err(StegoError::UnsupportedCoverType {
384 reason: format!("LSB audio requires WAV, got {:?}", cover.kind),
385 });
386 }
387
388 let sample_count = cover.data.len() / 2;
390
391 if sample_count < 32 {
393 return Err(StegoError::MalformedCoverData {
394 reason: "audio too short for LSB embedding (need at least 32 samples)".to_string(),
395 });
396 }
397
398 let capacity_bits =
400 sample_count
401 .checked_sub(32)
402 .ok_or_else(|| StegoError::MalformedCoverData {
403 reason: "capacity calculation underflow".to_string(),
404 })?;
405
406 let bytes = (capacity_bits / 8) as u64;
407
408 Ok(Capacity {
409 bytes,
410 technique: StegoTechnique::LsbAudio,
411 })
412 }
413
414 fn embed(&self, mut cover: CoverMedia, payload: &Payload) -> Result<CoverMedia, StegoError> {
415 if cover.kind != CoverMediaKind::WavAudio {
417 return Err(StegoError::UnsupportedCoverType {
418 reason: format!("LSB audio requires WAV, got {:?}", cover.kind),
419 });
420 }
421
422 let cap = self.capacity(&cover)?;
424 let payload_len = payload.as_bytes().len() as u64;
425 if payload_len > cap.bytes {
426 return Err(StegoError::PayloadTooLarge {
427 needed: payload_len,
428 available: cap.bytes,
429 });
430 }
431
432 if payload_len > u64::from(u32::MAX) {
434 return Err(StegoError::PayloadTooLarge {
435 needed: payload_len,
436 available: u64::from(u32::MAX),
437 });
438 }
439
440 let mut samples = cover.data.to_vec();
442
443 #[expect(
445 clippy::cast_possible_truncation,
446 reason = "checked above: payload_len <= u32::MAX"
447 )]
448 let len_bytes = (payload_len as u32).to_be_bytes();
449 for (byte_idx, byte) in len_bytes.iter().enumerate() {
450 for bit_idx in 0..8 {
451 let bit = (byte >> (7 - bit_idx)) & 1;
452 let sample_idx = byte_idx * 8 + bit_idx;
453
454 let byte_pos = sample_idx * 2; let sample =
457 samples
458 .get_mut(byte_pos)
459 .ok_or_else(|| StegoError::MalformedCoverData {
460 reason: "sample index out of bounds".to_string(),
461 })?;
462 *sample = (*sample & 0xFE) | bit;
463 }
464 }
465
466 let payload_bytes = payload.as_bytes();
468 for (byte_idx, byte) in payload_bytes.iter().enumerate() {
469 for bit_idx in 0..8 {
470 let bit = (byte >> (7 - bit_idx)) & 1;
471 let sample_idx = 32 + byte_idx * 8 + bit_idx;
472
473 let byte_pos = sample_idx * 2;
474 let sample =
475 samples
476 .get_mut(byte_pos)
477 .ok_or_else(|| StegoError::MalformedCoverData {
478 reason: "sample index out of bounds".to_string(),
479 })?;
480 *sample = (*sample & 0xFE) | bit;
481 }
482 }
483
484 cover.data = samples.into();
485 Ok(cover)
486 }
487}
488
489impl ExtractTechnique for LsbAudio {
490 fn technique(&self) -> StegoTechnique {
491 StegoTechnique::LsbAudio
492 }
493
494 fn extract(&self, cover: &CoverMedia) -> Result<Payload, StegoError> {
495 if cover.kind != CoverMediaKind::WavAudio {
497 return Err(StegoError::UnsupportedCoverType {
498 reason: format!("LSB audio requires WAV, got {:?}", cover.kind),
499 });
500 }
501
502 let samples = cover.data.as_ref();
503
504 if samples.len() < 64 {
506 return Err(StegoError::MalformedCoverData {
508 reason: "audio too short to extract payload".to_string(),
509 });
510 }
511
512 let mut len_bytes = [0u8; 4];
514 for (byte_idx, len_byte) in len_bytes.iter_mut().enumerate() {
515 for bit_idx in 0..8 {
516 let sample_idx = byte_idx * 8 + bit_idx;
517 let byte_pos = sample_idx * 2;
518
519 let bit = samples
520 .get(byte_pos)
521 .ok_or_else(|| StegoError::MalformedCoverData {
522 reason: "sample index out of bounds".to_string(),
523 })?
524 & 1;
525 *len_byte |= bit << (7 - bit_idx);
526 }
527 }
528
529 let payload_len = u32::from_be_bytes(len_bytes) as usize;
530
531 let max_samples = samples.len() / 2;
533 if payload_len > (max_samples.saturating_sub(32)) / 8 {
534 return Err(StegoError::MalformedCoverData {
535 reason: format!("invalid payload length: {payload_len}"),
536 });
537 }
538
539 let mut payload_bytes = vec![0u8; payload_len];
541 for (byte_idx, payload_byte) in payload_bytes.iter_mut().enumerate() {
542 for bit_idx in 0..8 {
543 let sample_idx = 32 + byte_idx * 8 + bit_idx;
544 let byte_pos = sample_idx * 2;
545
546 let bit = samples
547 .get(byte_pos)
548 .ok_or_else(|| StegoError::MalformedCoverData {
549 reason: "sample index out of bounds".to_string(),
550 })?
551 & 1;
552 *payload_byte |= bit << (7 - bit_idx);
553 }
554 }
555
556 Ok(Payload::from_bytes(payload_bytes))
557 }
558}
559
560#[derive(Debug, Default)]
572pub struct PhaseEncoding;
573
574impl PhaseEncoding {
575 #[must_use]
577 pub const fn new() -> Self {
578 Self
579 }
580}
581
582impl EmbedTechnique for PhaseEncoding {
583 fn technique(&self) -> StegoTechnique {
584 StegoTechnique::PhaseEncoding
585 }
586
587 fn capacity(&self, _cover: &CoverMedia) -> Result<Capacity, StegoError> {
588 Err(StegoError::UnsupportedCoverType {
589 reason: "Phase encoding not yet implemented (requires FFT/phase manipulation)"
590 .to_string(),
591 })
592 }
593
594 fn embed(&self, _cover: CoverMedia, _payload: &Payload) -> Result<CoverMedia, StegoError> {
595 Err(StegoError::UnsupportedCoverType {
596 reason: "Phase encoding not yet implemented (requires FFT/phase manipulation)"
597 .to_string(),
598 })
599 }
600}
601
602impl ExtractTechnique for PhaseEncoding {
603 fn technique(&self) -> StegoTechnique {
604 StegoTechnique::PhaseEncoding
605 }
606
607 fn extract(&self, _cover: &CoverMedia) -> Result<Payload, StegoError> {
608 Err(StegoError::UnsupportedCoverType {
609 reason: "Phase encoding not yet implemented (requires FFT/phase manipulation)"
610 .to_string(),
611 })
612 }
613}
614
615#[derive(Debug, Default)]
626pub struct EchoHiding;
627
628impl EchoHiding {
629 #[must_use]
631 pub const fn new() -> Self {
632 Self
633 }
634}
635
636impl EmbedTechnique for EchoHiding {
637 fn technique(&self) -> StegoTechnique {
638 StegoTechnique::EchoHiding
639 }
640
641 fn capacity(&self, _cover: &CoverMedia) -> Result<Capacity, StegoError> {
642 Err(StegoError::UnsupportedCoverType {
643 reason: "Echo hiding not yet implemented (requires echo synthesis and autocorrelation)"
644 .to_string(),
645 })
646 }
647
648 fn embed(&self, _cover: CoverMedia, _payload: &Payload) -> Result<CoverMedia, StegoError> {
649 Err(StegoError::UnsupportedCoverType {
650 reason: "Echo hiding not yet implemented (requires echo synthesis and autocorrelation)"
651 .to_string(),
652 })
653 }
654}
655
656impl ExtractTechnique for EchoHiding {
657 fn technique(&self) -> StegoTechnique {
658 StegoTechnique::EchoHiding
659 }
660
661 fn extract(&self, _cover: &CoverMedia) -> Result<Payload, StegoError> {
662 Err(StegoError::UnsupportedCoverType {
663 reason: "Echo hiding not yet implemented (requires echo synthesis and autocorrelation)"
664 .to_string(),
665 })
666 }
667}
668
669#[derive(Debug, Default)]
682pub struct ZeroWidthText;
683
684impl ZeroWidthText {
685 #[must_use]
687 pub const fn new() -> Self {
688 Self
689 }
690}
691
692impl EmbedTechnique for ZeroWidthText {
693 fn technique(&self) -> StegoTechnique {
694 StegoTechnique::ZeroWidthText
695 }
696
697 fn capacity(&self, _cover: &CoverMedia) -> Result<Capacity, StegoError> {
698 Err(StegoError::UnsupportedCoverType {
699 reason: "Zero-width text steganography not yet implemented (Unicode grapheme segmentation complexity)".to_string(),
700 })
701 }
702
703 fn embed(&self, _cover: CoverMedia, _payload: &Payload) -> Result<CoverMedia, StegoError> {
704 Err(StegoError::UnsupportedCoverType {
705 reason: "Zero-width text steganography not yet implemented (Unicode grapheme segmentation complexity)".to_string(),
706 })
707 }
708}
709
710impl ExtractTechnique for ZeroWidthText {
711 fn technique(&self) -> StegoTechnique {
712 StegoTechnique::ZeroWidthText
713 }
714
715 fn extract(&self, _cover: &CoverMedia) -> Result<Payload, StegoError> {
716 Err(StegoError::UnsupportedCoverType {
717 reason: "Zero-width text steganography not yet implemented (Unicode grapheme segmentation complexity)".to_string(),
718 })
719 }
720}
721
722pub struct DualPayloadEmbedder;
737
738impl Default for DualPayloadEmbedder {
739 fn default() -> Self {
740 Self
741 }
742}
743
744impl DualPayloadEmbedder {
745 #[must_use]
747 pub const fn new() -> Self {
748 Self
749 }
750
751 fn derive_seed_with_channel(key: &[u8], channel: u8) -> [u8; 32] {
756 let mut hasher = Sha256::new();
757 hasher.update(key);
758 hasher.update([channel]);
759 hasher.finalize().into()
760 }
761
762 fn generate_pattern(seed: [u8; 32], total: usize, count: usize) -> Vec<usize> {
768 let mut rng = ChaCha20Rng::from_seed(seed);
769 let mut indices: Vec<usize> = (0..total).collect();
770 indices.shuffle(&mut rng);
771 indices.truncate(count);
772 indices
773 }
774
775 fn embed_at_positions(
777 cover_data: &mut [u8],
778 payload: &[u8],
779 positions: &[usize],
780 ) -> Result<(), DeniableError> {
781 let payload_bits = payload.len() * 8;
782
783 if payload_bits > positions.len() {
784 return Err(DeniableError::InsufficientCapacity);
785 }
786
787 for (bit_idx, &pos) in positions.iter().enumerate().take(payload_bits) {
789 let payload_byte_idx = bit_idx / 8;
790 let payload_bit_idx = 7 - (bit_idx % 8); let payload_byte = payload
792 .get(payload_byte_idx)
793 .ok_or(DeniableError::InsufficientCapacity)?;
794 let payload_bit = (payload_byte >> payload_bit_idx) & 1;
795
796 let cover_byte_idx = pos / 8;
797 let cover_bit_idx = pos % 8;
798
799 let byte = cover_data
801 .get_mut(cover_byte_idx)
802 .ok_or(DeniableError::InsufficientCapacity)?;
803 if payload_bit == 1 {
804 *byte |= 1 << cover_bit_idx;
805 } else {
806 *byte &= !(1 << cover_bit_idx);
807 }
808 }
809
810 Ok(())
811 }
812
813 fn extract_from_positions(
815 cover_data: &[u8],
816 positions: &[usize],
817 payload_len: usize,
818 ) -> Result<Vec<u8>, DeniableError> {
819 let payload_bits = payload_len * 8;
820
821 if payload_bits > positions.len() {
822 return Err(DeniableError::ExtractionFailed {
823 reason: "insufficient embedding positions for expected payload length".to_string(),
824 });
825 }
826
827 let mut payload = vec![0u8; payload_len];
828
829 for (bit_idx, &pos) in positions.iter().enumerate().take(payload_bits) {
830 let cover_byte_idx = pos / 8;
831 let cover_bit_idx = pos % 8;
832 let cover_byte =
833 cover_data
834 .get(cover_byte_idx)
835 .ok_or_else(|| DeniableError::ExtractionFailed {
836 reason: "cover byte index out of bounds".to_string(),
837 })?;
838 let cover_bit = (cover_byte >> cover_bit_idx) & 1;
839
840 let payload_byte_idx = bit_idx / 8;
841 let payload_bit_idx = 7 - (bit_idx % 8); if cover_bit == 1 {
844 let byte = payload.get_mut(payload_byte_idx).ok_or_else(|| {
845 DeniableError::ExtractionFailed {
846 reason: "payload byte index out of bounds".to_string(),
847 }
848 })?;
849 *byte |= 1 << payload_bit_idx;
850 }
851 }
852
853 Ok(payload)
854 }
855}
856
857impl DeniableEmbedder for DualPayloadEmbedder {
858 fn embed_dual(
859 &self,
860 mut cover: CoverMedia,
861 pair: &DeniablePayloadPair,
862 keys: &DeniableKeySet,
863 _embedder: &dyn EmbedTechnique,
864 ) -> Result<CoverMedia, DeniableError> {
865 let cover_bytes = cover.data.len();
866 let cover_bits = cover_bytes * 8;
867
868 let channel_capacity = cover_bits / 2;
872
873 let real_total_bits = (pair.real_payload.len() + 4) * 8;
876 let decoy_total_bits = (pair.decoy_payload.len() + 4) * 8;
877
878 if real_total_bits > channel_capacity || decoy_total_bits > channel_capacity {
879 return Err(DeniableError::InsufficientCapacity);
880 }
881
882 let primary_seed = Self::derive_seed_with_channel(&keys.primary_key, 0);
886 let decoy_seed = Self::derive_seed_with_channel(&keys.decoy_key, 1);
887
888 let primary_positions =
891 Self::generate_pattern(primary_seed, channel_capacity, real_total_bits)
892 .into_iter()
893 .map(|i| i * 2) .collect::<Vec<_>>();
895
896 let decoy_positions =
898 Self::generate_pattern(decoy_seed, channel_capacity, decoy_total_bits)
899 .into_iter()
900 .map(|i| i * 2 + 1) .collect::<Vec<_>>();
902
903 let real_len = pair.real_payload.len();
905 let decoy_len = pair.decoy_payload.len();
906
907 #[expect(
908 clippy::cast_possible_truncation,
909 reason = "payload size checked against u32::MAX in capacity validation"
910 )]
911 let mut real_with_header = (real_len as u32).to_be_bytes().to_vec();
912 real_with_header.extend_from_slice(&pair.real_payload);
913
914 #[expect(
915 clippy::cast_possible_truncation,
916 reason = "payload size checked against u32::MAX in capacity validation"
917 )]
918 let mut decoy_with_header = (decoy_len as u32).to_be_bytes().to_vec();
919 decoy_with_header.extend_from_slice(&pair.decoy_payload);
920
921 let mut cover_data = cover.data.to_vec();
923
924 Self::embed_at_positions(&mut cover_data, &real_with_header, &primary_positions).map_err(
926 |e| DeniableError::EmbedFailed {
927 reason: format!("real payload embed failed: {e}"),
928 },
929 )?;
930
931 Self::embed_at_positions(&mut cover_data, &decoy_with_header, &decoy_positions).map_err(
932 |e| DeniableError::EmbedFailed {
933 reason: format!("decoy payload embed failed: {e}"),
934 },
935 )?;
936
937 cover.data = cover_data.into();
938 Ok(cover)
939 }
940
941 fn extract_with_key(
942 &self,
943 stego: &CoverMedia,
944 key: &[u8],
945 _extractor: &dyn ExtractTechnique,
946 ) -> Result<Payload, DeniableError> {
947 let cover_bytes = stego.data.len();
948 let cover_bits = cover_bytes * 8;
949 let channel_capacity = cover_bits / 2;
950
951 for channel in 0..2 {
956 let seed = Self::derive_seed_with_channel(key, channel);
957
958 let header_bits = 32;
960 let header_positions = Self::generate_pattern(seed, channel_capacity, header_bits)
961 .into_iter()
962 .map(|i| i * 2 + channel as usize) .collect::<Vec<_>>();
964
965 if header_positions.len() < header_bits {
966 continue; }
968
969 let Ok(header_bytes) =
971 Self::extract_from_positions(stego.data.as_ref(), &header_positions, 4)
972 else {
973 continue; };
975
976 let Ok(header_arr) = <[u8; 4]>::try_from(header_bytes.as_slice()) else {
977 continue;
978 };
979 let payload_len = u32::from_be_bytes(header_arr) as usize;
980
981 if payload_len == 0 {
984 continue; }
986
987 let max_payload_len = channel_capacity / 8;
988 if payload_len > max_payload_len {
989 continue; }
991
992 let total_bits = (payload_len + 4) * 8;
994 if total_bits > channel_capacity {
995 continue; }
997
998 let positions = Self::generate_pattern(seed, channel_capacity, total_bits)
999 .into_iter()
1000 .map(|i| i * 2 + channel as usize)
1001 .collect::<Vec<_>>();
1002
1003 let Ok(with_header) =
1005 Self::extract_from_positions(stego.data.as_ref(), &positions, payload_len + 4)
1006 else {
1007 continue; };
1009
1010 let Ok(extracted_arr) = <[u8; 4]>::try_from(with_header.get(..4).unwrap_or_default())
1012 else {
1013 continue;
1014 };
1015 let extracted_header = u32::from_be_bytes(extracted_arr) as usize;
1016
1017 if extracted_header == payload_len {
1018 let payload_data = with_header.get(4..).unwrap_or_default();
1020 return Ok(Payload::from_bytes(payload_data.to_vec()));
1021 }
1022 }
1023
1024 Err(DeniableError::ExtractionFailed {
1026 reason: "failed to extract valid payload from either channel".to_string(),
1027 })
1028 }
1029}
1030
1031#[cfg(test)]
1039mod tests {
1040 use super::*;
1041
1042 type TestResult = Result<(), Box<dyn std::error::Error>>;
1043
1044 #[test]
1045 fn test_lsb_image_roundtrip_256x256() -> TestResult {
1046 let embedder = LsbImage::new();
1047
1048 let width = 256_u32;
1050 let height = 256_u32;
1051 let pixel_count = width * height;
1052 let data = vec![255u8; (pixel_count * 4) as usize]; let mut metadata = std::collections::HashMap::new();
1055 metadata.insert("width".to_string(), width.to_string());
1056 metadata.insert("height".to_string(), height.to_string());
1057
1058 let cover = CoverMedia {
1059 kind: CoverMediaKind::PngImage,
1060 data: data.into(),
1061 metadata,
1062 };
1063
1064 let payload = Payload::from_bytes(vec![0xAB; 64]);
1066
1067 let stego = embedder.embed(cover.clone(), &payload)?;
1069
1070 let orig_pixels = cover.data.as_ref();
1072 let stego_pixels = stego.data.as_ref();
1073 for (i, (orig, stego_val)) in orig_pixels.iter().zip(stego_pixels.iter()).enumerate() {
1074 let diff = orig.abs_diff(*stego_val);
1075 assert!(
1076 diff <= 1,
1077 "pixel at index {i} changed by more than 1: {orig} -> {stego_val}"
1078 );
1079 }
1080
1081 let extracted = embedder.extract(&stego)?;
1083 assert_eq!(extracted.as_bytes(), payload.as_bytes());
1084 Ok(())
1085 }
1086
1087 #[test]
1088 fn test_lsb_image_capacity_10x10() -> TestResult {
1089 let embedder = LsbImage::new();
1090
1091 let width = 10_u32;
1092 let height = 10_u32;
1093 let pixel_count = width * height;
1094 let data = vec![0u8; (pixel_count * 4) as usize];
1095
1096 let mut metadata = std::collections::HashMap::new();
1097 metadata.insert("width".to_string(), width.to_string());
1098 metadata.insert("height".to_string(), height.to_string());
1099
1100 let cover = CoverMedia {
1101 kind: CoverMediaKind::PngImage,
1102 data: data.into(),
1103 metadata,
1104 };
1105
1106 let cap = embedder.capacity(&cover)?;
1107
1108 assert_eq!(cap.bytes, 33);
1113 assert_eq!(cap.technique, StegoTechnique::LsbImage);
1114 Ok(())
1115 }
1116
1117 #[test]
1118 fn test_lsb_image_insufficient_capacity() {
1119 let embedder = LsbImage::new();
1120
1121 let width = 10_u32;
1122 let height = 10_u32;
1123 let pixel_count = width * height;
1124 let data = vec![0u8; (pixel_count * 4) as usize];
1125
1126 let mut metadata = std::collections::HashMap::new();
1127 metadata.insert("width".to_string(), width.to_string());
1128 metadata.insert("height".to_string(), height.to_string());
1129
1130 let cover = CoverMedia {
1131 kind: CoverMediaKind::PngImage,
1132 data: data.into(),
1133 metadata,
1134 };
1135
1136 let payload = Payload::from_bytes(vec![0xAB; 100]);
1138
1139 let result = embedder.embed(cover, &payload);
1140 assert!(matches!(result, Err(StegoError::PayloadTooLarge { .. })));
1141 }
1142
1143 #[test]
1144 fn test_lsb_image_bmp_support() -> TestResult {
1145 let embedder = LsbImage::new();
1146
1147 let width = 100_u32;
1148 let height = 100_u32;
1149 let pixel_count = width * height;
1150 let data = vec![128u8; (pixel_count * 4) as usize];
1151
1152 let mut metadata = std::collections::HashMap::new();
1153 metadata.insert("width".to_string(), width.to_string());
1154 metadata.insert("height".to_string(), height.to_string());
1155
1156 let cover = CoverMedia {
1157 kind: CoverMediaKind::BmpImage,
1158 data: data.into(),
1159 metadata,
1160 };
1161
1162 let payload = Payload::from_bytes(vec![1, 2, 3, 4, 5]);
1163
1164 let stego = embedder.embed(cover, &payload)?;
1166
1167 let extracted = embedder.extract(&stego)?;
1169 assert_eq!(extracted.as_bytes(), payload.as_bytes());
1170 Ok(())
1171 }
1172
1173 #[test]
1174 fn test_dct_jpeg_stub_returns_not_implemented() {
1175 let embedder = DctJpeg::new();
1176
1177 let cover = CoverMedia {
1178 kind: CoverMediaKind::JpegImage,
1179 data: vec![].into(),
1180 metadata: std::collections::HashMap::new(),
1181 };
1182
1183 let payload = Payload::from_bytes(vec![1, 2, 3]);
1184
1185 let result = embedder.embed(cover.clone(), &payload);
1187 assert!(matches!(
1188 result,
1189 Err(StegoError::UnsupportedCoverType { .. })
1190 ));
1191
1192 let result = embedder.extract(&cover);
1193 assert!(matches!(
1194 result,
1195 Err(StegoError::UnsupportedCoverType { .. })
1196 ));
1197
1198 let result = embedder.capacity(&cover);
1199 assert!(matches!(
1200 result,
1201 Err(StegoError::UnsupportedCoverType { .. })
1202 ));
1203 }
1204
1205 #[test]
1206 fn test_palette_stego_stub_returns_not_implemented() {
1207 let embedder = PaletteStego::new();
1208
1209 let cover = CoverMedia {
1210 kind: CoverMediaKind::GifImage,
1211 data: vec![].into(),
1212 metadata: std::collections::HashMap::new(),
1213 };
1214
1215 let payload = Payload::from_bytes(vec![1, 2, 3]);
1216
1217 let result = embedder.embed(cover.clone(), &payload);
1219 assert!(matches!(
1220 result,
1221 Err(StegoError::UnsupportedCoverType { .. })
1222 ));
1223
1224 let result = embedder.extract(&cover);
1225 assert!(matches!(
1226 result,
1227 Err(StegoError::UnsupportedCoverType { .. })
1228 ));
1229
1230 let result = embedder.capacity(&cover);
1231 assert!(matches!(
1232 result,
1233 Err(StegoError::UnsupportedCoverType { .. })
1234 ));
1235 }
1236
1237 #[test]
1238 fn test_lsb_audio_roundtrip() -> TestResult {
1239 let embedder = LsbAudio::new();
1240
1241 let sample_rate = 44100;
1243 let sample_count = sample_rate; let mut data = Vec::new();
1245 for _ in 0..sample_count {
1246 data.extend_from_slice(&0_i16.to_le_bytes());
1247 }
1248
1249 let mut metadata = std::collections::HashMap::new();
1250 metadata.insert("sample_rate".to_string(), sample_rate.to_string());
1251 metadata.insert("channels".to_string(), "1".to_string());
1252 metadata.insert("bits_per_sample".to_string(), "16".to_string());
1253
1254 let cover = CoverMedia {
1255 kind: CoverMediaKind::WavAudio,
1256 data: data.into(),
1257 metadata,
1258 };
1259
1260 let payload = Payload::from_bytes(vec![0xAB; 512]);
1262
1263 let stego = embedder.embed(cover, &payload)?;
1265
1266 let extracted = embedder.extract(&stego)?;
1268 assert_eq!(extracted.as_bytes(), payload.as_bytes());
1269 Ok(())
1270 }
1271
1272 #[test]
1273 fn test_lsb_audio_capacity() -> TestResult {
1274 let embedder = LsbAudio::new();
1275
1276 let sample_count = 1000;
1278 let mut data = Vec::new();
1279 for _ in 0..sample_count {
1280 data.extend_from_slice(&0_i16.to_le_bytes());
1281 }
1282
1283 let mut metadata = std::collections::HashMap::new();
1284 metadata.insert("sample_rate".to_string(), "44100".to_string());
1285 metadata.insert("channels".to_string(), "1".to_string());
1286 metadata.insert("bits_per_sample".to_string(), "16".to_string());
1287
1288 let cover = CoverMedia {
1289 kind: CoverMediaKind::WavAudio,
1290 data: data.into(),
1291 metadata,
1292 };
1293
1294 let cap = embedder.capacity(&cover)?;
1295
1296 assert_eq!(cap.bytes, 121);
1298 assert_eq!(cap.technique, StegoTechnique::LsbAudio);
1299 Ok(())
1300 }
1301
1302 #[test]
1303 fn test_lsb_audio_insufficient_capacity() {
1304 let embedder = LsbAudio::new();
1305
1306 let sample_count = 100;
1308 let mut data = Vec::new();
1309 for _ in 0..sample_count {
1310 data.extend_from_slice(&0_i16.to_le_bytes());
1311 }
1312
1313 let mut metadata = std::collections::HashMap::new();
1314 metadata.insert("sample_rate".to_string(), "44100".to_string());
1315 metadata.insert("channels".to_string(), "1".to_string());
1316 metadata.insert("bits_per_sample".to_string(), "16".to_string());
1317
1318 let cover = CoverMedia {
1319 kind: CoverMediaKind::WavAudio,
1320 data: data.into(),
1321 metadata,
1322 };
1323
1324 let payload = Payload::from_bytes(vec![0xAB; 100]);
1326
1327 let result = embedder.embed(cover, &payload);
1328 assert!(matches!(result, Err(StegoError::PayloadTooLarge { .. })));
1329 }
1330
1331 #[test]
1332 fn test_phase_encoding_stub_returns_not_implemented() {
1333 let embedder = PhaseEncoding::new();
1334
1335 let mut metadata = std::collections::HashMap::new();
1336 metadata.insert("sample_rate".to_string(), "44100".to_string());
1337 metadata.insert("channels".to_string(), "1".to_string());
1338 metadata.insert("bits_per_sample".to_string(), "16".to_string());
1339
1340 let cover = CoverMedia {
1341 kind: CoverMediaKind::WavAudio,
1342 data: vec![0; 1000].into(),
1343 metadata,
1344 };
1345
1346 let payload = Payload::from_bytes(vec![1, 2, 3]);
1347
1348 let result = embedder.embed(cover.clone(), &payload);
1349 assert!(matches!(
1350 result,
1351 Err(StegoError::UnsupportedCoverType { .. })
1352 ));
1353
1354 let result = embedder.extract(&cover);
1355 assert!(matches!(
1356 result,
1357 Err(StegoError::UnsupportedCoverType { .. })
1358 ));
1359
1360 let result = embedder.capacity(&cover);
1361 assert!(matches!(
1362 result,
1363 Err(StegoError::UnsupportedCoverType { .. })
1364 ));
1365 }
1366
1367 #[test]
1368 fn test_echo_hiding_stub_returns_not_implemented() {
1369 let embedder = EchoHiding::new();
1370
1371 let mut metadata = std::collections::HashMap::new();
1372 metadata.insert("sample_rate".to_string(), "44100".to_string());
1373 metadata.insert("channels".to_string(), "1".to_string());
1374 metadata.insert("bits_per_sample".to_string(), "16".to_string());
1375
1376 let cover = CoverMedia {
1377 kind: CoverMediaKind::WavAudio,
1378 data: vec![0; 1000].into(),
1379 metadata,
1380 };
1381
1382 let payload = Payload::from_bytes(vec![1, 2, 3]);
1383
1384 let result = embedder.embed(cover.clone(), &payload);
1385 assert!(matches!(
1386 result,
1387 Err(StegoError::UnsupportedCoverType { .. })
1388 ));
1389
1390 let result = embedder.extract(&cover);
1391 assert!(matches!(
1392 result,
1393 Err(StegoError::UnsupportedCoverType { .. })
1394 ));
1395
1396 let result = embedder.capacity(&cover);
1397 assert!(matches!(
1398 result,
1399 Err(StegoError::UnsupportedCoverType { .. })
1400 ));
1401 }
1402
1403 #[test]
1404 fn test_zero_width_text_stub_returns_not_implemented() {
1405 let embedder = ZeroWidthText::new();
1406
1407 let cover = CoverMedia {
1408 kind: CoverMediaKind::PlainText,
1409 data: b"Hello, world!".to_vec().into(),
1410 metadata: std::collections::HashMap::new(),
1411 };
1412
1413 let payload = Payload::from_bytes(vec![1, 2, 3]);
1414
1415 let result = embedder.embed(cover.clone(), &payload);
1416 assert!(matches!(
1417 result,
1418 Err(StegoError::UnsupportedCoverType { .. })
1419 ));
1420
1421 let result = embedder.extract(&cover);
1422 assert!(matches!(
1423 result,
1424 Err(StegoError::UnsupportedCoverType { .. })
1425 ));
1426
1427 let result = embedder.capacity(&cover);
1428 assert!(matches!(
1429 result,
1430 Err(StegoError::UnsupportedCoverType { .. })
1431 ));
1432 }
1433
1434 #[test]
1435 fn test_dual_payload_roundtrip() -> TestResult {
1436 let embedder = DualPayloadEmbedder::new();
1437 let lsb_image = LsbImage::new();
1438
1439 let width = 100u32;
1441 let height = 100u32;
1442 let pixel_count = (width * height) as usize;
1443 let data = vec![0u8; pixel_count * 3]; let mut metadata = std::collections::HashMap::new();
1446 metadata.insert("width".to_string(), width.to_string());
1447 metadata.insert("height".to_string(), height.to_string());
1448 metadata.insert("channels".to_string(), "3".to_string());
1449
1450 let cover = CoverMedia {
1451 kind: CoverMediaKind::PngImage,
1452 data: data.into(),
1453 metadata,
1454 };
1455
1456 let real_payload = b"This is the REAL secret message".to_vec();
1458 let decoy_payload = b"This is just a decoy".to_vec();
1459
1460 let pair = DeniablePayloadPair {
1461 real_payload: real_payload.clone(),
1462 decoy_payload: decoy_payload.clone(),
1463 };
1464
1465 let keys = DeniableKeySet {
1467 primary_key: b"primary_key_12345".to_vec(),
1468 decoy_key: b"decoy_key_67890".to_vec(),
1469 };
1470
1471 let stego = embedder.embed_dual(cover, &pair, &keys, &lsb_image)?;
1473
1474 let extracted_real = embedder.extract_with_key(&stego, &keys.primary_key, &lsb_image)?;
1476 assert_eq!(extracted_real.as_bytes(), &real_payload);
1477
1478 let extracted_decoy = embedder.extract_with_key(&stego, &keys.decoy_key, &lsb_image)?;
1480 assert_eq!(extracted_decoy.as_bytes(), &decoy_payload);
1481 Ok(())
1482 }
1483
1484 #[test]
1485 fn test_dual_payload_insufficient_capacity() {
1486 let embedder = DualPayloadEmbedder::new();
1487 let lsb_image = LsbImage::new();
1488
1489 let width = 10u32;
1491 let height = 10u32;
1492 let pixel_count = (width * height) as usize;
1493 let data = vec![0u8; pixel_count * 3];
1494
1495 let mut metadata = std::collections::HashMap::new();
1496 metadata.insert("width".to_string(), width.to_string());
1497 metadata.insert("height".to_string(), height.to_string());
1498 metadata.insert("channels".to_string(), "3".to_string());
1499
1500 let cover = CoverMedia {
1501 kind: CoverMediaKind::PngImage,
1502 data: data.into(),
1503 metadata,
1504 };
1505
1506 let real_payload = vec![0u8; 200];
1508 let decoy_payload = vec![0u8; 200];
1509
1510 let pair = DeniablePayloadPair {
1511 real_payload,
1512 decoy_payload,
1513 };
1514
1515 let keys = DeniableKeySet {
1516 primary_key: b"primary_key".to_vec(),
1517 decoy_key: b"decoy_key".to_vec(),
1518 };
1519
1520 let result = embedder.embed_dual(cover, &pair, &keys, &lsb_image);
1522 assert!(matches!(result, Err(DeniableError::InsufficientCapacity)));
1523 }
1524
1525 #[test]
1526 fn test_dual_payload_different_keys_produce_different_results() -> TestResult {
1527 let embedder = DualPayloadEmbedder::new();
1528 let lsb_image = LsbImage::new();
1529
1530 let width = 100u32;
1532 let height = 100u32;
1533 let pixel_count = (width * height) as usize;
1534 let data = vec![0u8; pixel_count * 3];
1535
1536 let mut metadata = std::collections::HashMap::new();
1537 metadata.insert("width".to_string(), width.to_string());
1538 metadata.insert("height".to_string(), height.to_string());
1539 metadata.insert("channels".to_string(), "3".to_string());
1540
1541 let cover = CoverMedia {
1542 kind: CoverMediaKind::PngImage,
1543 data: data.into(),
1544 metadata,
1545 };
1546
1547 let real_payload = b"Real secret".to_vec();
1548 let decoy_payload = b"Fake data".to_vec();
1549
1550 let pair = DeniablePayloadPair {
1551 real_payload: real_payload.clone(),
1552 decoy_payload: decoy_payload.clone(),
1553 };
1554
1555 let keys = DeniableKeySet {
1556 primary_key: b"key1".to_vec(),
1557 decoy_key: b"key2".to_vec(),
1558 };
1559
1560 let stego = embedder.embed_dual(cover, &pair, &keys, &lsb_image)?;
1561
1562 let extracted1 = embedder.extract_with_key(&stego, &keys.primary_key, &lsb_image)?;
1564
1565 let extracted2 = embedder.extract_with_key(&stego, &keys.decoy_key, &lsb_image)?;
1567
1568 assert_ne!(extracted1.as_bytes(), extracted2.as_bytes());
1570 assert_eq!(extracted1.as_bytes(), &real_payload);
1571 assert_eq!(extracted2.as_bytes(), &decoy_payload);
1572 Ok(())
1573 }
1574
1575 #[test]
1576 fn test_dual_payload_wrong_key_produces_garbage() -> TestResult {
1577 let embedder = DualPayloadEmbedder::new();
1578 let lsb_image = LsbImage::new();
1579
1580 let width = 100u32;
1582 let height = 100u32;
1583 let pixel_count = (width * height) as usize;
1584 let data = vec![0u8; pixel_count * 3];
1585
1586 let mut metadata = std::collections::HashMap::new();
1587 metadata.insert("width".to_string(), width.to_string());
1588 metadata.insert("height".to_string(), height.to_string());
1589 metadata.insert("channels".to_string(), "3".to_string());
1590
1591 let cover = CoverMedia {
1592 kind: CoverMediaKind::PngImage,
1593 data: data.into(),
1594 metadata,
1595 };
1596
1597 let real_payload = b"Real secret message".to_vec();
1598 let decoy_payload = b"Decoy message".to_vec();
1599
1600 let pair = DeniablePayloadPair {
1601 real_payload: real_payload.clone(),
1602 decoy_payload: decoy_payload.clone(),
1603 };
1604
1605 let keys = DeniableKeySet {
1606 primary_key: b"correct_primary".to_vec(),
1607 decoy_key: b"correct_decoy".to_vec(),
1608 };
1609
1610 let stego = embedder.embed_dual(cover, &pair, &keys, &lsb_image)?;
1611
1612 let wrong_key = b"wrong_key";
1614 let result = embedder.extract_with_key(&stego, wrong_key, &lsb_image);
1615
1616 match result {
1619 Ok(extracted) => {
1620 assert_ne!(extracted.as_bytes(), &real_payload);
1622 assert_ne!(extracted.as_bytes(), &decoy_payload);
1623 }
1624 Err(DeniableError::ExtractionFailed { .. }) => {
1625 }
1627 Err(e) => return Err(e.into()),
1628 }
1629 Ok(())
1630 }
1631
1632 #[test]
1635 fn test_lsb_image_wrong_cover_type_embed() {
1636 let embedder = LsbImage::new();
1637 let cover = CoverMedia {
1638 kind: CoverMediaKind::WavAudio,
1639 data: vec![0u8; 100].into(),
1640 metadata: std::collections::HashMap::new(),
1641 };
1642 let payload = Payload::from_bytes(vec![1, 2, 3]);
1643 let result = embedder.embed(cover, &payload);
1644 assert!(matches!(
1645 result,
1646 Err(StegoError::UnsupportedCoverType { .. })
1647 ));
1648 }
1649
1650 #[test]
1651 fn test_lsb_image_wrong_cover_type_extract() {
1652 let embedder = LsbImage::new();
1653 let cover = CoverMedia {
1654 kind: CoverMediaKind::JpegImage,
1655 data: vec![0u8; 100].into(),
1656 metadata: std::collections::HashMap::new(),
1657 };
1658 let result = embedder.extract(&cover);
1659 assert!(matches!(
1660 result,
1661 Err(StegoError::UnsupportedCoverType { .. })
1662 ));
1663 }
1664
1665 #[test]
1666 fn test_lsb_image_missing_width_metadata() {
1667 let embedder = LsbImage::new();
1668 let mut metadata = std::collections::HashMap::new();
1669 metadata.insert("height".to_string(), "100".to_string());
1670 metadata.insert("channels".to_string(), "3".to_string());
1671 let cover = CoverMedia {
1672 kind: CoverMediaKind::PngImage,
1673 data: vec![0u8; 30000].into(),
1674 metadata,
1675 };
1676 let result = embedder.capacity(&cover);
1677 assert!(matches!(result, Err(StegoError::MalformedCoverData { .. })));
1678 }
1679
1680 #[test]
1681 fn test_lsb_image_missing_height_metadata() {
1682 let embedder = LsbImage::new();
1683 let mut metadata = std::collections::HashMap::new();
1684 metadata.insert("width".to_string(), "100".to_string());
1685 metadata.insert("channels".to_string(), "3".to_string());
1686 let cover = CoverMedia {
1687 kind: CoverMediaKind::PngImage,
1688 data: vec![0u8; 30000].into(),
1689 metadata,
1690 };
1691 let result = embedder.capacity(&cover);
1692 assert!(matches!(result, Err(StegoError::MalformedCoverData { .. })));
1693 }
1694
1695 #[test]
1696 fn test_lsb_image_invalid_width_metadata() {
1697 let embedder = LsbImage::new();
1698 let mut metadata = std::collections::HashMap::new();
1699 metadata.insert("width".to_string(), "not_a_number".to_string());
1700 metadata.insert("height".to_string(), "100".to_string());
1701 metadata.insert("channels".to_string(), "3".to_string());
1702 let cover = CoverMedia {
1703 kind: CoverMediaKind::PngImage,
1704 data: vec![0u8; 30000].into(),
1705 metadata,
1706 };
1707 let result = embedder.capacity(&cover);
1708 assert!(matches!(result, Err(StegoError::MalformedCoverData { .. })));
1709 }
1710
1711 #[test]
1712 fn test_lsb_image_wrong_cover_type_capacity() {
1713 let embedder = LsbImage::new();
1714 let cover = CoverMedia {
1715 kind: CoverMediaKind::GifImage,
1716 data: vec![0u8; 100].into(),
1717 metadata: std::collections::HashMap::new(),
1718 };
1719 let result = embedder.capacity(&cover);
1720 assert!(matches!(
1721 result,
1722 Err(StegoError::UnsupportedCoverType { .. })
1723 ));
1724 }
1725
1726 #[test]
1727 fn test_lsb_audio_wrong_cover_type() {
1728 let embedder = LsbAudio::new();
1729 let cover = CoverMedia {
1730 kind: CoverMediaKind::PngImage,
1731 data: vec![0u8; 1000].into(),
1732 metadata: std::collections::HashMap::new(),
1733 };
1734 let payload = Payload::from_bytes(vec![1, 2, 3]);
1735 let result = embedder.embed(cover, &payload);
1736 assert!(matches!(
1737 result,
1738 Err(StegoError::UnsupportedCoverType { .. })
1739 ));
1740 }
1741
1742 #[test]
1743 fn test_lsb_audio_wrong_cover_type_extract() {
1744 let embedder = LsbAudio::new();
1745 let cover = CoverMedia {
1746 kind: CoverMediaKind::PngImage,
1747 data: vec![0u8; 1000].into(),
1748 metadata: std::collections::HashMap::new(),
1749 };
1750 let result = embedder.extract(&cover);
1751 assert!(matches!(
1752 result,
1753 Err(StegoError::UnsupportedCoverType { .. })
1754 ));
1755 }
1756
1757 #[test]
1758 fn test_lsb_audio_wrong_cover_type_capacity() {
1759 let embedder = LsbAudio::new();
1760 let cover = CoverMedia {
1761 kind: CoverMediaKind::PngImage,
1762 data: vec![0u8; 1000].into(),
1763 metadata: std::collections::HashMap::new(),
1764 };
1765 let result = embedder.capacity(&cover);
1766 assert!(matches!(
1767 result,
1768 Err(StegoError::UnsupportedCoverType { .. })
1769 ));
1770 }
1771
1772 fn dummy_cover() -> CoverMedia {
1775 CoverMedia {
1776 kind: CoverMediaKind::PngImage,
1777 data: vec![0u8; 64].into(),
1778 metadata: std::collections::HashMap::new(),
1779 }
1780 }
1781
1782 #[test]
1783 fn dct_jpeg_stub_capacity_returns_error() {
1784 let dct = DctJpeg::new();
1785 assert!(dct.capacity(&dummy_cover()).is_err());
1786 assert_eq!(EmbedTechnique::technique(&dct), StegoTechnique::DctJpeg);
1787 }
1788
1789 #[test]
1790 fn dct_jpeg_stub_embed_returns_error() {
1791 let dct = DctJpeg::new();
1792 let result = dct.embed(dummy_cover(), &Payload::from_bytes(vec![1]));
1793 assert!(matches!(
1794 result,
1795 Err(StegoError::UnsupportedCoverType { .. })
1796 ));
1797 }
1798
1799 #[test]
1800 fn dct_jpeg_stub_extract_returns_error() {
1801 let dct = DctJpeg::new();
1802 let result = dct.extract(&dummy_cover());
1803 assert!(matches!(
1804 result,
1805 Err(StegoError::UnsupportedCoverType { .. })
1806 ));
1807 assert_eq!(ExtractTechnique::technique(&dct), StegoTechnique::DctJpeg);
1808 }
1809
1810 #[test]
1811 fn palette_stego_stub_capacity_returns_error() {
1812 let pal = PaletteStego::new();
1813 assert!(pal.capacity(&dummy_cover()).is_err());
1814 assert_eq!(EmbedTechnique::technique(&pal), StegoTechnique::Palette);
1815 }
1816
1817 #[test]
1818 fn palette_stego_stub_embed_returns_error() {
1819 let pal = PaletteStego::new();
1820 let result = pal.embed(dummy_cover(), &Payload::from_bytes(vec![1]));
1821 assert!(matches!(
1822 result,
1823 Err(StegoError::UnsupportedCoverType { .. })
1824 ));
1825 }
1826
1827 #[test]
1828 fn palette_stego_stub_extract_returns_error() {
1829 let pal = PaletteStego::new();
1830 let result = pal.extract(&dummy_cover());
1831 assert!(matches!(
1832 result,
1833 Err(StegoError::UnsupportedCoverType { .. })
1834 ));
1835 assert_eq!(ExtractTechnique::technique(&pal), StegoTechnique::Palette);
1836 }
1837
1838 #[test]
1839 fn phase_encoding_stub_capacity_returns_error() {
1840 let pe = PhaseEncoding::new();
1841 assert!(pe.capacity(&dummy_cover()).is_err());
1842 assert_eq!(
1843 EmbedTechnique::technique(&pe),
1844 StegoTechnique::PhaseEncoding
1845 );
1846 }
1847
1848 #[test]
1849 fn phase_encoding_stub_embed_returns_error() {
1850 let pe = PhaseEncoding::new();
1851 let result = pe.embed(dummy_cover(), &Payload::from_bytes(vec![1]));
1852 assert!(matches!(
1853 result,
1854 Err(StegoError::UnsupportedCoverType { .. })
1855 ));
1856 }
1857
1858 #[test]
1859 fn phase_encoding_stub_extract_returns_error() {
1860 let pe = PhaseEncoding::new();
1861 let result = pe.extract(&dummy_cover());
1862 assert!(matches!(
1863 result,
1864 Err(StegoError::UnsupportedCoverType { .. })
1865 ));
1866 assert_eq!(
1867 ExtractTechnique::technique(&pe),
1868 StegoTechnique::PhaseEncoding
1869 );
1870 }
1871
1872 #[test]
1873 fn echo_hiding_stub_capacity_returns_error() {
1874 let eh = EchoHiding::new();
1875 assert!(eh.capacity(&dummy_cover()).is_err());
1876 assert_eq!(EmbedTechnique::technique(&eh), StegoTechnique::EchoHiding);
1877 }
1878
1879 #[test]
1880 fn echo_hiding_stub_embed_returns_error() {
1881 let eh = EchoHiding::new();
1882 let result = eh.embed(dummy_cover(), &Payload::from_bytes(vec![1]));
1883 assert!(matches!(
1884 result,
1885 Err(StegoError::UnsupportedCoverType { .. })
1886 ));
1887 }
1888
1889 #[test]
1890 fn echo_hiding_stub_extract_returns_error() {
1891 let eh = EchoHiding::new();
1892 let result = eh.extract(&dummy_cover());
1893 assert!(matches!(
1894 result,
1895 Err(StegoError::UnsupportedCoverType { .. })
1896 ));
1897 assert_eq!(ExtractTechnique::technique(&eh), StegoTechnique::EchoHiding);
1898 }
1899
1900 #[test]
1901 fn zero_width_text_stub_capacity_returns_error() {
1902 let zwt = ZeroWidthText::new();
1903 assert!(zwt.capacity(&dummy_cover()).is_err());
1904 assert_eq!(
1905 EmbedTechnique::technique(&zwt),
1906 StegoTechnique::ZeroWidthText
1907 );
1908 }
1909
1910 #[test]
1911 fn zero_width_text_stub_embed_returns_error() {
1912 let zwt = ZeroWidthText::new();
1913 let result = zwt.embed(dummy_cover(), &Payload::from_bytes(vec![1]));
1914 assert!(matches!(
1915 result,
1916 Err(StegoError::UnsupportedCoverType { .. })
1917 ));
1918 }
1919
1920 #[test]
1921 fn zero_width_text_stub_extract_returns_error() {
1922 let zwt = ZeroWidthText::new();
1923 let result = zwt.extract(&dummy_cover());
1924 assert!(matches!(
1925 result,
1926 Err(StegoError::UnsupportedCoverType { .. })
1927 ));
1928 assert_eq!(
1929 ExtractTechnique::technique(&zwt),
1930 StegoTechnique::ZeroWidthText
1931 );
1932 }
1933
1934 #[test]
1935 fn lsb_image_insufficient_capacity() {
1936 let embedder = LsbImage::new();
1937 let mut metadata = std::collections::HashMap::new();
1938 metadata.insert("width".to_string(), "2".to_string());
1939 metadata.insert("height".to_string(), "2".to_string());
1940 metadata.insert("channels".to_string(), "3".to_string());
1941 let cover = CoverMedia {
1942 kind: CoverMediaKind::PngImage,
1943 data: vec![0u8; 12].into(),
1945 metadata,
1946 };
1947 let payload = Payload::from_bytes(vec![0xAB; 100]);
1949 let result = embedder.embed(cover, &payload);
1950 assert!(result.is_err());
1951 }
1952
1953 #[test]
1954 fn lsb_audio_insufficient_capacity() {
1955 let embedder = LsbAudio::new();
1956 let mut metadata = std::collections::HashMap::new();
1957 metadata.insert("bits_per_sample".to_string(), "16".to_string());
1958 let cover = CoverMedia {
1959 kind: CoverMediaKind::WavAudio,
1960 data: vec![0u8; 10].into(),
1962 metadata,
1963 };
1964 let payload = Payload::from_bytes(vec![0xAB; 100]);
1965 let result = embedder.embed(cover, &payload);
1966 assert!(result.is_err());
1967 }
1968}