use std::collections::HashSet;
use std::fs;
use std::io::{Seek, SeekFrom, Write};
use std::path::Path;
use crate::blocks::conversion::ConversionType;
use crate::blocks::BLOCK_HEADER_SIZE;
use crate::error::{Mf4Error, Result};
use crate::inspect::BlockMap;
use crate::io::{ByteSource, IoBackend};
use crate::parser::{parse_cc_block, parse_id_block};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ScrambleReport {
pub blocks_scrambled: usize,
pub bytes_scrambled: u64,
pub blocks_preserved: usize,
}
pub fn scramble_file<P: AsRef<Path>, Q: AsRef<Path>>(
src: P,
dst: Q,
seed: u64,
) -> Result<ScrambleReport> {
let src = src.as_ref();
let dst = dst.as_ref();
let patches = {
let source = IoBackend::open(src)?;
let id = parse_id_block(&source)?;
if id.version_number < 400 {
return Err(Mf4Error::unsupported(
"scramble",
format!(
"file declares MDF version {}, and this walks MF4 blocks; \
only version 400 and above can be scrambled",
id.version_number
),
));
}
plan_patches(&source, seed)?
};
fs::copy(src, dst)?;
let mut out = fs::OpenOptions::new().write(true).open(dst)?;
let mut report = ScrambleReport {
blocks_scrambled: 0,
bytes_scrambled: 0,
blocks_preserved: patches.preserved,
};
for patch in &patches.writes {
if patch.replacement.len() as u64 != patch.original_len {
return Err(Mf4Error::write_error(format!(
"refusing to scramble: replacement text for the block at {:#x} is {} bytes \
where the original is {}, and writing it would shift every later block",
patch.address,
patch.replacement.len(),
patch.original_len
)));
}
out.seek(SeekFrom::Start(patch.address))?;
out.write_all(&patch.replacement)?;
report.blocks_scrambled += 1;
report.bytes_scrambled += patch.changed;
}
out.flush()?;
Ok(report)
}
struct Patch {
address: u64,
replacement: Vec<u8>,
original_len: u64,
changed: u64,
}
struct Patches {
writes: Vec<Patch>,
preserved: usize,
}
fn plan_patches<S: ByteSource>(source: &S, seed: u64) -> Result<Patches> {
let map = BlockMap::scan(source);
let protected = decode_critical_text(source, &map);
let mut rng = Rng::new(seed);
let mut writes = Vec::new();
let mut preserved = 0;
for block in &map.blocks {
let is_md = match block.block_type.as_str() {
"##TX" => false,
"##MD" => true,
_ => continue,
};
if protected.contains(&block.address) {
preserved += 1;
continue;
}
let start = block.address + BLOCK_HEADER_SIZE as u64 + 8 * block.link_count;
let len = block.data_size as usize;
if len == 0 {
continue;
}
let data = source.read_bytes(start, len)?;
let (replacement, changed) = if is_md {
scramble_markup(&data, &mut rng)
} else {
scramble_text(&data, &mut rng)
};
if changed > 0 {
writes.push(Patch {
address: start,
replacement,
original_len: len as u64,
changed,
});
}
}
Ok(Patches { writes, preserved })
}
fn decode_critical_text<S: ByteSource>(source: &S, map: &BlockMap) -> HashSet<u64> {
let mut protected = HashSet::new();
for block in &map.blocks {
if block.block_type != "##CC" {
continue;
}
let Ok(cc) = parse_cc_block(source, block.address) else {
continue;
};
match cc.conversion_type {
ConversionType::Algebraic => protected.extend(cc.references.first().copied()),
ConversionType::TabTextToValue => protected.extend(cc.references.iter().copied()),
ConversionType::TabTextToText => {
protected.extend(cc.references.iter().step_by(2).copied())
}
_ => {}
}
}
protected.remove(&0);
protected
}
fn scramble_text(data: &[u8], rng: &mut Rng) -> (Vec<u8>, u64) {
let end = data.iter().position(|&b| b == 0).unwrap_or(data.len());
let mut out = data.to_vec();
for byte in &mut out[..end] {
*byte = rng.letter();
}
(out, end as u64)
}
fn scramble_markup(data: &[u8], rng: &mut Rng) -> (Vec<u8>, u64) {
let end = data.iter().position(|&b| b == 0).unwrap_or(data.len());
let mut out = data.to_vec();
let mut in_tag = false;
let mut changed = 0;
for byte in &mut out[..end] {
match *byte {
b'<' => in_tag = true,
b'>' => in_tag = false,
_ if in_tag || byte.is_ascii_whitespace() => {}
_ => {
*byte = rng.letter();
changed += 1;
}
}
}
(out, changed)
}
struct Rng(u64);
impl Rng {
fn new(seed: u64) -> Self {
Rng(seed)
}
fn next_u64(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
fn letter(&mut self) -> u8 {
b'A' + (self.next_u64() % 26) as u8
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn text_keeps_its_length_terminator_and_padding() {
let data = b"Speed\0\0\0";
let (out, changed) = scramble_text(data, &mut Rng::new(1));
assert_eq!(
out.len(),
data.len(),
"the data section may not change size"
);
assert_eq!(changed, 5);
assert_eq!(&out[5..], b"\0\0\0", "terminator and padding are untouched");
assert_ne!(&out[..5], b"Speed");
assert!(out[..5].iter().all(|b| b.is_ascii_uppercase()));
}
#[test]
fn a_multibyte_name_stays_the_same_number_of_bytes() {
let data = "Temperatur°C\0".as_bytes();
assert_eq!(data.len(), 14, "13 text bytes and a terminator");
let (out, changed) = scramble_text(data, &mut Rng::new(2));
assert_eq!(out.len(), data.len());
assert_eq!(changed, 13, "the degree sign counts as its two bytes");
assert!(
std::str::from_utf8(&out[..13]).is_ok(),
"replacing bytes with ASCII letters must leave valid UTF-8"
);
}
#[test]
fn empty_text_has_nothing_to_replace() {
let (out, changed) = scramble_text(b"\0\0\0\0", &mut Rng::new(3));
assert_eq!(changed, 0);
assert_eq!(&out, b"\0\0\0\0");
}
#[test]
fn markup_survives_the_scrambling_of_what_it_holds() {
let data = b"<TXcomment><TX>Engine speed</TX></TXcomment>\0";
let (out, changed) = scramble_markup(data, &mut Rng::new(4));
let text = std::str::from_utf8(&out[..out.len() - 1]).unwrap();
assert_eq!(out.len(), data.len());
assert_eq!(changed, 11, "'Engine speed' less its space");
assert!(text.starts_with("<TXcomment><TX>"), "tags are kept: {text}");
assert!(text.ends_with("</TX></TXcomment>"), "tags are kept: {text}");
assert!(!text.contains("Engine"), "the content is gone: {text}");
assert!(text.contains(' '), "word spacing is kept: {text}");
}
#[test]
fn the_same_seed_gives_the_same_scrambling() {
let data = b"Coolant temperature\0";
let (a, _) = scramble_text(data, &mut Rng::new(7));
let (b, _) = scramble_text(data, &mut Rng::new(7));
let (c, _) = scramble_text(data, &mut Rng::new(8));
assert_eq!(a, b, "one seed, one result");
assert_ne!(a, c, "a different seed is a different result");
}
}