140 lines
4.1 KiB
Rust
140 lines
4.1 KiB
Rust
use clap::{Parser, Args};
|
|
use rand::{Rng, distributions::{Distribution, Uniform}};
|
|
|
|
use std::io::{self, Read, Write};
|
|
|
|
const BUF_SIZE: usize = 8192;
|
|
|
|
#[derive(Parser, Debug)]
|
|
#[clap(author, version)]
|
|
/// Introduce transcription errors into a pipeline
|
|
///
|
|
/// badpipe passes data from stdin to stdout while randomly
|
|
/// introducing single-byte transcription errors. Its intended use is
|
|
/// for testing how other programs handle data corruption caused by
|
|
/// poor network conditions or bitrot.
|
|
struct Cli {
|
|
/// Log the location and details of each transcription error to stderr
|
|
#[clap(short, long)]
|
|
verbose: bool,
|
|
|
|
/// Introduce an error every N bytes on average
|
|
#[clap(short, long, value_name = "N", default_value_t = 1000)]
|
|
rarity: u32,
|
|
|
|
#[clap(flatten, help_heading = "Types of errors to introduce. If none are specified individually, all will be allowed")]
|
|
error_spec: ErrorSpec,
|
|
}
|
|
|
|
#[derive(Args, Debug, Clone, Copy)]
|
|
struct ErrorSpec {
|
|
/// Insert extra bytes into the stream
|
|
#[clap(short, long)]
|
|
insert: bool,
|
|
|
|
/// Delete bytes from the stream
|
|
#[clap(short, long)]
|
|
delete: bool,
|
|
|
|
/// Alter bytes in the stream
|
|
#[clap(short, long)]
|
|
alter: bool,
|
|
}
|
|
|
|
enum Error {
|
|
Insert(u8),
|
|
Delete,
|
|
Alter(u8),
|
|
}
|
|
|
|
impl ErrorSpec {
|
|
fn gen(self) -> Error {
|
|
let spec = if let ErrorSpec { insert: false, delete: false, alter: false } = self {
|
|
ErrorSpec { insert: true, delete: true, alter: true }
|
|
} else {
|
|
self
|
|
};
|
|
|
|
let mut options_remaining =
|
|
if spec.insert { 1 } else { 0 } +
|
|
if spec.delete { 1 } else { 0 } +
|
|
if spec.alter { 1 } else { 0 };
|
|
|
|
let mut rng = rand::thread_rng();
|
|
|
|
if spec.insert {
|
|
if rng.gen_range(0..options_remaining) == 0 {
|
|
return Error::Insert(rng.gen())
|
|
}
|
|
options_remaining -= 1;
|
|
}
|
|
if spec.delete {
|
|
if rng.gen_range(0..options_remaining) == 0 {
|
|
return Error::Delete
|
|
}
|
|
options_remaining -= 1;
|
|
}
|
|
if spec.alter {
|
|
if rng.gen_range(0..options_remaining) == 0 {
|
|
return Error::Alter(rng.gen())
|
|
}
|
|
options_remaining -= 1;
|
|
}
|
|
panic!("Expected 0 options remaining, got {}, this shouldn't happen", options_remaining)
|
|
}
|
|
}
|
|
|
|
fn read_handling_interruptions<T: Read>(source: &mut T, dest: &mut [u8]) -> io::Result<usize> {
|
|
loop {
|
|
let result = source.read(dest);
|
|
if let Err(e) = &result {
|
|
if let io::ErrorKind::Interrupted = e.kind() {
|
|
continue
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
}
|
|
|
|
fn main() {
|
|
let cli = Cli::parse();
|
|
|
|
let mut stdin = io::stdin();
|
|
let mut stdout = io::stdout();
|
|
let mut buffer = [0u8; BUF_SIZE];
|
|
let mut total = 0;
|
|
let rarity = Uniform::from(0..cli.rarity);
|
|
let mut rng = rand::thread_rng();
|
|
|
|
loop {
|
|
let fill = read_handling_interruptions(&mut stdin, &mut buffer).unwrap();
|
|
if fill == 0 {
|
|
break
|
|
}
|
|
let mut pos = 0;
|
|
let error_indices = (0..fill).filter(|_| rarity.sample(&mut rng) == 0);
|
|
for idx in error_indices {
|
|
stdout.write_all(&buffer[pos..idx]).unwrap();
|
|
if cli.verbose { eprint!("@ 0x{:08x}: ", total + idx); }
|
|
match cli.error_spec.gen() {
|
|
Error::Insert(b) => {
|
|
stdout.write_all(&[buffer[idx], b]).unwrap();
|
|
if cli.verbose { eprintln!("Inserted byte {:02x}", b); }
|
|
}
|
|
Error::Delete => {
|
|
if cli.verbose { eprintln!("Deleted byte {:02x}", buffer[idx]); }
|
|
}
|
|
Error::Alter(b) => {
|
|
stdout.write_all(&[b]).unwrap();
|
|
if cli.verbose { eprintln!("Altered byte {:02x} to {:02x}", buffer[idx], b); }
|
|
}
|
|
}
|
|
pos = idx + 1;
|
|
}
|
|
stdout.write_all(&buffer[pos..fill]).unwrap();
|
|
total += fill;
|
|
}
|
|
|
|
if cli.verbose { eprintln!("Reached EOF after {} bytes", total); }
|
|
}
|