1use serde::{Deserialize, Serialize};
2
3use crate::analysis::{self, CrossCorrMatrix, SourceAnalysis};
4use crate::chaos::{self, ChaosAnalysis};
5use crate::conditioning::{self, MinEntropyReport};
6use crate::trials::{self, TrialAnalysis, TrialConfig};
7use crate::verdict::{self, Verdict};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct AnalysisConfig {
12 pub forensic: bool,
14 pub entropy: bool,
16 pub chaos: bool,
18 pub trials: Option<TrialConfig>,
20 pub cross_correlation: bool,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
26pub enum AnalysisProfile {
27 Quick,
28 Standard,
29 Deep,
30 Security,
31}
32
33impl AnalysisProfile {
34 pub fn to_config(self) -> AnalysisConfig {
35 match self {
36 Self::Quick => AnalysisConfig {
37 forensic: true,
38 entropy: false,
39 chaos: false,
40 trials: None,
41 cross_correlation: false,
42 },
43 Self::Standard => AnalysisConfig {
44 forensic: true,
45 entropy: false,
46 chaos: false,
47 trials: None,
48 cross_correlation: false,
49 },
50 Self::Deep => AnalysisConfig {
51 forensic: true,
52 entropy: true,
53 chaos: true,
54 trials: Some(TrialConfig::default()),
55 cross_correlation: true,
56 },
57 Self::Security => AnalysisConfig {
58 forensic: true,
59 entropy: true,
60 chaos: false,
61 trials: None,
62 cross_correlation: false,
63 },
64 }
65 }
66
67 pub fn parse(s: &str) -> Self {
68 match s.to_lowercase().as_str() {
69 "quick" => Self::Quick,
70 "deep" => Self::Deep,
71 "security" => Self::Security,
72 _ => Self::Standard,
73 }
74 }
75}
76
77impl Default for AnalysisConfig {
78 fn default() -> Self {
79 AnalysisProfile::Standard.to_config()
80 }
81}
82
83#[derive(Debug, Clone, Serialize)]
85pub struct VerdictSummary {
86 #[serde(skip_serializing_if = "Option::is_none")]
87 pub autocorrelation: Option<Verdict>,
88 #[serde(skip_serializing_if = "Option::is_none")]
89 pub spectral: Option<Verdict>,
90 #[serde(skip_serializing_if = "Option::is_none")]
91 pub bias: Option<Verdict>,
92 #[serde(skip_serializing_if = "Option::is_none")]
93 pub distribution: Option<Verdict>,
94 #[serde(skip_serializing_if = "Option::is_none")]
95 pub stationarity: Option<Verdict>,
96 #[serde(skip_serializing_if = "Option::is_none")]
97 pub runs: Option<Verdict>,
98 #[serde(skip_serializing_if = "Option::is_none")]
99 pub hurst: Option<Verdict>,
100 #[serde(skip_serializing_if = "Option::is_none")]
101 pub lyapunov: Option<Verdict>,
102 #[serde(skip_serializing_if = "Option::is_none")]
103 pub correlation_dimension: Option<Verdict>,
104 #[serde(skip_serializing_if = "Option::is_none")]
105 pub bientropy: Option<Verdict>,
106 #[serde(skip_serializing_if = "Option::is_none")]
107 pub compression: Option<Verdict>,
108}
109
110#[derive(Debug, Clone, Serialize)]
112pub struct SourceReport {
113 pub label: String,
114 pub size: usize,
115 #[serde(skip_serializing_if = "Option::is_none")]
116 pub forensic: Option<SourceAnalysis>,
117 #[serde(skip_serializing_if = "Option::is_none")]
118 pub entropy: Option<MinEntropyReport>,
119 #[serde(skip_serializing_if = "Option::is_none")]
120 pub chaos: Option<ChaosAnalysis>,
121 #[serde(skip_serializing_if = "Option::is_none")]
122 pub trials: Option<TrialAnalysis>,
123 pub verdicts: VerdictSummary,
124}
125
126#[derive(Debug, Clone, Serialize)]
128pub struct AnalysisReport {
129 pub config: AnalysisConfig,
130 pub sources: Vec<SourceReport>,
131 #[serde(skip_serializing_if = "Option::is_none")]
132 pub cross_correlation: Option<CrossCorrMatrix>,
133}
134
135pub fn analyze(sources: &[(&str, &[u8])], config: &AnalysisConfig) -> AnalysisReport {
140 let mut source_reports = Vec::with_capacity(sources.len());
141
142 for &(label, data) in sources {
143 let forensic = if config.forensic {
144 Some(analysis::full_analysis(label, data))
145 } else {
146 None
147 };
148
149 let entropy = if config.entropy {
150 Some(conditioning::min_entropy_estimate(data))
151 } else {
152 None
153 };
154
155 let chaos = if config.chaos {
156 Some(chaos::chaos_analysis(data))
157 } else {
158 None
159 };
160
161 let trials = config
162 .trials
163 .as_ref()
164 .map(|tc| trials::trial_analysis(data, tc));
165
166 let verdicts = build_verdicts(forensic.as_ref(), chaos.as_ref());
167
168 source_reports.push(SourceReport {
169 label: label.to_string(),
170 size: data.len(),
171 forensic,
172 entropy,
173 chaos,
174 trials,
175 verdicts,
176 });
177 }
178
179 let cross_correlation = if config.cross_correlation && sources.len() >= 2 {
180 let sources_data: Vec<(String, Vec<u8>)> = sources
181 .iter()
182 .map(|&(label, data)| (label.to_string(), data.to_vec()))
183 .collect();
184 Some(analysis::cross_correlation_matrix(&sources_data))
185 } else {
186 None
187 };
188
189 AnalysisReport {
190 config: config.clone(),
191 sources: source_reports,
192 cross_correlation,
193 }
194}
195
196fn build_verdicts(
197 forensic: Option<&SourceAnalysis>,
198 chaos_result: Option<&ChaosAnalysis>,
199) -> VerdictSummary {
200 let (autocorrelation, spectral, bias, distribution, stationarity, runs) =
201 if let Some(f) = forensic {
202 (
203 Some(verdict::verdict_autocorr(
204 f.autocorrelation.max_abs_correlation,
205 )),
206 Some(verdict::verdict_spectral(f.spectral.flatness)),
207 Some(verdict::verdict_bias(
208 f.bit_bias.overall_bias,
209 f.bit_bias.has_significant_bias,
210 )),
211 Some(verdict::verdict_distribution(f.distribution.ks_p_value)),
212 Some(verdict::verdict_stationarity(
213 f.stationarity.f_statistic,
214 f.stationarity.is_stationary,
215 )),
216 Some(verdict::verdict_runs(&f.runs, f.sample_size)),
217 )
218 } else {
219 (None, None, None, None, None, None)
220 };
221
222 let (hurst, lyapunov, correlation_dimension, bientropy, compression) =
223 if let Some(c) = chaos_result {
224 (
225 Some(verdict::verdict_hurst(c.hurst.hurst_exponent)),
226 Some(verdict::verdict_lyapunov(c.lyapunov.lyapunov_exponent)),
227 Some(verdict::verdict_corrdim(c.correlation_dimension.dimension)),
228 Some(verdict::verdict_bientropy(c.bientropy.bien)),
229 Some(verdict::verdict_compression(c.epiplexity.compression_ratio)),
230 )
231 } else {
232 (None, None, None, None, None)
233 };
234
235 VerdictSummary {
236 autocorrelation,
237 spectral,
238 bias,
239 distribution,
240 stationarity,
241 runs,
242 hurst,
243 lyapunov,
244 correlation_dimension,
245 bientropy,
246 compression,
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253
254 fn test_data() -> Vec<u8> {
255 (0..1000).map(|i| (i % 256) as u8).collect()
256 }
257
258 #[test]
259 fn profile_quick_config() {
260 let config = AnalysisProfile::Quick.to_config();
261 assert!(config.forensic);
262 assert!(!config.entropy);
263 assert!(!config.chaos);
264 assert!(config.trials.is_none());
265 assert!(!config.cross_correlation);
266 }
267
268 #[test]
269 fn profile_deep_config() {
270 let config = AnalysisProfile::Deep.to_config();
271 assert!(config.forensic);
272 assert!(config.entropy);
273 assert!(config.chaos);
274 assert!(config.trials.is_some());
275 assert!(config.cross_correlation);
276 }
277
278 #[test]
279 fn profile_security_config() {
280 let config = AnalysisProfile::Security.to_config();
281 assert!(config.forensic);
282 assert!(config.entropy);
283 assert!(!config.chaos);
284 assert!(config.trials.is_none());
285 assert!(!config.cross_correlation);
286 }
287
288 #[test]
289 fn profile_parse() {
290 assert_eq!(AnalysisProfile::parse("quick"), AnalysisProfile::Quick);
291 assert_eq!(AnalysisProfile::parse("DEEP"), AnalysisProfile::Deep);
292 assert_eq!(
293 AnalysisProfile::parse("security"),
294 AnalysisProfile::Security
295 );
296 assert_eq!(AnalysisProfile::parse("unknown"), AnalysisProfile::Standard);
297 }
298
299 #[test]
300 fn analyze_forensic_only() {
301 let data = test_data();
302 let config = AnalysisConfig {
303 forensic: true,
304 entropy: false,
305 chaos: false,
306 trials: None,
307 cross_correlation: false,
308 };
309 let report = analyze(&[("test", &data)], &config);
310 assert_eq!(report.sources.len(), 1);
311 assert!(report.sources[0].forensic.is_some());
312 assert!(report.sources[0].entropy.is_none());
313 assert!(report.sources[0].chaos.is_none());
314 assert!(report.sources[0].trials.is_none());
315 assert!(report.cross_correlation.is_none());
316 assert!(report.sources[0].verdicts.autocorrelation.is_some());
317 assert!(report.sources[0].verdicts.hurst.is_none());
318 }
319
320 #[test]
321 fn analyze_deep_profile() {
322 let data = test_data();
323 let config = AnalysisProfile::Deep.to_config();
324 let report = analyze(&[("src_a", &data), ("src_b", &data)], &config);
325 assert_eq!(report.sources.len(), 2);
326 assert!(report.sources[0].forensic.is_some());
327 assert!(report.sources[0].entropy.is_some());
328 assert!(report.sources[0].chaos.is_some());
329 assert!(report.sources[0].trials.is_some());
330 assert!(report.cross_correlation.is_some());
331 assert!(report.sources[0].verdicts.autocorrelation.is_some());
332 assert!(report.sources[0].verdicts.hurst.is_some());
333 }
334
335 #[test]
336 fn analyze_no_modules() {
337 let data = test_data();
338 let config = AnalysisConfig {
339 forensic: false,
340 entropy: false,
341 chaos: false,
342 trials: None,
343 cross_correlation: false,
344 };
345 let report = analyze(&[("test", &data)], &config);
346 assert!(report.sources[0].forensic.is_none());
347 assert!(report.sources[0].entropy.is_none());
348 assert!(report.sources[0].chaos.is_none());
349 assert!(report.sources[0].trials.is_none());
350 assert!(report.sources[0].verdicts.autocorrelation.is_none());
351 }
352
353 #[test]
354 fn analyze_cross_correlation_needs_two_sources() {
355 let data = test_data();
356 let config = AnalysisConfig {
357 forensic: false,
358 entropy: false,
359 chaos: false,
360 trials: None,
361 cross_correlation: true,
362 };
363 let report = analyze(&[("test", &data)], &config);
364 assert!(report.cross_correlation.is_none());
365 let report = analyze(&[("a", &data), ("b", &data)], &config);
366 assert!(report.cross_correlation.is_some());
367 }
368
369 #[test]
370 fn default_config_is_standard() {
371 let config = AnalysisConfig::default();
372 let standard = AnalysisProfile::Standard.to_config();
373 assert_eq!(config.forensic, standard.forensic);
374 assert_eq!(config.entropy, standard.entropy);
375 assert_eq!(config.chaos, standard.chaos);
376 assert_eq!(config.cross_correlation, standard.cross_correlation);
377 }
378
379 #[test]
380 fn analysis_report_serializes() {
381 let data = test_data();
382 let config = AnalysisProfile::Quick.to_config();
383 let report = analyze(&[("test", &data)], &config);
384 let json = serde_json::to_string(&report).expect("should serialize");
385 assert!(json.contains("\"forensic\""));
386 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
388 let source = &v["sources"][0];
389 assert!(source.get("chaos").is_none());
390 assert!(source.get("entropy").is_none());
391 assert!(source.get("trials").is_none());
392 }
393}