import java.util.ArrayList; /** * @author Arran Stewart * */ public class Encoder { public static final int ALPHABET_SIZE = 26; private final Deck deck; /** Create an Encoder using the default ordering * of a deck of cards. * */ public Encoder() { deck = new Deck(); } /** Create an Encoder using a particular deck * of cards to generate the key. * */ public Encoder(Deck d) { deck = d; } /** Remove all non-alphabetic characters from a string, * and convert all alphabetic characters to upper-case. * * @param s Input string * @return Sanitized string */ public static String sanitize(String s) { char[] chars = s.toUpperCase().toCharArray(); StringBuilder sb = new StringBuilder(); for (char c : chars) { if (c >= 'A' && c <= 'Z') { sb.append(c); } } return sb.toString(); } /** Return the position in the alphabet of an uppercase * character, starting from 1 (i.e., charToInt('A') returns 1, * charToInt('B') returns 2, and so on). * * @param c Character to convert to an int * @return Result of conversion */ public static int charToInt(char c) { return c - 'A' + 1; } /** Given a position in the alphabet (starting from 1), * return the character at that position. * (i.e. intToChar(1) returns 'A', intToChar(2) returns 'B', * and so on). If a number larger than 26 is passed in, * subtract 26 from it before applying this conversion. * * @param c int to convert to a character * @return Result of conversion */ public static char intToChar(int i) { if (i > ALPHABET_SIZE) { i -= ALPHABET_SIZE; } if (i < 1 || i > ALPHABET_SIZE) { throw new RuntimeException("bad val: " + i); } return (char) ('A' + i - 1); } /** Encode a character (inputChar) using a character from the keystream * (keyChar). * * To do this, firstly convert both characters into integers, * as described in charToInt. * * Then add the numbers together. If the result is greater than 26, * subtract 26 from it; then convert that result back to a character. * * @param inputChar Character from message * @param keyChar Character from keystream * @return Encoded character */ public static char encodeChar(char inputChar, char keyChar) { int inputInt = charToInt(inputChar); int keyInt = charToInt(keyChar); int res = inputInt + keyInt; if (res > ALPHABET_SIZE) { res = res - ALPHABET_SIZE; } return intToChar(res); } /** Decode a character (inputChar) from an encrypted message using a character * from the keystream (keyChar). * * Convert both characters to integers, as described for * charToInt. If inputChar is less than or equal to keyChar, * add 26 to it. Then subtract keyChar from inputChar, * and convert the result to a character. * * @param inputChar Character from an encrypted message * @param keyChar Character from keystream * @return Decoded character */ public static char decodeChar(char inputChar, char keyChar) { int inputInt = charToInt(inputChar); int keyInt = charToInt(keyChar); if (inputInt <= keyInt) { inputInt += ALPHABET_SIZE; } int tmp = inputInt - keyInt; if (tmp < charToInt('A') || tmp > charToInt('Z')) { throw new RuntimeException("bad: " + tmp); } char res = intToChar(tmp); return res; } /** Encode the string inputText using the keystream characters * in keyChars, by repeatedly calling encodeChar. * * @param inputText Message text to encode * @param keyChars Characters from keystream * @return Encoded message */ public static String encodeString(String inputText, String keyChars) { StringBuffer sb = new StringBuffer(); for (int i = 0; i < inputText.length(); i++) { char inputC = inputText.charAt(i); char keyC = keyChars.charAt(i); char encoded = encodeChar(inputC, keyC); sb.append( encoded ); } return sb.toString(); } /** Decode the string inputText using the keystream characters * in keyChars, by repeatedly calling decodeChar. * * @param inputText Encoded text which needs to be decoded * @param keyChars Characters from keystream * @return Decoded message */ public static String decodeString(String inputText, String keyChars) { StringBuffer sb = new StringBuffer(); for (int i = 0; i < inputText.length(); i++) { char inputC = inputText.charAt(i); char keyC = keyChars.charAt(i); char decoded = decodeChar(inputC, keyC); sb.append( decoded ); } return sb.toString(); } /** Apply the Pontoon algorithm to generate the * next character in the *keystream*. The character * returned will depend on the state of the "deck" * instance variable when the method is called. * * @return A keystream character */ public char nextKeyStreamChar() { // find first joker, move it down one deck.shiftDownOne(Deck.JOKER1); // find second joker, move it down two deck.shiftDownOne(Deck.JOKER2); deck.shiftDownOne(Deck.JOKER2); // triple cut around the jokers int j1idx = deck.findCard(Deck.JOKER1); int j2idx = deck.findCard(Deck.JOKER2); deck.tripleCut(j1idx, j2idx); // get value of bottom card // convert it to a value from 1 to 53: cards // have the value given, except for jokers, which both have // value 53. int bottomCard = deck.getCard( deck.size()-1 ); if ( bottomCard == Deck.JOKER2 ) { bottomCard = Deck.JOKER1; } // perform a count cut using that value. deck.countCut(bottomCard); // return an *output*. Using the top card as a value from // 1 to 53, count that many cards down -- that's the // output value. int topCard = deck.getCard(0); if ( topCard == Deck.JOKER2 ) { topCard = Deck.JOKER1; } // ... except that if we hit a joker, start again int outputCard = deck.getCard( topCard ); if (outputCard == Deck.JOKER1 || outputCard == Deck.JOKER2) { return nextKeyStreamChar(); } return intToChar(outputCard); } public String setState(String s) { for (char c: s.toCharArray()) { deck.shiftDownOne(Deck.JOKER1); deck.shiftDownOne(Deck.JOKER2); deck.shiftDownOne(Deck.JOKER2); int j1idx = deck.findCard(Deck.JOKER1); int j2idx = deck.findCard(Deck.JOKER2); deck.tripleCut(j1idx, j2idx); int bottomCard = deck.getCard( deck.size()-1 ); if ( bottomCard == Deck.JOKER2 ) { bottomCard = Deck.JOKER1; } deck.countCut(bottomCard); deck.countCut( charToInt(c) ); } ArrayList out = new ArrayList<>(); for (int i = 0; i < deck.size(); i++) { out.add( "" + deck.getCard(i) ); } return String.join(",", out); } /** Encrypt a string, using the deck to generate * *keystream* characters which can be passed * to encodeChar. * * @param inputString The string to encrypt * @return The result of encryption */ public String encrypt(String inputString) { StringBuilder sb = new StringBuilder(); for (char c : inputString.toCharArray()) { char key = nextKeyStreamChar() ; char encryptedChar = encodeChar(c, key) ; sb.append(encryptedChar); } return sb.toString(); } /** Decrypt a string, using the deck to generate * *keystream* characters which can be passed * to decodeChar. * * @param inputString The string to decrypt * @return The result of decryption */ public String decrypt(String inputString) { StringBuilder sb = new StringBuilder(); for (char c : inputString.toCharArray()) { char key = nextKeyStreamChar(); char decryptedChar = decodeChar(c, key); sb.append(decryptedChar); } return sb.toString(); } }