Загрузить файлы в «src»

This commit is contained in:
Gregory Bednov 2026-04-09 21:50:13 +03:00
commit c52756d17c
5 changed files with 901 additions and 0 deletions

433
src/alphabet.rs Normal file
View 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
View 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
View 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
View 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
View 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(())
}
}