package com.mattrixwv.cipherstream.monosubstitution; import java.util.ArrayList; import java.util.Arrays; import java.util.StringJoiner; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mattrixwv.cipherstream.exceptions.InvalidCharacterException; import com.mattrixwv.cipherstream.exceptions.InvalidInputException; /** * Implements the Baconian cipher, a method of steganography where each letter of the alphabet is represented by a unique sequence of five characters ('a' or 'b'). * The Baconian cipher is a simple substitution cipher that can encode and decode text based on these sequences. * *

* The cipher uses a predefined list of five-character strings to represent each letter of the alphabet (A-Z). The input string is converted to these sequences for encoding, * and sequences are converted back to letters for decoding. *

*/ public final class Baconian{ private static final Logger logger = LoggerFactory.getLogger(Baconian.class); //?Conversions /** Predefined code for Baconian cipher (5-character strings for A-Z) */ protected static final ArrayList code = new ArrayList<>(Arrays.asList( "aaaaa", "aaaab", "aaaba", "aaabb", "aabaa", "aabab", "aabba","aabbb", "abaaa", "abaaa", "abaab", "ababa", "ababb", //A-M "abbaa", "abbab", "abbba", "abbbb", "baaaa", "baaab", "baaba", "baabb", "baabb", "babaa", "babab", "babba", "babbb" //N-Z )); /** The string that needs encoded/decoded */ protected String inputString; /** The encoded/decoded string */ protected String outputString; /** Persist capitals in the output string */ protected boolean preserveCapitals; /** * Sets the input string for encoding, removing whitespace and symbols, and handling capitalization based on the flag. * * @param inputString the string to be encoded * @throws InvalidInputException if the input string is null, empty, or invalid */ protected void setInputStringEncode(String inputString) throws InvalidInputException{ if(inputString == null){ throw new InvalidInputException("Input cannot be null"); } logger.debug("Setting input string for encoding '{}'", inputString); //Remove all whitespace and symbols inputString = inputString.replaceAll("[^A-Za-z]", ""); if(!preserveCapitals){ logger.debug("Removing case"); inputString = inputString.toLowerCase(); } this.inputString = inputString; logger.debug("Cleaned input string '{}'", inputString); if(this.inputString.isBlank()){ throw new InvalidInputException("Input must contain at least 1 letter"); } } /** * Sets the input string for decoding, ensuring it contains only valid Baconian characters (a's and b's) with a length of 5. * * @param inputString the string to be decoded * @throws InvalidCharacterException if the input string contains invalid Baconian characters * @throws InvalidInputException if the input string is null or empty */ protected void setInputStringDecode(String inputString) throws InvalidCharacterException, InvalidInputException{ if(inputString == null){ throw new InvalidInputException("Input cannot be null"); } else if(inputString.isBlank()){ throw new InvalidInputException("Input cannot be empty"); } logger.debug("Setting input string for decoding '{}'", inputString); if(!preserveCapitals){ logger.debug("Removing case"); inputString = inputString.toLowerCase(); } //Check each Baconian Cipher letter for validity logger.debug("Ensuring all 'letters' contain 5 characters"); for(String str : inputString.split(" ")){ logger.debug("Current 'letter' {}", str); //Make sure each letter contains 5 characters if(str.length() != 5){ throw new InvalidCharacterException("All Baconian 'letters' contain exactly 5 characters. Invalid 'letter': " + str); } //Make sure the letter contains only a's and b's logger.debug("Replacing all non-abAB characters"); String temp = str.replaceAll("[^abAB]", ""); if(!temp.equals(str)){ throw new InvalidCharacterException("Baconian 'letters' contain only a's and b's. Invalid 'letter': " + str); } } this.inputString = inputString; logger.debug("Cleaned input string '{}'", inputString); } /** * Encodes the input string using the Baconian cipher. * Each character in the input string is converted to its corresponding Baconian sequence. */ protected void encode(){ logger.debug("Encoding"); StringJoiner output = new StringJoiner(" "); //Go through every character in the inputString and encode it for(char ch : inputString.toCharArray()){ logger.debug("Working character {}", ch); //Convert the character to a binary string with A = 0 String binary = null; if(Character.isUpperCase(ch)){ logger.debug("Encoding uppercase"); binary = code.get(ch - 'A').toUpperCase(); } else{ logger.debug("Encoding lowercase"); binary = code.get(ch - 'a').toLowerCase(); } //Add the encoded character to the output logger.debug("Output letter {}", binary); output.add(binary); } //Save and return the output outputString = output.toString(); logger.debug("Saving output string '{}'", outputString); } /** * Decodes the input string using the Baconian cipher. * Each Baconian sequence in the input string is converted back to its corresponding character. */ protected void decode(){ logger.debug("Decoding"); StringBuilder output = new StringBuilder(); //Go through every Baconian Cipher character in the inputString and decode it for(String baconianCharacter : inputString.split(" ")){ logger.debug("Working letter {}", baconianCharacter); //Get the location of the Baconian character in the array int location = code.indexOf(baconianCharacter.toLowerCase()); logger.debug("Location of letter {}", location); //Convert the Baconian character to an ASCII character char ch; if(Character.isUpperCase(baconianCharacter.charAt(0))){ logger.debug("Decoding uppercase"); ch = (char)(location + 'A'); } else{ logger.debug("Decoding lowercase"); ch = (char)(location + 'a'); } //Add the decoded character to the output logger.debug("Decoded character {}", ch); output.append(ch); } //Save and return the output outputString = output.toString(); logger.debug("Saving output string '{}'", outputString); } //?Constructor /** * Constructs a new {@code Baconian} instance with default settings for preserving capitals. */ public Baconian(){ reset(); preserveCapitals = false; } /** * Constructs a new {@code Baconian} instance with a specified setting for preserving capitals. * * @param preserveCapitals whether to preserve capital letters in the output */ public Baconian(boolean preserveCapitals){ reset(); this.preserveCapitals = preserveCapitals; } /** * Encodes the input string using the Baconian cipher. * * @param inputString the string to be encoded * @return the encoded string * @throws InvalidInputException if the input string is invalid */ public String encode(String inputString) throws InvalidInputException{ reset(); setInputStringEncode(inputString); encode(); return outputString; } /** * Decodes the input string using the Baconian cipher. * * @param inputString the string to be decoded * @return the decoded string * @throws InvalidCharacterException if the input string contains invalid Baconian characters * @throws InvalidInputException if the input string is invalid */ public String decode(String inputString) throws InvalidCharacterException, InvalidInputException{ reset(); setInputStringDecode(inputString); decode(); return outputString; } //?Getters /** * Gets the current input string. * * @return the input string */ public String getInputString(){ return inputString; } /** * Gets the current output string. * * @return the output string */ public String getOutputString(){ return outputString; } /** * Resets the input and output strings to empty. */ public void reset(){ logger.debug("Resetting fields"); inputString = ""; outputString = ""; } }