From ff049620d556c957a36dcd31dabf2676e1bfb650 Mon Sep 17 00:00:00 2001 From: Gregory Bednov Date: Mon, 30 Mar 2026 01:41:24 +0300 Subject: [PATCH] =?UTF-8?q?=09=D0=BD=D0=BE=D0=B2=D1=8B=D0=B9=20=D1=84?= =?UTF-8?q?=D0=B0=D0=B9=D0=BB:=20=20=20=20.gitignore=20=09=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=D1=8B=D0=B9=20=D1=84=D0=B0=D0=B9=D0=BB:=20=20=20=20Cargo?= =?UTF-8?q?.lock=20=09=D0=BD=D0=BE=D0=B2=D1=8B=D0=B9=20=D1=84=D0=B0=D0=B9?= =?UTF-8?q?=D0=BB:=20=20=20=20Cargo.toml=20=09=D0=BD=D0=BE=D0=B2=D1=8B?= =?UTF-8?q?=D0=B9=20=D1=84=D0=B0=D0=B9=D0=BB:=20=20=20=20english.chrs=20?= =?UTF-8?q?=09=D0=BD=D0=BE=D0=B2=D1=8B=D0=B9=20=D1=84=D0=B0=D0=B9=D0=BB:?= =?UTF-8?q?=20=20=20=20russian.chrs=20=09=D0=BD=D0=BE=D0=B2=D1=8B=D0=B9=20?= =?UTF-8?q?=D1=84=D0=B0=D0=B9=D0=BB:=20=20=20=20src/alphabet.rs=20=09?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D1=8B=D0=B9=20=D1=84=D0=B0=D0=B9=D0=BB:=20?= =?UTF-8?q?=20=20=20src/app.rs=20=09=D0=BD=D0=BE=D0=B2=D1=8B=D0=B9=20?= =?UTF-8?q?=D1=84=D0=B0=D0=B9=D0=BB:=20=20=20=20src/ciphertext.rs=20=09?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D1=8B=D0=B9=20=D1=84=D0=B0=D0=B9=D0=BB:=20?= =?UTF-8?q?=20=20=20src/cli.rs=20=09=D0=BD=D0=BE=D0=B2=D1=8B=D0=B9=20?= =?UTF-8?q?=D1=84=D0=B0=D0=B9=D0=BB:=20=20=20=20src/decoder.rs=20=09=D0=BD?= =?UTF-8?q?=D0=BE=D0=B2=D1=8B=D0=B9=20=D1=84=D0=B0=D0=B9=D0=BB:=20=20=20?= =?UTF-8?q?=20src/main.rs=20=09=D0=BD=D0=BE=D0=B2=D1=8B=D0=B9=20=D1=84?= =?UTF-8?q?=D0=B0=D0=B9=D0=BB:=20=20=20=20src/storage.rs=20=09=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=D1=8B=D0=B9=20=D1=84=D0=B0=D0=B9=D0=BB:=20=20=20=20src/t?= =?UTF-8?q?ui.rs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + Cargo.lock | 1119 +++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 13 + english.chrs | 2 + russian.chrs | 2 + src/alphabet.rs | 433 ++++++++++++++++++ src/app.rs | 156 +++++++ src/ciphertext.rs | 54 +++ src/cli.rs | 56 +++ src/decoder.rs | 202 ++++++++ src/main.rs | 35 ++ src/storage.rs | 32 ++ src/tui.rs | 168 +++++++ 13 files changed, 2273 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 english.chrs create mode 100644 russian.chrs create mode 100644 src/alphabet.rs create mode 100644 src/app.rs create mode 100644 src/ciphertext.rs create mode 100644 src/cli.rs create mode 100644 src/decoder.rs create mode 100644 src/main.rs create mode 100644 src/storage.rs create mode 100644 src/tui.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..25b3fc9 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1119 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "bit-set" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ddef2995421ab6a5c779542c81ee77c115206f4ad9d5a8e05f4ff49716a3dd" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core", +] + +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "compact_str" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags", + "crossterm_winapi", + "mio", + "parking_lot", + "rustix", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "evolution_crypting" +version = "0.1.0" +dependencies = [ + "bit-set", + "bitvec", + "clap", + "crossterm", + "rand", + "rand_chacha", + "ratatui", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "rand_core", + "wasip2", + "wasip3", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "instability" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971" +dependencies = [ + "darling", + "indoc", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +dependencies = [ + "chacha20", + "getrandom", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e6af7f3e25ded52c41df4e0b1af2d047e45896c2f3281792ed68a1c243daedb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" + +[[package]] +name = "ratatui" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" +dependencies = [ + "bitflags", + "cassowary", + "compact_str", + "crossterm", + "indoc", + "instability", + "itertools", + "lru", + "paste", + "strum", + "unicode-segmentation", + "unicode-truncate", + "unicode-width 0.2.0", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools", + "unicode-segmentation", + "unicode-width 0.1.14", +] + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..3504d4c --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "evolution_crypting" +version = "0.1.0" +edition = "2024" + +[dependencies] +bit-set = "0.9.1" +bitvec = "1.0.1" +clap = { version = "4.6.0", features = ["derive"] } +rand = "0.10.0" +rand_chacha = "0.10.0" +ratatui = "0.29" +crossterm = "0.28" diff --git a/english.chrs b/english.chrs new file mode 100644 index 0000000..e6fe032 --- /dev/null +++ b/english.chrs @@ -0,0 +1,2 @@ +QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzcvbnm +,. ?!"'/ diff --git a/russian.chrs b/russian.chrs new file mode 100644 index 0000000..b584594 --- /dev/null +++ b/russian.chrs @@ -0,0 +1,2 @@ +ЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЭЯЧСМИТЬБЮ +йцукенгшщзхъфывапролджэячсмитьбю .,!?"/ diff --git a/src/alphabet.rs b/src/alphabet.rs new file mode 100644 index 0000000..3df87e4 --- /dev/null +++ b/src/alphabet.rs @@ -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, +} + +pub struct Alphabet { + r: R, + chars: String, + dict: BitVec, + len: usize, + mut_rate: usize, +} + +impl Alphabet { + + fn symbol_index(&self, c: char) -> Result { + 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> { + 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, +) -> Result> { + 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, +) -> Result> { + 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) -> Result { + 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, + symbol: char, + ) -> Result { + 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) -> 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::::from_vec(random_bytes); + dict.truncate(dict_bit_len); + Alphabet { + r: rng, + chars, + dict, + len, + mut_rate, + } + } + + pub fn into_bytes(self) -> Vec { + 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 { + 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::::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> { + 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, + top: usize, + ) -> Result> { + 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, + ) -> Result { + 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, + 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 = (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 { + fn cipher(self, a: &mut Alphabet) -> Result>; +} +impl MutationallyCipherable for char { + fn cipher(self, a: &mut Alphabet) -> Result> { + let res = a.cipher_no_effect(self)?; + a.mutate(); + Ok(res) + } +} +impl MutationallyCipherable for String { + fn cipher(self, a: &mut Alphabet) -> Result> { + let mut res: BitVec = BitVec::new(); + for c in self.chars() { + res.extend(c.cipher(a)?) + } + Ok(res) + } +} +impl MutationallyCipherable for &str { + fn cipher(self, a: &mut Alphabet) -> Result> { + let mut res: BitVec = 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) + } +} diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..e5ed9c2 --- /dev/null +++ b/src/app.rs @@ -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> { + let mut bits = BitVec::::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) -> io::Result { + 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, + chars_file: Option, + 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, + output: Option, +) -> 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, chars_file: Option) -> io::Result { + 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) -> io::Result> { + 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, + output: Option, +) -> 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, bytes: &[u8]) -> io::Result<()> { + match output { + Some(path) => fs::write(path, bytes), + None => io::stdout().write_all(bytes), + } +} + +fn write_output_text(output: Option, text: &str) -> io::Result<()> { + match output { + Some(path) => fs::write(path, text), + None => { + println!("{text}"); + Ok(()) + } + } +} diff --git a/src/ciphertext.rs b/src/ciphertext.rs new file mode 100644 index 0000000..ab96f67 --- /dev/null +++ b/src/ciphertext.rs @@ -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, +} + +impl Ciphertext { + pub fn from_bits(bits: BitVec) -> Self { + Self { bits } + } + + pub fn into_bytes(self) -> Vec { + 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 { + 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::::from_vec(payload); + bits.truncate(bit_len); + + Ok(Self { bits }) + } +} diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..861c87a --- /dev/null +++ b/src/cli.rs @@ -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, + + #[arg(long = "chars-file")] + chars_file: Option, + + #[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, + + #[arg(short = 'o', long = "output")] + output: Option, + }, + Decipher { + #[arg(short = 'a', long = "alphabet")] + alphabet: PathBuf, + + #[arg(short = 'f', long = "file")] + file: Option, + + #[arg(short = 'o', long = "output")] + output: Option, + }, + Tui { + #[arg(short = 'a', long = "alphabet")] + alphabet: PathBuf, + + #[arg(short = 'f', long = "file")] + file: PathBuf, + }, +} diff --git a/src/decoder.rs b/src/decoder.rs new file mode 100644 index 0000000..9b597f7 --- /dev/null +++ b/src/decoder.rs @@ -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, + rng_seed: ::Seed, + base_ciphertext: BitVec, + + pub cursor: usize, + pub text_scroll: usize, + pub choices: Vec>, + pub rendered_text: String, + pub working_bits: BitVec, + pub ranking_mode: RankingMode, +} + +impl DecoderState { + pub fn new( + alphabet_bytes: Vec, + rng_seed: ::Seed, + ciphertext: BitVec, + ) -> Result { + 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> { + 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 { + let start = self.cursor * symbol_len; + let end = start + symbol_len; + start..end + } + + pub fn current_candidates(&self) -> Result> { + 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 { + 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(()) + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..6e6de87 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,35 @@ +mod alphabet; +mod app; +mod ciphertext; +mod cli; +mod decoder; +mod storage; +mod tui; + +use clap::Parser; +use cli::{Cli, Commands}; + +fn main() -> std::io::Result<()> { + let cli = Cli::parse(); + + match cli.command { + Commands::Generate { + path, + chars, + chars_file, + len, + mut_rate, + } => app::generate_cmd(path, chars, chars_file, len, mut_rate), + Commands::Cipher { + alphabet, + file, + output, + } => app::cipher_cmd(alphabet, file, output), + Commands::Decipher { + alphabet, + file, + output, + } => app::decipher_cmd(alphabet, file, output), + Commands::Tui { alphabet, file } => tui::run_decoder_tui(alphabet, file), + } +} diff --git a/src/storage.rs b/src/storage.rs new file mode 100644 index 0000000..f4dc608 --- /dev/null +++ b/src/storage.rs @@ -0,0 +1,32 @@ +use crate::alphabet::Alphabet; +use rand::{CryptoRng, Rng}; +use std::fs::File; +use std::io::{Error, ErrorKind, Read, Result, Write}; +use std::path::PathBuf; + +pub struct AlphabetFile { + file: File, + pub dict: Alphabet, +} + +impl AlphabetFile { + pub fn create(path: PathBuf, dict: Alphabet) -> Result { + let file = File::create(path)?; + Ok(Self { file, dict }) + } + + pub fn save(mut self) -> Result<()> { + self.file.write_all(self.dict.into_bytes().as_slice()) + } + + pub fn open(name: PathBuf, rng: R) -> Result { + let mut file = File::open(&name)?; + let mut bytes = Vec::new(); + file.read_to_end(&mut bytes)?; + + let dict = + Alphabet::from_bytes(rng, &bytes).map_err(|e| Error::new(ErrorKind::InvalidData, e))?; + + Ok(Self { file, dict }) + } +} diff --git a/src/tui.rs b/src/tui.rs new file mode 100644 index 0000000..cb6aeaf --- /dev/null +++ b/src/tui.rs @@ -0,0 +1,168 @@ +use crate::ciphertext::Ciphertext; +use crate::decoder::DecoderState; +use crate::storage::AlphabetFile; + +use crossterm::{ + event::{self, Event, KeyCode, KeyEventKind}, + execute, + terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, +}; +use rand_chacha::ChaCha20Rng; +use ratatui::{ + prelude::*, + widgets::{Block, Borders, List, ListItem, Paragraph}, +}; +use std::io; +use std::path::PathBuf; +use std::time::Duration; + +struct TuiGuard; + +impl Drop for TuiGuard { + fn drop(&mut self) { + let _ = disable_raw_mode(); + let _ = execute!(io::stdout(), LeaveAlternateScreen); + } +} + +fn get_visible_symbols( + terminal: &Terminal>, +) -> io::Result { + let size = terminal.size()?; + let inner_width = size.width.saturating_sub(2) as usize; + Ok(inner_width.max(1)) +} + +pub fn run() -> io::Result<()> { + todo!("call run_decoder_tui(alphabet_path, ciphertext_path) from CLI later") +} + +pub fn run_decoder_tui(alphabet_path: PathBuf, ciphertext_path: PathBuf) -> io::Result<()> { + let alphabet_bytes = std::fs::read(&alphabet_path)?; + let bytes = std::fs::read(ciphertext_path)?; + let ciphertext = Ciphertext::from_bytes(&bytes).map_err(io::Error::other)?; + + let rng_seed = [7u8; 32]; + let mut state = DecoderState::new(alphabet_bytes, rng_seed, ciphertext.bits)?; + + enable_raw_mode()?; + execute!(io::stdout(), EnterAlternateScreen)?; + let _guard = TuiGuard; + + let mut terminal = Terminal::new(CrosstermBackend::new(io::stdout()))?; + + loop { + terminal.draw(|f| draw_ui(f, &state))?; + + if event::poll(Duration::from_millis(100))? { + if let Event::Key(key) = event::read()? { + if key.kind != KeyEventKind::Press { + continue; + } + + let visible = get_visible_symbols(&terminal)?; + + match key.code { + KeyCode::Char('q') => break, + KeyCode::Left | KeyCode::Char('h') => state.move_left(visible), + KeyCode::Right | KeyCode::Char('l') => state.move_right(visible), + KeyCode::Char('u') => { + let _ = state.undo_current(); + } + KeyCode::Char('1') => { + let _ = state.apply_candidate(0); + } + KeyCode::Char('2') => { + let _ = state.apply_candidate(1); + } + KeyCode::Char('3') => { + let _ = state.apply_candidate(2); + } + KeyCode::Char('4') => { + let _ = state.apply_candidate(3); + } + KeyCode::Char('5') => { + let _ = state.apply_candidate(4); + } + KeyCode::Char('m') => { + state.set_ranking_mode(crate::decoder::RankingMode::LocalFirst); + } + KeyCode::Char('M') => { + state.set_ranking_mode(crate::decoder::RankingMode::FutureFirst); + } + _ => {} + } + } + } + } + + Ok(()) +} + +fn draw_ui(f: &mut Frame, state: &DecoderState) { + let layout = Layout::vertical([ + Constraint::Length(5), + Constraint::Min(10), + Constraint::Length(3), + ]) + .split(f.area()); + + draw_text_panel(f, layout[0], state); + draw_candidates_panel(f, layout[1], state); + draw_help_panel(f, layout[2]); +} + +fn draw_text_panel(f: &mut Frame, area: Rect, state: &DecoderState) { + let cursor = state.cursor; + let rendered: String = state + .rendered_text + .chars() + .enumerate() + .flat_map(|(i, ch)| { + if i == cursor { + vec!['[', ch, ']'] + } else { + vec![ch] + } + }) + .collect(); + + let p = Paragraph::new(rendered) + .block(Block::default().title("Decoded text").borders(Borders::ALL)); + + f.render_widget(p, area); +} + +fn draw_candidates_panel(f: &mut Frame, area: Rect, state: &DecoderState) { + let items = match state.current_candidates() { + Ok(cands) => cands + .into_iter() + .enumerate() + .map(|(i, c)| { + ListItem::new(format!( + "{}. {:?} local={} future={}", + i + 1, + c.symbol, + c.distance, + c.future_distance + )) + }) + .collect::>(), + Err(err) => vec![ListItem::new(format!("error: {}", err))], + }; + + let list = List::new(items).block( + Block::default() + .title("Top-5 candidates") + .borders(Borders::ALL), + ); + + f.render_widget(list, area); +} + +fn draw_help_panel(f: &mut Frame, area: Rect) { + let p = Paragraph::new("h/← left l/→ right 1..5 choose u undo q quit") + .block(Block::default().title("Keys").borders(Borders::ALL)); + + f.render_widget(p, area); +}