#![allow(unknown_lints, clippy::chunks_exact_to_as_chunks)]
use crate::format::{FileType, AUDIO_BYTES_PER_SAMPLE};
use rand::{Rng, RngCore};
const DECAY_SCALE: f64 = 10.0;
const RGBA_BYTES_PER_PIXEL: usize = 4;
const ALPHA_INDEX: usize = 3;
const AUDIO_MAX_NOISE: u16 = 32_767;
const PRINTABLE_ASCII_LOW: u8 = 0x20;
const PRINTABLE_ASCII_HIGH: u8 = 0x7E;
const PRINTABLE_ASCII_COUNT: u16 = (PRINTABLE_ASCII_HIGH - PRINTABLE_ASCII_LOW) as u16 + 1;
const TEXT_REPLACEMENT_ACCEPT_MAX: u16 = PRINTABLE_ASCII_COUNT * 689;
const CHUNK_BYTES: usize = 64 * 1024;
const PAR_MIN_BYTES: usize = 1024 * 1024;
const REGION_BYTES: usize = 4 * 1024 * 1024;
fn corruption_probability(x: f64) -> f64 {
1.0 - (-x / DECAY_SCALE).exp()
}
fn probability_threshold(x: f64) -> u16 {
(corruption_probability(x) * 65536.0) as u16
}
pub fn corrupt(payload: &mut [u8], file_type: FileType, x: f64) {
let threshold = probability_threshold(x);
corrupt_payload(payload, file_type, threshold);
}
fn corrupt_payload(payload: &mut [u8], file_type: FileType, threshold: u16) {
let eligible_len = match file_type {
FileType::Text => payload.len(),
FileType::Image => payload.len() - payload.len() % RGBA_BYTES_PER_PIXEL,
FileType::Audio => payload.len() - payload.len() % AUDIO_BYTES_PER_SAMPLE,
};
if eligible_len == 0 {
return;
}
let eligible = &mut payload[..eligible_len];
let parallel = eligible_len >= PAR_MIN_BYTES
&& std::thread::available_parallelism()
.map(|cores| cores.get() > 1)
.unwrap_or(false);
if parallel {
use rayon::prelude::*;
eligible.par_chunks_mut(REGION_BYTES).for_each(|region| {
let mut rng = rand::thread_rng();
region_work(region, file_type, threshold, &mut rng);
});
} else {
let mut rng = rand::thread_rng();
region_work(eligible, file_type, threshold, &mut rng);
}
}
fn region_work<R: RngCore>(
mut region: &mut [u8],
file_type: FileType,
threshold: u16,
rng: &mut R,
) {
let mut pool = vec![0u16; CHUNK_BYTES * 2];
while !region.is_empty() {
let chunk_len = region.len().min(CHUNK_BYTES);
let (chunk, rest) = region.split_at_mut(chunk_len);
region = rest;
rng.fill(&mut pool[..chunk_len * 2]);
match file_type {
FileType::Text => corrupt_text_chunk(chunk, threshold, &pool, rng),
FileType::Image => corrupt_image_chunk(chunk, threshold, &pool),
FileType::Audio => corrupt_audio_chunk(chunk, threshold, &pool),
}
}
}
fn corrupt_image_chunk(chunk: &mut [u8], threshold: u16, pool: &[u16]) {
let mut draw = 0;
for pixel in chunk.chunks_exact_mut(RGBA_BYTES_PER_PIXEL) {
for channel in pixel.iter_mut().take(ALPHA_INDEX) {
if pool[draw] < threshold {
draw += 1;
*channel = pool[draw] as u8;
}
draw += 1;
}
}
}
fn corrupt_audio_chunk(chunk: &mut [u8], threshold: u16, pool: &[u16]) {
let amplitude = audio_noise_amplitude(threshold);
let mut draw = 0;
for sample in chunk.chunks_exact_mut(AUDIO_BYTES_PER_SAMPLE) {
if pool[draw] < threshold {
draw += 1;
let span = amplitude.saturating_mul(2).saturating_add(1) as u32;
let offset = (pool[draw] as u32 % span) as i32 - amplitude as i32;
let old = i16::from_le_bytes([sample[0], sample[1]]) as i32;
let new = (old + offset).clamp(i16::MIN as i32, i16::MAX as i32) as i16;
sample.copy_from_slice(&new.to_le_bytes());
}
draw += 1;
}
}
fn audio_noise_amplitude(threshold: u16) -> u16 {
let fraction = f64::from(threshold) / f64::from(u16::MAX);
(fraction * f64::from(AUDIO_MAX_NOISE)) as u16
}
fn corrupt_text_chunk<R: RngCore>(chunk: &mut [u8], threshold: u16, pool: &[u16], rng: &mut R) {
let mut draw = 0;
for byte in chunk.iter_mut() {
if pool[draw] < threshold {
draw += 1;
let r = pool[draw];
*byte = if r < TEXT_REPLACEMENT_ACCEPT_MAX {
PRINTABLE_ASCII_LOW + (r % PRINTABLE_ASCII_COUNT) as u8
} else {
rng.gen_range(PRINTABLE_ASCII_LOW..=PRINTABLE_ASCII_HIGH)
};
}
draw += 1;
}
}
#[cfg(test)]
mod tests {
use super::*;
use rand::rngs::StdRng;
use rand::SeedableRng;
const FRACTION_TOLERANCE: f64 = 0.01;
fn changed_count(before: &[u8], after: &[u8]) -> usize {
before
.iter()
.zip(after.iter())
.filter(|(a, b)| a != b)
.count()
}
#[test]
fn probability_matches_known_values() {
assert!((corruption_probability(1.0) - 0.095).abs() < 0.001);
assert!((corruption_probability(10.0) - 0.632).abs() < 0.001);
}
#[test]
fn threshold_quantization_stays_within_one_65536th() {
for x in [0.5, 1.0, 2.0, 5.0, 10.0, 25.0] {
let p = corruption_probability(x);
let t = probability_threshold(x);
assert!((t as f64 / 65536.0 - p).abs() <= 1.0 / 65536.0 + 1e-12);
}
}
#[test]
fn text_corruption_fraction_matches_probability() {
for (x, expected) in [(1.0, 0.095), (10.0, 0.632)] {
let original = vec![0u8; 100_000];
let mut payload = original.clone();
corrupt(&mut payload, FileType::Text, x);
let fraction = changed_count(&original, &payload) as f64 / original.len() as f64;
assert!(
(fraction - expected).abs() < FRACTION_TOLERANCE,
"x={x} text fraction {fraction} not within {FRACTION_TOLERANCE} of {expected}",
);
}
}
#[test]
fn audio_displaces_about_the_expected_share_of_samples() {
let original = vec![0u8; 100_000];
let mut payload = original.clone();
corrupt(&mut payload, FileType::Audio, 10.0);
let changed = original
.chunks_exact(AUDIO_BYTES_PER_SAMPLE)
.zip(payload.chunks_exact(AUDIO_BYTES_PER_SAMPLE))
.filter(|(before, after)| before != after)
.count();
let total = original.len() / AUDIO_BYTES_PER_SAMPLE;
let fraction = changed as f64 / total as f64;
assert!(
(fraction - 0.632).abs() < FRACTION_TOLERANCE,
"x=10 displaced {fraction} of samples, expected near 0.632",
);
}
#[test]
fn audio_displacement_is_bounded_and_scales_with_x() {
fn mean_shift(x: f64) -> f64 {
let original: Vec<u8> = (0..50_000).flat_map(|_| 8_000i16.to_le_bytes()).collect();
let mut payload = original.clone();
corrupt(&mut payload, FileType::Audio, x);
let shifts: Vec<i32> = original
.chunks_exact(AUDIO_BYTES_PER_SAMPLE)
.zip(payload.chunks_exact(AUDIO_BYTES_PER_SAMPLE))
.map(|(before, after)| {
let before = i16::from_le_bytes([before[0], before[1]]) as i32;
let after = i16::from_le_bytes([after[0], after[1]]) as i32;
(after - before).abs()
})
.collect();
let bound = i32::from(audio_noise_amplitude(probability_threshold(x)));
assert!(
shifts.iter().all(|shift| *shift <= bound),
"a sample moved further than the amplitude x allows"
);
shifts.iter().sum::<i32>() as f64 / shifts.len() as f64
}
let gentle = mean_shift(1.0);
let severe = mean_shift(10.0);
assert!(
severe > gentle * 4.0,
"x=10 mean shift {severe} was not well above x=1 mean shift {gentle}"
);
}
#[test]
fn audio_corruption_preserves_length() {
let original = vec![7u8; 4096];
let mut payload = original.clone();
corrupt(&mut payload, FileType::Audio, 10.0);
assert_eq!(
payload.len(),
original.len(),
"audio corruption must never change the payload length"
);
}
#[test]
fn audio_trailing_partial_sample_is_untouched() {
let mut payload = vec![0u8; 8193];
corrupt(&mut payload, FileType::Audio, 10.0);
assert_eq!(
payload[8192], 0,
"the trailing partial sample must not be corrupted"
);
}
#[test]
fn higher_x_corrupts_at_least_as_much_as_lower_x_on_average() {
const TRIALS: usize = 50;
const LEN: usize = 10_000;
let low_x = 2.0;
let high_x = 8.0;
let mut low_total = 0usize;
let mut high_total = 0usize;
for _ in 0..TRIALS {
let original = vec![0u8; LEN];
let mut low_payload = original.clone();
corrupt(&mut low_payload, FileType::Text, low_x);
low_total += changed_count(&original, &low_payload);
let mut high_payload = original.clone();
corrupt(&mut high_payload, FileType::Text, high_x);
high_total += changed_count(&original, &high_payload);
}
assert!(
high_total > low_total,
"higher x mean corruption ({}) was not greater than lower x ({})",
high_total,
low_total
);
}
#[test]
fn image_alpha_channel_is_never_modified() {
const PIXELS: usize = 2_000;
const ALPHA_SENTINEL: u8 = 0xAB;
const RUNS: usize = 20;
for _ in 0..RUNS {
let mut payload = Vec::with_capacity(PIXELS * RGBA_BYTES_PER_PIXEL);
for _ in 0..PIXELS {
payload.extend_from_slice(&[0x00, 0x00, 0x00, ALPHA_SENTINEL]);
}
corrupt(&mut payload, FileType::Image, 10.0);
for pixel in payload.chunks_exact(RGBA_BYTES_PER_PIXEL) {
assert_eq!(
pixel[ALPHA_INDEX], ALPHA_SENTINEL,
"alpha channel was modified by corruption"
);
}
}
}
#[test]
fn image_rgb_corruption_fraction_is_near_expected() {
const PIXELS: usize = 50_000;
let mut payload = vec![0u8; PIXELS * RGBA_BYTES_PER_PIXEL];
corrupt(&mut payload, FileType::Image, 10.0);
let mut changed = 0usize;
let rgb_channels = PIXELS * 3;
for pixel in payload.chunks_exact(RGBA_BYTES_PER_PIXEL) {
for channel in pixel.iter().take(ALPHA_INDEX) {
if *channel != 0 {
changed += 1;
}
}
}
let fraction = changed as f64 / rgb_channels as f64;
assert!(
(fraction - 0.632).abs() < FRACTION_TOLERANCE,
"x=10 image RGB fraction {} not within {} of 0.632",
fraction,
FRACTION_TOLERANCE
);
}
#[test]
fn image_alpha_never_modified_large_parallel() {
const PAYLOAD_BYTES: usize = 8 * 1024 * 1024;
const ALPHA_SENTINEL: u8 = 0xCD;
let mut payload = Vec::with_capacity(PAYLOAD_BYTES);
while payload.len() < PAYLOAD_BYTES {
payload.extend_from_slice(&[0x00, 0x00, 0x00, ALPHA_SENTINEL]);
}
corrupt(&mut payload, FileType::Image, 10.0);
assert_eq!(
payload.len(),
PAYLOAD_BYTES,
"corruption changed the length"
);
for pixel in payload.chunks_exact(RGBA_BYTES_PER_PIXEL) {
assert_eq!(
pixel[ALPHA_INDEX], ALPHA_SENTINEL,
"alpha channel was modified by parallel corruption"
);
}
}
#[test]
fn large_image_rgb_fraction_near_expected() {
let payload = vec![0u8; 8 * 1024 * 1024];
for (x, expected) in [(1.0, 0.095), (10.0, 0.632)] {
let mut corrupted = payload.clone();
corrupt(&mut corrupted, FileType::Image, x);
let mut changed = 0usize;
for pixel in corrupted.chunks_exact(RGBA_BYTES_PER_PIXEL) {
for channel in pixel.iter().take(ALPHA_INDEX) {
if *channel != 0 {
changed += 1;
}
}
}
let rgb_channels = corrupted.len() / RGBA_BYTES_PER_PIXEL * 3;
let fraction = changed as f64 / rgb_channels as f64;
assert!(
(fraction - expected).abs() < FRACTION_TOLERANCE,
"x={} image RGB fraction {} not within {} of {}",
x,
fraction,
FRACTION_TOLERANCE,
expected
);
}
}
#[test]
fn image_trailing_bytes_untouched() {
const PIXELS: usize = 1_000;
const TRAILING: [u8; 2] = [0xDE, 0xAD];
let mut payload = vec![0u8; PIXELS * RGBA_BYTES_PER_PIXEL];
payload.extend_from_slice(&TRAILING);
corrupt(&mut payload, FileType::Image, 10.0);
assert_eq!(
&payload[payload.len() - TRAILING.len()..],
&TRAILING,
"trailing bytes were modified by image corruption"
);
}
#[test]
fn region_worker_reproduces_with_seeded_rng() {
let threshold = probability_threshold(10.0);
let original: Vec<u8> = (0..200_000u32).map(|i| (i % 251) as u8).collect();
let mut a = original.clone();
region_work(
&mut a,
FileType::Text,
threshold,
&mut StdRng::seed_from_u64(42),
);
let mut b = original.clone();
region_work(
&mut b,
FileType::Text,
threshold,
&mut StdRng::seed_from_u64(42),
);
assert_eq!(a, b, "same seed must reproduce identical corruption");
let image_payload = vec![0u8; 4_000 * RGBA_BYTES_PER_PIXEL];
let mut image_a = image_payload.clone();
region_work(
&mut image_a,
FileType::Image,
threshold,
&mut StdRng::seed_from_u64(42),
);
let mut image_b = image_payload.clone();
region_work(
&mut image_b,
FileType::Image,
threshold,
&mut StdRng::seed_from_u64(42),
);
assert_eq!(
image_a, image_b,
"same seed must reproduce identical corruption"
);
}
}