Загрузить файлы в «src»
This commit is contained in:
parent
0c679ee232
commit
c52756d17c
5 changed files with 901 additions and 0 deletions
433
src/alphabet.rs
Normal file
433
src/alphabet.rs
Normal file
|
|
@ -0,0 +1,433 @@
|
||||||
|
use bitvec::prelude::{BitSlice, BitVec};
|
||||||
|
use rand::{CryptoRng, Rng, RngExt};
|
||||||
|
use std::io::{Error, ErrorKind, Result};
|
||||||
|
|
||||||
|
const MAGIC: [u8; 4] = [b'E', b'C', b'A', b'1'];
|
||||||
|
|
||||||
|
pub struct BitFlip {
|
||||||
|
pub index: usize,
|
||||||
|
pub old: bool,
|
||||||
|
pub new: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DecipherPatch {
|
||||||
|
pub symbol: char,
|
||||||
|
pub distance: usize,
|
||||||
|
pub flips: Vec<BitFlip>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Alphabet<R: CryptoRng> {
|
||||||
|
r: R,
|
||||||
|
chars: String,
|
||||||
|
dict: BitVec<u8>,
|
||||||
|
len: usize,
|
||||||
|
mut_rate: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: CryptoRng + Rng> Alphabet<R> {
|
||||||
|
|
||||||
|
fn symbol_index(&self, c: char) -> Result<usize> {
|
||||||
|
self.chars
|
||||||
|
.chars()
|
||||||
|
.position(|x| x == c)
|
||||||
|
.ok_or_else(|| Error::new(ErrorKind::InvalidInput, format!("symbol {:?} not found", c)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn symbol_bits(&self, c: char) -> Result<BitVec<u8>> {
|
||||||
|
let index = self.symbol_index(c)?;
|
||||||
|
let start = index * self.len;
|
||||||
|
let end = (index + 1) * self.len;
|
||||||
|
Ok(self.dict[start..end].to_bitvec())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn replace_symbol_bits(
|
||||||
|
&mut self,
|
||||||
|
c: char,
|
||||||
|
bits: &BitSlice<u8>,
|
||||||
|
) -> Result<BitVec<u8>> {
|
||||||
|
if bits.len() < self.len {
|
||||||
|
return Err(Error::new(
|
||||||
|
ErrorKind::InvalidInput,
|
||||||
|
"not enough bits for symbol replacement",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let index = self.symbol_index(c)?;
|
||||||
|
let start = index * self.len;
|
||||||
|
let end = (index + 1) * self.len;
|
||||||
|
|
||||||
|
let old = self.dict[start..end].to_bitvec();
|
||||||
|
|
||||||
|
for i in 0..self.len {
|
||||||
|
self.dict.set(start + i, bits[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(old)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn replace_symbol_bits_by_index(
|
||||||
|
&mut self,
|
||||||
|
index: usize,
|
||||||
|
bits: &BitSlice<u8>,
|
||||||
|
) -> Result<BitVec<u8>> {
|
||||||
|
if bits.len() < self.len {
|
||||||
|
return Err(Error::new(
|
||||||
|
ErrorKind::InvalidInput,
|
||||||
|
"not enough bits for symbol replacement",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let start = index * self.len;
|
||||||
|
let end = (index + 1) * self.len;
|
||||||
|
|
||||||
|
if end > self.dict.len() {
|
||||||
|
return Err(Error::new(ErrorKind::InvalidInput, "symbol index out of range"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let old = self.dict[start..end].to_bitvec();
|
||||||
|
|
||||||
|
for i in 0..self.len {
|
||||||
|
self.dict.set(start + i, bits[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(old)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn symbol_distance(&self, c: char, bits: &BitSlice<u8>) -> Result<usize> {
|
||||||
|
if bits.len() < self.len {
|
||||||
|
return Err(Error::new(
|
||||||
|
ErrorKind::InvalidInput,
|
||||||
|
"not enough bits for one symbol",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let index = self.symbol_index(c)?;
|
||||||
|
let start = index * self.len;
|
||||||
|
let end = (index + 1) * self.len;
|
||||||
|
let letter = &self.dict[start..end];
|
||||||
|
|
||||||
|
Ok(bits[..self.len]
|
||||||
|
.iter()
|
||||||
|
.zip(letter.iter())
|
||||||
|
.filter(|(a, b)| *a != *b)
|
||||||
|
.count())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_symbol_choice(
|
||||||
|
&self,
|
||||||
|
bits: &mut BitSlice<u8>,
|
||||||
|
symbol: char,
|
||||||
|
) -> Result<DecipherPatch> {
|
||||||
|
if bits.len() < self.len {
|
||||||
|
return Err(Error::new(
|
||||||
|
ErrorKind::InvalidInput,
|
||||||
|
"not enough bits for one symbol",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let index = self
|
||||||
|
.chars
|
||||||
|
.chars()
|
||||||
|
.position(|x| x == symbol)
|
||||||
|
.ok_or_else(|| Error::new(ErrorKind::InvalidInput, "symbol not found in alphabet"))?;
|
||||||
|
|
||||||
|
let start = index * self.len;
|
||||||
|
let end = (index + 1) * self.len;
|
||||||
|
let letter = &self.dict[start..end];
|
||||||
|
|
||||||
|
let mut flips = Vec::new();
|
||||||
|
let mut distance = 0;
|
||||||
|
|
||||||
|
for i in 0..self.len {
|
||||||
|
let old = bits[i];
|
||||||
|
let new = letter[i];
|
||||||
|
|
||||||
|
if old != new {
|
||||||
|
distance += 1;
|
||||||
|
bits.set(i, new);
|
||||||
|
flips.push(BitFlip { index: i, old, new });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(DecipherPatch {
|
||||||
|
symbol,
|
||||||
|
distance,
|
||||||
|
flips,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn best_symbol_distance_no_effect(&self, bits: &BitSlice<u8>) -> Result<(char, usize)> {
|
||||||
|
self.decipher_candidates_no_effect(bits, 1)?
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| Error::new(ErrorKind::InvalidData, "alphabet is empty"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate(mut rng: R, chars: String, len: usize, mut_rate: usize) -> Self {
|
||||||
|
let str_len = chars.chars().count();
|
||||||
|
let dict_bit_len = len * str_len + rng.random_range(0..len);
|
||||||
|
let dict_byte_len = dict_bit_len.div_ceil(8);
|
||||||
|
let mut random_bytes = vec![0u8; dict_byte_len];
|
||||||
|
rng.fill(&mut random_bytes[..]);
|
||||||
|
let mut dict = BitVec::<u8>::from_vec(random_bytes);
|
||||||
|
dict.truncate(dict_bit_len);
|
||||||
|
Alphabet {
|
||||||
|
r: rng,
|
||||||
|
chars,
|
||||||
|
dict,
|
||||||
|
len,
|
||||||
|
mut_rate,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_bytes(self) -> Vec<u8> {
|
||||||
|
let dict_bit_len = (self.dict.len() as u64).to_le_bytes();
|
||||||
|
let d = self.dict.into_vec();
|
||||||
|
let s = self.chars.into_bytes();
|
||||||
|
let bl = (self.len as u64).to_le_bytes();
|
||||||
|
let sl_bytes = (s.len() as u64).to_le_bytes();
|
||||||
|
let mut_rate = (self.mut_rate as u64).to_le_bytes();
|
||||||
|
|
||||||
|
[
|
||||||
|
&MAGIC[..],
|
||||||
|
&bl[..],
|
||||||
|
&sl_bytes[..],
|
||||||
|
&dict_bit_len[..],
|
||||||
|
&mut_rate[..],
|
||||||
|
&s[..],
|
||||||
|
&d[..],
|
||||||
|
]
|
||||||
|
.concat()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn symbol_len(&self) -> usize {
|
||||||
|
self.len
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_bytes(rng: R, bytes: &[u8]) -> Result<Self> {
|
||||||
|
const HEADER_LEN: usize = 4 + 8 + 8 + 8 + 8;
|
||||||
|
|
||||||
|
if bytes.len() < HEADER_LEN {
|
||||||
|
return Err(Error::new(ErrorKind::InvalidData, "file too short"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if bytes[0..4] != MAGIC {
|
||||||
|
return Err(Error::new(ErrorKind::InvalidData, "bad magic"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let bl = u64::from_le_bytes(
|
||||||
|
bytes[4..12]
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| Error::new(ErrorKind::InvalidData, "bad len field"))?,
|
||||||
|
) as usize;
|
||||||
|
|
||||||
|
let sl_bytes = u64::from_le_bytes(
|
||||||
|
bytes[12..20]
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| Error::new(ErrorKind::InvalidData, "bad string length field"))?,
|
||||||
|
) as usize;
|
||||||
|
|
||||||
|
let dict_bit_len = u64::from_le_bytes(
|
||||||
|
bytes[20..28]
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| Error::new(ErrorKind::InvalidData, "bad dict bit length field"))?,
|
||||||
|
) as usize;
|
||||||
|
|
||||||
|
let mut_rate = u64::from_le_bytes(
|
||||||
|
bytes[28..36]
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| Error::new(ErrorKind::InvalidData, "bad mut_rate field"))?,
|
||||||
|
) as usize;
|
||||||
|
|
||||||
|
let dict_byte_len = dict_bit_len.div_ceil(8);
|
||||||
|
let expected_len = HEADER_LEN + sl_bytes + dict_byte_len;
|
||||||
|
|
||||||
|
if bytes.len() < expected_len {
|
||||||
|
return Err(Error::new(ErrorKind::InvalidData, "wrong file size"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let s_start = HEADER_LEN;
|
||||||
|
let s_end = s_start + sl_bytes;
|
||||||
|
let d_start = s_end;
|
||||||
|
let d_end = d_start + dict_byte_len;
|
||||||
|
|
||||||
|
let chars = String::from_utf8(bytes[s_start..s_end].to_vec())
|
||||||
|
.map_err(|_| Error::new(ErrorKind::InvalidData, "chars is not valid UTF-8"))?;
|
||||||
|
|
||||||
|
let mut dict = BitVec::<u8>::from_vec(bytes[d_start..d_end].to_vec());
|
||||||
|
dict.truncate(dict_bit_len);
|
||||||
|
|
||||||
|
Ok(Alphabet {
|
||||||
|
r: rng,
|
||||||
|
chars,
|
||||||
|
dict,
|
||||||
|
len: bl,
|
||||||
|
mut_rate,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cipher_no_effect(&self, c: char) -> Result<BitVec<u8>> {
|
||||||
|
let index = self.chars.chars().position(|x| x == c).ok_or_else(|| {
|
||||||
|
Error::new(ErrorKind::InvalidInput, format!("symbol {:?} not found", c))
|
||||||
|
})?;
|
||||||
|
let start = index * self.len;
|
||||||
|
let end = (index + 1) * self.len;
|
||||||
|
let letter = self.dict[start..end].to_bitvec();
|
||||||
|
|
||||||
|
Ok(letter)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decipher_candidates_no_effect(
|
||||||
|
&self,
|
||||||
|
bits: &bitvec::slice::BitSlice<u8>,
|
||||||
|
top: usize,
|
||||||
|
) -> Result<Vec<(char, usize)>> {
|
||||||
|
if bits.len() < self.len {
|
||||||
|
return Err(Error::new(
|
||||||
|
ErrorKind::InvalidInput,
|
||||||
|
"not enough bits for one symbol",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let needle = &bits[..self.len];
|
||||||
|
let mut candidates = Vec::new();
|
||||||
|
|
||||||
|
for (index, ch) in self.chars.chars().enumerate() {
|
||||||
|
let start = index * self.len;
|
||||||
|
let end = (index + 1) * self.len;
|
||||||
|
let letter = &self.dict[start..end];
|
||||||
|
|
||||||
|
let distance = needle
|
||||||
|
.iter()
|
||||||
|
.zip(letter.iter())
|
||||||
|
.filter(|(a, b)| *a != *b)
|
||||||
|
.count();
|
||||||
|
|
||||||
|
candidates.push((ch, distance));
|
||||||
|
}
|
||||||
|
|
||||||
|
candidates.sort_by_key(|(_, distance)| *distance);
|
||||||
|
|
||||||
|
if candidates.len() > top {
|
||||||
|
candidates.truncate(top);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(candidates)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_symbol_repair(
|
||||||
|
&self,
|
||||||
|
bits: &mut bitvec::slice::BitSlice<u8>,
|
||||||
|
) -> Result<DecipherPatch> {
|
||||||
|
if bits.len() < self.len {
|
||||||
|
return Err(Error::new(
|
||||||
|
ErrorKind::InvalidInput,
|
||||||
|
"not enough bits for one symbol",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let best = self
|
||||||
|
.decipher_candidates_no_effect(bits, 1)?
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| Error::new(ErrorKind::InvalidData, "alphabet is empty"))?;
|
||||||
|
|
||||||
|
let (symbol, distance) = best;
|
||||||
|
|
||||||
|
let index = self
|
||||||
|
.chars
|
||||||
|
.chars()
|
||||||
|
.position(|x| x == symbol)
|
||||||
|
.ok_or_else(|| Error::new(ErrorKind::InvalidData, "best symbol not found"))?;
|
||||||
|
|
||||||
|
let start = index * self.len;
|
||||||
|
let end = (index + 1) * self.len;
|
||||||
|
let letter = &self.dict[start..end];
|
||||||
|
|
||||||
|
let mut flips = Vec::new();
|
||||||
|
|
||||||
|
for i in 0..self.len {
|
||||||
|
let old = bits[i];
|
||||||
|
let new = letter[i];
|
||||||
|
|
||||||
|
if old != new {
|
||||||
|
bits.set(i, new);
|
||||||
|
flips.push(BitFlip { index: i, old, new });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(DecipherPatch {
|
||||||
|
symbol,
|
||||||
|
distance,
|
||||||
|
flips,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn undo_patch(
|
||||||
|
&self,
|
||||||
|
bits: &mut bitvec::slice::BitSlice<u8>,
|
||||||
|
patch: &DecipherPatch,
|
||||||
|
) -> Result<()> {
|
||||||
|
if bits.len() < self.len {
|
||||||
|
return Err(Error::new(
|
||||||
|
ErrorKind::InvalidInput,
|
||||||
|
"not enough bits for one symbol",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
for flip in &patch.flips {
|
||||||
|
bits.set(flip.index, flip.old);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mutate(&mut self) {
|
||||||
|
let end = self.dict.len();
|
||||||
|
|
||||||
|
if end == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let v: Vec<usize> = (0..self.mut_rate)
|
||||||
|
.map(|_| self.r.random_range(0..end))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
for i in v {
|
||||||
|
let val = self.dict[i];
|
||||||
|
self.dict.set(i, !val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub trait MutationallyCipherable<R: CryptoRng + Rng> {
|
||||||
|
fn cipher(self, a: &mut Alphabet<R>) -> Result<BitVec<u8>>;
|
||||||
|
}
|
||||||
|
impl<R: CryptoRng + Rng> MutationallyCipherable<R> for char {
|
||||||
|
fn cipher(self, a: &mut Alphabet<R>) -> Result<BitVec<u8>> {
|
||||||
|
let res = a.cipher_no_effect(self)?;
|
||||||
|
a.mutate();
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<R: CryptoRng + Rng> MutationallyCipherable<R> for String {
|
||||||
|
fn cipher(self, a: &mut Alphabet<R>) -> Result<BitVec<u8>> {
|
||||||
|
let mut res: BitVec<u8> = BitVec::new();
|
||||||
|
for c in self.chars() {
|
||||||
|
res.extend(c.cipher(a)?)
|
||||||
|
}
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<R: CryptoRng + Rng> MutationallyCipherable<R> for &str {
|
||||||
|
fn cipher(self, a: &mut Alphabet<R>) -> Result<BitVec<u8>> {
|
||||||
|
let mut res: BitVec<u8> = BitVec::new();
|
||||||
|
for c in self.chars() {
|
||||||
|
res.extend(c.cipher(a)?)
|
||||||
|
}
|
||||||
|
let tail_len = a.r.random_range(..a.dict.len());
|
||||||
|
for _ in 1..tail_len {
|
||||||
|
res.push(a.r.random_bool(0.5));
|
||||||
|
}
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
156
src/app.rs
Normal file
156
src/app.rs
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
use crate::alphabet::{Alphabet, MutationallyCipherable};
|
||||||
|
use crate::ciphertext::Ciphertext;
|
||||||
|
use crate::storage::AlphabetFile;
|
||||||
|
use bitvec::prelude::BitVec;
|
||||||
|
use rand_chacha::ChaCha20Rng;
|
||||||
|
use std::fs;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::{self, Error, ErrorKind, Read, Write};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
fn parse_bitvec_input(s: &str) -> io::Result<BitVec<u8>> {
|
||||||
|
let mut bits = BitVec::<u8>::new();
|
||||||
|
|
||||||
|
for ch in s.chars() {
|
||||||
|
match ch {
|
||||||
|
'0' => bits.push(false),
|
||||||
|
'1' => bits.push(true),
|
||||||
|
c if c.is_whitespace() => {}
|
||||||
|
other => {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
format!("unexpected character in bitstream: {:?}", other),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(bits)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_input(file: Option<PathBuf>) -> io::Result<String> {
|
||||||
|
let mut s = String::new();
|
||||||
|
|
||||||
|
match file {
|
||||||
|
Some(path) => File::open(path)?.read_to_string(&mut s)?,
|
||||||
|
None => io::stdin().read_to_string(&mut s)?,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_cmd(
|
||||||
|
path: PathBuf,
|
||||||
|
chars: Option<String>,
|
||||||
|
chars_file: Option<PathBuf>,
|
||||||
|
len: usize,
|
||||||
|
mut_rate: usize,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
if len == 0 {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"len must be > 0",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let rng: ChaCha20Rng = rand::make_rng();
|
||||||
|
let chars = read_chars(chars, chars_file)?;
|
||||||
|
let alphabet = Alphabet::generate(rng, chars, len, mut_rate);
|
||||||
|
let af = AlphabetFile::create(path, alphabet)?;
|
||||||
|
af.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cipher_cmd(
|
||||||
|
alphabet_path: PathBuf,
|
||||||
|
file: Option<PathBuf>,
|
||||||
|
output: Option<PathBuf>,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
let rng: ChaCha20Rng = rand::make_rng();
|
||||||
|
let mut af = AlphabetFile::open(alphabet_path, rng)?;
|
||||||
|
let input = read_input(file)?;
|
||||||
|
let bits = input
|
||||||
|
.as_str()
|
||||||
|
.cipher(&mut af.dict)
|
||||||
|
.map_err(io::Error::other)?;
|
||||||
|
|
||||||
|
let ciphertext = Ciphertext::from_bits(bits);
|
||||||
|
let bytes = ciphertext.into_bytes();
|
||||||
|
write_output_bytes(output, &bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_chars(chars: Option<String>, chars_file: Option<PathBuf>) -> io::Result<String> {
|
||||||
|
if let Some(path) = chars_file {
|
||||||
|
let s = fs::read_to_string(path)?;
|
||||||
|
if s.is_empty() {
|
||||||
|
return Err(Error::new(ErrorKind::InvalidInput, "chars file is empty"));
|
||||||
|
}
|
||||||
|
return Ok(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(s) = chars {
|
||||||
|
if s.is_empty() {
|
||||||
|
return Err(Error::new(ErrorKind::InvalidInput, "chars is empty"));
|
||||||
|
}
|
||||||
|
return Ok(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok("abcdefghijklmoprqstuvwxyz ".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_input_bytes(file: Option<PathBuf>) -> io::Result<Vec<u8>> {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
|
||||||
|
match file {
|
||||||
|
Some(path) => File::open(path)?.read_to_end(&mut buf)?,
|
||||||
|
None => io::stdin().read_to_end(&mut buf)?,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decipher_cmd(
|
||||||
|
alphabet_path: PathBuf,
|
||||||
|
file: Option<PathBuf>,
|
||||||
|
output: Option<PathBuf>,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
let rng: ChaCha20Rng = rand::make_rng();
|
||||||
|
let af = AlphabetFile::open(alphabet_path, rng)?;
|
||||||
|
let bytes = read_input_bytes(file)?;
|
||||||
|
let mut bits = Ciphertext::from_bytes(&bytes)
|
||||||
|
.map_err(io::Error::other)?
|
||||||
|
.bits;
|
||||||
|
|
||||||
|
let mut out = String::new();
|
||||||
|
let mut pos = 0usize;
|
||||||
|
let sym_len = af.dict.symbol_len();
|
||||||
|
|
||||||
|
while pos + sym_len <= bits.len() {
|
||||||
|
let end = pos + sym_len;
|
||||||
|
let patch = af
|
||||||
|
.dict
|
||||||
|
.apply_symbol_repair(&mut bits[pos..end])
|
||||||
|
.map_err(io::Error::other)?;
|
||||||
|
|
||||||
|
out.push(patch.symbol);
|
||||||
|
pos = end;
|
||||||
|
}
|
||||||
|
|
||||||
|
write_output_text(output, &out)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_output_bytes(output: Option<PathBuf>, bytes: &[u8]) -> io::Result<()> {
|
||||||
|
match output {
|
||||||
|
Some(path) => fs::write(path, bytes),
|
||||||
|
None => io::stdout().write_all(bytes),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_output_text(output: Option<PathBuf>, text: &str) -> io::Result<()> {
|
||||||
|
match output {
|
||||||
|
Some(path) => fs::write(path, text),
|
||||||
|
None => {
|
||||||
|
println!("{text}");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
54
src/ciphertext.rs
Normal file
54
src/ciphertext.rs
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
use bitvec::prelude::BitVec;
|
||||||
|
use std::io::{Error, ErrorKind, Result};
|
||||||
|
|
||||||
|
const MAGIC: [u8; 4] = [b'E', b'C', b'T', b'1'];
|
||||||
|
|
||||||
|
pub struct Ciphertext {
|
||||||
|
pub bits: BitVec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ciphertext {
|
||||||
|
pub fn from_bits(bits: BitVec<u8>) -> Self {
|
||||||
|
Self { bits }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_bytes(self) -> Vec<u8> {
|
||||||
|
let bit_len = (self.bits.len() as u64).to_le_bytes();
|
||||||
|
let payload = self.bits.into_vec();
|
||||||
|
|
||||||
|
[&MAGIC[..], &bit_len[..], &payload[..]].concat()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
|
||||||
|
const HEADER_LEN: usize = 4 + 8;
|
||||||
|
|
||||||
|
if bytes.len() < HEADER_LEN {
|
||||||
|
return Err(Error::new(ErrorKind::InvalidData, "ciphertext too short"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if bytes[0..4] != MAGIC {
|
||||||
|
return Err(Error::new(ErrorKind::InvalidData, "bad ciphertext magic"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let bit_len = u64::from_le_bytes(
|
||||||
|
bytes[4..12]
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| Error::new(ErrorKind::InvalidData, "bad ciphertext bit length"))?,
|
||||||
|
) as usize;
|
||||||
|
|
||||||
|
let payload = bytes[12..].to_vec();
|
||||||
|
let expected_payload_len = bit_len.div_ceil(8);
|
||||||
|
|
||||||
|
if payload.len() < expected_payload_len {
|
||||||
|
return Err(Error::new(
|
||||||
|
ErrorKind::InvalidData,
|
||||||
|
"ciphertext payload too short",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut bits = BitVec::<u8>::from_vec(payload);
|
||||||
|
bits.truncate(bit_len);
|
||||||
|
|
||||||
|
Ok(Self { bits })
|
||||||
|
}
|
||||||
|
}
|
||||||
56
src/cli.rs
Normal file
56
src/cli.rs
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
use clap::{Parser, Subcommand};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(name = "cli")]
|
||||||
|
#[command(version)]
|
||||||
|
pub struct Cli {
|
||||||
|
#[command(subcommand)]
|
||||||
|
pub command: Commands,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
pub enum Commands {
|
||||||
|
Generate {
|
||||||
|
path: PathBuf,
|
||||||
|
|
||||||
|
#[arg(long = "chars")]
|
||||||
|
chars: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long = "chars-file")]
|
||||||
|
chars_file: Option<PathBuf>,
|
||||||
|
|
||||||
|
#[arg(long = "len", default_value_t = 8)]
|
||||||
|
len: usize,
|
||||||
|
|
||||||
|
#[arg(long = "mut-rate", default_value_t = 8)]
|
||||||
|
mut_rate: usize,
|
||||||
|
},
|
||||||
|
Cipher {
|
||||||
|
#[arg(short = 'a', long = "alphabet")]
|
||||||
|
alphabet: PathBuf,
|
||||||
|
|
||||||
|
#[arg(short = 'f', long = "file")]
|
||||||
|
file: Option<PathBuf>,
|
||||||
|
|
||||||
|
#[arg(short = 'o', long = "output")]
|
||||||
|
output: Option<PathBuf>,
|
||||||
|
},
|
||||||
|
Decipher {
|
||||||
|
#[arg(short = 'a', long = "alphabet")]
|
||||||
|
alphabet: PathBuf,
|
||||||
|
|
||||||
|
#[arg(short = 'f', long = "file")]
|
||||||
|
file: Option<PathBuf>,
|
||||||
|
|
||||||
|
#[arg(short = 'o', long = "output")]
|
||||||
|
output: Option<PathBuf>,
|
||||||
|
},
|
||||||
|
Tui {
|
||||||
|
#[arg(short = 'a', long = "alphabet")]
|
||||||
|
alphabet: PathBuf,
|
||||||
|
|
||||||
|
#[arg(short = 'f', long = "file")]
|
||||||
|
file: PathBuf,
|
||||||
|
},
|
||||||
|
}
|
||||||
202
src/decoder.rs
Normal file
202
src/decoder.rs
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
use crate::alphabet::Alphabet;
|
||||||
|
use bitvec::prelude::BitVec;
|
||||||
|
use rand::SeedableRng;
|
||||||
|
use rand_chacha::ChaCha20Rng;
|
||||||
|
use std::io::{Error, ErrorKind, Result};
|
||||||
|
|
||||||
|
pub struct CandidateScore {
|
||||||
|
pub symbol: char,
|
||||||
|
pub distance: usize,
|
||||||
|
pub future_distance: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum RankingMode {
|
||||||
|
LocalFirst,
|
||||||
|
FutureFirst,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DecoderState {
|
||||||
|
alphabet_bytes: Vec<u8>,
|
||||||
|
rng_seed: <ChaCha20Rng as SeedableRng>::Seed,
|
||||||
|
base_ciphertext: BitVec<u8>,
|
||||||
|
|
||||||
|
pub cursor: usize,
|
||||||
|
pub text_scroll: usize,
|
||||||
|
pub choices: Vec<Option<char>>,
|
||||||
|
pub rendered_text: String,
|
||||||
|
pub working_bits: BitVec<u8>,
|
||||||
|
pub ranking_mode: RankingMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DecoderState {
|
||||||
|
pub fn new(
|
||||||
|
alphabet_bytes: Vec<u8>,
|
||||||
|
rng_seed: <ChaCha20Rng as SeedableRng>::Seed,
|
||||||
|
ciphertext: BitVec<u8>,
|
||||||
|
) -> Result<Self> {
|
||||||
|
let rng = ChaCha20Rng::from_seed(rng_seed);
|
||||||
|
let alphabet = Alphabet::from_bytes(rng, &alphabet_bytes)?;
|
||||||
|
let symbol_count = ciphertext.len() / alphabet.symbol_len();
|
||||||
|
|
||||||
|
let mut this = Self {
|
||||||
|
alphabet_bytes,
|
||||||
|
rng_seed,
|
||||||
|
base_ciphertext: ciphertext.clone(),
|
||||||
|
cursor: 0,
|
||||||
|
text_scroll: 0,
|
||||||
|
choices: vec![None; symbol_count],
|
||||||
|
rendered_text: String::new(),
|
||||||
|
working_bits: ciphertext,
|
||||||
|
ranking_mode: RankingMode::LocalFirst,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
this.rebuild_all()?;
|
||||||
|
Ok(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_alphabet(&self) -> Result<Alphabet<ChaCha20Rng>> {
|
||||||
|
let rng = ChaCha20Rng::from_seed(self.rng_seed);
|
||||||
|
Alphabet::from_bytes(rng, &self.alphabet_bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ensure_cursor_visible(&mut self, visible_symbols: usize) {
|
||||||
|
if self.cursor < self.text_scroll {
|
||||||
|
self.text_scroll = self.cursor;
|
||||||
|
} else if self.cursor >= self.text_scroll + visible_symbols {
|
||||||
|
self.text_scroll = self.cursor + 1 - visible_symbols;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn symbol_count(&self) -> usize {
|
||||||
|
self.choices.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_left(&mut self, visible: usize) {
|
||||||
|
if self.cursor > 0 {
|
||||||
|
self.cursor -= 1;
|
||||||
|
}
|
||||||
|
self.ensure_cursor_visible(visible);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_right(&mut self, visible: usize) {
|
||||||
|
if self.cursor + 1 < self.symbol_count() {
|
||||||
|
self.cursor += 1;
|
||||||
|
}
|
||||||
|
self.ensure_cursor_visible(visible);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_ranking_mode(&mut self, mode: RankingMode) {
|
||||||
|
self.ranking_mode = mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn current_range(&self, symbol_len: usize) -> std::ops::Range<usize> {
|
||||||
|
let start = self.cursor * symbol_len;
|
||||||
|
let end = start + symbol_len;
|
||||||
|
start..end
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_candidates(&self) -> Result<Vec<CandidateScore>> {
|
||||||
|
let mut alphabet = self.make_alphabet()?;
|
||||||
|
let len = alphabet.symbol_len();
|
||||||
|
|
||||||
|
for i in 0..self.cursor {
|
||||||
|
let start = i * len;
|
||||||
|
let end = start + len;
|
||||||
|
let chunk = &self.base_ciphertext[start..end];
|
||||||
|
|
||||||
|
let chosen = if let Some(ch) = self.choices[i] {
|
||||||
|
ch
|
||||||
|
} else {
|
||||||
|
alphabet.best_symbol_distance_no_effect(chunk)?.0
|
||||||
|
};
|
||||||
|
|
||||||
|
alphabet.replace_symbol_bits(chosen, chunk)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cur = self.current_range(len);
|
||||||
|
let top = alphabet.decipher_candidates_no_effect(&self.base_ciphertext[cur], 5)?;
|
||||||
|
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for (symbol, distance) in top {
|
||||||
|
let future_distance = self.simulate_future_distance(symbol)?;
|
||||||
|
out.push(CandidateScore {
|
||||||
|
symbol,
|
||||||
|
distance,
|
||||||
|
future_distance,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.ranking_mode {
|
||||||
|
RankingMode::LocalFirst => out.sort_by_key(|c| (c.distance, c.future_distance)),
|
||||||
|
RankingMode::FutureFirst => out.sort_by_key(|c| (c.future_distance, c.distance)),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn simulate_future_distance(&self, chosen_symbol: char) -> Result<usize> {
|
||||||
|
let mut alphabet = self.make_alphabet()?;
|
||||||
|
let len = alphabet.symbol_len();
|
||||||
|
let mut total = 0usize;
|
||||||
|
|
||||||
|
for i in 0..self.symbol_count() {
|
||||||
|
let start = i * len;
|
||||||
|
let end = start + len;
|
||||||
|
let chunk = &self.base_ciphertext[start..end];
|
||||||
|
|
||||||
|
let symbol = if i == self.cursor {
|
||||||
|
chosen_symbol
|
||||||
|
} else if let Some(ch) = self.choices[i] {
|
||||||
|
ch
|
||||||
|
} else {
|
||||||
|
alphabet.best_symbol_distance_no_effect(chunk)?.0
|
||||||
|
};
|
||||||
|
|
||||||
|
total += alphabet.symbol_distance(symbol, chunk)?;
|
||||||
|
alphabet.replace_symbol_bits(symbol, chunk)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(total)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_candidate(&mut self, rank: usize) -> Result<()> {
|
||||||
|
let candidates = self.current_candidates()?;
|
||||||
|
let cand = candidates
|
||||||
|
.get(rank)
|
||||||
|
.ok_or_else(|| Error::new(ErrorKind::InvalidInput, "candidate rank out of range"))?;
|
||||||
|
|
||||||
|
self.choices[self.cursor] = Some(cand.symbol);
|
||||||
|
self.rebuild_all()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn undo_current(&mut self) -> Result<()> {
|
||||||
|
self.choices[self.cursor] = None;
|
||||||
|
self.rebuild_all()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rebuild_all(&mut self) -> Result<()> {
|
||||||
|
let mut alphabet = self.make_alphabet()?;
|
||||||
|
let len = alphabet.symbol_len();
|
||||||
|
let mut out = String::new();
|
||||||
|
|
||||||
|
for i in 0..self.symbol_count() {
|
||||||
|
let start = i * len;
|
||||||
|
let end = start + len;
|
||||||
|
let chunk = &self.base_ciphertext[start..end];
|
||||||
|
|
||||||
|
let symbol = if let Some(ch) = self.choices[i] {
|
||||||
|
ch
|
||||||
|
} else {
|
||||||
|
alphabet.best_symbol_distance_no_effect(chunk)?.0
|
||||||
|
};
|
||||||
|
|
||||||
|
out.push(symbol);
|
||||||
|
alphabet.replace_symbol_bits(symbol, chunk)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.rendered_text = out;
|
||||||
|
self.working_bits = self.base_ciphertext.clone();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue