shadowforge_lib/adapters/
analysis.rs1use 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
10pub struct CapacityAnalyserImpl;
12
13impl Default for CapacityAnalyserImpl {
14 fn default() -> Self {
15 Self
16 }
17}
18
19impl CapacityAnalyserImpl {
20 #[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 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 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}