Compare commits

...
14 Commits
Author SHA1 Message Date
KKlochko 5a4b88bd08 Add the parsers to parse keys.
continuous-integration/drone/push Build is passing
2023-10-22 17:54:16 +03:00
KKlochko 79ae13aa5a Add a generator for the sorted substitution table.
continuous-integration/drone/push Build is passing
2023-10-22 17:26:54 +03:00
KKlochko 126a706292 Update the simple substitution cipher to split the functions. 2023-10-22 17:26:04 +03:00
KKlochko 178c85b0cb Add the simple substitution cipher.
continuous-integration/drone/push Build is passing
2023-10-21 21:45:43 +03:00
KKlochko 2db631d8da Update the diffs in the CHANGELOG.
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2023-10-21 16:56:32 +03:00
KKlochko 05009f1a96 Update the CI/CD configuration to publish the artifacts to the release.
continuous-integration/drone/push Build is passing
2023-10-21 16:47:22 +03:00
KKlochko 7c19e48828 Update the README. 2023-10-21 16:46:57 +03:00
KKlochko 40725f55a1 Update the version. 2023-10-21 16:46:32 +03:00
KKlochko 633cdc471f Update the CHANGELOG. 2023-10-21 16:46:15 +03:00
KKlochko dab131a32e Remove the option for FrequencyAnalyzer.
continuous-integration/drone/push Build is passing
2023-10-20 22:04:05 +03:00
KKlochko f465d1112e Remove the test for FrequencyAnalyzer. 2023-10-20 22:03:10 +03:00
KKlochko 5919ba8864 Refactor to move actions to separated files. 2023-10-20 22:01:20 +03:00
KKlochko a56704226b Add the IsNonsense interface for FrequencyAnalyzer.
continuous-integration/drone/push Build is failing
2023-10-14 21:32:56 +03:00
Kryls1fer b53559030f Update the frequency analyzer. 2023-10-14 21:18:50 +03:00
18 changed files with 467 additions and 65 deletions
+36
View File
@@ -5,6 +5,42 @@ name: default
steps:
- name: test
image: clojure:temurin-11-lein-2.10.0-alpine
volumes:
- name: package_cache
path: /root/.m2/
commands:
- lein test
- name: build
image: clojure:temurin-11-lein-2.10.0-alpine
volumes:
- name: package_cache
path: /root/.m2/
commands:
- lein uberjar
when:
event: tag
- name: gitea_release
image: plugins/gitea-release
settings:
api_key:
from_secret: API_KEY
base_url:
from_secret: BASE_URL
files:
- target/uberjar/*.jar
checksum:
- md5
- sha1
- sha256
- sha512
- adler32
- crc32
when:
event: tag
volumes:
- name: package_cache
temp: {}
+80 -1
View File
@@ -7,6 +7,76 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.7.2] - 2023-10-21
### Changed
- Update the CHANGELOG.
- Update the version.
- Update the README.
- Update the CI/CD configuration to publish the artifacts to the release.
- Update the diffs in the CHANGELOG.
## [0.7.1] - 2023-10-20
### Removed
- Remove the test for FrequencyAnalyzer.
- Remove the option for FrequencyAnalyzer.
## [0.7.0] - 2023-10-20
### Changed
- Refactor to move actions to separated files.
## [0.6.3] - 2023-10-14
### Added
- Add the IsNonsense interface for FrequencyAnalyzer.
### Changed
- Update the frequency analyzer.
## [0.6.2] - 2023-10-14
### Added
- Add the analyzer options and their actions.
- Add the available ciphers and analyzer for the help.
- Add validations to check if a cipher or an analyzer is available.
### Changed
- Refactor to move options to a separated file.
## [0.6.1] - 2023-10-13
### Added
- Add the function to make a frequency string from a map.
### Fixed
- Fix the bug that the default language symbols override the symbols.
### Changed
- Update the FrequencyAnalyzer to use the frequency string instead.
## [0.6.0] - 2023-10-12
### Fixed
- Fix the typo "frequences" to frequencies.
### Changed
- Refactor the project structure.
## [0.5.5] - 2023-10-09
### Added
- Add the Gitlab CI/CD configuration.
## [0.5.4] - 2023-10-05
### Added
- Add the java source path.
- Add the Caesar decrypted interface for Java.
- Add a frequency analyzer as a stub.
- Add the clojure wrapper for the frequency analizer and its test.
## [0.5.3] - 2023-10-05
### Added
@@ -107,7 +177,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Project initialization.
[Unreleased]: https://gitlab.com/KKlochko/cipher-analytical-machine/-/compare/0.5.3...main?from_project_id=50218541&straight=false
[Unreleased]: https://gitlab.com/KKlochko/cipher-analytical-machine/-/compare/0.7.2...main?from_project_id=50218541&straight=false
[0.7.2]: https://gitlab.com/KKlochko/cipher-analytical-machine/-/compare/0.7.1...0.7.2?from_project_id=50218541&straight=false
[0.7.1]: https://gitlab.com/KKlochko/cipher-analytical-machine/-/compare/0.7.0...0.7.1?from_project_id=50218541&straight=false
[0.7.0]: https://gitlab.com/KKlochko/cipher-analytical-machine/-/compare/0.6.3...0.7.0?from_project_id=50218541&straight=false
[0.6.3]: https://gitlab.com/KKlochko/cipher-analytical-machine/-/compare/0.6.2...0.6.3?from_project_id=50218541&straight=false
[0.6.2]: https://gitlab.com/KKlochko/cipher-analytical-machine/-/compare/0.6.1...0.6.2?from_project_id=50218541&straight=false
[0.6.1]: https://gitlab.com/KKlochko/cipher-analytical-machine/-/compare/0.6.0...0.6.1?from_project_id=50218541&straight=false
[0.6.0]: https://gitlab.com/KKlochko/cipher-analytical-machine/-/compare/0.5.5...0.6.0?from_project_id=50218541&straight=false
[0.5.5]: https://gitlab.com/KKlochko/cipher-analytical-machine/-/compare/0.5.4...0.5.5?from_project_id=50218541&straight=false
[0.5.4]: https://gitlab.com/KKlochko/cipher-analytical-machine/-/compare/0.5.3...0.5.4?from_project_id=50218541&straight=false
[0.5.3]: https://gitlab.com/KKlochko/cipher-analytical-machine/-/compare/0.5.2...0.5.3?from_project_id=50218541&straight=false
[0.5.2]: https://gitlab.com/KKlochko/cipher-analytical-machine/-/compare/0.5.1...0.5.2?from_project_id=50218541&straight=false
[0.5.1]: https://gitlab.com/KKlochko/cipher-analytical-machine/-/compare/0.5.0...0.5.1?from_project_id=50218541&straight=false
+3 -1
View File
@@ -11,10 +11,12 @@ Download from https://gitlab.com/KKlochko/cipher-analytical-machine.
You need install java, before run the application.
Run the application with a command:
$ java -jar cipher-analytical-machine-0.1.0-standalone.jar [args]
$ java -jar cipher-analytical-machine-VERSION-standalone.jar [args]
$ java -jar cipher-analytical-machine-0.7.2-standalone.jar [args]
## License
Copyright © 2023 Kostiantyn Klochko
Under the GNU Lesser General Public License v3.0 or later.
+3 -2
View File
@@ -1,4 +1,4 @@
(defproject cipher-analytical-machine "0.1.0-SNAPSHOT"
(defproject cipher-analytical-machine "0.7.2"
:description "A program helps to learn how ciphers works."
:url "https://gitlab.com/KKlochko/cipher-analytical-machine"
:license {:name "LGPL"
@@ -6,7 +6,8 @@
:dependencies [[org.clojure/clojure "1.11.1"]
[org.clojure/tools.cli "1.0.219"]
[org.apache.tika/tika-core "1.28.5"]
[org.apache.tika/tika-langdetect "1.28.5"]]
[org.apache.tika/tika-langdetect "1.28.5"]
[cheshire "5.12.0"]]
:main ^:skip-aot cipher-analytical-machine.core
:java-source-paths ["src/main/java"]
:target-path "target/%s"
@@ -1,8 +1,10 @@
(ns cipher-analytical-machine.analyzers.caesar
(:require [cipher-analytical-machine.ciphers.caesar :as caesar]
[clojure.string :as cs]
[cipher-analytical-machine.analyzers.language :as language]
[cipher-analytical-machine.analyzers.analyzers :as ca])
(:import [cipher_analytical_machine.ciphers.caesar Decrypted]
[cipher_analytical_machine.analyzers.caesar IsNonsense]
[cipher_analytical_machine.analyzers.caesar FrequencyAnalyzer])
(:gen-class))
@@ -50,10 +52,13 @@
(defn frequency-analizer-get-plaintext
"Return the plaintext from a ciphertext using simple analizer. The function is case-insensitive."
[ciphertext symbols letter-frequencies-string]
[ciphertext symbols letter-frequencies-string language-code]
(let [decrypt (reify Decrypted
(decrypt [this message key symbols]
(caesar/decrypt-message message key symbols)))]
(-> (new FrequencyAnalyzer ciphertext symbols letter-frequencies-string decrypt)
(caesar/decrypt-message message key symbols)))
is-nonsense (reify IsNonsense
(isNonsense [this message]
(language/is-nonsense? message language-code)))]
(-> (new FrequencyAnalyzer ciphertext symbols letter-frequencies-string decrypt is-nonsense)
.crack)))
@@ -0,0 +1,63 @@
(ns cipher-analytical-machine.ciphers.simple-substitution
(:require [clojure.string :as cs]
[cipher-analytical-machine.ciphers.caesar :as caesar])
(:gen-class))
(defn shuffled-numbers
"Generate the shuffled order for integer list."
[size]
(-> size
(take (range))
(shuffle)))
(defn generate-substitution-table
"Generate the map (char, int) for the substittion."
[symbols]
(->> (count symbols)
(shuffled-numbers)
(zipmap symbols)))
(defn generate-sorted-substitution-table
"Generate the map (char, int) for the substittion. BUT a value is the index of a symbol (from 0)."
[symbols]
(-> (count symbols)
(take (range))
(zipmap symbols)))
(defn find-value-in-table
"It uses the substitution table to find the value of a char or a number."
[char substitution-table]
(get substitution-table char))
(defn encrypt-message
"Encrypt a message using the simple substitution cipher. The function is case-insensitive. If a symbol isn't in the symbols list, then it will be removed."
[message substitution-table]
(->> message
(map (fn [char] (find-value-in-table char substitution-table)))
(remove nil?)
(cs/join \,)))
(defn encrypt-message-with-caesar
"Encrypt a message using the simple substitution cipher and the Caesar cipher. The function is case-insensitive. If a symbol isn't in the symbols list, then it will be removed."
[message key substitution-table symbols]
(-> message
(caesar/encrypt-message key symbols)
(encrypt-message substitution-table)))
(defn decrypt-message
"Decrypt a message using the simple substitution cipher. The function is case-insensitive. The substitution-table must be "
[message substitution-table]
(let [message (cs/split message #",")]
(->> message
(map #(Integer/parseInt %))
(map (fn [char] (find-value-in-table char substitution-table)))
(remove nil?)
(cs/join))))
(defn decrypt-message-with-caesar
"Decrypt a message using the simple substitution cipher and the Caesar cipher. The function is case-insensitive."
[message key substitution-table symbols]
(-> message
(decrypt-message substitution-table)
(caesar/decrypt-message key symbols)))
@@ -0,0 +1,26 @@
(ns cipher-analytical-machine.cli.actions.cracking
(:require
[cipher-analytical-machine.analyzers.caesar :as caesar-analyzers]
[cipher-analytical-machine.symbols.frequencies :as symbol-frequencies])
(:gen-class))
(defn cracking-caesar
[options arguments]
(let [analyzer (:analyzer options)
message (:message options)
symbols (:symbols options)
frequencies symbol-frequencies/english-letter-frequencies]
(cond
(= analyzer "Chi^2")
(caesar-analyzers/get-plaintext message symbols frequencies)
:else
(caesar-analyzers/get-plaintext message symbols frequencies))))
(defn cracking-actions
[options arguments action-type]
(let [cipher (:cipher options)]
(cond
(= cipher "Caesar")
(cracking-caesar options arguments))))
@@ -0,0 +1,24 @@
(ns cipher-analytical-machine.cli.actions.cryptography
(:require
[cipher-analytical-machine.ciphers.caesar :as caesar])
(:gen-class))
(defn caesar-actions
[options arguments action-type]
(let [message (:message options)
key (Integer/parseInt (:key options))
symbols (:symbols options)]
(cond
(= action-type :encrypt)
(caesar/encrypt-message message key symbols)
(= action-type :decrypt)
(caesar/decrypt-message message key symbols))))
(defn cryptography-actions
[options arguments action-type]
(let [cipher (:cipher options)]
(cond
(= cipher "Caesar")
(caesar-actions options arguments action-type))))
+4 -39
View File
@@ -1,12 +1,11 @@
(ns cipher-analytical-machine.cli.cli
(:require
[cipher-analytical-machine.cli.file :as file]
[cipher-analytical-machine.ciphers.caesar :as caesar]
[cipher-analytical-machine.analyzers.caesar :as caesar-analyzers]
[cipher-analytical-machine.symbols.factories :as sf]
[cipher-analytical-machine.symbols.frequencies :as symbol-frequencies]
[cipher-analytical-machine.cli.options :as options]
[clojure.string :as cs]
[cipher-analytical-machine.cli.actions.cryptography :as cryptography]
[cipher-analytical-machine.cli.actions.cracking :as cracking]
[clojure.tools.cli :refer [parse-opts]])
(:gen-class))
@@ -148,50 +147,16 @@
(save-output output options))
(println output))
(defn cracking-caesar
[options arguments]
(let [analyzer (:analyzer options)
message (:message options)
symbols (:symbols options)
frequencies symbol-frequencies/english-letter-frequencies]
(cond
(= analyzer "Chi^2")
(caesar-analyzers/get-plaintext message symbols frequencies)
(= analyzer "Frequency")
(caesar-analyzers/frequency-analizer-get-plaintext message symbols
(symbol-frequencies/map-to-string frequencies)))))
(defn cracking-actions
[options arguments action-type]
(let [cipher (:cipher options)]
(cond
(= cipher "Caesar")
(cracking-caesar options arguments)
)))
(defn crypt-actions
[options arguments action-type]
(let [message (:message options)
key (Integer/parseInt (:key options))
symbols (:symbols options)]
(cond
(= action-type :encrypt)
(caesar/encrypt-message message key symbols)
(= action-type :decrypt)
(caesar/decrypt-message message key symbols))))
(defn actions
[options arguments action-type]
(let [options (load-all-options options)]
(->
(cond
(contains? #{:encrypt :decrypt} action-type)
(crypt-actions options arguments action-type)
(cryptography/cryptography-actions options arguments action-type)
(= action-type :cracking)
(cracking-actions options arguments action-type))
(cracking/cracking-actions options arguments action-type))
(show-and-save-output options))))
@@ -32,7 +32,7 @@
(option-values-as-row "Ciphers" cipher-options))
(def analyzers-options
{"Caesar" #{"Chi^2" "Frequency"}})
{"Caesar" #{"Chi^2"}})
(defn get-available-analyzers-options-as-string
"Return the formatted string of analyzers options."
@@ -0,0 +1,9 @@
(ns cipher-analytical-machine.parsers.parsers
(:require [cheshire.core :as cc])
(:gen-class))
(defn unsigned-int?
[str]
(if (re-matches #"\d+" str)
true false))
@@ -0,0 +1,44 @@
(ns cipher-analytical-machine.parsers.simple-substitution
(:require
[cipher-analytical-machine.ciphers.simple-substitution :as ss]
[cipher-analytical-machine.parsers.parsers :as ps]
[cheshire.core :as cc])
(:gen-class))
(defn encode-key-and-substitution-table-to-json
[key substitution-table]
(-> {"key" key}
(assoc "table" substitution-table)
(cc/generate-string)))
(defn decode-key
"Returns the first character if the string is a char. Return the number if the string is a number."
[str]
(if (ps/unsigned-int? str)
(Integer/parseInt str)
(first str)))
(defn decode-pair
"Decode a numbers as a string and a char as a string"
[acc [key value]]
(if (ps/unsigned-int? key)
(assoc acc (decode-key key) (decode-key value))
(assoc acc (decode-key key) value)))
(defn decode-substitution-table
[substitution-table-map-from-json]
(reduce decode-pair {} substitution-table-map-from-json))
(defn decode-key-and-substitution-table-from-json
[json]
(let [data (cc/parse-string json)]
(assoc data "table" (decode-substitution-table (get data "table")))))
(defn generate-table-or-decode-json
"It generate the table and return map with the key and the table if the argument is a key. Else it decode the json."
[key-or-json symbols]
(if (ps/unsigned-int? key-or-json)
(-> {"key" key-or-json}
(assoc "table" (ss/generate-substitution-table symbols)))
(decode-key-and-substitution-table-from-json key-or-json)))
@@ -1,25 +1,53 @@
package cipher_analytical_machine.analyzers.caesar;
import cipher_analytical_machine.analyzers.caesar.IsNonsense;
import cipher_analytical_machine.ciphers.caesar.Decrypted;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
public class FrequencyAnalyzer {
private String ciphertext;
private int key;
private String symbols;
private String symbol_frequences;
private Decrypted decryptor;
private IsNonsense isNonsense;
public FrequencyAnalyzer(String ciphertext, String symbols, String symbol_frequences, Decrypted decryptor) {
public FrequencyAnalyzer(String ciphertext, String symbols, String symbol_frequences, Decrypted decryptor, IsNonsense isNonsense) {
this.ciphertext = ciphertext;
this.key = 1;
this.symbols = symbols;
this.symbol_frequences = symbol_frequences;
this.symbols = symbols;
this.decryptor = decryptor;
this.isNonsense = isNonsense;
}
public String crack() {
return decryptor.decrypt(ciphertext, key, symbols);
// Знаходимо кількість літер
Map<Character, Integer> encryptedLetterCounts = new HashMap<>();
for (char c : ciphertext.toLowerCase().toCharArray()) {
encryptedLetterCounts.put(c, encryptedLetterCounts.getOrDefault(c, 0) + 1);
}
// Знаходимо букву, яка зустрічається найчастіше
char mostFrequentEncryptedLetter = ' ';
int mostFrequentEncryptedLetterCount = 0;
for (Entry<Character, Integer> entry : encryptedLetterCounts.entrySet()) {
if (entry.getValue() > mostFrequentEncryptedLetterCount) {
mostFrequentEncryptedLetter = entry.getKey();
mostFrequentEncryptedLetterCount = entry.getValue();
}
}
// Обчислимо зміщення
int size = symbols.length();
int indexOfMostFrequentEncryptedLetter = symbols.indexOf(mostFrequentEncryptedLetter);
int indexOfMostFrequentLetter = symbols.indexOf(symbol_frequences.charAt(0));
int shift = (indexOfMostFrequentEncryptedLetter - indexOfMostFrequentLetter) % size;
return decryptor.decrypt(ciphertext, shift, symbols);
}
}
@@ -0,0 +1,6 @@
package cipher_analytical_machine.analyzers.caesar;
public interface IsNonsense {
boolean isNonsense(String message);
}
@@ -74,13 +74,3 @@
(is (= (cs/lower-case plaintext)
(get-plaintext ciphertext symbols frequencies))))))
(deftest frequency-analizer-get-plaintext-test
(let [plaintext "abc"
ciphertext "bca"
key 1
symbols "abc"
frequencies "etaoinsrhldcumfpgwybvkxjqz"]
(testing "The ciphertext is 'bca' and key is 1."
(is (= plaintext
(frequency-analizer-get-plaintext ciphertext symbols frequencies))))))
@@ -0,0 +1,68 @@
(ns cipher-analytical-machine.ciphers.simple-substitution-test
(:require
[clojure.test :refer :all]
[cipher-analytical-machine.ciphers.simple-substitution :refer :all]))
(deftest find-value-in-table-test
(let [table {\a 1 \b 2 \c 3}
rtable {1 \a 2 \b 3 \c}]
(testing "If the symbol is in the table as a key, then the result won't nil."
(are [key expected]
(= expected (find-value-in-table key table))
\a 1
\d nil
1 nil
5 nil))
(testing "If the digit is in the reversed table as a key, then the result won't nil."
(are [key expected]
(= expected (find-value-in-table key rtable))
\a nil
\d nil
1 \a
5 nil))))
(deftest generate-sorted-substitution-table-test
(testing "If the symbol is in the table as a key, then the result won't nil."
(are [symbols expected-table]
(= expected-table (generate-sorted-substitution-table symbols))
"abc" {0 \a 1 \b 2 \c}
"a" {0 \a})))
(deftest encrypt-message-test
(let [symbols "abc"
table {\a 1 \b 2 \c 3}]
(testing "The function must encrypt the message and remove unknown symbols."
(are [message expected]
(= expected (encrypt-message message table))
"abc" "1,2,3"
"aDbdc" "1,2,3"))))
(deftest encrypt-message-with-caesar-test
(let [symbols "abc"
table {\a 1 \b 2 \c 3}]
(testing "The function must encrypt the message and remove unknown symbols."
(are [message key expected]
(= expected (encrypt-message-with-caesar message key table symbols))
"abc" 0 "1,2,3"
"abc" 1 "2,3,1"
"aDbdc" 0 "1,2,3"))))
(deftest decrypt-message-test
(let [table {1 \a 2 \b 3 \c}]
(testing "The function must decrypt the message and remove unknown numbers."
(are [message expected]
(= expected (decrypt-message message table))
"1,2,3" "abc"
"1,12,2,3" "abc"))))
(deftest decrypt-message-with-caesar-test
(let [symbols "abc"
table {1 \a 2 \b 3 \c}]
(testing "The function must decrypt the message and remove unknown numbers."
(are [message key expected]
(= expected (decrypt-message-with-caesar message key table symbols))
"1,2,3" 0 "abc"
"2,3,1" 1 "abc"
"1,12,2,3" 0 "abc"))))
@@ -0,0 +1,15 @@
(ns cipher-analytical-machine.parsers.parsers-test
(:require
[clojure.test :refer :all]
[cipher-analytical-machine.parsers.parsers :refer :all]))
(deftest unsigned-int?-test
(testing ""
(are [str expected]
(= expected
(unsigned-int? str))
"9" true
"10" true
"-10" false
"abc" false)))
@@ -0,0 +1,41 @@
(ns cipher-analytical-machine.parsers.simple-substitution-test
(:require
[clojure.test :refer :all]
[cipher-analytical-machine.parsers.simple-substitution :refer :all]))
(deftest encode-key-and-substitution-table-to-json-test
(testing "The encoder must parse the map and create a json."
(are [key table expected-json]
(= expected-json
(encode-key-and-substitution-table-to-json key table))
1 {0 \a 1 \b 2 \c} "{\"key\":1,\"table\":{\"0\":\"a\",\"1\":\"b\",\"2\":\"c\"}}"
2 {\a 0 \b 1 \c 2} "{\"key\":2,\"table\":{\"a\":0,\"b\":1,\"c\":2}}")))
(deftest decode-key-and-substitution-table-from-json-test
(testing "The decoder must parse the json and create a map."
(are [json expected-data]
(= expected-data
(decode-key-and-substitution-table-from-json json))
"{\"key\":1, \"table\":{\"0\":\"a\",\"1\":\"b\",\"2\":\"c\"}}" {"key" 1 "table" {0 \a 1 \b 2 \c}}
"{\"key\":2, \"table\":{\"a\":0,\"b\":1,\"c\":2}}" {"key" 2 "table" {\a 0 \b 1 \c 2}})))
(deftest generate-table-or-decode-json-test
(let [symbols "abc"]
(testing "The function decodes the json."
(are [json expected-data]
(= expected-data
(generate-table-or-decode-json json symbols))
"{\"key\":1, \"table\":{\"0\":\"a\",\"1\":\"b\",\"2\":\"c\"}}" {"key" 1 "table" {0 \a 1 \b 2 \c}}
"{\"key\":2, \"table\":{\"a\":0,\"b\":1,\"c\":2}}" {"key" 2 "table" {\a 0 \b 1 \c 2}}))
(testing "The function generates the table, because the argument is a key."
(are [json expected-data]
(let [data (generate-table-or-decode-json json symbols)]
(and (= (get expected-data "key")
(get data "key"))
(= (keys (get expected-data "table"))
(keys (get data "table"))))
(generate-table-or-decode-json json symbols))
"1" {"key" 1 "table" {0 \a 1 \b 2 \c}}
"2" {"key" 2 "table" {\a 0 \b 1 \c 2}}))))