1use crate::codec::jpeg::JpegImage;
21use crate::codec::jpeg::dct::DctGrid;
22use crate::codec::jpeg::pixels;
23use crate::stego::armor::ecc;
24use crate::stego::armor::embedding::{self, stdm_embed, stdm_extract_soft};
25use crate::stego::armor::fft2d;
26use crate::stego::armor::fortress;
27use crate::stego::armor::repetition;
28use crate::stego::armor::resample;
29use crate::stego::armor::selection::compute_stability_map;
30use crate::stego::armor::spreading::{generate_spreading_vectors, SPREAD_LEN};
31use crate::stego::armor::template;
32use crate::stego::crypto;
33use crate::stego::error::StegoError;
34use crate::stego::frame;
35use crate::stego::payload::{self, PayloadData};
36use crate::stego::permute;
37use crate::stego::progress;
38
39#[cfg(feature = "parallel")]
40use rayon::prelude::*;
41
42use crate::stego::quality::{self, EncodeQuality, ArmorMetrics};
43
44const HEADER_UNITS: usize = embedding::HEADER_UNITS; const HEADER_COPIES: usize = embedding::HEADER_COPIES; pub const ARMOR_ENCODE_STEPS: u32 = 6;
53
54const ARMOR_ENCODE_FORTRESS_STEPS: u32 = 3;
57
58pub fn armor_encode(
75 image_bytes: &[u8],
76 message: &str,
77 passphrase: &str,
78) -> Result<Vec<u8>, StegoError> {
79 armor_encode_impl(image_bytes, message, passphrase)
80 .map(|(bytes, _)| bytes)
81}
82
83pub fn armor_encode_with_quality(
85 image_bytes: &[u8],
86 message: &str,
87 passphrase: &str,
88) -> Result<(Vec<u8>, EncodeQuality), StegoError> {
89 armor_encode_impl(image_bytes, message, passphrase)
90}
91
92fn armor_encode_impl(
93 image_bytes: &[u8],
94 message: &str,
95 passphrase: &str,
96) -> Result<(Vec<u8>, EncodeQuality), StegoError> {
97 progress::init(ARMOR_ENCODE_STEPS);
99
100 let payload_bytes = payload::encode_payload(message, &[])?;
102
103 let mut img = JpegImage::from_bytes(image_bytes)?;
104
105 let fi = img.frame_info();
107 crate::stego::validate_encode_dimensions(fi.width as u32, fi.height as u32)?;
108
109 if img.num_components() == 0 {
110 return Err(StegoError::NoLuminanceChannel);
111 }
112
113 let use_compact = passphrase.is_empty();
116 if let Ok(max_fort) = fortress::fortress_max_frame_bytes_ext(&img, use_compact) {
117 let fortress_frame = if use_compact {
118 let ct = crypto::encrypt_with(
119 &payload_bytes,
120 passphrase,
121 &crypto::FORTRESS_EMPTY_SALT,
122 &crypto::FORTRESS_EMPTY_NONCE,
123 )?;
124 frame::build_fortress_compact_frame(payload_bytes.len(), &ct)
125 } else {
126 let (ct, nonce, salt) = crypto::encrypt(&payload_bytes, passphrase)?;
127 frame::build_frame(payload_bytes.len(), &salt, &nonce, &ct)
128 };
129
130 if fortress_frame.len() <= max_fort {
131 progress::set_total(ARMOR_ENCODE_FORTRESS_STEPS);
133
134 pre_settle_for_fortress(&mut img)?;
136 progress::advance(); let fort_result = fortress::fortress_encode(&mut img, &fortress_frame, passphrase)?;
139 progress::advance(); let stego_bytes = if let Ok(bytes) = img.to_bytes() { bytes } else {
142 img.rebuild_huffman_tables();
143 img.to_bytes().map_err(StegoError::InvalidJpeg)?
144 };
145 progress::advance(); let fill_ratio = fortress_frame.len() as f64 / max_fort as f64;
149 let fort_qt_id = img.frame_info().components[0].quant_table_id as usize;
151 let fort_mean_qt = img.quant_table(fort_qt_id)
152 .map_or(10.0, |qt| embedding::compute_mean_qt(&qt.values));
153 let encode_quality = quality::armor_robustness_score(&ArmorMetrics {
154 repetition_factor: fort_result.repetition_factor,
155 parity_symbols: fort_result.parity_symbols,
156 fortress: true,
157 mean_qt: fort_mean_qt,
158 fill_ratio,
159 delta: 12.0,
160 });
161 return Ok((stego_bytes, encode_quality));
162 }
163 }
164
165 let (ciphertext, nonce, salt) = crypto::encrypt(&payload_bytes, passphrase)?;
167 let frame_bytes = frame::build_frame(payload_bytes.len(), &salt, &nonce, &ciphertext);
168
169 embed_dft_template(&mut img, passphrase, message)?;
171 progress::advance(); pre_clamp_y_channel(&mut img)?;
175 progress::advance(); let qt_id = img.frame_info().components[0].quant_table_id as usize;
179 let qt = img
180 .quant_table(qt_id)
181 .ok_or(StegoError::NoLuminanceChannel)?;
182 let cost_map = compute_stability_map(img.dct_grid(0), qt);
183 progress::advance(); let structural_key = crypto::derive_armor_structural_key(passphrase)?;
187 let perm_seed: [u8; 32] = structural_key[..32].try_into().unwrap();
188 let spread_seed: [u8; 32] = structural_key[32..].try_into().unwrap();
189
190 let positions = permute::select_and_permute(&cost_map, &perm_seed);
192 let num_units = positions.len() / SPREAD_LEN;
193 if num_units == 0 {
194 return Err(StegoError::ImageTooSmall);
195 }
196 let n_used = num_units * SPREAD_LEN;
197 let positions = &positions[..n_used];
198
199 let mean_qt = embedding::compute_mean_qt(&qt.values);
203 let header_byte = embedding::encode_mean_qt(mean_qt);
204 let bootstrap_delta = embedding::BOOTSTRAP_DELTA;
205 let reference_delta = embedding::compute_delta_from_mean_qt(mean_qt, 1);
206
207 let mut all_bits = Vec::with_capacity(num_units);
209 for _ in 0..HEADER_COPIES {
210 for bp in (0..8).rev() {
211 all_bits.push((header_byte >> bp) & 1);
212 }
213 }
214
215 let payload_units = if num_units > HEADER_UNITS {
217 num_units - HEADER_UNITS
218 } else {
219 return Err(StegoError::ImageTooSmall);
220 };
221
222 let phase2_result: Option<(usize, Vec<u8>)> = {
224 let mut found = None;
225 for &parity in &ecc::PARITY_TIERS {
226 let rs_encoded = ecc::rs_encode_blocks_with_parity(&frame_bytes, parity);
227 let rs_bits_len = rs_encoded.len() * 8;
228 if rs_bits_len <= payload_units {
229 let r = repetition::compute_r(rs_bits_len, payload_units);
230 if r >= 3 {
231 found = Some((parity, rs_encoded));
232 break;
233 }
234 }
235 }
236 found
237 };
238
239 let (armor_r, armor_parity, armor_delta);
241
242 let embed_delta_fn: Box<dyn Fn(usize) -> f64> = if let Some((chosen_parity, rs_encoded)) = phase2_result {
243 let rs_bits = frame::bytes_to_bits(&rs_encoded);
245
246 let r = repetition::compute_r(rs_bits.len(), payload_units);
247 let rs_bit_count_aligned = payload_units / r;
248 let mut rs_bits_padded = rs_bits;
249 rs_bits_padded.resize(rs_bit_count_aligned, 0);
250 let (rep_bits, _) = repetition::repetition_encode(&rs_bits_padded, payload_units);
251
252 let adaptive_delta = embedding::compute_delta_from_mean_qt(mean_qt, r);
253
254 armor_r = r;
255 armor_parity = chosen_parity;
256 armor_delta = adaptive_delta;
257
258 all_bits.extend_from_slice(&rep_bits[..payload_units.min(rep_bits.len())]);
259
260 Box::new(move |bit_idx| {
261 if bit_idx < HEADER_UNITS { bootstrap_delta } else { adaptive_delta }
262 })
263 } else {
264 let rs_encoded = ecc::rs_encode_blocks(&frame_bytes);
266 let rs_bits = frame::bytes_to_bits(&rs_encoded);
267
268 if rs_bits.len() > payload_units {
269 return Err(StegoError::MessageTooLarge);
270 }
271
272 armor_r = 1;
273 armor_parity = 64;
274 armor_delta = reference_delta;
275
276 let mut payload_bits = rs_bits;
277 payload_bits.resize(payload_units, 0);
278 all_bits.extend_from_slice(&payload_bits);
279
280 Box::new(move |bit_idx| {
281 if bit_idx < HEADER_UNITS { bootstrap_delta } else { reference_delta }
282 })
283 };
284
285 let embed_count = all_bits.len().min(num_units);
286
287 let vectors = generate_spreading_vectors(&spread_seed, embed_count);
289 progress::advance(); let grid_mut = img.dct_grid_mut(0);
293 for bit_idx in 0..embed_count {
294 let group_start = bit_idx * SPREAD_LEN;
295 let group = &positions[group_start..group_start + SPREAD_LEN];
296
297 let mut coeffs = [0.0f64; SPREAD_LEN];
298 for (k, pos) in group.iter().enumerate() {
299 coeffs[k] = flat_get(grid_mut, pos.flat_idx as usize) as f64;
300 }
301
302 let delta = embed_delta_fn(bit_idx);
303 stdm_embed(&mut coeffs, &vectors[bit_idx], all_bits[bit_idx], delta);
304
305 for (k, pos) in group.iter().enumerate() {
306 let new_val = coeffs[k].round() as i16;
307 flat_set(grid_mut, pos.flat_idx as usize, new_val);
308 }
309 }
310
311 progress::advance(); let stego_bytes = if let Ok(bytes) = img.to_bytes() { bytes } else {
315 img.rebuild_huffman_tables();
316 img.to_bytes().map_err(StegoError::InvalidJpeg)?
317 };
318
319 progress::advance(); let fill_ratio = frame_bytes.len() as f64 / (payload_units / 8).max(1) as f64;
323 let encode_quality = quality::armor_robustness_score(&ArmorMetrics {
324 repetition_factor: armor_r,
325 parity_symbols: armor_parity,
326 fortress: false,
327 mean_qt,
328 fill_ratio,
329 delta: armor_delta,
330 });
331
332 Ok((stego_bytes, encode_quality))
333}
334
335#[derive(Debug, Clone)]
337pub struct DecodeQuality {
338 pub mode: u8,
340 pub rs_errors_corrected: u32,
342 pub rs_error_capacity: u32,
344 pub integrity_percent: u8,
346 pub repetition_factor: u8,
348 pub parity_len: u16,
350 pub geometry_corrected: bool,
352 pub template_peaks_detected: u8,
354 pub estimated_rotation_deg: f32,
356 pub estimated_scale: f32,
358 pub dft_ring_used: bool,
360 pub dft_ring_capacity: u16,
362 pub fortress_used: bool,
364 pub signal_strength: f64,
367}
368
369impl DecodeQuality {
370 pub fn ghost() -> Self {
372 Self {
373 mode: crate::stego::frame::MODE_GHOST,
374 rs_errors_corrected: 0,
375 rs_error_capacity: 0,
376 integrity_percent: 100,
377 repetition_factor: 0,
378 parity_len: 0,
379 geometry_corrected: false,
380 template_peaks_detected: 0,
381 estimated_rotation_deg: 0.0,
382 estimated_scale: 1.0,
383 dft_ring_used: false,
384 dft_ring_capacity: 0,
385 fortress_used: false,
386 signal_strength: 0.0,
387 }
388 }
389
390 pub fn from_rs_stats_with_signal(
400 stats: &ecc::RsDecodeStats,
401 repetition_factor: u8,
402 parity_len: u16,
403 signal_strength: f64,
404 reference_llr: f64,
405 ) -> Self {
406 let integrity = compute_integrity(signal_strength, stats, reference_llr);
407 Self {
408 mode: crate::stego::frame::MODE_ARMOR,
409 rs_errors_corrected: stats.total_errors as u32,
410 rs_error_capacity: stats.error_capacity as u32,
411 integrity_percent: integrity,
412 repetition_factor,
413 parity_len,
414 geometry_corrected: false,
415 template_peaks_detected: 0,
416 estimated_rotation_deg: 0.0,
417 estimated_scale: 1.0,
418 dft_ring_used: false,
419 dft_ring_capacity: 0,
420 fortress_used: false,
421 signal_strength,
422 }
423 }
424}
425
426fn compute_integrity(signal_strength: f64, rs_stats: &ecc::RsDecodeStats, reference_llr: f64) -> u8 {
438 let llr_score = if reference_llr > 0.0 {
439 (signal_strength / reference_llr).clamp(0.0, 1.0)
440 } else {
441 1.0 };
443 let rs_score = if rs_stats.error_capacity == 0 {
444 1.0
445 } else {
446 let ratio = rs_stats.total_errors as f64 / rs_stats.error_capacity as f64;
447 (1.0 - ratio).max(0.0)
448 };
449 let combined = 0.7 * llr_score + 0.3 * rs_score;
451 (combined * 100.0).round().clamp(0.0, 100.0) as u8
452}
453
454fn compute_avg_abs_llr(llrs: &[f64]) -> f64 {
456 if llrs.is_empty() {
457 return 0.0;
458 }
459 let sum: f64 = llrs.iter().map(|llr| llr.abs()).sum();
460 sum / llrs.len() as f64
461}
462
463pub fn armor_decode(stego_bytes: &[u8], passphrase: &str) -> Result<(PayloadData, DecodeQuality), StegoError> {
475 let img = JpegImage::from_bytes(stego_bytes)?;
477 if img.num_components() > 0
478 && let Ok(result) = fortress::fortress_decode(&img, passphrase) {
479 return Ok(result);
480 }
481
482 match try_armor_decode(&img, passphrase) {
485 Ok(result) => Ok(result),
486 Err(new_err) => {
487 progress::advance(); match try_geometric_recovery(stego_bytes, passphrase) {
490 Ok(result) => Ok(result),
491 Err(_) => Err(new_err),
492 }
493 }
494 }
495}
496
497pub(crate) fn try_armor_decode(img: &JpegImage, passphrase: &str) -> Result<(PayloadData, DecodeQuality), StegoError> {
499 if img.num_components() == 0 {
500 return Err(StegoError::NoLuminanceChannel);
501 }
502
503 let qt_id = img.frame_info().components[0].quant_table_id as usize;
505 let qt = img
506 .quant_table(qt_id)
507 .ok_or(StegoError::NoLuminanceChannel)?;
508 let cost_map = compute_stability_map(img.dct_grid(0), qt);
509
510 let structural_key = crypto::derive_armor_structural_key(passphrase)?;
512 let perm_seed: [u8; 32] = structural_key[..32].try_into().unwrap();
513 let spread_seed: [u8; 32] = structural_key[32..].try_into().unwrap();
514
515 let positions = permute::select_and_permute(&cost_map, &perm_seed);
517 let num_units = positions.len() / SPREAD_LEN;
518 if num_units == 0 {
519 return Err(StegoError::ImageTooSmall);
520 }
521 let n_used = num_units * SPREAD_LEN;
522 let positions = &positions[..n_used];
523
524 let vectors = generate_spreading_vectors(&spread_seed, num_units);
526
527 let grid = img.dct_grid(0);
528
529 if num_units <= HEADER_UNITS {
531 return Err(StegoError::ImageTooSmall);
532 }
533 let header_byte = extract_header_byte(grid, positions, &vectors, embedding::BOOTSTRAP_DELTA, 0);
534 let header_mean_qt = embedding::decode_mean_qt(header_byte);
535
536 let current_mean_qt = embedding::compute_mean_qt(&qt.values);
538
539 let payload_units = num_units - HEADER_UNITS;
540
541 let mut raw_candidates = Vec::with_capacity(24);
544 raw_candidates.push(header_mean_qt);
545 raw_candidates.push(current_mean_qt);
546 for step in 1..=10 {
547 let factor = step as f64 * 0.03;
548 raw_candidates.push(header_mean_qt * (1.0 - factor));
549 raw_candidates.push(header_mean_qt * (1.0 + factor));
550 }
551
552 let mut candidates: Vec<f64> = Vec::with_capacity(raw_candidates.len());
554 for &c in &raw_candidates {
555 if c > 0.1 && !candidates.iter().any(|&existing| (existing - c).abs() < 0.1) {
556 candidates.push(c);
557 }
558 }
559
560 let nc = candidates.len() as u32;
565 if progress::get().1 == 0 {
567 let total = (2 * (1 + nc + nc) + 1).max(50);
568 progress::set_total(total);
571 }
572 progress::advance(); #[cfg(feature = "parallel")]
576 {
577 let result = candidates.par_iter().find_map_first(|&mean_qt| {
578 if progress::is_cancelled() { return Some(Err(StegoError::Cancelled)); }
579 let reference_delta = embedding::compute_delta_from_mean_qt(mean_qt, 1);
580 match decode_phase1_with_offset(
581 grid, positions, &vectors, reference_delta, num_units, HEADER_UNITS,
582 passphrase,
583 ) {
584 Ok(result) => Some(Ok(result)),
585 Err(StegoError::DecryptionFailed) => Some(Err(StegoError::DecryptionFailed)),
586 Err(_) => { progress::advance(); None }
587 }
588 });
589 match result {
590 Some(Ok(payload)) => return Ok(payload),
591 Some(Err(e)) => return Err(e),
592 None => {} }
594 }
595 #[cfg(not(feature = "parallel"))]
596 for &mean_qt in &candidates {
597 progress::check_cancelled()?;
598 let reference_delta = embedding::compute_delta_from_mean_qt(mean_qt, 1);
599
600 if let Ok(result) = decode_phase1_with_offset(
601 grid, positions, &vectors, reference_delta, num_units, HEADER_UNITS,
602 passphrase,
603 ) {
604 return Ok(result);
605 }
606 progress::advance();
607 }
608
609 let mut all_p2_candidates: Vec<(usize, usize, f64)> = Vec::new();
613 for &mean_qt in &candidates {
614 for &parity in &ecc::PARITY_TIERS {
615 let candidate_rs = compute_candidate_rs(payload_units, parity);
616 for r in candidate_rs {
617 let delta = embedding::compute_delta_from_mean_qt(mean_qt, r);
618 all_p2_candidates.push((parity, r, delta));
619 }
620 }
621 }
622
623 let mut cached_llrs: Vec<(f64, Vec<f64>)> = Vec::new();
625 for &(_, _, delta) in &all_p2_candidates {
626 get_or_extract_llrs(
627 &mut cached_llrs, delta,
628 grid, positions, &vectors, num_units, HEADER_UNITS,
629 );
630 }
631 progress::check_cancelled()?;
632
633 let llr_cache: &[(f64, Vec<f64>)] = &cached_llrs;
635
636 let find_llrs = |delta: f64| -> &[f64] {
637 for (cached_delta, llrs) in llr_cache.iter() {
638 if (cached_delta - delta).abs() < 0.001 {
639 return llrs;
640 }
641 }
642 &[]
643 };
644
645 let try_p2_candidate = |&(parity, r, adaptive_delta): &(usize, usize, f64)| -> Option<Result<(PayloadData, DecodeQuality), StegoError>> {
646 if progress::is_cancelled() { return Some(Err(StegoError::Cancelled)); }
647 let raw_llrs = find_llrs(adaptive_delta);
648
649 let rs_bit_count = payload_units / r;
650 if rs_bit_count == 0 { return None; }
651 let used_llrs = rs_bit_count * r;
652 if used_llrs > raw_llrs.len() { return None; }
653
654 let (voted_bits, rep_quality) = repetition::repetition_decode_soft_with_quality(
655 &raw_llrs[..used_llrs], rs_bit_count,
656 );
657 let voted_bytes = frame::bits_to_bytes(&voted_bits);
658
659 let (decoded_frame, rs_stats) = try_rs_decode_frame_with_parity(&voted_bytes, parity)?;
660 let parsed = frame::parse_frame(&decoded_frame).ok()?;
661 match crypto::decrypt(&parsed.ciphertext, passphrase, &parsed.salt, &parsed.nonce) {
662 Ok(plaintext) => {
663 let len = parsed.plaintext_len as usize;
664 if len > plaintext.len() { return None; }
665 let payload_data = payload::decode_payload(&plaintext[..len]).ok()?;
666 let reference_llr = adaptive_delta / 2.0;
667 let quality = DecodeQuality::from_rs_stats_with_signal(
668 &rs_stats, r as u8, parity as u16,
669 rep_quality.avg_abs_llr_per_copy, reference_llr,
670 );
671 Some(Ok((payload_data, quality)))
672 }
673 Err(StegoError::DecryptionFailed) => Some(Err(StegoError::DecryptionFailed)),
674 Err(_) => None,
675 }
676 };
677
678 #[cfg(feature = "parallel")]
679 let p2_result = all_p2_candidates.par_iter().find_map_first(try_p2_candidate);
680 #[cfg(not(feature = "parallel"))]
681 let p2_result = all_p2_candidates.iter().find_map(try_p2_candidate);
682
683 for _ in 0..candidates.len() { progress::advance(); }
685
686 match p2_result {
687 Some(Ok(payload)) => return Ok(payload),
688 Some(Err(e)) => return Err(e),
689 None => {}
690 }
691
692 Err(StegoError::FrameCorrupted)
693}
694
695#[allow(dead_code)] pub(crate) fn armor_decode_no_fortress(img: &JpegImage, stego_bytes: &[u8], passphrase: &str) -> Result<(PayloadData, DecodeQuality), StegoError> {
701 match try_armor_decode(img, passphrase) {
702 Ok(result) => Ok(result),
703 Err(_stdm_err) => {
704 progress::check_cancelled()?;
705 match try_geometric_recovery(stego_bytes, passphrase) {
706 Ok(result) => Ok(result),
707 Err(_) => Err(_stdm_err),
708 }
709 }
710 }
711}
712
713pub(crate) fn try_geometric_recovery(stego_bytes: &[u8], passphrase: &str) -> Result<(PayloadData, DecodeQuality), StegoError> {
716 use crate::stego::armor::dft_payload;
717
718 let img = JpegImage::from_bytes(stego_bytes)?;
719
720 if img.num_components() == 0 {
721 return Err(StegoError::NoLuminanceChannel);
722 }
723
724 let (luma_pixels, w, h) = pixels::jpeg_to_luma_f64(&img)
726 .ok_or(StegoError::NoLuminanceChannel)?;
727 let spectrum = fft2d::fft2d(&luma_pixels, w, h);
728
729 let peaks = template::generate_template_peaks(passphrase, w, h)?;
731 let detected = template::detect_template(&spectrum, &peaks);
732
733 let transform = template::estimate_transform(&detected)
735 .ok_or(StegoError::FrameCorrupted)?;
736
737 if transform.rotation_rad.abs() < 0.001 && (transform.scale - 1.0).abs() < 0.001 {
739 if let Some(ring_bytes) = dft_payload::extract_ring_payload(&spectrum, passphrase)
741 && let Ok(text) = std::str::from_utf8(&ring_bytes) {
742 let ring_cap = dft_payload::ring_capacity(w, h);
743 return Ok((PayloadData { text: text.to_string(), files: vec![] }, DecodeQuality {
744 mode: crate::stego::frame::MODE_ARMOR,
745 rs_errors_corrected: 0,
746 rs_error_capacity: 0,
747 integrity_percent: 50, repetition_factor: 0,
749 parity_len: 0,
750 geometry_corrected: false,
751 template_peaks_detected: detected.len() as u8,
752 estimated_rotation_deg: 0.0,
753 estimated_scale: 1.0,
754 dft_ring_used: true,
755 dft_ring_capacity: ring_cap as u16,
756 fortress_used: false,
757 signal_strength: 0.0,
758 }));
759 }
760 return Err(StegoError::FrameCorrupted);
761 }
762
763 drop(spectrum);
765
766 let corrected_pixels = resample::resample_bilinear(
768 &luma_pixels, w, h, &transform, w, h,
769 );
770
771 drop(luma_pixels);
773
774 let mut corrected_img = img;
776 pixels::luma_f64_to_jpeg(&corrected_pixels, w, h, &mut corrected_img)
777 .ok_or(StegoError::NoLuminanceChannel)?;
778
779 drop(corrected_pixels);
781
782 match try_armor_decode(&corrected_img, passphrase) {
784 Ok((text, mut quality)) => {
785 quality.geometry_corrected = true;
786 quality.template_peaks_detected = detected.len() as u8;
787 quality.estimated_rotation_deg = transform.rotation_rad.to_degrees() as f32;
788 quality.estimated_scale = transform.scale as f32;
789 return Ok((text, quality));
790 }
791 Err(_) => {
792 {
795 let (cp, cw, ch) = pixels::jpeg_to_luma_f64(&corrected_img)
796 .ok_or(StegoError::NoLuminanceChannel)?;
797 let corrected_spectrum = fft2d::fft2d(&cp, cw, ch);
798 if let Some(ring_bytes) = dft_payload::extract_ring_payload(&corrected_spectrum, passphrase)
799 && let Ok(text) = std::str::from_utf8(&ring_bytes) {
800 let ring_cap = dft_payload::ring_capacity(cw, ch);
801 return Ok((PayloadData { text: text.to_string(), files: vec![] }, DecodeQuality {
802 mode: crate::stego::frame::MODE_ARMOR,
803 rs_errors_corrected: 0,
804 rs_error_capacity: 0,
805 integrity_percent: 50,
806 repetition_factor: 0,
807 parity_len: 0,
808 geometry_corrected: true,
809 template_peaks_detected: detected.len() as u8,
810 estimated_rotation_deg: transform.rotation_rad.to_degrees() as f32,
811 estimated_scale: transform.scale as f32,
812 dft_ring_used: true,
813 dft_ring_capacity: ring_cap as u16,
814 fortress_used: false,
815 signal_strength: 0.0,
816 }));
817 }
818 }
819 }
820 }
821
822 Err(StegoError::FrameCorrupted)
823}
824
825fn extract_header_byte(
829 grid: &DctGrid,
830 positions: &[crate::stego::permute::CoeffPos],
831 vectors: &[[f64; SPREAD_LEN]],
832 delta: f64,
833 offset: usize,
834) -> u8 {
835 let mut header_llrs = [0.0f64; 56]; for i in 0..56 {
837 let unit_idx = offset + i;
838 let group_start = unit_idx * SPREAD_LEN;
839 let group = &positions[group_start..group_start + SPREAD_LEN];
840
841 let mut coeffs = [0.0f64; SPREAD_LEN];
842 for (k, pos) in group.iter().enumerate() {
843 coeffs[k] = flat_get(grid, pos.flat_idx as usize) as f64;
844 }
845
846 header_llrs[i] = stdm_extract_soft(&coeffs, &vectors[unit_idx], delta);
847 }
848
849 let mut byte = 0u8;
851 for bit_pos in 0..8 {
852 let mut total = 0.0;
853 for copy in 0..7 {
854 total += header_llrs[copy * 8 + bit_pos];
855 }
856 if total < 0.0 {
857 byte |= 1 << (7 - bit_pos);
858 }
859 }
860 byte
861}
862
863fn decode_phase1_with_offset(
865 grid: &DctGrid,
866 positions: &[crate::stego::permute::CoeffPos],
867 vectors: &[[f64; SPREAD_LEN]],
868 delta: f64,
869 num_units: usize,
870 payload_offset: usize,
871 passphrase: &str,
872) -> Result<(PayloadData, DecodeQuality), StegoError> {
873 let payload_units = num_units - payload_offset;
874
875 let mut all_llrs = Vec::with_capacity(payload_units);
877 for unit_idx in payload_offset..num_units {
878 let group_start = unit_idx * SPREAD_LEN;
879 let group = &positions[group_start..group_start + SPREAD_LEN];
880
881 let mut coeffs = [0.0f64; SPREAD_LEN];
882 for (k, pos) in group.iter().enumerate() {
883 coeffs[k] = flat_get(grid, pos.flat_idx as usize) as f64;
884 }
885
886 all_llrs.push(stdm_extract_soft(&coeffs, &vectors[unit_idx], delta));
887 }
888
889 let signal_strength = compute_avg_abs_llr(&all_llrs);
891 let reference_llr = delta / 2.0;
893
894 let extracted_bits: Vec<u8> = all_llrs.iter()
896 .map(|&llr| if llr >= 0.0 { 0 } else { 1 })
897 .collect();
898
899 let extracted_bytes = frame::bits_to_bytes(&extracted_bits);
900 let (decoded_frame, rs_stats) = try_rs_decode_frame(&extracted_bytes)?;
901
902 let parsed = frame::parse_frame(&decoded_frame)?;
903 let plaintext = crypto::decrypt(
904 &parsed.ciphertext,
905 passphrase,
906 &parsed.salt,
907 &parsed.nonce,
908 )?;
909
910 let len = parsed.plaintext_len as usize;
911 if len > plaintext.len() {
912 return Err(StegoError::FrameCorrupted);
913 }
914
915 let payload_data = payload::decode_payload(&plaintext[..len])?;
916 let quality = DecodeQuality::from_rs_stats_with_signal(
917 &rs_stats, 1, ecc::parity_len() as u16, signal_strength, reference_llr,
918 );
919 Ok((payload_data, quality))
920}
921
922
923
924pub(super) fn compute_candidate_rs(payload_units: usize, parity: usize) -> Vec<usize> {
926 let mut rs_set = std::collections::BTreeSet::new();
927
928 let min_frame = frame::FRAME_OVERHEAD;
932 let max_frame = frame::MAX_FRAME_BYTES;
933
934 for frame_len in min_frame..=max_frame {
935 let rs_encoded_len = ecc::rs_encoded_len_with_parity(frame_len, parity);
936 let rs_bits = rs_encoded_len * 8;
937 if rs_bits > payload_units {
938 break;
939 }
940 let r = repetition::compute_r(rs_bits, payload_units);
941 if r >= 3 {
942 rs_set.insert(r);
943 }
944 }
945
946 rs_set.into_iter().collect()
947}
948
949pub(super) fn compute_candidate_rs_compact(payload_units: usize, parity: usize) -> Vec<usize> {
954 let mut rs_set = std::collections::BTreeSet::new();
955
956 let min_frame = frame::FORTRESS_COMPACT_FRAME_OVERHEAD;
957 let max_frame = frame::MAX_FRAME_BYTES;
958
959 for frame_len in min_frame..=max_frame {
960 let rs_encoded_len = ecc::rs_encoded_len_with_parity(frame_len, parity);
961 let rs_bits = rs_encoded_len * 8;
962 if rs_bits > payload_units {
963 break;
964 }
965 let r = repetition::compute_r(rs_bits, payload_units);
966 if r >= 3 {
967 rs_set.insert(r);
968 }
969 }
970
971 rs_set.into_iter().collect()
972}
973
974const LLR_CACHE_MAX: usize = 5;
977
978fn get_or_extract_llrs(
983 cache: &mut Vec<(f64, Vec<f64>)>,
984 delta: f64,
985 grid: &DctGrid,
986 positions: &[crate::stego::permute::CoeffPos],
987 vectors: &[[f64; SPREAD_LEN]],
988 num_units: usize,
989 payload_offset: usize,
990) {
991 for i in 0..cache.len() {
993 if (cache[i].0 - delta).abs() < 0.001 {
994 if i < cache.len() - 1 {
996 let entry = cache.remove(i);
997 cache.push(entry);
998 }
999 return;
1000 }
1001 }
1002
1003 let unit_indices: Vec<usize> = (payload_offset..num_units).collect();
1005
1006 let extract_one = |&unit_idx: &usize| -> f64 {
1007 let group_start = unit_idx * SPREAD_LEN;
1008 let group = &positions[group_start..group_start + SPREAD_LEN];
1009
1010 let mut coeffs = [0.0f64; SPREAD_LEN];
1011 for (k, pos) in group.iter().enumerate() {
1012 coeffs[k] = flat_get(grid, pos.flat_idx as usize) as f64;
1013 }
1014
1015 stdm_extract_soft(&coeffs, &vectors[unit_idx], delta)
1016 };
1017
1018 #[cfg(feature = "parallel")]
1019 let llrs: Vec<f64> = unit_indices.par_iter().map(extract_one).collect();
1020 #[cfg(not(feature = "parallel"))]
1021 let llrs: Vec<f64> = unit_indices.iter().map(extract_one).collect();
1022
1023 if cache.len() >= LLR_CACHE_MAX {
1025 cache.remove(0); }
1027
1028 cache.push((delta, llrs));
1029}
1030
1031fn pre_clamp_y_channel(img: &mut JpegImage) -> Result<(), StegoError> {
1037 let qt_id = img.frame_info().components[0].quant_table_id as usize;
1038 let qt_values = img.quant_table(qt_id)
1039 .ok_or(StegoError::NoLuminanceChannel)?.values;
1040 let grid = img.dct_grid_mut(0);
1041
1042 let process_block = |chunk: &mut [i16]| {
1043 let quantized: [i16; 64] = chunk.try_into().unwrap();
1044 let mut px = pixels::idct_block(&quantized, &qt_values);
1045 for p in px.iter_mut() {
1046 *p = p.clamp(0.0, 255.0);
1047 }
1048 let settled = pixels::dct_block(&px, &qt_values);
1049 chunk.copy_from_slice(&settled);
1050 };
1051
1052 #[cfg(feature = "parallel")]
1053 grid.coeffs_mut().par_chunks_mut(64).for_each(process_block);
1054 #[cfg(not(feature = "parallel"))]
1055 grid.coeffs_mut().chunks_mut(64).for_each(process_block);
1056 Ok(())
1057}
1058
1059pub(super) fn try_rs_decode_frame_with_parity(
1066 extracted_bytes: &[u8],
1067 parity: usize,
1068) -> Option<(Vec<u8>, ecc::RsDecodeStats)> {
1069 let k_max = 255 - parity;
1070 let min_data = 2usize.min(k_max);
1071
1072 for data_len in min_data..=k_max.min(extracted_bytes.len().saturating_sub(parity)) {
1073 let block_len = data_len + parity;
1074 if block_len > extracted_bytes.len() {
1075 break;
1076 }
1077
1078 if let Ok((first_block_data, first_errors)) =
1079 ecc::rs_decode_with_parity(&extracted_bytes[..block_len], data_len, parity)
1080 && first_block_data.len() >= 2 {
1081 let pt_len =
1082 u16::from_be_bytes([first_block_data[0], first_block_data[1]]) as usize;
1083
1084 if pt_len == 0 {
1086 continue;
1087 }
1088
1089 let ct_len = pt_len + 16;
1090 let total_frame_len = 2 + 16 + 12 + ct_len + 4;
1091
1092 if total_frame_len > frame::MAX_FRAME_BYTES {
1093 continue;
1094 }
1095
1096 let rs_encoded_len =
1098 ecc::rs_encoded_len_with_parity(total_frame_len, parity);
1099 if rs_encoded_len > extracted_bytes.len() {
1100 continue;
1101 }
1102
1103 if total_frame_len == data_len {
1104 let t_max = parity / 2;
1105 let stats = ecc::RsDecodeStats {
1106 total_errors: first_errors,
1107 error_capacity: t_max,
1108 max_block_errors: first_errors,
1109 num_blocks: 1,
1110 };
1111 return Some((first_block_data, stats));
1112 }
1113
1114 if total_frame_len < data_len {
1115 let t_max = parity / 2;
1116 let stats = ecc::RsDecodeStats {
1117 total_errors: first_errors,
1118 error_capacity: t_max,
1119 max_block_errors: first_errors,
1120 num_blocks: 1,
1121 };
1122 return Some((first_block_data[..total_frame_len].to_vec(), stats));
1123 }
1124
1125 if total_frame_len > data_len
1126 && let Ok((decoded, stats)) = ecc::rs_decode_blocks_with_parity(
1127 &extracted_bytes[..rs_encoded_len],
1128 total_frame_len,
1129 parity,
1130 ) {
1131 return Some((decoded, stats));
1132 }
1133 }
1136 }
1137
1138 None
1139}
1140
1141pub(super) fn try_rs_decode_compact_frame_with_parity(
1146 extracted_bytes: &[u8],
1147 parity: usize,
1148) -> Option<(Vec<u8>, ecc::RsDecodeStats)> {
1149 let k_max = 255 - parity;
1150 let min_data = 2usize.min(k_max);
1151
1152 for data_len in min_data..=k_max.min(extracted_bytes.len().saturating_sub(parity)) {
1153 let block_len = data_len + parity;
1154 if block_len > extracted_bytes.len() {
1155 break;
1156 }
1157
1158 if let Ok((first_block_data, first_errors)) =
1159 ecc::rs_decode_with_parity(&extracted_bytes[..block_len], data_len, parity)
1160 && first_block_data.len() >= 2 {
1161 let pt_len =
1162 u16::from_be_bytes([first_block_data[0], first_block_data[1]]) as usize;
1163
1164 if pt_len == 0 {
1166 continue;
1167 }
1168
1169 let ct_len = pt_len + 16;
1170 let total_frame_len = 2 + ct_len + 4;
1172
1173 if total_frame_len > frame::MAX_FRAME_BYTES {
1174 continue;
1175 }
1176
1177 let rs_encoded_len =
1179 ecc::rs_encoded_len_with_parity(total_frame_len, parity);
1180 if rs_encoded_len > extracted_bytes.len() {
1181 continue;
1182 }
1183
1184 if total_frame_len == data_len {
1185 let t_max = parity / 2;
1186 let stats = ecc::RsDecodeStats {
1187 total_errors: first_errors,
1188 error_capacity: t_max,
1189 max_block_errors: first_errors,
1190 num_blocks: 1,
1191 };
1192 return Some((first_block_data, stats));
1193 }
1194
1195 if total_frame_len < data_len {
1196 let t_max = parity / 2;
1197 let stats = ecc::RsDecodeStats {
1198 total_errors: first_errors,
1199 error_capacity: t_max,
1200 max_block_errors: first_errors,
1201 num_blocks: 1,
1202 };
1203 return Some((first_block_data[..total_frame_len].to_vec(), stats));
1204 }
1205
1206 if total_frame_len > data_len
1207 && let Ok((decoded, stats)) = ecc::rs_decode_blocks_with_parity(
1208 &extracted_bytes[..rs_encoded_len],
1209 total_frame_len,
1210 parity,
1211 ) {
1212 return Some((decoded, stats));
1213 }
1214 }
1217 }
1218
1219 None
1220}
1221
1222fn try_rs_decode_frame(extracted_bytes: &[u8]) -> Result<(Vec<u8>, ecc::RsDecodeStats), StegoError> {
1224 let parity = ecc::parity_len();
1225
1226 let min_data = crate::stego::frame::FRAME_OVERHEAD;
1227 let max_first_block_data = 191usize; for data_len in min_data..=max_first_block_data.min(extracted_bytes.len().saturating_sub(parity))
1230 {
1231 let block_len = data_len + parity;
1232 if block_len > extracted_bytes.len() {
1233 break;
1234 }
1235
1236 if let Ok((first_block_data, first_errors)) = ecc::rs_decode(&extracted_bytes[..block_len], data_len)
1237 && first_block_data.len() >= 2 {
1238 let pt_len =
1239 u16::from_be_bytes([first_block_data[0], first_block_data[1]]) as usize;
1240
1241 if pt_len == 0 {
1243 continue;
1244 }
1245
1246 let ct_len = pt_len + 16;
1247 let total_frame_len = 2 + 16 + 12 + ct_len + 4;
1248
1249 if total_frame_len > frame::MAX_FRAME_BYTES {
1250 continue;
1251 }
1252
1253 let rs_encoded_len = ecc::rs_encoded_len(total_frame_len);
1255 if rs_encoded_len > extracted_bytes.len() {
1256 continue;
1257 }
1258
1259 if total_frame_len <= data_len {
1260 let stats = ecc::RsDecodeStats {
1261 total_errors: first_errors,
1262 error_capacity: ecc::T_MAX,
1263 max_block_errors: first_errors,
1264 num_blocks: 1,
1265 };
1266 let frame_data = if total_frame_len == data_len {
1268 first_block_data
1269 } else {
1270 first_block_data[..total_frame_len].to_vec()
1271 };
1272 return Ok((frame_data, stats));
1273 }
1274
1275 if let Ok((decoded, stats)) = ecc::rs_decode_blocks(
1277 &extracted_bytes[..rs_encoded_len],
1278 total_frame_len,
1279 ) {
1280 return Ok((decoded, stats));
1281 }
1282 }
1285 }
1286
1287 Err(StegoError::FrameCorrupted)
1288}
1289
1290fn flat_get(grid: &DctGrid, flat_idx: usize) -> i16 {
1294 let bw = grid.blocks_wide();
1295 let block_idx = flat_idx / 64;
1296 let pos = flat_idx % 64;
1297 let br = block_idx / bw;
1298 let bc = block_idx % bw;
1299 let i = pos / 8;
1300 let j = pos % 8;
1301 grid.get(br, bc, i, j)
1302}
1303
1304fn flat_set(grid: &mut DctGrid, flat_idx: usize, val: i16) {
1306 let bw = grid.blocks_wide();
1307 let block_idx = flat_idx / 64;
1308 let pos = flat_idx % 64;
1309 let br = block_idx / bw;
1310 let bc = block_idx % bw;
1311 let i = pos / 8;
1312 let j = pos % 8;
1313 grid.set(br, bc, i, j, val);
1314}
1315
1316#[derive(Debug, Clone)]
1321pub struct ArmorCapacityInfo {
1322 pub fortress_capacity: usize,
1324 pub stdm_capacity: usize,
1326}
1327
1328pub fn armor_capacity_info(jpeg_bytes: &[u8]) -> Result<ArmorCapacityInfo, StegoError> {
1333 let img = JpegImage::from_bytes(jpeg_bytes)?;
1334
1335 if img.num_components() == 0 {
1336 return Err(StegoError::NoLuminanceChannel);
1337 }
1338
1339 let fortress_cap = fortress::fortress_capacity(&img).unwrap_or(0);
1340 let stdm_cap = super::capacity::estimate_armor_capacity(&img).unwrap_or(0);
1341
1342 Ok(ArmorCapacityInfo {
1343 fortress_capacity: fortress_cap,
1344 stdm_capacity: stdm_cap,
1345 })
1346}
1347
1348fn embed_dft_template(img: &mut JpegImage, passphrase: &str, message: &str) -> Result<(), StegoError> {
1353 let (luma_pixels, w, h) = pixels::jpeg_to_luma_f64(img)
1354 .ok_or(StegoError::NoLuminanceChannel)?;
1355
1356 let mut spectrum = fft2d::fft2d(&luma_pixels, w, h);
1358 drop(luma_pixels);
1359
1360 let peaks = template::generate_template_peaks(passphrase, w, h)?;
1362 template::embed_template(&mut spectrum, &peaks);
1363
1364 use crate::stego::armor::dft_payload;
1366 let ring_cap = dft_payload::ring_capacity(w, h);
1367 if ring_cap > 0 && !message.is_empty() {
1368 let max_byte = message.len().min(ring_cap);
1371 let truncated_len = message[..max_byte]
1372 .char_indices()
1373 .last()
1374 .map_or(0, |(i, c)| i + c.len_utf8());
1375 let truncated = &message.as_bytes()[..truncated_len];
1376 dft_payload::embed_ring_payload(&mut spectrum, truncated, passphrase)?;
1377 }
1378
1379 let modified = fft2d::ifft2d(&spectrum);
1381 drop(spectrum);
1382
1383 pixels::luma_f64_to_jpeg(&modified, w, h, img)
1384 .ok_or(StegoError::NoLuminanceChannel)?;
1385 Ok(())
1386}
1387
1388const JPEG_BASE_LUMINANCE_QT: [u16; 64] = [
1395 16, 11, 10, 16, 24, 40, 51, 61,
1396 12, 12, 14, 19, 26, 58, 60, 55,
1397 14, 13, 16, 24, 40, 57, 69, 56,
1398 14, 17, 22, 29, 51, 87, 80, 62,
1399 18, 22, 37, 56, 68, 109, 103, 77,
1400 24, 35, 55, 64, 81, 104, 113, 92,
1401 49, 64, 78, 87, 103, 121, 120, 101,
1402 72, 92, 95, 98, 112, 100, 103, 99,
1403];
1404
1405const JPEG_BASE_CHROMINANCE_QT: [u16; 64] = [
1408 17, 18, 24, 47, 99, 99, 99, 99,
1409 18, 21, 26, 66, 99, 99, 99, 99,
1410 24, 26, 56, 99, 99, 99, 99, 99,
1411 47, 66, 99, 99, 99, 99, 99, 99,
1412 99, 99, 99, 99, 99, 99, 99, 99,
1413 99, 99, 99, 99, 99, 99, 99, 99,
1414 99, 99, 99, 99, 99, 99, 99, 99,
1415 99, 99, 99, 99, 99, 99, 99, 99,
1416];
1417
1418fn compute_jpeg_qt(base: &[u16; 64], qf: u32) -> [u16; 64] {
1427 let scale = if qf >= 50 { 200 - 2 * qf } else { 5000 / qf };
1428 let mut qt = [0u16; 64];
1429 for i in 0..64 {
1430 let val = (base[i] as u32 * scale + 50) / 100;
1431 qt[i] = val.clamp(1, 255) as u16;
1432 }
1433 qt
1434}
1435
1436fn pre_settle_for_fortress(img: &mut JpegImage) -> Result<(), StegoError> {
1447 use crate::codec::jpeg::dct::QuantTable;
1448
1449 let num_components = img.num_components();
1450 let target_qf = 75u32;
1451
1452 let mut new_qts: Vec<(usize, [u16; 64], [u16; 64])> = Vec::new(); for comp_idx in 0..num_components {
1458 let qt_id = img.frame_info().components[comp_idx].quant_table_id as usize;
1459 let old_qt = img.quant_table(qt_id)
1460 .ok_or(StegoError::NoLuminanceChannel)?.values;
1461 let base = if comp_idx == 0 {
1462 &JPEG_BASE_LUMINANCE_QT
1463 } else {
1464 &JPEG_BASE_CHROMINANCE_QT
1465 };
1466 let new_qt = compute_jpeg_qt(base, target_qf);
1467
1468 let grid = img.dct_grid_mut(comp_idx);
1470
1471 let process_block = |chunk: &mut [i16]| {
1472 let quantized: [i16; 64] = chunk.try_into().unwrap();
1473 let mut px = pixels::idct_block(&quantized, &old_qt);
1474 for p in px.iter_mut() {
1475 *p = p.clamp(0.0, 255.0);
1476 }
1477 let settled = pixels::dct_block(&px, &new_qt);
1478 chunk.copy_from_slice(&settled);
1479 };
1480
1481 #[cfg(feature = "parallel")]
1482 grid.coeffs_mut().par_chunks_mut(64).for_each(process_block);
1483 #[cfg(not(feature = "parallel"))]
1484 grid.coeffs_mut().chunks_mut(64).for_each(process_block);
1485
1486 new_qts.push((qt_id, old_qt, new_qt));
1487 }
1488
1489 let mut replaced = [false; 4];
1491 for (qt_id, _old, new_qt) in &new_qts {
1492 if !replaced[*qt_id] {
1493 img.set_quant_table(*qt_id, QuantTable::new(*new_qt));
1494 replaced[*qt_id] = true;
1495 }
1496 }
1497 Ok(())
1498}
1499
1500#[cfg(test)]
1501mod tests {
1502 use super::*;
1503
1504 #[test]
1505 fn compute_integrity_pristine() {
1506 let stats = ecc::RsDecodeStats {
1508 total_errors: 0,
1509 error_capacity: 32,
1510 max_block_errors: 0,
1511 num_blocks: 1,
1512 };
1513 let integrity = compute_integrity(15.0, &stats, 15.0);
1514 assert_eq!(integrity, 100);
1516 }
1517
1518 #[test]
1519 fn compute_integrity_half_signal() {
1520 let stats = ecc::RsDecodeStats {
1522 total_errors: 0,
1523 error_capacity: 32,
1524 max_block_errors: 0,
1525 num_blocks: 1,
1526 };
1527 let integrity = compute_integrity(7.5, &stats, 15.0);
1528 assert_eq!(integrity, 65);
1530 }
1531
1532 #[test]
1533 fn compute_integrity_zero_signal() {
1534 let stats = ecc::RsDecodeStats {
1536 total_errors: 0,
1537 error_capacity: 32,
1538 max_block_errors: 0,
1539 num_blocks: 1,
1540 };
1541 let integrity = compute_integrity(0.0, &stats, 15.0);
1542 assert_eq!(integrity, 30);
1544 }
1545
1546 #[test]
1547 fn compute_integrity_with_rs_errors() {
1548 let stats = ecc::RsDecodeStats {
1550 total_errors: 16,
1551 error_capacity: 32,
1552 max_block_errors: 16,
1553 num_blocks: 1,
1554 };
1555 let integrity = compute_integrity(15.0, &stats, 15.0);
1556 assert_eq!(integrity, 85);
1558 }
1559
1560 #[test]
1561 fn compute_integrity_both_degraded() {
1562 let stats = ecc::RsDecodeStats {
1564 total_errors: 16,
1565 error_capacity: 32,
1566 max_block_errors: 16,
1567 num_blocks: 1,
1568 };
1569 let integrity = compute_integrity(7.5, &stats, 15.0);
1570 assert_eq!(integrity, 50);
1572 }
1573
1574 #[test]
1575 fn compute_integrity_signal_exceeds_reference() {
1576 let stats = ecc::RsDecodeStats {
1578 total_errors: 0,
1579 error_capacity: 32,
1580 max_block_errors: 0,
1581 num_blocks: 1,
1582 };
1583 let integrity = compute_integrity(20.0, &stats, 15.0);
1584 assert_eq!(integrity, 100);
1586 }
1587
1588 #[test]
1589 fn compute_integrity_zero_reference() {
1590 let stats = ecc::RsDecodeStats {
1592 total_errors: 0,
1593 error_capacity: 0,
1594 max_block_errors: 0,
1595 num_blocks: 0,
1596 };
1597 let integrity = compute_integrity(5.0, &stats, 0.0);
1598 assert_eq!(integrity, 100);
1600 }
1601
1602 #[test]
1603 fn compute_avg_abs_llr_basic() {
1604 let llrs = vec![5.0, -3.0, 4.0, -2.0];
1605 let avg = compute_avg_abs_llr(&llrs);
1606 assert!((avg - 3.5).abs() < 1e-10);
1608 }
1609
1610 #[test]
1611 fn compute_avg_abs_llr_empty() {
1612 assert_eq!(compute_avg_abs_llr(&[]), 0.0);
1613 }
1614
1615 #[test]
1616 fn decode_quality_ghost_unchanged() {
1617 let q = DecodeQuality::ghost();
1618 assert_eq!(q.integrity_percent, 100, "Ghost always 100%");
1619 assert_eq!(q.signal_strength, 0.0);
1620 }
1621
1622 #[test]
1623 fn decode_quality_from_rs_stats_with_signal_pristine() {
1624 let stats = ecc::RsDecodeStats {
1625 total_errors: 0,
1626 error_capacity: 32,
1627 max_block_errors: 0,
1628 num_blocks: 1,
1629 };
1630 let q = DecodeQuality::from_rs_stats_with_signal(&stats, 5, 64, 15.0, 15.0);
1631 assert_eq!(q.integrity_percent, 100);
1632 assert!((q.signal_strength - 15.0).abs() < 1e-10);
1633 assert_eq!(q.repetition_factor, 5);
1634 assert_eq!(q.parity_len, 64);
1635 }
1636
1637}