/ assign4 / DecryptCaesar.java
DecryptCaesar.java
  1  import java.util.ArrayList;
  2  import java.util.HashMap;
  3  import java.util.List;
  4  import java.util.Map;
  5  import java.util.Optional;
  6  
  7  /// Caesar cipher decoder
  8  ///
  9  /// SPDX-License-Identifier: GPL-3.0-or-later
 10  /// SPDX-FileCopyrightText: 2024 Jonas Smedegaard <dr@jones.dk>
 11  ///
 12  /// Decrypts a string encoded with the Caesar cipher.
 13  /// The encrypted string is passed as first command-line argument,
 14  /// and all possible decryptions are emitted to stdout.
 15  ///
 16  /// @version 0.0.1
 17  /// @since JDK 15
 18  /// @see <https://app.radicle.xyz/nodes/ash.radicle.garden/rad:z3YzABgyz2D36LiKe3YcdJ6PcDCXM/tree/assign4/README.md>
 19  /// @see <https://en.wikipedia.org/wiki/Caesar_cipher>
 20  /// @see <https://moodle.ruc.dk/mod/assign/view.php?id=507318>
 21  public class DecryptCaesar {
 22  
 23  	/// Constructs a new DecryptCaesar object.
 24  	public DecryptCaesar() {
 25  	}
 26  
 27  	/// main method for the CLI tool
 28  	///
 29  	/// Accepts ciphertext as first argument.
 30  	///
 31  	/// @param args  command-line arguments
 32  	public static void main(String[] args) {
 33  		final String alphabet = envString("ALPHABET", "ABCDEFGHIJKLMNOPQRSTUVWXYZ");
 34  
 35  		SimpleOptArgs optargs = new SimpleOptArgs(args);
 36  
 37  		if (optargs.opt("--help") != null) {
 38  			System.err.println(help);
 39  			System.exit(0);
 40  		}
 41  
 42  		// basic sanity check of ciphertext
 43  		if (optargs.args.length <= 0) {
 44  			System.err.println("ERROR: No ciphertext provided.");
 45  			System.err.println(help);
 46  			System.exit(1);
 47  		}
 48  
 49  		// get string to decrypt as first command-line argument
 50  		final String ciphertext = optargs.args[0];
 51  
 52  		// instantiate core class
 53  		String[] coreArgs = {ciphertext, alphabet, optargs.opt("--key", "0")};
 54  		DecryptCaesarCore core = new DecryptCaesarCore(coreArgs);
 55  		core.decipherAsNeeded();
 56  	}
 57  
 58  	/// Help message
 59  	///
 60  	/// @since JDK 15
 61  	/// @see <https://openjdk.org/jeps/378>
 62  	public static String help = """
 63  Usage: java DecryptCaesar [OPTION]... CIPHERTEXT
 64  
 65  Decrypts Caesar-encrypted CIPHERTEXT by brute force:
 66  Computes and prints all possible combinations.
 67  
 68  OPTIONS:
 69    --key                 decrypt using specific key (i.e. not brute force)
 70    --help                this message
 71  
 72  By default, a modern american alphabet is used.
 73  Environment variable ALPHABET can override the alphabet to use,
 74  e.g. like this to crack a ciphertext contemporary to Julius Caesar:
 75  
 76    ALPHABET=ABCDEFZGHIKLMNOPQRSTVX java DecryptCaesar 'OVH KCD XBVGZDPV'
 77  	""";
 78  
 79  	/// Decode a ciphertext
 80  	///
 81  	/// @param alphabet    the ordered character set, a.k.a. the alphabet
 82  	/// @param ciphertext  the excyptet text
 83  	/// @param key         the key to decrypt the ciphertext
 84  	public static void decipher(String alphabet, String ciphertext, int key) {
 85  		// compose a plaintext iterating over each ciphertext character
 86  		final int space = alphabet.length();
 87  		String plaintext = "";
 88  		for ( int j = 0; j < ciphertext.length(); j++) {
 89  
 90  			// extract character, and look it up in alphabet
 91  			char c = ciphertext.charAt(j);
 92  			int k = alphabet.indexOf(c);
 93  
 94  			// return alien character as-is,
 95  			// or if found return rotated position, modulo the space
 96  			plaintext += (k < 0) ? c : alphabet.charAt((key + k) % space);
 97  		}
 98  		System.out.printf("%s: %s%n", key, plaintext);
 99  	}
100  
101  	/// Environment variable lookup
102  	///
103  	/// @param variable  the environment variable to look up
104  	/// @param fallback  a fallback string if variable is undefined
105  	/// @return          contents of environment variable, or fallback string
106  	public static String envString(String variable, String fallback) {
107  		String s = System.getenv(variable);
108  		if (s != null && s.length() > 0) {
109  			return s;
110  		}
111  		return fallback;
112  	}
113  
114  	/// Simple generic GNU-ish options and argument parser
115  	///
116  	/// * Arguments beginning with a dash are stored as options.
117  	/// * Other arguments are stored non-options.
118  	/// * Any option after a bare `--` are stored as non-options.
119  	/// * A bare `-` is stored as a non-option.
120  	/// * Options containing an equal sign are split.
121  	/// * An option shadows any earlier option by the same name.
122  	///
123  	/// @see <https://stackoverflow.com/a/26376532/18619283>
124  	/// @see <https://stackoverflow.com/a/51421535/18619283>
125  	public static class SimpleOptArgs {
126  
127  		/// Command-line arguments, with options stripped
128  		public String[] args;
129  
130  		private final Map<String, List<String>> params = new HashMap<>();
131  
132  		/// Constructs a new SimpleGetOpts object.
133  		///
134  		/// @param orig_args  command-line arguments
135  		public SimpleOptArgs(String[] orig_args) {
136  			List<String> options = null;
137  			List<String> non_options = new ArrayList<>();
138  			params.put("--", non_options);
139  
140  			boolean persist = true;
141  			for (int i = 0; i < orig_args.length; i++) {
142  				final String arg = orig_args[i];
143  
144  				if ("-".equals(arg)) {
145  					non_options.add(arg);
146  				}
147  				else if (persist && "--".equals(arg)) {
148  					persist = false;
149  				}
150  				else if (persist && arg.charAt(0) == '-') {
151  					String[] parts = arg.split("=", 2);
152  					options = new ArrayList<>();
153  					params.put(parts[0], options);
154  					if (parts.length > 1) {
155  						options.add(parts[1]);
156  					}
157  				}
158  				else {
159  					non_options.add(arg);
160  				}
161  			}
162  
163  			args = params.get("--").toArray(new String[0]);
164  		}
165  
166  		/// Get an option
167  		///
168  		/// @param key  the option string including dash(es)
169  		/// @return     option value or the empty string,
170  		///             or null if option is missing
171  		public String opt(String key) {
172  			List<String> list = params.get(key);
173  			if (list == null) {
174  				return null;
175  			}
176  			if (list.isEmpty()) {
177  				return "";
178  			}
179  			return list.get(0);
180  		}
181  
182  		/// Get an option, or a fallback value
183  		///
184  		/// @param key       the option string including dash(es)
185  		/// @param fallback  the option string including dash(es)
186  		/// @return          option value or the empty string,
187  		///                  or the fallback value if option is missing
188  		public String opt(String key, String fallback) {
189  			List<String> list = params.get(key);
190  			if (list == null) {
191  				return fallback;
192  			}
193  			if (list.isEmpty()) {
194  				return "";
195  			}
196  			return list.get(0);
197  		}
198  	}
199  }