Skip to main content

shadowforge_lib/adapters/
analysis.rs

1//! Adapter implementing the [`CapacityAnalyser`] port.
2
3use crate::domain::analysis::{
4    chi_square_score, classify_risk, estimate_capacity, recommended_payload,
5};
6use crate::domain::errors::AnalysisError;
7use crate::domain::ports::CapacityAnalyser;
8use crate::domain::types::{AnalysisReport, Capacity, CoverMedia, StegoTechnique};
9
10/// Concrete [`CapacityAnalyser`] implementation.
11pub struct CapacityAnalyserImpl;
12
13impl Default for CapacityAnalyserImpl {
14    fn default() -> Self {
15        Self
16    }
17}
18
19impl CapacityAnalyserImpl {
20    /// Create a new analyser.
21    #[must_use]
22    pub const fn new() -> Self {
23        Self
24    }
25}
26
27impl CapacityAnalyser for CapacityAnalyserImpl {
28    fn analyse(
29        &self,
30        cover: &CoverMedia,
31        technique: StegoTechnique,
32    ) -> Result<AnalysisReport, AnalysisError> {
33        let cap_bytes = estimate_capacity(cover, technique);
34        if cap_bytes == 0 {
35            return Err(AnalysisError::UnsupportedCoverType {
36                reason: format!("{:?} is not compatible with {:?}", cover.kind, technique),
37            });
38        }
39
40        let chi_sq = chi_square_score(&cover.data);
41        let risk = classify_risk(chi_sq);
42        let recommended = recommended_payload(cap_bytes, risk);
43
44        Ok(AnalysisReport {
45            technique,
46            cover_capacity: Capacity {
47                bytes: cap_bytes,
48                technique,
49            },
50            chi_square_score: chi_sq,
51            detectability_risk: risk,
52            recommended_max_payload_bytes: recommended,
53        })
54    }
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60    use crate::domain::types::{CoverMediaKind, DetectabilityRisk};
61    use bytes::Bytes;
62    use std::collections::HashMap;
63
64    type TestResult = Result<(), Box<dyn std::error::Error>>;
65
66    fn make_cover(kind: CoverMediaKind, data: Vec<u8>) -> CoverMedia {
67        CoverMedia {
68            kind,
69            data: Bytes::from(data),
70            metadata: HashMap::new(),
71        }
72    }
73
74    #[test]
75    fn analyse_png_lsb_low_risk_for_uniform() -> TestResult {
76        let analyser = CapacityAnalyserImpl::new();
77        // Uniform-ish data for low chi-square
78        let data: Vec<u8> = (0..=255).cycle().take(256 * 40).collect();
79        let cover = make_cover(CoverMediaKind::PngImage, data);
80        let report = analyser.analyse(&cover, StegoTechnique::LsbImage)?;
81
82        assert!(report.cover_capacity.bytes > 0);
83        assert_eq!(report.detectability_risk, DetectabilityRisk::Low);
84        assert!(report.recommended_max_payload_bytes > 0);
85        Ok(())
86    }
87
88    #[test]
89    fn analyse_returns_error_for_incompatible_type() {
90        let analyser = CapacityAnalyserImpl::new();
91        let cover = make_cover(CoverMediaKind::WavAudio, vec![0u8; 1000]);
92        let result = analyser.analyse(&cover, StegoTechnique::LsbImage);
93        assert!(result.is_err());
94    }
95
96    #[test]
97    fn analyse_pdf_content_stream() -> TestResult {
98        let analyser = CapacityAnalyserImpl::new();
99        let cover = make_cover(CoverMediaKind::PdfDocument, vec![0u8; 50_000]);
100        let report = analyser.analyse(&cover, StegoTechnique::PdfContentStream)?;
101        assert!(report.cover_capacity.bytes > 0);
102        Ok(())
103    }
104
105    #[test]
106    fn analyse_corpus_selection_low_risk() -> TestResult {
107        let analyser = CapacityAnalyserImpl::new();
108        let data: Vec<u8> = (0..=255).cycle().take(256 * 32).collect();
109        let cover = make_cover(CoverMediaKind::PngImage, data);
110        let report = analyser.analyse(&cover, StegoTechnique::CorpusSelection)?;
111        // Corpus selection should always be low risk if the cover data is uniform
112        assert_eq!(report.detectability_risk, DetectabilityRisk::Low);
113        Ok(())
114    }
115
116    #[test]
117    fn report_serialises_to_json() -> TestResult {
118        let analyser = CapacityAnalyserImpl::new();
119        let data: Vec<u8> = (0..=255).cycle().take(8192).collect();
120        let cover = make_cover(CoverMediaKind::PngImage, data);
121        let report = analyser.analyse(&cover, StegoTechnique::LsbImage)?;
122        let json = serde_json::to_string(&report)?;
123        assert!(json.contains("\"technique\""));
124        assert!(json.contains("\"chi_square_score\""));
125        Ok(())
126    }
127}