msrt 0.1.5

Portable MSRT protocol implementation.
Documentation
#[derive(Clone, Debug)]
pub(crate) struct NoiseLcg {
    state: u32,
}

#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub(crate) struct NoiseConfig {
    pub(crate) corrupt_per_mille: u16,
    pub(crate) drop_byte_per_mille: u16,
    pub(crate) insert_byte_per_mille: u16,
    pub(crate) burst_corrupt_per_mille: u16,
    pub(crate) burst_drop_per_mille: u16,
    pub(crate) packet_drop_per_mille: u16,
}

#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub(crate) struct NoiseStats {
    pub(crate) corrupted: usize,
    pub(crate) dropped: usize,
    pub(crate) inserted: usize,
    pub(crate) burst_corrupted: usize,
    pub(crate) burst_dropped: usize,
    pub(crate) packet_dropped: usize,
}

impl NoiseLcg {
    pub(crate) const fn new() -> Self {
        Self { state: 0x4d535254 }
    }

    pub(crate) const fn with_seed(seed: u32) -> Self {
        Self { state: seed }
    }

    pub(crate) fn next_byte(&mut self) -> u8 {
        self.state = self
            .state
            .wrapping_mul(1_664_525)
            .wrapping_add(1_013_904_223);
        (self.state >> 24) as u8
    }

    fn next_u16(&mut self) -> u16 {
        u16::from(self.next_byte()) << 8 | u16::from(self.next_byte())
    }

    pub(crate) fn mutate_bytes(
        &mut self,
        bytes: &[u8],
        config: NoiseConfig,
    ) -> (Vec<u8>, NoiseStats) {
        let mut out = bytes.to_vec();
        let mut stats = NoiseStats::default();

        if self.roll_per_mille(config.packet_drop_per_mille) {
            stats.packet_dropped += 1;
            return (Vec::new(), stats);
        }

        if self.roll_per_mille(config.burst_corrupt_per_mille) && !out.is_empty() {
            let len = self.burst_len(out.len());
            let start = self.next_byte() as usize % (out.len() - len + 1);
            for byte in &mut out[start..start + len] {
                *byte ^= self.next_byte() | 1;
            }
            stats.burst_corrupted += 1;
        }

        if self.roll_per_mille(config.burst_drop_per_mille) && !out.is_empty() {
            let len = self.burst_len(out.len());
            let start = self.next_byte() as usize % (out.len() - len + 1);
            out.drain(start..start + len);
            stats.burst_dropped += 1;
        }

        match self.choose_action(config) {
            NoiseAction::Corrupt if !out.is_empty() => {
                let pos = self.next_byte() as usize % out.len();
                out[pos] ^= self.next_byte() | 1;
                stats.corrupted += 1;
            }
            NoiseAction::Drop if !out.is_empty() => {
                let pos = self.next_byte() as usize % out.len();
                out.remove(pos);
                stats.dropped += 1;
            }
            NoiseAction::Insert => {
                let pos = if out.is_empty() {
                    0
                } else {
                    self.next_byte() as usize % (out.len() + 1)
                };
                out.insert(pos, self.next_byte());
                stats.inserted += 1;
            }
            NoiseAction::None | NoiseAction::Corrupt | NoiseAction::Drop => {}
        }

        (out, stats)
    }

    fn roll_per_mille(&mut self, threshold: u16) -> bool {
        threshold != 0 && self.next_u16() % 1000 < threshold
    }

    fn burst_len(&mut self, max_len: usize) -> usize {
        let cap = core::cmp::min(max_len, 16);
        1 + self.next_byte() as usize % cap
    }

    fn choose_action(&mut self, config: NoiseConfig) -> NoiseAction {
        let roll = self.next_u16() % 1000;
        let corrupt_end = config.corrupt_per_mille;
        let drop_end = corrupt_end.saturating_add(config.drop_byte_per_mille);
        let insert_end = drop_end.saturating_add(config.insert_byte_per_mille);

        if roll < corrupt_end {
            NoiseAction::Corrupt
        } else if roll < drop_end {
            NoiseAction::Drop
        } else if roll < insert_end {
            NoiseAction::Insert
        } else {
            NoiseAction::None
        }
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum NoiseAction {
    None,
    Corrupt,
    Drop,
    Insert,
}

pub(crate) fn parse_percent_per_mille(value: String, name: &str) -> Result<u16, String> {
    let percent = value
        .parse::<f64>()
        .map_err(|error| format!("invalid {name}: {error}"))?;

    if !(0.0..=100.0).contains(&percent) {
        return Err(format!("{name} must be between 0 and 100"));
    }

    Ok((percent * 10.0).round() as u16)
}

pub(crate) fn has_noise(config: NoiseConfig) -> bool {
    config.corrupt_per_mille != 0
        || config.drop_byte_per_mille != 0
        || config.insert_byte_per_mille != 0
        || config.burst_corrupt_per_mille != 0
        || config.burst_drop_per_mille != 0
        || config.packet_drop_per_mille != 0
}

#[allow(dead_code)]
pub(crate) fn make_noise_bytes(len: usize) -> Vec<u8> {
    let mut lcg = NoiseLcg::new();
    (0..len).map(|_| lcg.next_byte()).collect()
}

pub(crate) fn mutate_or_copy(
    noise_state: &mut NoiseLcg,
    bytes: &[u8],
    config: NoiseConfig,
) -> (Vec<u8>, NoiseStats) {
    if !has_noise(config) {
        return (bytes.to_vec(), NoiseStats::default());
    }

    noise_state.mutate_bytes(bytes, config)
}