Skip to main content

shadowforge_lib/adapters/
stego.rs

1//! Steganography technique adapters implementing `EmbedTechnique` port.
2
3use crate::domain::errors::{DeniableError, StegoError};
4use crate::domain::ports::{DeniableEmbedder, EmbedTechnique, ExtractTechnique, PdfProcessor};
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/// PDF content-stream LSB steganography adapter.
15///
16/// Wraps the `PdfProcessor`'s content-stream embedding methods to implement
17/// the `EmbedTechnique` trait.
18pub struct PdfContentStreamLsb {
19    processor: Box<dyn PdfProcessor>,
20}
21
22impl PdfContentStreamLsb {
23    /// Create a new PDF content-stream LSB embedder.
24    #[must_use]
25    pub fn new(processor: Box<dyn PdfProcessor>) -> Self {
26        Self { processor }
27    }
28}
29
30impl EmbedTechnique for PdfContentStreamLsb {
31    fn technique(&self) -> StegoTechnique {
32        StegoTechnique::PdfContentStream
33    }
34
35    fn capacity(&self, cover: &CoverMedia) -> Result<Capacity, StegoError> {
36        // Check if cover is a PDF
37        if cover.kind != CoverMediaKind::PdfDocument {
38            return Err(StegoError::UnsupportedCoverType {
39                reason: format!(
40                    "PDF content-stream LSB requires PdfDocument, got {:?}",
41                    cover.kind
42                ),
43            });
44        }
45
46        // TODO(T10): Implement capacity estimation for content-stream LSB
47        // For now, return a conservative estimate based on typical PDF content streams
48        // Real implementation would parse the PDF and count numeric tokens
49        Ok(Capacity {
50            bytes: 1024, // Conservative estimate
51            technique: StegoTechnique::PdfContentStream,
52        })
53    }
54
55    fn embed(&self, cover: CoverMedia, payload: &Payload) -> Result<CoverMedia, StegoError> {
56        if cover.kind != CoverMediaKind::PdfDocument {
57            return Err(StegoError::UnsupportedCoverType {
58                reason: format!(
59                    "PDF content-stream LSB requires PdfDocument, got {:?}",
60                    cover.kind
61                ),
62            });
63        }
64
65        self.processor
66            .embed_in_content_stream(cover, payload)
67            .map_err(|e| StegoError::MalformedCoverData {
68                reason: format!("embed failed: {e}"),
69            })
70    }
71}
72
73impl ExtractTechnique for PdfContentStreamLsb {
74    fn technique(&self) -> StegoTechnique {
75        StegoTechnique::PdfContentStream
76    }
77
78    fn extract(&self, cover: &CoverMedia) -> Result<Payload, StegoError> {
79        if cover.kind != CoverMediaKind::PdfDocument {
80            return Err(StegoError::UnsupportedCoverType {
81                reason: format!(
82                    "PDF content-stream LSB requires PdfDocument, got {:?}",
83                    cover.kind
84                ),
85            });
86        }
87
88        self.processor
89            .extract_from_content_stream(cover)
90            .map_err(|e| StegoError::IntegrityCheckFailed {
91                reason: format!("extract failed: {e}"),
92            })
93    }
94}
95
96/// PDF XMP metadata steganography adapter.
97///
98/// Wraps the `PdfProcessor`'s metadata embedding methods to implement
99/// the `EmbedTechnique` trait.
100pub struct PdfMetadataEmbed {
101    processor: Box<dyn PdfProcessor>,
102}
103
104impl PdfMetadataEmbed {
105    /// Create a new PDF metadata embedder.
106    #[must_use]
107    pub fn new(processor: Box<dyn PdfProcessor>) -> Self {
108        Self { processor }
109    }
110}
111
112impl EmbedTechnique for PdfMetadataEmbed {
113    fn technique(&self) -> StegoTechnique {
114        StegoTechnique::PdfMetadata
115    }
116
117    fn capacity(&self, cover: &CoverMedia) -> Result<Capacity, StegoError> {
118        if cover.kind != CoverMediaKind::PdfDocument {
119            return Err(StegoError::UnsupportedCoverType {
120                reason: format!(
121                    "PDF metadata embedding requires PdfDocument, got {:?}",
122                    cover.kind
123                ),
124            });
125        }
126
127        // XMP metadata can hold large base64-encoded payloads
128        // Base64 encoding adds ~33% overhead
129        Ok(Capacity {
130            bytes: 1_000_000, // XMP can hold very large payloads
131            technique: StegoTechnique::PdfMetadata,
132        })
133    }
134
135    fn embed(&self, cover: CoverMedia, payload: &Payload) -> Result<CoverMedia, StegoError> {
136        if cover.kind != CoverMediaKind::PdfDocument {
137            return Err(StegoError::UnsupportedCoverType {
138                reason: format!(
139                    "PDF metadata embedding requires PdfDocument, got {:?}",
140                    cover.kind
141                ),
142            });
143        }
144
145        self.processor
146            .embed_in_metadata(cover, payload)
147            .map_err(|e| StegoError::MalformedCoverData {
148                reason: format!("embed failed: {e}"),
149            })
150    }
151}
152
153impl ExtractTechnique for PdfMetadataEmbed {
154    fn technique(&self) -> StegoTechnique {
155        StegoTechnique::PdfMetadata
156    }
157
158    fn extract(&self, cover: &CoverMedia) -> Result<Payload, StegoError> {
159        if cover.kind != CoverMediaKind::PdfDocument {
160            return Err(StegoError::UnsupportedCoverType {
161                reason: format!(
162                    "PDF metadata embedding requires PdfDocument, got {:?}",
163                    cover.kind
164                ),
165            });
166        }
167
168        self.processor
169            .extract_from_metadata(cover)
170            .map_err(|e| StegoError::IntegrityCheckFailed {
171                reason: format!("extract failed: {e}"),
172            })
173    }
174}
175
176/// LSB image steganography adapter for PNG/BMP.
177///
178/// Embeds payload in the least significant bits of RGB channels only
179/// (alpha channel is untouched). Header encodes 32-bit big-endian payload length.
180#[derive(Debug, Default)]
181pub struct LsbImage;
182
183impl LsbImage {
184    /// Create a new LSB image embedder.
185    #[must_use]
186    pub const fn new() -> Self {
187        Self
188    }
189}
190
191impl EmbedTechnique for LsbImage {
192    fn technique(&self) -> StegoTechnique {
193        StegoTechnique::LsbImage
194    }
195
196    fn capacity(&self, cover: &CoverMedia) -> Result<Capacity, StegoError> {
197        // Only PNG and BMP are supported
198        match cover.kind {
199            CoverMediaKind::PngImage | CoverMediaKind::BmpImage => {}
200            _ => {
201                return Err(StegoError::UnsupportedCoverType {
202                    reason: format!("LSB image requires PNG or BMP, got {:?}", cover.kind),
203                });
204            }
205        }
206
207        // Parse dimensions from metadata
208        let width: u32 = cover
209            .metadata
210            .get("width")
211            .ok_or_else(|| StegoError::MalformedCoverData {
212                reason: "missing width metadata".to_string(),
213            })?
214            .parse()
215            .map_err(
216                |e: std::num::ParseIntError| StegoError::MalformedCoverData {
217                    reason: format!("invalid width: {e}"),
218                },
219            )?;
220
221        let height: u32 = cover
222            .metadata
223            .get("height")
224            .ok_or_else(|| StegoError::MalformedCoverData {
225                reason: "missing height metadata".to_string(),
226            })?
227            .parse()
228            .map_err(
229                |e: std::num::ParseIntError| StegoError::MalformedCoverData {
230                    reason: format!("invalid height: {e}"),
231                },
232            )?;
233
234        let pixel_count =
235            width
236                .checked_mul(height)
237                .ok_or_else(|| StegoError::MalformedCoverData {
238                    reason: "pixel count overflow".to_string(),
239                })?;
240
241        // Capacity: 3 bits per pixel (R, G, B), minus 32 bits for header
242        // = (pixel_count * 3 - 32) / 8 bytes
243        let bits = pixel_count
244            .checked_mul(3)
245            .and_then(|b| b.checked_sub(32))
246            .ok_or_else(|| StegoError::MalformedCoverData {
247                reason: "capacity calculation overflow".to_string(),
248            })?;
249
250        let bytes = u64::from(bits / 8);
251
252        Ok(Capacity {
253            bytes,
254            technique: StegoTechnique::LsbImage,
255        })
256    }
257
258    fn embed(&self, mut cover: CoverMedia, payload: &Payload) -> Result<CoverMedia, StegoError> {
259        // Check cover type
260        match cover.kind {
261            CoverMediaKind::PngImage | CoverMediaKind::BmpImage => {}
262            _ => {
263                return Err(StegoError::UnsupportedCoverType {
264                    reason: format!("LSB image requires PNG or BMP, got {:?}", cover.kind),
265                });
266            }
267        }
268
269        // Check capacity
270        let cap = self.capacity(&cover)?;
271        let payload_len = payload.as_bytes().len() as u64;
272        if payload_len > cap.bytes {
273            return Err(StegoError::PayloadTooLarge {
274                needed: payload_len,
275                available: cap.bytes,
276            });
277        }
278
279        // Check that payload length fits in 32-bit header
280        if payload_len > u64::from(u32::MAX) {
281            return Err(StegoError::PayloadTooLarge {
282                needed: payload_len,
283                available: u64::from(u32::MAX),
284            });
285        }
286
287        // Get mutable access to pixel data
288        let data = cover.data.to_vec();
289        let mut pixels = data;
290
291        // Embed 32-bit big-endian payload length in first 32 LSBs
292        #[expect(
293            clippy::cast_possible_truncation,
294            reason = "checked above: payload_len <= u32::MAX"
295        )]
296        let len_bytes = (payload_len as u32).to_be_bytes();
297        for (byte_idx, byte) in len_bytes.iter().enumerate() {
298            for bit_idx in 0..8 {
299                let bit = (byte >> (7 - bit_idx)) & 1;
300                let pixel_idx = byte_idx * 8 + bit_idx;
301
302                // RGB only, skip alpha (every 4th byte)
303                let channel_idx = pixel_idx / 3;
304                let rgb_offset = pixel_idx % 3;
305                let byte_pos = channel_idx * 4 + rgb_offset;
306
307                let pixel =
308                    pixels
309                        .get_mut(byte_pos)
310                        .ok_or_else(|| StegoError::MalformedCoverData {
311                            reason: "pixel index out of bounds".to_string(),
312                        })?;
313                *pixel = (*pixel & 0xFE) | bit;
314            }
315        }
316
317        // Embed payload bits starting after header (32 bits)
318        let payload_bytes = payload.as_bytes();
319        for (byte_idx, byte) in payload_bytes.iter().enumerate() {
320            for bit_idx in 0..8 {
321                let bit = (byte >> (7 - bit_idx)) & 1;
322                let pixel_idx = 32 + byte_idx * 8 + bit_idx;
323
324                // RGB only, skip alpha
325                let channel_idx = pixel_idx / 3;
326                let rgb_offset = pixel_idx % 3;
327                let byte_pos = channel_idx * 4 + rgb_offset;
328
329                let pixel =
330                    pixels
331                        .get_mut(byte_pos)
332                        .ok_or_else(|| StegoError::MalformedCoverData {
333                            reason: "pixel index out of bounds".to_string(),
334                        })?;
335                *pixel = (*pixel & 0xFE) | bit;
336            }
337        }
338
339        cover.data = pixels.into();
340        Ok(cover)
341    }
342}
343
344impl ExtractTechnique for LsbImage {
345    fn technique(&self) -> StegoTechnique {
346        StegoTechnique::LsbImage
347    }
348
349    fn extract(&self, cover: &CoverMedia) -> Result<Payload, StegoError> {
350        // Check cover type
351        match cover.kind {
352            CoverMediaKind::PngImage | CoverMediaKind::BmpImage => {}
353            _ => {
354                return Err(StegoError::UnsupportedCoverType {
355                    reason: format!("LSB image requires PNG or BMP, got {:?}", cover.kind),
356                });
357            }
358        }
359
360        let pixels = cover.data.as_ref();
361
362        // Extract 32-bit big-endian payload length from first 32 LSBs
363        let mut len_bytes = [0u8; 4];
364        for (byte_idx, len_byte) in len_bytes.iter_mut().enumerate() {
365            for bit_idx in 0..8 {
366                let pixel_idx = byte_idx * 8 + bit_idx;
367
368                // RGB only, skip alpha
369                let channel_idx = pixel_idx / 3;
370                let rgb_offset = pixel_idx % 3;
371                let byte_pos = channel_idx * 4 + rgb_offset;
372
373                let bit = pixels
374                    .get(byte_pos)
375                    .ok_or_else(|| StegoError::MalformedCoverData {
376                        reason: "pixel index out of bounds".to_string(),
377                    })?
378                    & 1;
379                *len_byte |= bit << (7 - bit_idx);
380            }
381        }
382
383        let payload_len = u32::from_be_bytes(len_bytes) as usize;
384
385        // Extract payload bits
386        let mut payload_bytes = vec![0u8; payload_len];
387        for (byte_idx, payload_byte) in payload_bytes.iter_mut().enumerate() {
388            for bit_idx in 0..8 {
389                let pixel_idx = 32 + byte_idx * 8 + bit_idx;
390
391                // RGB only, skip alpha
392                let channel_idx = pixel_idx / 3;
393                let rgb_offset = pixel_idx % 3;
394                let byte_pos = channel_idx * 4 + rgb_offset;
395
396                let bit = pixels
397                    .get(byte_pos)
398                    .ok_or_else(|| StegoError::MalformedCoverData {
399                        reason: "pixel index out of bounds".to_string(),
400                    })?
401                    & 1;
402                *payload_byte |= bit << (7 - bit_idx);
403            }
404        }
405
406        Ok(Payload::from_bytes(payload_bytes))
407    }
408}
409
410/// DCT-based JPEG steganography adapter (STUB).
411///
412/// **NOT YET IMPLEMENTED**: Requires a pure-Rust JPEG library that exposes
413/// DCT coefficients without unsafe code. Current Rust JPEG libraries either:
414/// - Decode to pixels only (jpeg-decoder, image crate)
415/// - Require unsafe bindings (mozjpeg-sys, libjpeg-turbo-sys)
416///
417/// TODO(T12): Implement DCT coefficient access and modification:
418/// - Parse JPEG to access non-zero AC DCT coefficients
419/// - Embed payload in LSBs of coefficients (skip DC and zeros)
420/// - Preserve quantization and Huffman tables
421/// - Re-encode JPEG with modified coefficients
422#[derive(Debug, Default)]
423pub struct DctJpeg;
424
425impl DctJpeg {
426    /// Create a new DCT JPEG embedder.
427    #[must_use]
428    pub const fn new() -> Self {
429        Self
430    }
431}
432
433impl EmbedTechnique for DctJpeg {
434    fn technique(&self) -> StegoTechnique {
435        StegoTechnique::DctJpeg
436    }
437
438    fn capacity(&self, _cover: &CoverMedia) -> Result<Capacity, StegoError> {
439        Err(StegoError::UnsupportedCoverType {
440            reason: "DCT JPEG steganography not yet implemented (requires DCT coefficient access)"
441                .to_string(),
442        })
443    }
444
445    fn embed(&self, _cover: CoverMedia, _payload: &Payload) -> Result<CoverMedia, StegoError> {
446        Err(StegoError::UnsupportedCoverType {
447            reason: "DCT JPEG steganography not yet implemented (requires DCT coefficient access)"
448                .to_string(),
449        })
450    }
451}
452
453impl ExtractTechnique for DctJpeg {
454    fn technique(&self) -> StegoTechnique {
455        StegoTechnique::DctJpeg
456    }
457
458    fn extract(&self, _cover: &CoverMedia) -> Result<Payload, StegoError> {
459        Err(StegoError::UnsupportedCoverType {
460            reason: "DCT JPEG steganography not yet implemented (requires DCT coefficient access)"
461                .to_string(),
462        })
463    }
464}
465
466/// Palette-based steganography adapter for GIF/PNG indexed images (STUB).
467///
468/// **NOT YET IMPLEMENTED**: Requires palette extraction from indexed color images.
469/// The `image` crate converts all images to RGBA8, losing original palette data.
470///
471/// TODO(T13): Implement palette steganography:
472/// - Extract palette data from GIF/PNG indexed color images
473/// - Store palette as bytes in `CoverMedia.metadata["palette"]`
474/// - Embed payload in LSBs of palette R/G/B bytes
475/// - Capacity: (`palette_size` * 3) / 8 bytes
476/// - Re-encode image with modified palette (pixel indices unchanged)
477/// - Requires format-specific handling (GIF vs indexed PNG)
478#[derive(Debug, Default)]
479pub struct PaletteStego;
480
481impl PaletteStego {
482    /// Create a new palette steganography embedder.
483    #[must_use]
484    pub const fn new() -> Self {
485        Self
486    }
487}
488
489impl EmbedTechnique for PaletteStego {
490    fn technique(&self) -> StegoTechnique {
491        StegoTechnique::Palette
492    }
493
494    fn capacity(&self, _cover: &CoverMedia) -> Result<Capacity, StegoError> {
495        Err(StegoError::UnsupportedCoverType {
496            reason: "Palette steganography not yet implemented (requires palette extraction)"
497                .to_string(),
498        })
499    }
500
501    fn embed(&self, _cover: CoverMedia, _payload: &Payload) -> Result<CoverMedia, StegoError> {
502        Err(StegoError::UnsupportedCoverType {
503            reason: "Palette steganography not yet implemented (requires palette extraction)"
504                .to_string(),
505        })
506    }
507}
508
509impl ExtractTechnique for PaletteStego {
510    fn technique(&self) -> StegoTechnique {
511        StegoTechnique::Palette
512    }
513
514    fn extract(&self, _cover: &CoverMedia) -> Result<Payload, StegoError> {
515        Err(StegoError::UnsupportedCoverType {
516            reason: "Palette steganography not yet implemented (requires palette extraction)"
517                .to_string(),
518        })
519    }
520}
521
522/// LSB audio steganography adapter for WAV files.
523///
524/// Embeds payload in the least significant bits of i16 audio samples.
525/// Header encodes 32-bit big-endian payload length in first 32 sample LSBs.
526#[derive(Debug, Default)]
527pub struct LsbAudio;
528
529impl LsbAudio {
530    /// Create a new LSB audio embedder.
531    #[must_use]
532    pub const fn new() -> Self {
533        Self
534    }
535}
536
537impl EmbedTechnique for LsbAudio {
538    fn technique(&self) -> StegoTechnique {
539        StegoTechnique::LsbAudio
540    }
541
542    fn capacity(&self, cover: &CoverMedia) -> Result<Capacity, StegoError> {
543        // Only WAV audio is supported
544        if cover.kind != CoverMediaKind::WavAudio {
545            return Err(StegoError::UnsupportedCoverType {
546                reason: format!("LSB audio requires WAV, got {:?}", cover.kind),
547            });
548        }
549
550        // Sample count is data length / 2 (i16 = 2 bytes)
551        let sample_count = cover.data.len() / 2;
552
553        // Need at least 32 samples for header
554        if sample_count < 32 {
555            return Err(StegoError::MalformedCoverData {
556                reason: "audio too short for LSB embedding (need at least 32 samples)".to_string(),
557            });
558        }
559
560        // Capacity: (sample_count - 32) / 8 bytes
561        let capacity_bits =
562            sample_count
563                .checked_sub(32)
564                .ok_or_else(|| StegoError::MalformedCoverData {
565                    reason: "capacity calculation underflow".to_string(),
566                })?;
567
568        let bytes = (capacity_bits / 8) as u64;
569
570        Ok(Capacity {
571            bytes,
572            technique: StegoTechnique::LsbAudio,
573        })
574    }
575
576    fn embed(&self, mut cover: CoverMedia, payload: &Payload) -> Result<CoverMedia, StegoError> {
577        // Check cover type
578        if cover.kind != CoverMediaKind::WavAudio {
579            return Err(StegoError::UnsupportedCoverType {
580                reason: format!("LSB audio requires WAV, got {:?}", cover.kind),
581            });
582        }
583
584        // Check capacity
585        let cap = self.capacity(&cover)?;
586        let payload_len = payload.as_bytes().len() as u64;
587        if payload_len > cap.bytes {
588            return Err(StegoError::PayloadTooLarge {
589                needed: payload_len,
590                available: cap.bytes,
591            });
592        }
593
594        // Check that payload length fits in 32-bit header
595        if payload_len > u64::from(u32::MAX) {
596            return Err(StegoError::PayloadTooLarge {
597                needed: payload_len,
598                available: u64::from(u32::MAX),
599            });
600        }
601
602        // Get mutable access to sample data (i16 little-endian)
603        let mut samples = cover.data.to_vec();
604
605        // Embed 32-bit big-endian payload length in first 32 sample LSBs
606        #[expect(
607            clippy::cast_possible_truncation,
608            reason = "checked above: payload_len <= u32::MAX"
609        )]
610        let len_bytes = (payload_len as u32).to_be_bytes();
611        for (byte_idx, byte) in len_bytes.iter().enumerate() {
612            for bit_idx in 0..8 {
613                let bit = (byte >> (7 - bit_idx)) & 1;
614                let sample_idx = byte_idx * 8 + bit_idx;
615
616                // Modify LSB of i16 sample (little-endian)
617                let byte_pos = sample_idx * 2; // i16 = 2 bytes
618                let sample =
619                    samples
620                        .get_mut(byte_pos)
621                        .ok_or_else(|| StegoError::MalformedCoverData {
622                            reason: "sample index out of bounds".to_string(),
623                        })?;
624                *sample = (*sample & 0xFE) | bit;
625            }
626        }
627
628        // Embed payload bits starting after header (32 samples)
629        let payload_bytes = payload.as_bytes();
630        for (byte_idx, byte) in payload_bytes.iter().enumerate() {
631            for bit_idx in 0..8 {
632                let bit = (byte >> (7 - bit_idx)) & 1;
633                let sample_idx = 32 + byte_idx * 8 + bit_idx;
634
635                let byte_pos = sample_idx * 2;
636                let sample =
637                    samples
638                        .get_mut(byte_pos)
639                        .ok_or_else(|| StegoError::MalformedCoverData {
640                            reason: "sample index out of bounds".to_string(),
641                        })?;
642                *sample = (*sample & 0xFE) | bit;
643            }
644        }
645
646        cover.data = samples.into();
647        Ok(cover)
648    }
649}
650
651impl ExtractTechnique for LsbAudio {
652    fn technique(&self) -> StegoTechnique {
653        StegoTechnique::LsbAudio
654    }
655
656    fn extract(&self, cover: &CoverMedia) -> Result<Payload, StegoError> {
657        // Check cover type
658        if cover.kind != CoverMediaKind::WavAudio {
659            return Err(StegoError::UnsupportedCoverType {
660                reason: format!("LSB audio requires WAV, got {:?}", cover.kind),
661            });
662        }
663
664        let samples = cover.data.as_ref();
665
666        // Need at least 32 samples for header
667        if samples.len() < 64 {
668            // 32 samples * 2 bytes
669            return Err(StegoError::MalformedCoverData {
670                reason: "audio too short to extract payload".to_string(),
671            });
672        }
673
674        // Extract 32-bit big-endian payload length from first 32 sample LSBs
675        let mut len_bytes = [0u8; 4];
676        for (byte_idx, len_byte) in len_bytes.iter_mut().enumerate() {
677            for bit_idx in 0..8 {
678                let sample_idx = byte_idx * 8 + bit_idx;
679                let byte_pos = sample_idx * 2;
680
681                let bit = samples
682                    .get(byte_pos)
683                    .ok_or_else(|| StegoError::MalformedCoverData {
684                        reason: "sample index out of bounds".to_string(),
685                    })?
686                    & 1;
687                *len_byte |= bit << (7 - bit_idx);
688            }
689        }
690
691        let payload_len = u32::from_be_bytes(len_bytes) as usize;
692
693        // Sanity check payload length
694        let max_samples = samples.len() / 2;
695        if payload_len > (max_samples.saturating_sub(32)) / 8 {
696            return Err(StegoError::MalformedCoverData {
697                reason: format!("invalid payload length: {payload_len}"),
698            });
699        }
700
701        // Extract payload bits
702        let mut payload_bytes = vec![0u8; payload_len];
703        for (byte_idx, payload_byte) in payload_bytes.iter_mut().enumerate() {
704            for bit_idx in 0..8 {
705                let sample_idx = 32 + byte_idx * 8 + bit_idx;
706                let byte_pos = sample_idx * 2;
707
708                let bit = samples
709                    .get(byte_pos)
710                    .ok_or_else(|| StegoError::MalformedCoverData {
711                        reason: "sample index out of bounds".to_string(),
712                    })?
713                    & 1;
714                *payload_byte |= bit << (7 - bit_idx);
715            }
716        }
717
718        Ok(Payload::from_bytes(payload_bytes))
719    }
720}
721
722/// Phase encoding (DSSS) audio steganography adapter (STUB).
723///
724/// **NOT YET IMPLEMENTED**: Requires FFT/IFFT and phase manipulation.
725///
726/// TODO(T14): Implement phase encoding:
727/// - Segment audio into blocks
728/// - Apply FFT to each segment
729/// - Embed one bit per segment by phase shift
730/// - Adaptive alpha: scale shift by segment energy
731/// - Apply IFFT to reconstruct samples
732/// - Requires audio DSP library (rustfft or similar)
733#[derive(Debug, Default)]
734pub struct PhaseEncoding;
735
736impl PhaseEncoding {
737    /// Create a new phase encoding embedder.
738    #[must_use]
739    pub const fn new() -> Self {
740        Self
741    }
742}
743
744impl EmbedTechnique for PhaseEncoding {
745    fn technique(&self) -> StegoTechnique {
746        StegoTechnique::PhaseEncoding
747    }
748
749    fn capacity(&self, _cover: &CoverMedia) -> Result<Capacity, StegoError> {
750        Err(StegoError::UnsupportedCoverType {
751            reason: "Phase encoding not yet implemented (requires FFT/phase manipulation)"
752                .to_string(),
753        })
754    }
755
756    fn embed(&self, _cover: CoverMedia, _payload: &Payload) -> Result<CoverMedia, StegoError> {
757        Err(StegoError::UnsupportedCoverType {
758            reason: "Phase encoding not yet implemented (requires FFT/phase manipulation)"
759                .to_string(),
760        })
761    }
762}
763
764impl ExtractTechnique for PhaseEncoding {
765    fn technique(&self) -> StegoTechnique {
766        StegoTechnique::PhaseEncoding
767    }
768
769    fn extract(&self, _cover: &CoverMedia) -> Result<Payload, StegoError> {
770        Err(StegoError::UnsupportedCoverType {
771            reason: "Phase encoding not yet implemented (requires FFT/phase manipulation)"
772                .to_string(),
773        })
774    }
775}
776
777/// Echo hiding audio steganography adapter (STUB).
778///
779/// **NOT YET IMPLEMENTED**: Requires echo synthesis and autocorrelation.
780///
781/// TODO(T14): Implement echo hiding:
782/// - Two echo delays (d0, d1) for bit 0/1
783/// - Embed by adding delayed echo to audio
784/// - Extract via autocorrelation peak detection
785/// - Use `array_windows` for autocorrelation computation
786/// - Requires audio DSP operations
787#[derive(Debug, Default)]
788pub struct EchoHiding;
789
790impl EchoHiding {
791    /// Create a new echo hiding embedder.
792    #[must_use]
793    pub const fn new() -> Self {
794        Self
795    }
796}
797
798impl EmbedTechnique for EchoHiding {
799    fn technique(&self) -> StegoTechnique {
800        StegoTechnique::EchoHiding
801    }
802
803    fn capacity(&self, _cover: &CoverMedia) -> Result<Capacity, StegoError> {
804        Err(StegoError::UnsupportedCoverType {
805            reason: "Echo hiding not yet implemented (requires echo synthesis and autocorrelation)"
806                .to_string(),
807        })
808    }
809
810    fn embed(&self, _cover: CoverMedia, _payload: &Payload) -> Result<CoverMedia, StegoError> {
811        Err(StegoError::UnsupportedCoverType {
812            reason: "Echo hiding not yet implemented (requires echo synthesis and autocorrelation)"
813                .to_string(),
814        })
815    }
816}
817
818impl ExtractTechnique for EchoHiding {
819    fn technique(&self) -> StegoTechnique {
820        StegoTechnique::EchoHiding
821    }
822
823    fn extract(&self, _cover: &CoverMedia) -> Result<Payload, StegoError> {
824        Err(StegoError::UnsupportedCoverType {
825            reason: "Echo hiding not yet implemented (requires echo synthesis and autocorrelation)"
826                .to_string(),
827        })
828    }
829}
830
831/// Zero-width character text steganography adapter (STUB).
832///
833/// **NOT YET IMPLEMENTED**: Zero-width Unicode characters (ZWSP, ZWNJ, ZWJ, etc.)
834/// have complex grapheme clustering rules that make reliable embedding/extraction
835/// difficult. Format characters can be combined with adjacent characters by the
836/// Unicode grapheme segmentation algorithm in context-dependent ways.
837///
838/// TODO(T15): Implement zero-width text steganography:
839/// - Research Unicode-safe zero-width character pairs that remain separate graphemes
840/// - Consider alternative approaches (variation selectors, combining marks)
841/// - Extensive testing with all Unicode scripts (Arabic, Thai, Devanagari, emoji ZWJ sequences)
842/// - Validate grapheme-cluster safety across all contexts
843#[derive(Debug, Default)]
844pub struct ZeroWidthText;
845
846impl ZeroWidthText {
847    /// Create a new zero-width text embedder.
848    #[must_use]
849    pub const fn new() -> Self {
850        Self
851    }
852}
853
854impl EmbedTechnique for ZeroWidthText {
855    fn technique(&self) -> StegoTechnique {
856        StegoTechnique::ZeroWidthText
857    }
858
859    fn capacity(&self, _cover: &CoverMedia) -> Result<Capacity, StegoError> {
860        Err(StegoError::UnsupportedCoverType {
861            reason: "Zero-width text steganography not yet implemented (Unicode grapheme segmentation complexity)".to_string(),
862        })
863    }
864
865    fn embed(&self, _cover: CoverMedia, _payload: &Payload) -> Result<CoverMedia, StegoError> {
866        Err(StegoError::UnsupportedCoverType {
867            reason: "Zero-width text steganography not yet implemented (Unicode grapheme segmentation complexity)".to_string(),
868        })
869    }
870}
871
872impl ExtractTechnique for ZeroWidthText {
873    fn technique(&self) -> StegoTechnique {
874        StegoTechnique::ZeroWidthText
875    }
876
877    fn extract(&self, _cover: &CoverMedia) -> Result<Payload, StegoError> {
878        Err(StegoError::UnsupportedCoverType {
879            reason: "Zero-width text steganography not yet implemented (Unicode grapheme segmentation complexity)".to_string(),
880        })
881    }
882}
883
884// ─── Dual-Payload Deniable Steganography ─────────────────────────────────────
885
886/// Dual-payload deniable steganography adapter.
887///
888/// Embeds two independent payloads (real and decoy) into a single cover using
889/// key-derived pseudo-random patterns. Each key produces a deterministic but
890/// non-overlapping set of embedding positions, ensuring that:
891///
892/// - Extracting with the primary key yields the real payload
893/// - Extracting with the decoy key yields the decoy payload
894/// - No observer can prove which payload is "real" vs "decoy"
895///
896/// The implementation uses `ChaCha20` PRNG seeded with SHA-256 hashes of the keys
897/// to generate reproducible embedding patterns.
898pub struct DualPayloadEmbedder;
899
900impl Default for DualPayloadEmbedder {
901    fn default() -> Self {
902        Self
903    }
904}
905
906impl DualPayloadEmbedder {
907    /// Create a new dual-payload embedder.
908    #[must_use]
909    pub const fn new() -> Self {
910        Self
911    }
912
913    /// Derive a 32-byte seed from a key and channel using SHA-256.
914    ///
915    /// The channel tag ensures different seeds for primary (channel 0) and decoy (channel 1),
916    /// preventing pattern overlap.
917    fn derive_seed_with_channel(key: &[u8], channel: u8) -> [u8; 32] {
918        let mut hasher = Sha256::new();
919        hasher.update(key);
920        hasher.update([channel]);
921        hasher.finalize().into()
922    }
923
924    /// Generate a pseudo-random permutation of indices for embedding.
925    ///
926    /// Returns a vector of `count` indices in the range `[0, total)`, shuffled
927    /// deterministically based on the `seed`. The indices are NOT sorted, preserving
928    /// the pseudo-random access pattern for better security.
929    fn generate_pattern(seed: [u8; 32], total: usize, count: usize) -> Vec<usize> {
930        let mut rng = ChaCha20Rng::from_seed(seed);
931        let mut indices: Vec<usize> = (0..total).collect();
932        indices.shuffle(&mut rng);
933        indices.truncate(count);
934        indices
935    }
936
937    /// Embed a single payload into the cover at specified bit positions.
938    fn embed_at_positions(
939        cover_data: &mut [u8],
940        payload: &[u8],
941        positions: &[usize],
942    ) -> Result<(), DeniableError> {
943        let payload_bits = payload.len() * 8;
944
945        if payload_bits > positions.len() {
946            return Err(DeniableError::InsufficientCapacity);
947        }
948
949        // Embed each bit of the payload at its designated position
950        for (bit_idx, &pos) in positions.iter().enumerate().take(payload_bits) {
951            let payload_byte_idx = bit_idx / 8;
952            let payload_bit_idx = 7 - (bit_idx % 8); // MSB-first
953            let payload_byte = payload
954                .get(payload_byte_idx)
955                .ok_or(DeniableError::InsufficientCapacity)?;
956            let payload_bit = (payload_byte >> payload_bit_idx) & 1;
957
958            let cover_byte_idx = pos / 8;
959            let cover_bit_idx = pos % 8;
960
961            // Set the LSB at this position
962            let byte = cover_data
963                .get_mut(cover_byte_idx)
964                .ok_or(DeniableError::InsufficientCapacity)?;
965            if payload_bit == 1 {
966                *byte |= 1 << cover_bit_idx;
967            } else {
968                *byte &= !(1 << cover_bit_idx);
969            }
970        }
971
972        Ok(())
973    }
974
975    /// Extract a payload from the cover at specified bit positions.
976    fn extract_from_positions(
977        cover_data: &[u8],
978        positions: &[usize],
979        payload_len: usize,
980    ) -> Result<Vec<u8>, DeniableError> {
981        let payload_bits = payload_len * 8;
982
983        if payload_bits > positions.len() {
984            return Err(DeniableError::ExtractionFailed {
985                reason: "insufficient embedding positions for expected payload length".to_string(),
986            });
987        }
988
989        let mut payload = vec![0u8; payload_len];
990
991        for (bit_idx, &pos) in positions.iter().enumerate().take(payload_bits) {
992            let cover_byte_idx = pos / 8;
993            let cover_bit_idx = pos % 8;
994            let cover_byte =
995                cover_data
996                    .get(cover_byte_idx)
997                    .ok_or_else(|| DeniableError::ExtractionFailed {
998                        reason: "cover byte index out of bounds".to_string(),
999                    })?;
1000            let cover_bit = (cover_byte >> cover_bit_idx) & 1;
1001
1002            let payload_byte_idx = bit_idx / 8;
1003            let payload_bit_idx = 7 - (bit_idx % 8); // MSB-first
1004
1005            if cover_bit == 1 {
1006                let byte = payload.get_mut(payload_byte_idx).ok_or_else(|| {
1007                    DeniableError::ExtractionFailed {
1008                        reason: "payload byte index out of bounds".to_string(),
1009                    }
1010                })?;
1011                *byte |= 1 << payload_bit_idx;
1012            }
1013        }
1014
1015        Ok(payload)
1016    }
1017}
1018
1019impl DeniableEmbedder for DualPayloadEmbedder {
1020    fn embed_dual(
1021        &self,
1022        mut cover: CoverMedia,
1023        pair: &DeniablePayloadPair,
1024        keys: &DeniableKeySet,
1025        _embedder: &dyn EmbedTechnique,
1026    ) -> Result<CoverMedia, DeniableError> {
1027        let cover_bytes = cover.data.len();
1028        let cover_bits = cover_bytes * 8;
1029
1030        // Split cover into two non-overlapping channels to avoid interference
1031        // Channel 0: even bit indices (0, 2, 4, ...)
1032        // Channel 1: odd bit indices (1, 3, 5, ...)
1033        let channel_capacity = cover_bits / 2;
1034
1035        // Calculate required capacity (both payloads + length headers)
1036        // Header: 4 bytes (32 bits) for each payload length
1037        let real_total_bits = (pair.real_payload.len() + 4) * 8;
1038        let decoy_total_bits = (pair.decoy_payload.len() + 4) * 8;
1039
1040        if real_total_bits > channel_capacity || decoy_total_bits > channel_capacity {
1041            return Err(DeniableError::InsufficientCapacity);
1042        }
1043
1044        // Derive channel-tagged seeds
1045        // Primary key uses channel 0 (even bits)
1046        // Decoy key uses channel 1 (odd bits)
1047        let primary_seed = Self::derive_seed_with_channel(&keys.primary_key, 0);
1048        let decoy_seed = Self::derive_seed_with_channel(&keys.decoy_key, 1);
1049
1050        // Generate patterns within each channel
1051        // Channel 0: select from even-indexed bits
1052        let primary_positions =
1053            Self::generate_pattern(primary_seed, channel_capacity, real_total_bits)
1054                .into_iter()
1055                .map(|i| i * 2) // Map to even indices
1056                .collect::<Vec<_>>();
1057
1058        // Channel 1: select from odd-indexed bits
1059        let decoy_positions =
1060            Self::generate_pattern(decoy_seed, channel_capacity, decoy_total_bits)
1061                .into_iter()
1062                .map(|i| i * 2 + 1) // Map to odd indices
1063                .collect::<Vec<_>>();
1064
1065        // Prepare payloads with length headers (32-bit big-endian)
1066        let real_len = pair.real_payload.len();
1067        let decoy_len = pair.decoy_payload.len();
1068
1069        #[expect(
1070            clippy::cast_possible_truncation,
1071            reason = "payload size checked against u32::MAX in capacity validation"
1072        )]
1073        let mut real_with_header = (real_len as u32).to_be_bytes().to_vec();
1074        real_with_header.extend_from_slice(&pair.real_payload);
1075
1076        #[expect(
1077            clippy::cast_possible_truncation,
1078            reason = "payload size checked against u32::MAX in capacity validation"
1079        )]
1080        let mut decoy_with_header = (decoy_len as u32).to_be_bytes().to_vec();
1081        decoy_with_header.extend_from_slice(&pair.decoy_payload);
1082
1083        // Get mutable access to cover data
1084        let mut cover_data = cover.data.to_vec();
1085
1086        // Embed both payloads (guaranteed non-overlapping due to channel separation)
1087        Self::embed_at_positions(&mut cover_data, &real_with_header, &primary_positions).map_err(
1088            |e| DeniableError::EmbedFailed {
1089                reason: format!("real payload embed failed: {e}"),
1090            },
1091        )?;
1092
1093        Self::embed_at_positions(&mut cover_data, &decoy_with_header, &decoy_positions).map_err(
1094            |e| DeniableError::EmbedFailed {
1095                reason: format!("decoy payload embed failed: {e}"),
1096            },
1097        )?;
1098
1099        cover.data = cover_data.into();
1100        Ok(cover)
1101    }
1102
1103    fn extract_with_key(
1104        &self,
1105        stego: &CoverMedia,
1106        key: &[u8],
1107        _extractor: &dyn ExtractTechnique,
1108    ) -> Result<Payload, DeniableError> {
1109        let cover_bytes = stego.data.len();
1110        let cover_bits = cover_bytes * 8;
1111        let channel_capacity = cover_bits / 2;
1112
1113        // Try both channels - we don't know which one was used for this key
1114        // Channel 0: even bits
1115        // Channel 1: odd bits
1116
1117        for channel in 0..2 {
1118            let seed = Self::derive_seed_with_channel(key, channel);
1119
1120            // Generate pattern for header extraction
1121            let header_bits = 32;
1122            let header_positions = Self::generate_pattern(seed, channel_capacity, header_bits)
1123                .into_iter()
1124                .map(|i| i * 2 + channel as usize) // Map to channel's bit indices
1125                .collect::<Vec<_>>();
1126
1127            if header_positions.len() < header_bits {
1128                continue; // Try next channel
1129            }
1130
1131            // Extract length header
1132            let Ok(header_bytes) =
1133                Self::extract_from_positions(stego.data.as_ref(), &header_positions, 4)
1134            else {
1135                continue; // Try next channel
1136            };
1137
1138            let Ok(header_arr) = <[u8; 4]>::try_from(header_bytes.as_slice()) else {
1139                continue;
1140            };
1141            let payload_len = u32::from_be_bytes(header_arr) as usize;
1142
1143            // Validate payload length is reasonable
1144            // Reject 0-length payloads (likely garbage from wrong channel)
1145            if payload_len == 0 {
1146                continue; // Try next channel
1147            }
1148
1149            let max_payload_len = channel_capacity / 8;
1150            if payload_len > max_payload_len {
1151                continue; // Try next channel
1152            }
1153
1154            // Generate full pattern including header + payload
1155            let total_bits = (payload_len + 4) * 8;
1156            if total_bits > channel_capacity {
1157                continue; // Try next channel
1158            }
1159
1160            let positions = Self::generate_pattern(seed, channel_capacity, total_bits)
1161                .into_iter()
1162                .map(|i| i * 2 + channel as usize)
1163                .collect::<Vec<_>>();
1164
1165            // Extract full payload
1166            let Ok(with_header) =
1167                Self::extract_from_positions(stego.data.as_ref(), &positions, payload_len + 4)
1168            else {
1169                continue; // Try next channel
1170            };
1171
1172            // Verify header matches
1173            let Ok(extracted_arr) = <[u8; 4]>::try_from(with_header.get(..4).unwrap_or_default())
1174            else {
1175                continue;
1176            };
1177            let extracted_header = u32::from_be_bytes(extracted_arr) as usize;
1178
1179            if extracted_header == payload_len {
1180                // Success! Return payload without header
1181                let payload_data = with_header.get(4..).unwrap_or_default();
1182                return Ok(Payload::from_bytes(payload_data.to_vec()));
1183            }
1184        }
1185
1186        // Failed to extract from both channels
1187        Err(DeniableError::ExtractionFailed {
1188            reason: "failed to extract valid payload from either channel".to_string(),
1189        })
1190    }
1191}
1192
1193// TODO(T11): Implement PdfPageStegoService after LsbImage is available
1194// This service will:
1195// - Render PDF pages to PNG images
1196// - RS-encode payload into N shards (N = page count)
1197// - Embed one shard per page using LsbImage
1198// - Rebuild PDF from stego pages
1199
1200#[cfg(test)]
1201mod tests {
1202    use super::*;
1203    use crate::adapters::pdf::PdfProcessorImpl;
1204    use lopdf::{Document, Object, dictionary};
1205
1206    type TestResult = Result<(), Box<dyn std::error::Error>>;
1207
1208    fn create_test_pdf() -> Result<Vec<u8>, Box<dyn std::error::Error>> {
1209        let mut doc = Document::with_version("1.7");
1210        let catalog_pages = doc.new_object_id();
1211        let first_page = doc.new_object_id();
1212
1213        doc.objects.insert(
1214            first_page,
1215            Object::Dictionary(dictionary! {
1216                "Type" => "Page",
1217                "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
1218                "Contents" => Object::Reference((first_page.0 + 1, 0)),
1219            }),
1220        );
1221
1222        // Content stream with many numeric values
1223        let content =
1224            b"BT\n/F1 12 Tf\n100 700 Td\n(Test) Tj\n200 650 Td\n(PDF) Tj\nET\n1 0 0 1 0 0 cm\n";
1225        doc.add_object(lopdf::Stream::new(dictionary! {}, content.to_vec()));
1226
1227        doc.objects.insert(
1228            catalog_pages,
1229            Object::Dictionary(dictionary! {
1230                "Type" => "Pages",
1231                "Kids" => vec![Object::Reference(first_page)],
1232                "Count" => 1,
1233            }),
1234        );
1235
1236        let catalog_id = doc.add_object(dictionary! {
1237            "Type" => "Catalog",
1238            "Pages" => Object::Reference(catalog_pages),
1239        });
1240
1241        doc.trailer.set("Root", Object::Reference(catalog_id));
1242
1243        let mut bytes = Vec::new();
1244        doc.save_to(&mut bytes)?;
1245        Ok(bytes)
1246    }
1247
1248    #[test]
1249    fn test_pdf_content_stream_lsb_roundtrip() -> TestResult {
1250        let processor = Box::new(PdfProcessorImpl::default());
1251        let embedder = PdfContentStreamLsb::new(processor);
1252
1253        let pdf_bytes = create_test_pdf()?;
1254        let cover = CoverMedia {
1255            kind: CoverMediaKind::PdfDocument,
1256            data: pdf_bytes.into(),
1257            metadata: std::collections::HashMap::new(),
1258        };
1259
1260        let payload = Payload::from_bytes(vec![0xAB]); // 1 byte
1261
1262        // Embed
1263        let stego = embedder.embed(cover, &payload)?;
1264
1265        // Extract
1266        let extracted = embedder.extract(&stego)?;
1267        assert_eq!(extracted.as_bytes(), payload.as_bytes());
1268        Ok(())
1269    }
1270
1271    #[test]
1272    fn test_pdf_metadata_embed_roundtrip() -> TestResult {
1273        let processor = Box::new(PdfProcessorImpl::default());
1274        let embedder = PdfMetadataEmbed::new(processor);
1275
1276        let pdf_bytes = create_test_pdf()?;
1277        let cover = CoverMedia {
1278            kind: CoverMediaKind::PdfDocument,
1279            data: pdf_bytes.into(),
1280            metadata: std::collections::HashMap::new(),
1281        };
1282
1283        let payload = Payload::from_bytes(vec![1, 2, 3, 4, 5]); // 5 bytes
1284
1285        // Embed
1286        let stego = embedder.embed(cover, &payload)?;
1287
1288        // Extract
1289        let extracted = embedder.extract(&stego)?;
1290        assert_eq!(extracted.as_bytes(), payload.as_bytes());
1291        Ok(())
1292    }
1293
1294    #[test]
1295    fn test_unsupported_cover_type() {
1296        let processor = Box::new(PdfProcessorImpl::default());
1297        let embedder = PdfContentStreamLsb::new(processor);
1298
1299        let cover = CoverMedia {
1300            kind: CoverMediaKind::PngImage,
1301            data: vec![].into(),
1302            metadata: std::collections::HashMap::new(),
1303        };
1304
1305        let payload = Payload::from_bytes(vec![1, 2, 3]);
1306
1307        let result = embedder.embed(cover, &payload);
1308        assert!(matches!(
1309            result,
1310            Err(StegoError::UnsupportedCoverType { .. })
1311        ));
1312    }
1313
1314    #[test]
1315    fn test_lsb_image_roundtrip_256x256() -> TestResult {
1316        let embedder = LsbImage::new();
1317
1318        // Create 256x256 white RGBA image
1319        let width = 256_u32;
1320        let height = 256_u32;
1321        let pixel_count = width * height;
1322        let data = vec![255u8; (pixel_count * 4) as usize]; // RGBA
1323
1324        let mut metadata = std::collections::HashMap::new();
1325        metadata.insert("width".to_string(), width.to_string());
1326        metadata.insert("height".to_string(), height.to_string());
1327
1328        let cover = CoverMedia {
1329            kind: CoverMediaKind::PngImage,
1330            data: data.into(),
1331            metadata,
1332        };
1333
1334        // 64-byte payload
1335        let payload = Payload::from_bytes(vec![0xAB; 64]);
1336
1337        // Embed
1338        let stego = embedder.embed(cover.clone(), &payload)?;
1339
1340        // Verify pixel changes are ±1
1341        let orig_pixels = cover.data.as_ref();
1342        let stego_pixels = stego.data.as_ref();
1343        for (i, (orig, stego_val)) in orig_pixels.iter().zip(stego_pixels.iter()).enumerate() {
1344            let diff = orig.abs_diff(*stego_val);
1345            assert!(
1346                diff <= 1,
1347                "pixel at index {i} changed by more than 1: {orig} -> {stego_val}"
1348            );
1349        }
1350
1351        // Extract
1352        let extracted = embedder.extract(&stego)?;
1353        assert_eq!(extracted.as_bytes(), payload.as_bytes());
1354        Ok(())
1355    }
1356
1357    #[test]
1358    fn test_lsb_image_capacity_10x10() -> TestResult {
1359        let embedder = LsbImage::new();
1360
1361        let width = 10_u32;
1362        let height = 10_u32;
1363        let pixel_count = width * height;
1364        let data = vec![0u8; (pixel_count * 4) as usize];
1365
1366        let mut metadata = std::collections::HashMap::new();
1367        metadata.insert("width".to_string(), width.to_string());
1368        metadata.insert("height".to_string(), height.to_string());
1369
1370        let cover = CoverMedia {
1371            kind: CoverMediaKind::PngImage,
1372            data: data.into(),
1373            metadata,
1374        };
1375
1376        let cap = embedder.capacity(&cover)?;
1377
1378        // 10x10 = 100 pixels
1379        // 100 * 3 = 300 bits
1380        // 300 - 32 (header) = 268 bits
1381        // 268 / 8 = 33 bytes
1382        assert_eq!(cap.bytes, 33);
1383        assert_eq!(cap.technique, StegoTechnique::LsbImage);
1384        Ok(())
1385    }
1386
1387    #[test]
1388    fn test_lsb_image_insufficient_capacity() {
1389        let embedder = LsbImage::new();
1390
1391        let width = 10_u32;
1392        let height = 10_u32;
1393        let pixel_count = width * height;
1394        let data = vec![0u8; (pixel_count * 4) as usize];
1395
1396        let mut metadata = std::collections::HashMap::new();
1397        metadata.insert("width".to_string(), width.to_string());
1398        metadata.insert("height".to_string(), height.to_string());
1399
1400        let cover = CoverMedia {
1401            kind: CoverMediaKind::PngImage,
1402            data: data.into(),
1403            metadata,
1404        };
1405
1406        // Try to embed 100 bytes (capacity is only 33)
1407        let payload = Payload::from_bytes(vec![0xAB; 100]);
1408
1409        let result = embedder.embed(cover, &payload);
1410        assert!(matches!(result, Err(StegoError::PayloadTooLarge { .. })));
1411    }
1412
1413    #[test]
1414    fn test_lsb_image_bmp_support() -> TestResult {
1415        let embedder = LsbImage::new();
1416
1417        let width = 100_u32;
1418        let height = 100_u32;
1419        let pixel_count = width * height;
1420        let data = vec![128u8; (pixel_count * 4) as usize];
1421
1422        let mut metadata = std::collections::HashMap::new();
1423        metadata.insert("width".to_string(), width.to_string());
1424        metadata.insert("height".to_string(), height.to_string());
1425
1426        let cover = CoverMedia {
1427            kind: CoverMediaKind::BmpImage,
1428            data: data.into(),
1429            metadata,
1430        };
1431
1432        let payload = Payload::from_bytes(vec![1, 2, 3, 4, 5]);
1433
1434        // Embed
1435        let stego = embedder.embed(cover, &payload)?;
1436
1437        // Extract
1438        let extracted = embedder.extract(&stego)?;
1439        assert_eq!(extracted.as_bytes(), payload.as_bytes());
1440        Ok(())
1441    }
1442
1443    #[test]
1444    fn test_dct_jpeg_stub_returns_not_implemented() {
1445        let embedder = DctJpeg::new();
1446
1447        let cover = CoverMedia {
1448            kind: CoverMediaKind::JpegImage,
1449            data: vec![].into(),
1450            metadata: std::collections::HashMap::new(),
1451        };
1452
1453        let payload = Payload::from_bytes(vec![1, 2, 3]);
1454
1455        // Should return UnsupportedCoverType error indicating not implemented
1456        let result = embedder.embed(cover.clone(), &payload);
1457        assert!(matches!(
1458            result,
1459            Err(StegoError::UnsupportedCoverType { .. })
1460        ));
1461
1462        let result = embedder.extract(&cover);
1463        assert!(matches!(
1464            result,
1465            Err(StegoError::UnsupportedCoverType { .. })
1466        ));
1467
1468        let result = embedder.capacity(&cover);
1469        assert!(matches!(
1470            result,
1471            Err(StegoError::UnsupportedCoverType { .. })
1472        ));
1473    }
1474
1475    #[test]
1476    fn test_palette_stego_stub_returns_not_implemented() {
1477        let embedder = PaletteStego::new();
1478
1479        let cover = CoverMedia {
1480            kind: CoverMediaKind::GifImage,
1481            data: vec![].into(),
1482            metadata: std::collections::HashMap::new(),
1483        };
1484
1485        let payload = Payload::from_bytes(vec![1, 2, 3]);
1486
1487        // Should return UnsupportedCoverType error indicating not implemented
1488        let result = embedder.embed(cover.clone(), &payload);
1489        assert!(matches!(
1490            result,
1491            Err(StegoError::UnsupportedCoverType { .. })
1492        ));
1493
1494        let result = embedder.extract(&cover);
1495        assert!(matches!(
1496            result,
1497            Err(StegoError::UnsupportedCoverType { .. })
1498        ));
1499
1500        let result = embedder.capacity(&cover);
1501        assert!(matches!(
1502            result,
1503            Err(StegoError::UnsupportedCoverType { .. })
1504        ));
1505    }
1506
1507    #[test]
1508    fn test_lsb_audio_roundtrip() -> TestResult {
1509        let embedder = LsbAudio::new();
1510
1511        // Create 1s of 44100 Hz 16-bit mono silence (44100 samples)
1512        let sample_rate = 44100;
1513        let sample_count = sample_rate; // 1 second
1514        let mut data = Vec::new();
1515        for _ in 0..sample_count {
1516            data.extend_from_slice(&0_i16.to_le_bytes());
1517        }
1518
1519        let mut metadata = std::collections::HashMap::new();
1520        metadata.insert("sample_rate".to_string(), sample_rate.to_string());
1521        metadata.insert("channels".to_string(), "1".to_string());
1522        metadata.insert("bits_per_sample".to_string(), "16".to_string());
1523
1524        let cover = CoverMedia {
1525            kind: CoverMediaKind::WavAudio,
1526            data: data.into(),
1527            metadata,
1528        };
1529
1530        // 512-byte payload
1531        let payload = Payload::from_bytes(vec![0xAB; 512]);
1532
1533        // Embed
1534        let stego = embedder.embed(cover, &payload)?;
1535
1536        // Extract
1537        let extracted = embedder.extract(&stego)?;
1538        assert_eq!(extracted.as_bytes(), payload.as_bytes());
1539        Ok(())
1540    }
1541
1542    #[test]
1543    fn test_lsb_audio_capacity() -> TestResult {
1544        let embedder = LsbAudio::new();
1545
1546        // 1000 samples
1547        let sample_count = 1000;
1548        let mut data = Vec::new();
1549        for _ in 0..sample_count {
1550            data.extend_from_slice(&0_i16.to_le_bytes());
1551        }
1552
1553        let mut metadata = std::collections::HashMap::new();
1554        metadata.insert("sample_rate".to_string(), "44100".to_string());
1555        metadata.insert("channels".to_string(), "1".to_string());
1556        metadata.insert("bits_per_sample".to_string(), "16".to_string());
1557
1558        let cover = CoverMedia {
1559            kind: CoverMediaKind::WavAudio,
1560            data: data.into(),
1561            metadata,
1562        };
1563
1564        let cap = embedder.capacity(&cover)?;
1565
1566        // 1000 samples - 32 (header) = 968 bits / 8 = 121 bytes
1567        assert_eq!(cap.bytes, 121);
1568        assert_eq!(cap.technique, StegoTechnique::LsbAudio);
1569        Ok(())
1570    }
1571
1572    #[test]
1573    fn test_lsb_audio_insufficient_capacity() {
1574        let embedder = LsbAudio::new();
1575
1576        // 100 samples (very short audio)
1577        let sample_count = 100;
1578        let mut data = Vec::new();
1579        for _ in 0..sample_count {
1580            data.extend_from_slice(&0_i16.to_le_bytes());
1581        }
1582
1583        let mut metadata = std::collections::HashMap::new();
1584        metadata.insert("sample_rate".to_string(), "44100".to_string());
1585        metadata.insert("channels".to_string(), "1".to_string());
1586        metadata.insert("bits_per_sample".to_string(), "16".to_string());
1587
1588        let cover = CoverMedia {
1589            kind: CoverMediaKind::WavAudio,
1590            data: data.into(),
1591            metadata,
1592        };
1593
1594        // Try to embed 100 bytes (capacity is only 8 bytes)
1595        let payload = Payload::from_bytes(vec![0xAB; 100]);
1596
1597        let result = embedder.embed(cover, &payload);
1598        assert!(matches!(result, Err(StegoError::PayloadTooLarge { .. })));
1599    }
1600
1601    #[test]
1602    fn test_phase_encoding_stub_returns_not_implemented() {
1603        let embedder = PhaseEncoding::new();
1604
1605        let mut metadata = std::collections::HashMap::new();
1606        metadata.insert("sample_rate".to_string(), "44100".to_string());
1607        metadata.insert("channels".to_string(), "1".to_string());
1608        metadata.insert("bits_per_sample".to_string(), "16".to_string());
1609
1610        let cover = CoverMedia {
1611            kind: CoverMediaKind::WavAudio,
1612            data: vec![0; 1000].into(),
1613            metadata,
1614        };
1615
1616        let payload = Payload::from_bytes(vec![1, 2, 3]);
1617
1618        let result = embedder.embed(cover.clone(), &payload);
1619        assert!(matches!(
1620            result,
1621            Err(StegoError::UnsupportedCoverType { .. })
1622        ));
1623
1624        let result = embedder.extract(&cover);
1625        assert!(matches!(
1626            result,
1627            Err(StegoError::UnsupportedCoverType { .. })
1628        ));
1629
1630        let result = embedder.capacity(&cover);
1631        assert!(matches!(
1632            result,
1633            Err(StegoError::UnsupportedCoverType { .. })
1634        ));
1635    }
1636
1637    #[test]
1638    fn test_echo_hiding_stub_returns_not_implemented() {
1639        let embedder = EchoHiding::new();
1640
1641        let mut metadata = std::collections::HashMap::new();
1642        metadata.insert("sample_rate".to_string(), "44100".to_string());
1643        metadata.insert("channels".to_string(), "1".to_string());
1644        metadata.insert("bits_per_sample".to_string(), "16".to_string());
1645
1646        let cover = CoverMedia {
1647            kind: CoverMediaKind::WavAudio,
1648            data: vec![0; 1000].into(),
1649            metadata,
1650        };
1651
1652        let payload = Payload::from_bytes(vec![1, 2, 3]);
1653
1654        let result = embedder.embed(cover.clone(), &payload);
1655        assert!(matches!(
1656            result,
1657            Err(StegoError::UnsupportedCoverType { .. })
1658        ));
1659
1660        let result = embedder.extract(&cover);
1661        assert!(matches!(
1662            result,
1663            Err(StegoError::UnsupportedCoverType { .. })
1664        ));
1665
1666        let result = embedder.capacity(&cover);
1667        assert!(matches!(
1668            result,
1669            Err(StegoError::UnsupportedCoverType { .. })
1670        ));
1671    }
1672
1673    #[test]
1674    fn test_zero_width_text_stub_returns_not_implemented() {
1675        let embedder = ZeroWidthText::new();
1676
1677        let cover = CoverMedia {
1678            kind: CoverMediaKind::PlainText,
1679            data: b"Hello, world!".to_vec().into(),
1680            metadata: std::collections::HashMap::new(),
1681        };
1682
1683        let payload = Payload::from_bytes(vec![1, 2, 3]);
1684
1685        let result = embedder.embed(cover.clone(), &payload);
1686        assert!(matches!(
1687            result,
1688            Err(StegoError::UnsupportedCoverType { .. })
1689        ));
1690
1691        let result = embedder.extract(&cover);
1692        assert!(matches!(
1693            result,
1694            Err(StegoError::UnsupportedCoverType { .. })
1695        ));
1696
1697        let result = embedder.capacity(&cover);
1698        assert!(matches!(
1699            result,
1700            Err(StegoError::UnsupportedCoverType { .. })
1701        ));
1702    }
1703
1704    #[test]
1705    fn test_dual_payload_roundtrip() -> TestResult {
1706        let embedder = DualPayloadEmbedder::new();
1707        let lsb_image = LsbImage::new();
1708
1709        // Create a 100x100 pixel RGB image (30000 bytes)
1710        let width = 100u32;
1711        let height = 100u32;
1712        let pixel_count = (width * height) as usize;
1713        let data = vec![0u8; pixel_count * 3]; // RGB
1714
1715        let mut metadata = std::collections::HashMap::new();
1716        metadata.insert("width".to_string(), width.to_string());
1717        metadata.insert("height".to_string(), height.to_string());
1718        metadata.insert("channels".to_string(), "3".to_string());
1719
1720        let cover = CoverMedia {
1721            kind: CoverMediaKind::PngImage,
1722            data: data.into(),
1723            metadata,
1724        };
1725
1726        // Create payload pair
1727        let real_payload = b"This is the REAL secret message".to_vec();
1728        let decoy_payload = b"This is just a decoy".to_vec();
1729
1730        let pair = DeniablePayloadPair {
1731            real_payload: real_payload.clone(),
1732            decoy_payload: decoy_payload.clone(),
1733        };
1734
1735        // Create key set
1736        let keys = DeniableKeySet {
1737            primary_key: b"primary_key_12345".to_vec(),
1738            decoy_key: b"decoy_key_67890".to_vec(),
1739        };
1740
1741        // Embed dual payloads
1742        let stego = embedder.embed_dual(cover, &pair, &keys, &lsb_image)?;
1743
1744        // Extract with primary key
1745        let extracted_real = embedder.extract_with_key(&stego, &keys.primary_key, &lsb_image)?;
1746        assert_eq!(extracted_real.as_bytes(), &real_payload);
1747
1748        // Extract with decoy key
1749        let extracted_decoy = embedder.extract_with_key(&stego, &keys.decoy_key, &lsb_image)?;
1750        assert_eq!(extracted_decoy.as_bytes(), &decoy_payload);
1751        Ok(())
1752    }
1753
1754    #[test]
1755    fn test_dual_payload_insufficient_capacity() {
1756        let embedder = DualPayloadEmbedder::new();
1757        let lsb_image = LsbImage::new();
1758
1759        // Create a very small cover (10x10 pixels = 300 bytes)
1760        let width = 10u32;
1761        let height = 10u32;
1762        let pixel_count = (width * height) as usize;
1763        let data = vec![0u8; pixel_count * 3];
1764
1765        let mut metadata = std::collections::HashMap::new();
1766        metadata.insert("width".to_string(), width.to_string());
1767        metadata.insert("height".to_string(), height.to_string());
1768        metadata.insert("channels".to_string(), "3".to_string());
1769
1770        let cover = CoverMedia {
1771            kind: CoverMediaKind::PngImage,
1772            data: data.into(),
1773            metadata,
1774        };
1775
1776        // Create large payloads that won't fit
1777        let real_payload = vec![0u8; 200];
1778        let decoy_payload = vec![0u8; 200];
1779
1780        let pair = DeniablePayloadPair {
1781            real_payload,
1782            decoy_payload,
1783        };
1784
1785        let keys = DeniableKeySet {
1786            primary_key: b"primary_key".to_vec(),
1787            decoy_key: b"decoy_key".to_vec(),
1788        };
1789
1790        // Should fail with InsufficientCapacity
1791        let result = embedder.embed_dual(cover, &pair, &keys, &lsb_image);
1792        assert!(matches!(result, Err(DeniableError::InsufficientCapacity)));
1793    }
1794
1795    #[test]
1796    fn test_dual_payload_different_keys_produce_different_results() -> TestResult {
1797        let embedder = DualPayloadEmbedder::new();
1798        let lsb_image = LsbImage::new();
1799
1800        // Create cover
1801        let width = 100u32;
1802        let height = 100u32;
1803        let pixel_count = (width * height) as usize;
1804        let data = vec![0u8; pixel_count * 3];
1805
1806        let mut metadata = std::collections::HashMap::new();
1807        metadata.insert("width".to_string(), width.to_string());
1808        metadata.insert("height".to_string(), height.to_string());
1809        metadata.insert("channels".to_string(), "3".to_string());
1810
1811        let cover = CoverMedia {
1812            kind: CoverMediaKind::PngImage,
1813            data: data.into(),
1814            metadata,
1815        };
1816
1817        let real_payload = b"Real secret".to_vec();
1818        let decoy_payload = b"Fake data".to_vec();
1819
1820        let pair = DeniablePayloadPair {
1821            real_payload: real_payload.clone(),
1822            decoy_payload: decoy_payload.clone(),
1823        };
1824
1825        let keys = DeniableKeySet {
1826            primary_key: b"key1".to_vec(),
1827            decoy_key: b"key2".to_vec(),
1828        };
1829
1830        let stego = embedder.embed_dual(cover, &pair, &keys, &lsb_image)?;
1831
1832        // Extract with primary key
1833        let extracted1 = embedder.extract_with_key(&stego, &keys.primary_key, &lsb_image)?;
1834
1835        // Extract with decoy key
1836        let extracted2 = embedder.extract_with_key(&stego, &keys.decoy_key, &lsb_image)?;
1837
1838        // Should be different
1839        assert_ne!(extracted1.as_bytes(), extracted2.as_bytes());
1840        assert_eq!(extracted1.as_bytes(), &real_payload);
1841        assert_eq!(extracted2.as_bytes(), &decoy_payload);
1842        Ok(())
1843    }
1844
1845    #[test]
1846    fn test_dual_payload_wrong_key_produces_garbage() -> TestResult {
1847        let embedder = DualPayloadEmbedder::new();
1848        let lsb_image = LsbImage::new();
1849
1850        // Create cover
1851        let width = 100u32;
1852        let height = 100u32;
1853        let pixel_count = (width * height) as usize;
1854        let data = vec![0u8; pixel_count * 3];
1855
1856        let mut metadata = std::collections::HashMap::new();
1857        metadata.insert("width".to_string(), width.to_string());
1858        metadata.insert("height".to_string(), height.to_string());
1859        metadata.insert("channels".to_string(), "3".to_string());
1860
1861        let cover = CoverMedia {
1862            kind: CoverMediaKind::PngImage,
1863            data: data.into(),
1864            metadata,
1865        };
1866
1867        let real_payload = b"Real secret message".to_vec();
1868        let decoy_payload = b"Decoy message".to_vec();
1869
1870        let pair = DeniablePayloadPair {
1871            real_payload: real_payload.clone(),
1872            decoy_payload: decoy_payload.clone(),
1873        };
1874
1875        let keys = DeniableKeySet {
1876            primary_key: b"correct_primary".to_vec(),
1877            decoy_key: b"correct_decoy".to_vec(),
1878        };
1879
1880        let stego = embedder.embed_dual(cover, &pair, &keys, &lsb_image)?;
1881
1882        // Try extracting with a wrong key
1883        let wrong_key = b"wrong_key";
1884        let result = embedder.extract_with_key(&stego, wrong_key, &lsb_image);
1885
1886        // Extraction may succeed but produce garbage, or may fail with ExtractionFailed
1887        // depending on what the garbage header decodes to
1888        match result {
1889            Ok(extracted) => {
1890                // If it succeeds, the extracted data should not match either payload
1891                assert_ne!(extracted.as_bytes(), &real_payload);
1892                assert_ne!(extracted.as_bytes(), &decoy_payload);
1893            }
1894            Err(DeniableError::ExtractionFailed { .. }) => {
1895                // This is also acceptable - garbage header caused extraction to fail
1896            }
1897            Err(e) => return Err(e.into()),
1898        }
1899        Ok(())
1900    }
1901
1902    // ─── Additional edge-case stego tests ─────────────────────────────────
1903
1904    #[test]
1905    fn test_lsb_image_wrong_cover_type_embed() {
1906        let embedder = LsbImage::new();
1907        let cover = CoverMedia {
1908            kind: CoverMediaKind::WavAudio,
1909            data: vec![0u8; 100].into(),
1910            metadata: std::collections::HashMap::new(),
1911        };
1912        let payload = Payload::from_bytes(vec![1, 2, 3]);
1913        let result = embedder.embed(cover, &payload);
1914        assert!(matches!(
1915            result,
1916            Err(StegoError::UnsupportedCoverType { .. })
1917        ));
1918    }
1919
1920    #[test]
1921    fn test_lsb_image_wrong_cover_type_extract() {
1922        let embedder = LsbImage::new();
1923        let cover = CoverMedia {
1924            kind: CoverMediaKind::JpegImage,
1925            data: vec![0u8; 100].into(),
1926            metadata: std::collections::HashMap::new(),
1927        };
1928        let result = embedder.extract(&cover);
1929        assert!(matches!(
1930            result,
1931            Err(StegoError::UnsupportedCoverType { .. })
1932        ));
1933    }
1934
1935    #[test]
1936    fn test_lsb_image_missing_width_metadata() {
1937        let embedder = LsbImage::new();
1938        let mut metadata = std::collections::HashMap::new();
1939        metadata.insert("height".to_string(), "100".to_string());
1940        metadata.insert("channels".to_string(), "3".to_string());
1941        let cover = CoverMedia {
1942            kind: CoverMediaKind::PngImage,
1943            data: vec![0u8; 30000].into(),
1944            metadata,
1945        };
1946        let result = embedder.capacity(&cover);
1947        assert!(matches!(result, Err(StegoError::MalformedCoverData { .. })));
1948    }
1949
1950    #[test]
1951    fn test_lsb_image_missing_height_metadata() {
1952        let embedder = LsbImage::new();
1953        let mut metadata = std::collections::HashMap::new();
1954        metadata.insert("width".to_string(), "100".to_string());
1955        metadata.insert("channels".to_string(), "3".to_string());
1956        let cover = CoverMedia {
1957            kind: CoverMediaKind::PngImage,
1958            data: vec![0u8; 30000].into(),
1959            metadata,
1960        };
1961        let result = embedder.capacity(&cover);
1962        assert!(matches!(result, Err(StegoError::MalformedCoverData { .. })));
1963    }
1964
1965    #[test]
1966    fn test_lsb_image_invalid_width_metadata() {
1967        let embedder = LsbImage::new();
1968        let mut metadata = std::collections::HashMap::new();
1969        metadata.insert("width".to_string(), "not_a_number".to_string());
1970        metadata.insert("height".to_string(), "100".to_string());
1971        metadata.insert("channels".to_string(), "3".to_string());
1972        let cover = CoverMedia {
1973            kind: CoverMediaKind::PngImage,
1974            data: vec![0u8; 30000].into(),
1975            metadata,
1976        };
1977        let result = embedder.capacity(&cover);
1978        assert!(matches!(result, Err(StegoError::MalformedCoverData { .. })));
1979    }
1980
1981    #[test]
1982    fn test_lsb_image_wrong_cover_type_capacity() {
1983        let embedder = LsbImage::new();
1984        let cover = CoverMedia {
1985            kind: CoverMediaKind::GifImage,
1986            data: vec![0u8; 100].into(),
1987            metadata: std::collections::HashMap::new(),
1988        };
1989        let result = embedder.capacity(&cover);
1990        assert!(matches!(
1991            result,
1992            Err(StegoError::UnsupportedCoverType { .. })
1993        ));
1994    }
1995
1996    #[test]
1997    fn test_lsb_audio_wrong_cover_type() {
1998        let embedder = LsbAudio::new();
1999        let cover = CoverMedia {
2000            kind: CoverMediaKind::PngImage,
2001            data: vec![0u8; 1000].into(),
2002            metadata: std::collections::HashMap::new(),
2003        };
2004        let payload = Payload::from_bytes(vec![1, 2, 3]);
2005        let result = embedder.embed(cover, &payload);
2006        assert!(matches!(
2007            result,
2008            Err(StegoError::UnsupportedCoverType { .. })
2009        ));
2010    }
2011
2012    #[test]
2013    fn test_lsb_audio_wrong_cover_type_extract() {
2014        let embedder = LsbAudio::new();
2015        let cover = CoverMedia {
2016            kind: CoverMediaKind::PngImage,
2017            data: vec![0u8; 1000].into(),
2018            metadata: std::collections::HashMap::new(),
2019        };
2020        let result = embedder.extract(&cover);
2021        assert!(matches!(
2022            result,
2023            Err(StegoError::UnsupportedCoverType { .. })
2024        ));
2025    }
2026
2027    #[test]
2028    fn test_lsb_audio_wrong_cover_type_capacity() {
2029        let embedder = LsbAudio::new();
2030        let cover = CoverMedia {
2031            kind: CoverMediaKind::PngImage,
2032            data: vec![0u8; 1000].into(),
2033            metadata: std::collections::HashMap::new(),
2034        };
2035        let result = embedder.capacity(&cover);
2036        assert!(matches!(
2037            result,
2038            Err(StegoError::UnsupportedCoverType { .. })
2039        ));
2040    }
2041
2042    #[test]
2043    fn test_pdf_content_stream_wrong_cover_type() {
2044        let processor = Box::new(PdfProcessorImpl::default());
2045        let embedder = PdfContentStreamLsb::new(processor);
2046        let cover = CoverMedia {
2047            kind: CoverMediaKind::PngImage,
2048            data: vec![0u8; 100].into(),
2049            metadata: std::collections::HashMap::new(),
2050        };
2051        let payload = Payload::from_bytes(vec![1, 2, 3]);
2052        let result = embedder.embed(cover, &payload);
2053        assert!(matches!(
2054            result,
2055            Err(StegoError::UnsupportedCoverType { .. })
2056        ));
2057    }
2058
2059    #[test]
2060    fn test_pdf_metadata_wrong_cover_type() {
2061        let processor = Box::new(PdfProcessorImpl::default());
2062        let embedder = PdfMetadataEmbed::new(processor);
2063        let cover = CoverMedia {
2064            kind: CoverMediaKind::PngImage,
2065            data: vec![0u8; 100].into(),
2066            metadata: std::collections::HashMap::new(),
2067        };
2068        let payload = Payload::from_bytes(vec![1, 2, 3]);
2069        let result = embedder.embed(cover, &payload);
2070        assert!(matches!(
2071            result,
2072            Err(StegoError::UnsupportedCoverType { .. })
2073        ));
2074    }
2075
2076    #[test]
2077    fn test_pdf_content_stream_extract_wrong_cover_type() {
2078        let processor = Box::new(PdfProcessorImpl::default());
2079        let embedder = PdfContentStreamLsb::new(processor);
2080        let cover = CoverMedia {
2081            kind: CoverMediaKind::WavAudio,
2082            data: vec![0u8; 100].into(),
2083            metadata: std::collections::HashMap::new(),
2084        };
2085        let result = embedder.extract(&cover);
2086        assert!(matches!(
2087            result,
2088            Err(StegoError::UnsupportedCoverType { .. })
2089        ));
2090    }
2091
2092    #[test]
2093    fn test_pdf_metadata_extract_wrong_cover_type() {
2094        let processor = Box::new(PdfProcessorImpl::default());
2095        let embedder = PdfMetadataEmbed::new(processor);
2096        let cover = CoverMedia {
2097            kind: CoverMediaKind::WavAudio,
2098            data: vec![0u8; 100].into(),
2099            metadata: std::collections::HashMap::new(),
2100        };
2101        let result = embedder.extract(&cover);
2102        assert!(matches!(
2103            result,
2104            Err(StegoError::UnsupportedCoverType { .. })
2105        ));
2106    }
2107
2108    // ─── Stub technique tests ─────────────────────────────────────────────
2109
2110    fn dummy_cover() -> CoverMedia {
2111        CoverMedia {
2112            kind: CoverMediaKind::PngImage,
2113            data: vec![0u8; 64].into(),
2114            metadata: std::collections::HashMap::new(),
2115        }
2116    }
2117
2118    #[test]
2119    fn dct_jpeg_stub_capacity_returns_error() {
2120        let dct = DctJpeg::new();
2121        assert!(dct.capacity(&dummy_cover()).is_err());
2122        assert_eq!(EmbedTechnique::technique(&dct), StegoTechnique::DctJpeg);
2123    }
2124
2125    #[test]
2126    fn dct_jpeg_stub_embed_returns_error() {
2127        let dct = DctJpeg::new();
2128        let result = dct.embed(dummy_cover(), &Payload::from_bytes(vec![1]));
2129        assert!(matches!(
2130            result,
2131            Err(StegoError::UnsupportedCoverType { .. })
2132        ));
2133    }
2134
2135    #[test]
2136    fn dct_jpeg_stub_extract_returns_error() {
2137        let dct = DctJpeg::new();
2138        let result = dct.extract(&dummy_cover());
2139        assert!(matches!(
2140            result,
2141            Err(StegoError::UnsupportedCoverType { .. })
2142        ));
2143        assert_eq!(ExtractTechnique::technique(&dct), StegoTechnique::DctJpeg);
2144    }
2145
2146    #[test]
2147    fn palette_stego_stub_capacity_returns_error() {
2148        let pal = PaletteStego::new();
2149        assert!(pal.capacity(&dummy_cover()).is_err());
2150        assert_eq!(EmbedTechnique::technique(&pal), StegoTechnique::Palette);
2151    }
2152
2153    #[test]
2154    fn palette_stego_stub_embed_returns_error() {
2155        let pal = PaletteStego::new();
2156        let result = pal.embed(dummy_cover(), &Payload::from_bytes(vec![1]));
2157        assert!(matches!(
2158            result,
2159            Err(StegoError::UnsupportedCoverType { .. })
2160        ));
2161    }
2162
2163    #[test]
2164    fn palette_stego_stub_extract_returns_error() {
2165        let pal = PaletteStego::new();
2166        let result = pal.extract(&dummy_cover());
2167        assert!(matches!(
2168            result,
2169            Err(StegoError::UnsupportedCoverType { .. })
2170        ));
2171        assert_eq!(ExtractTechnique::technique(&pal), StegoTechnique::Palette);
2172    }
2173
2174    #[test]
2175    fn phase_encoding_stub_capacity_returns_error() {
2176        let pe = PhaseEncoding::new();
2177        assert!(pe.capacity(&dummy_cover()).is_err());
2178        assert_eq!(
2179            EmbedTechnique::technique(&pe),
2180            StegoTechnique::PhaseEncoding
2181        );
2182    }
2183
2184    #[test]
2185    fn phase_encoding_stub_embed_returns_error() {
2186        let pe = PhaseEncoding::new();
2187        let result = pe.embed(dummy_cover(), &Payload::from_bytes(vec![1]));
2188        assert!(matches!(
2189            result,
2190            Err(StegoError::UnsupportedCoverType { .. })
2191        ));
2192    }
2193
2194    #[test]
2195    fn phase_encoding_stub_extract_returns_error() {
2196        let pe = PhaseEncoding::new();
2197        let result = pe.extract(&dummy_cover());
2198        assert!(matches!(
2199            result,
2200            Err(StegoError::UnsupportedCoverType { .. })
2201        ));
2202        assert_eq!(
2203            ExtractTechnique::technique(&pe),
2204            StegoTechnique::PhaseEncoding
2205        );
2206    }
2207
2208    #[test]
2209    fn echo_hiding_stub_capacity_returns_error() {
2210        let eh = EchoHiding::new();
2211        assert!(eh.capacity(&dummy_cover()).is_err());
2212        assert_eq!(EmbedTechnique::technique(&eh), StegoTechnique::EchoHiding);
2213    }
2214
2215    #[test]
2216    fn echo_hiding_stub_embed_returns_error() {
2217        let eh = EchoHiding::new();
2218        let result = eh.embed(dummy_cover(), &Payload::from_bytes(vec![1]));
2219        assert!(matches!(
2220            result,
2221            Err(StegoError::UnsupportedCoverType { .. })
2222        ));
2223    }
2224
2225    #[test]
2226    fn echo_hiding_stub_extract_returns_error() {
2227        let eh = EchoHiding::new();
2228        let result = eh.extract(&dummy_cover());
2229        assert!(matches!(
2230            result,
2231            Err(StegoError::UnsupportedCoverType { .. })
2232        ));
2233        assert_eq!(ExtractTechnique::technique(&eh), StegoTechnique::EchoHiding);
2234    }
2235
2236    #[test]
2237    fn zero_width_text_stub_capacity_returns_error() {
2238        let zwt = ZeroWidthText::new();
2239        assert!(zwt.capacity(&dummy_cover()).is_err());
2240        assert_eq!(
2241            EmbedTechnique::technique(&zwt),
2242            StegoTechnique::ZeroWidthText
2243        );
2244    }
2245
2246    #[test]
2247    fn zero_width_text_stub_embed_returns_error() {
2248        let zwt = ZeroWidthText::new();
2249        let result = zwt.embed(dummy_cover(), &Payload::from_bytes(vec![1]));
2250        assert!(matches!(
2251            result,
2252            Err(StegoError::UnsupportedCoverType { .. })
2253        ));
2254    }
2255
2256    #[test]
2257    fn zero_width_text_stub_extract_returns_error() {
2258        let zwt = ZeroWidthText::new();
2259        let result = zwt.extract(&dummy_cover());
2260        assert!(matches!(
2261            result,
2262            Err(StegoError::UnsupportedCoverType { .. })
2263        ));
2264        assert_eq!(
2265            ExtractTechnique::technique(&zwt),
2266            StegoTechnique::ZeroWidthText
2267        );
2268    }
2269
2270    #[test]
2271    fn lsb_image_insufficient_capacity() {
2272        let embedder = LsbImage::new();
2273        let mut metadata = std::collections::HashMap::new();
2274        metadata.insert("width".to_string(), "2".to_string());
2275        metadata.insert("height".to_string(), "2".to_string());
2276        metadata.insert("channels".to_string(), "3".to_string());
2277        let cover = CoverMedia {
2278            kind: CoverMediaKind::PngImage,
2279            // 12 bytes = 2x2x3 pixels, capacity = 12/8 = 1 byte minus header
2280            data: vec![0u8; 12].into(),
2281            metadata,
2282        };
2283        // Payload larger than capacity
2284        let payload = Payload::from_bytes(vec![0xAB; 100]);
2285        let result = embedder.embed(cover, &payload);
2286        assert!(result.is_err());
2287    }
2288
2289    #[test]
2290    fn lsb_audio_insufficient_capacity() {
2291        let embedder = LsbAudio::new();
2292        let mut metadata = std::collections::HashMap::new();
2293        metadata.insert("bits_per_sample".to_string(), "16".to_string());
2294        let cover = CoverMedia {
2295            kind: CoverMediaKind::WavAudio,
2296            // Very little audio data — only a few samples
2297            data: vec![0u8; 10].into(),
2298            metadata,
2299        };
2300        let payload = Payload::from_bytes(vec![0xAB; 100]);
2301        let result = embedder.embed(cover, &payload);
2302        assert!(result.is_err());
2303    }
2304}