public class ConvertOptions extends Options implements Serializable {

	/**
	 * Backward-compatible with 0.7.0
	 */
	private static final long serialVersionUID = 00000000007000001L;

	public static final int DEFAULT_STRICTNESS = 35;
	public static final Charset DEFAULT_CHARSET = Charsets.DETECT;
	public static final boolean DEFAULT_AUTOCOPY = true;
	public static final Replacements DEFAULT_REPLACEMENTS = new Replacements();

	public static final long DEFAULT_DEBUGGING_FLAGS = 0;

	@CommandLineArgument(
		command = "strictness",
		description = "The strictness of the conversion.\nThe strictness is a "
			+ "value determining at what point a string is considered garbage "
			+ "and should not be displayed. The lower the value, the higher "
			+ "the chance of having garbage in the result. The higher the "
			+ "value, the higher the chance of mistakenly excluding a valid "
			+ "string."
	)
	private Integer strictness = DEFAULT_STRICTNESS;
	public static final Bounds<Integer> STRICTNESS_DOMAIN =
		new Bounds<>(-500, 500);

	@CommandLineArgument(
		command = "encoding",
		description = "Defines the encoding used to decode the input"
	)
	private transient Charset charset = DEFAULT_CHARSET;
	public static final Values<Charset> CHARSET_DOMAIN =
		new Values<>(Charsets.ALL_CHARSETS);
	public static final Class<? extends ValueClass> CHARSET_VALUE_CLASS =
		Charsets.class;

	@CommandLineArgument(
		command = "autocopy",
		description = "Defines whether or not to automatically copy newly "
			+ "converted strings in the clipboard."
	)
	private Boolean autocopy = DEFAULT_AUTOCOPY;

	@CommandLineArgument(
		command = "debug",
		description = "Aimed at developers. Sets debugging flags.",
		usageExample = "--debug=i6n",
		flags = true
	)
	private Long debuggingFlags = DEFAULT_DEBUGGING_FLAGS;
	public static final
		Class<? extends ValueClass> DEBUGGING_FLAGS_VALUE_CLASS =
			DebuggingFlags.class;

	@CommandLineArgument(
		command = "replacements",
		description = "Replacements of sequences and patterns at different "
			+ "steps of the convertion process.",
		usage = "{[0x]\"<string>\">[0x]\"<string>\"[e][r]} "
			+ "(values are commas separated) where: \n"
			+ "  - <string> represent respectively either a sequence of "
			+ "hexadecimal numbers or a string.\n"
			+ "    Replacements of strings to hexadecimal is forbidden.\n"
			+ "  - 0x is optional and indicates that the following string is "
			+ "interpreted as hexadecimal or a pattern of hexadecimal.\n"
			+ "  - e is optional and indicates that antislashes must be "
			+ "interpreted as an escaping character.\n"
			+ "  - r is optional and indicates that X is a regular expression "
			+ "and Y its replacement, possibly containg backward references to "
			+ "groups in X.",
		usageExample = "--replacements=\"#RUBS(.*?)#RUBE\">\"[$1]\"r,"
			+ "0x\"8745\">\"\\u2049\"e"
	)
	private Replacements replacements = DEFAULT_REPLACEMENTS;
	public static final
		Class<? extends ArgumentParser<Replacements>> REPLACEMENTS_PARSER =
			ReplacementsParser.class;

	public ConvertOptions() {
		super();
	}

	/**
	 * Getter on the strictness of the conversion process.
	 *
	 * @return The strictness of the program.
	 */
	public synchronized int getStrictness() {
		return strictness;
	}

	/**
	 * Setter on the strictness of the conversion process.
	 *
	 * @param strictness
	 * 			The new strictness of the program.
	 */
	public synchronized void setStrictness(int strictness) {
		this.strictness = strictness;
	}

	/**
	 * Getter on the debugging flags currently activated.
	 *
	 * @return The debug level of the program.
	 */
	public synchronized long getDebuggingFlags() {
		return debuggingFlags;
	}

	/**
	 * Setter on the debugging flags currently activated.
	 *
	 * @param flags
	 * 			The flags affecting the current value.
	 * @param add
	 * 			True to add the value to the current flags, false to remove it.
	 */
	public synchronized void setDebuggingFlags(long flags, boolean add) {
		debuggingFlags = add
			? (debuggingFlags | flags)
			: (debuggingFlags & ~flags);
	}

	/**
	 * Getter on the charset to work with.
	 *
	 * @return The charset in which to convert hex input strings.
	 */
	public synchronized Charset getCharset() {
		return charset;
	}

	/**
	 * Setter on the charset to work with.
	 *
	 * @param charset
	 * 			The new charset in which to convert hex input strings.
	 */
	public synchronized void setCharset(Charset charset) {
		this.charset = charset;
	}

	/**
	 * Getter on whether or not the conversion result is set to the clipboard.
	 *
	 * @return Whether or not the conversion result is set to the clipboard.
	 */
	public synchronized boolean isAutocopy() {
		return autocopy;
	}

	/**
	 * Setter on whether or not the conversion result is set to the clipboard.
	 *
	 * @param autocopy
	 * 			True if the conversion result is set to the clipboard.
	 */
	public synchronized void setAutocopy(boolean autocopy) {
		this.autocopy = autocopy;
	}

	/**
	 * Getter on the replacements.
	 *
	 * @return The replacements.
	 */
	public synchronized Replacements getReplacements() {
		return replacements;
	}

	private void writeObject(ObjectOutputStream out) throws IOException {
		serialize(out);
	}

	private void readObject(ObjectInputStream in) throws IOException,
		ClassNotFoundException {

		deserialize(in);
	}

} public interface Converter {

	DebuggableStrings convert(String hex);

	void setReplacements(Replacements r);

} public class ConverterFactory {

	private static SJISConverter sjisConverterInstance = new SJISConverter();
	private static UTF16Converter utf16BEConverterInstance =
		new UTF16Converter(true);
	private static UTF16Converter utf16LEConverterInstance =
		new UTF16Converter(false);
	private static UTF8Converter utf8ConverterInstance = new UTF8Converter();

	private static EncodingAgnosticConverter encodingAgnosticConverterInstance =
		new EncodingAgnosticConverter();

	/**
	 * Provides a converter for a given charset.
	 *
	 * @param cs
	 * 			The charset encoding the hex input.
	 * @return A fitting converter.
	 */
	public static Converter getConverterInstance(Charset charset,
		Replacements r) {

		Converter c;
		if (charset == Charsets.DETECT) {
			c = encodingAgnosticConverterInstance;
		} else if (charset == Charsets.SHIFT_JIS) {
			c = sjisConverterInstance;
		} else if (charset == Charsets.UTF16_BE) {
			c = utf16BEConverterInstance;
		} else if (charset == Charsets.UTF16_LE) {
			c = utf16LEConverterInstance;
		} else if (charset == Charsets.UTF8) {
			c = utf8ConverterInstance;
		} else {
			throw new IllegalArgumentException("Invalid charset in options.");
		}
		c.setReplacements(r);
		return c;
	}

} public interface DebuggableStrings {

	/**
	 * Formats the lines depending on the debugging flags.
	 *
	 * @param debuggingFlags
	 * 			The debugging flags used to format these lines.
	 * @param converterStrictness
	 * 			The validity value below which a converted string is eliminated.
	 * @return A string representing these lines, with or without debug traces.
	 */
	String toString(long debuggingFlags, int converterStrictness);

	/**
	 * Provides a list of lines that can be formatted.
	 *
	 * @return A list of lines that can be formatted.
	 */
	DecorableList getDecorableList();

} public abstract class Formatter {

	private String linesDecorationBefore;
	private String linesDecorationBetween;
	private String linesDecorationAfter;
	private String lineDecorationBefore;
	private String lineDecorationAfter;

	public Formatter(String linesDecorationBefore,
		String linesDecorationBetween, String linesDecorationAfter,
		String lineDecorationBefore, String lineDecorationAfter) {

		this.linesDecorationBefore = linesDecorationBefore;
		this.linesDecorationBetween = linesDecorationBetween;
		this.linesDecorationAfter = linesDecorationAfter;
		this.lineDecorationBefore = lineDecorationBefore;
		this.lineDecorationAfter = lineDecorationAfter;
	}

	/**
	 * Formats the result of a conversion.
	 *
	 * @param lines
	 * 			A List of lines.
	 */
	public void format(DecorableList lines) {
		lines.setDecorationBefore(linesDecorationBefore);
		lines.setDecorationBetween(linesDecorationBetween);
		lines.setDecorationAfter(linesDecorationAfter);
		lines.setLinesDecorations(lineDecorationBefore, lineDecorationAfter);
	}

} public class FormatterFactory {

	public static final String STANDARD = "Standard";
	public static final String TESTS = "Test";
	public static final String MULTI_CONTENT = "Multi-content";

	private static StandardFormatter standardFormatterInstance =
		new StandardFormatter();
	private static TestsFormatter testsFormatterInstance =
		new TestsFormatter();
	private static MultiContentFormatter multiContentFormatterInstance =
		new MultiContentFormatter();

	/**
	 * Provides a formatter based on whether the current conversion session
	 * is intended for tests or not.
	 *
	 * @param tests
	 * 			True to use the same format as expected outputs
	 * 			during test runs. See {@link hextostring.tests.TestsLauncher}
	 * @return A formatter.
	 */
	public static Formatter getFormatterInstance(String type) {
		if (type.equals(STANDARD)) {
			return standardFormatterInstance;
		}
		if (type.equals(TESTS)) {
			return testsFormatterInstance;
		}
		if (type.equals(MULTI_CONTENT)) {
			return multiContentFormatterInstance;
		}
		throw new IllegalArgumentException("Unknown formatter type: " + type);
	}

} public class Replacements
	implements TableData<Replacement>, Serializable {

	/**
	 * Backward-compatible with 0.7.0
	 */
	private static final long serialVersionUID = 00000000007000000L;

	private List<Replacement> orderedReplacements;
	private Map<ReplacementType, List<Replacement>> replacementsByType;

	public Replacements() {
		orderedReplacements = new ArrayList<>();
		replacementsByType = new HashMap<>();
		for (ReplacementType type : ReplacementType.values()) {
			replacementsByType.put(type, new LinkedList<Replacement>());
		}
	}

	/**
	 * Getter on a replacement by its index.
	 *
	 * @param index
	 * 			The index of the replacement.
	 * @return The replacement at the specified index.
	 */
	@Override
	public Replacement get(int index) {
		return orderedReplacements.get(index);
	}

	/**
	 * Getter on the replacements.
	 *
	 * @return The replacements.
	 */
	@Override
	public List<Replacement> getAll() {
		return new ArrayList<Replacement>(orderedReplacements);
	}

	/**
	 * Adds a replacement at the end of the list.
	 *
	 * @param r
	 * 			The replacement to add.
	 */
	@Override
	public void add(Replacement r) {
		orderedReplacements.add(r);
		replacementsByType.get(r.getType()).add(r);
	}

	/**
	 * Removes a replacement at the specified index.
	 *
	 * @param index
	 * 			The index of the replacement to remove.
	 */
	@Override
	public void remove(int index) {
		Replacement deleted = orderedReplacements.remove(index);
		replacementsByType.get(deleted.getType()).remove(deleted);
	}

	/**
	 * Empties the list.
	 */
	public void clear() {
		orderedReplacements.clear();
		for (ReplacementType type : replacementsByType.keySet()) {
			replacementsByType.get(type).clear();
		}
	}

	/**
	 * Get all replacements of a specified type.
	 *
	 * @param type
	 * 			The type of the replacements.
	 * @return All replacements of a specified type.
	 */
	public List<Replacement> getByType(ReplacementType type) {
		return replacementsByType.get(type);
	}

	/**
	 * Setter on the type of a replacement.
	 *
	 * @param r
	 * 			The replacement to modify.
	 * @param type
	 * 			The new type of the replacement.
	 */
	public void setType(Replacement r, ReplacementType type) {
		for (ReplacementType previoustType : replacementsByType.keySet()) {
			replacementsByType.get(previoustType).remove(r);
		}
		replacementsByType.get(type).add(r);
		r.setType(type);
	}

	/**
	 * Applies all the replacements of a given type.
	 *
	 * @param s
	 * 			The string to which replacements are applied.
	 * @param type
	 * 			The type of the replacements to use.
	 * @return The string to which replacements were applied.
	 */
	public String apply(String s, ReplacementType type) {
		String res = new String(s);
		for (Replacement r : replacementsByType.get(type)) {
			res = r.apply(res);
		}
		return res;
	}

} public class Charsets implements ValueClass {

	// Charsets used in Japanese games
	@CommandLineValue(
		value = "sjis",
		shortcut = "j",
		description = "Shift JIS"
	)
	public static final Charset SHIFT_JIS = Charset.forName("Shift_JIS");
	@CommandLineValue(
		value = "utf16-le",
		shortcut = "l",
		description = "UTF16 Little Endian"
	)
	public static final Charset UTF16_LE = Charset.forName("UTF-16LE");
	@CommandLineValue(
		value = "utf16-be",
		shortcut = "b",
		description = "UTF16 Bid Endian"
	)
	public static final Charset UTF16_BE = Charset.forName("UTF-16BE");
	// also used for test files
	@CommandLineValue(
		value = "utf8",
		shortcut = "u",
		description = "UTF8"
	)
	public static final Charset UTF8 = Charset.forName("UTF-8");

	// not a charset, used for automatic recognition
	@CommandLineValue(
		value = "detect",
		shortcut = "d",
		description = "Detect the right encoding among the other ones"
	)
	public static final Charset DETECT = CharsetAutodetect.getInstance();

	public static final Charset[] ALL_CHARSETS = getAllCharsets();

	public static Charset getValidCharset(String charsetName) {
		for (Charset cs : ALL_CHARSETS) {
			if (cs.name().equals(charsetName)) {
				return cs;
			}
		}
		return null;
	}

	private static Charset[] getAllCharsets() {

		List<Field> charsetFields = ReflectionUtils.getAnnotatedFields(
			Charsets.class,
			CommandLineValue.class
		);
		Charset[] allCharsets = new Charset[charsetFields.size()];
		int eltCounter = 0;
		for (Field charsetField : charsetFields) {
			try {
				allCharsets[eltCounter++] = (Charset) charsetField.get(null);
			} catch (IllegalArgumentException | IllegalAccessException e) {
				e.printStackTrace();
			}
		}
		return allCharsets;
	}

} public class IndentablePrintStream {

	private PrintStream out;
	private String tab;

	public IndentablePrintStream() {
		this(System.out, "\t");
	}

	public IndentablePrintStream(PrintStream out, String tab) {
		this.out = out;
		this.tab = tab;
	}

	private String indent(String s, int nbTabs) {
		return StringUtils.indent(s, tab, nbTabs);
	}

	/**
	 * Prints a string.
	 *
	 * @param s
	 * 			The string to print.
	 */
	public void print(String s) {
		out.print(s);
	}

	/**
	 * Prints a string and a new line.
	 *
	 * @param s
	 * 			The string to print.
	 */
	public void println(String s) {
		out.println(s);
	}

	/**
	 * Prints an indented string.
	 *
	 * @param s
	 * 			The string to print.
	 * @param indentLevel
	 * 			The number of indentations to add.
	 */
	public void print(String s, int indentLevel) {
		out.print(indent(s, indentLevel));
	}

	/**
	 * Prints an indented string and a new line.
	 *
	 * @param s
	 * 			The string to print.
	 * @param indentLevel
	 * 			The number of indentations to add.
	 */
	public void println(String s, int indentLevel) {
		out.println(indent(s, indentLevel));
	}

} public class MainOptions extends Options implements Serializable,
	EncompassingOptions {

	/**
	 * Backward-compatible with 0.7.0
	 */
	private static final long serialVersionUID = 00000000007000000L;

	private HexOptions hexOptions;
	private ConvertOptions convertOptions;
	private GUIOptions GUIOptions;

	private Set<Options> subOptions = new HashSet<>();

	public MainOptions() {
		initSubOptions();
	}

	public MainOptions(String[] args) throws ValueOutOfDomainException {
		this();
		parseArgs(args);
	}

	private void initSubOptions() {
		this.hexOptions = new HexOptions();
		this.convertOptions = new ConvertOptions();
		this.GUIOptions = new GUIOptions();
		updateSubOptionsSet();

	}

	public void updateSubOptionsSet() {
		subOptions.clear();
		subOptions.add(this);
		subOptions.add(this.hexOptions);
		subOptions.add(this.convertOptions);
		subOptions.add(this.GUIOptions);
	}

	/**
	 * Getter on the option objects contained in this class.
	 *
	 * @return The option objects contained in this class.
	 */
	public Set<Options> getSubOptions() {
		return subOptions;
	}

	/**
	 * Getter on the options relative to the behavior of the main lua file.
	 *
	 * @return The options relative to the behavior of the main lua file.
	 */
	public synchronized HexOptions getHexOptions() {
		return hexOptions;
	}

	/**
	 * Getter on the conversion options.
	 *
	 * @return The conversion options.
	 */
	public synchronized ConvertOptions getConvertOptions() {
		return convertOptions;
	}

	/**
	 * Getter of the options for the GUI.
	 *
	 * @return The options for the GUI.
	 */
	public synchronized GUIOptions getGUIOptions() {
		return GUIOptions;
	}

	/**
	 * Parses and applies the command line arguments.
	 *
	 * @param args
	 * 			The command line arguments.
	 * @throws ValueOutOfDomainException
	 */
	public final void parseArgs(String[] args)
		throws ValueOutOfDomainException {

		if (args.length == 1 && args[0].equals("--help")) {
			System.out.println(usage(null, subOptions));
		} else {
			List<String> remainingArgs = Arrays.asList(args);
			for (Options opts : subOptions) {
				OptionsParser parser = new OptionsParser(opts);
				remainingArgs = parser.parse(remainingArgs);
			}
			if (!remainingArgs.isEmpty()) {
				throw new IllegalArgumentException(
					"Illegal command line argument(s) or value(s): "
						+ remainingArgs + "."
				);
			}
		}
	}

} @SuppressWarnings("serial")
public class ValueOutOfDomainException extends Exception {

	public ValueOutOfDomainException() {
		super();
	}

	public ValueOutOfDomainException(String msg) {
		super(msg);
	}

}
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import hextostring.ConvertOptions;
import hextostring.convert.Converter;
import hextostring.convert.ConverterFactory;
import hextostring.debug.DebuggableStrings;
import hextostring.format.Formatter;
import hextostring.format.FormatterFactory;
import hextostring.replacement.Replacements;
import hextostring.utils.Charsets;
import hextostring.utils.IndentablePrintStream;
import main.MainOptions;
import main.options.domain.ValueOutOfDomainException;
package hextostring.tests;




/**
 * Main test class.
 *
 * A test consists of an input and a corresponding expected,
 * manually written output.
 *
 * Every input file is converted and compared to the expected output.
 *
 * Test files are put into the "tests" directory at the root of the project.
 * Its architecture is as follows:
 *
 *   tests
 *   |-sjis (contains tests for games using Shift JIS)
 *   | |-handmade (contains tests crafted without relying on a game)
 *   | |-[game1]
 *   | | |-cmd.txt (contains description of options optimized for this game)
 *   | | |-input
 *   | | | |-0001.txt
 *   | | | |-0002.txt
 *   | | | |-...
 *   | | |-expected_output
 *   | |   |-0001.txt
 *   | |   |-0002.txt
 *   | |   |-...
 *   | |-[game2]
 *   | |-...
 *   |-utf16-be (contains tests for games using UTF-16 Big Endian)
 *   | |-[gameA]
 *   | | |-cmd.txt
 *   | | |-input
 *   | | | |-0001.txt
 *   | | | |-0002.txt
 *   | | | |-...
 *   | | |-expected_output
 *   | |   |-0001.txt
 *   | |   |-0002.txt
 *   | |   |-...
 *   | |-[gameB]
 *   | |-...
 *   |-utf16-le (contains tests for games using UTF-16 Little Endian)
 *   | |-...
 *   |-utf8 (contains tests for games using UTF-8)
 *     |-...
 *
 * It is assumed that all .txt files are encoded in UTF-8, without the BOM.
 * Their newline is always LF, never CR or CRLF
 *
 * @author Maxime PIA
 */
public class TestsLauncher {

	private static Formatter formatter =
		FormatterFactory.getFormatterInstance(FormatterFactory.TESTS);
	private static Converter currentConverter;
	private static IndentablePrintStream out = new IndentablePrintStream();

	private static final String INPUT_DIR = "input";
	private static final String EXPECTED_OUTPUT_DIR = "expected_output";
	private static final String CMD_FILE = "cmd.txt";

	private static final String SJIS_DIR = "sjis";
	private static final String UTF16BE_DIR = "utf16-be";
	private static final String UTF16LE_DIR = "utf16-le";
	private static final String UTF8_DIR = "utf8";

	private static Logger logger = LogManager.getLogger(TestsLauncher.class);

	private static File[] listSortedFiles(File f) {
		File[] files = f.listFiles();
		Arrays.sort(files);
		return files;
	}

	private static ConvertOptions getConvertOptions(File cmdFile)
		throws IOException, ValueOutOfDomainException {

		String cmd = new String(
			Files.readAllBytes(cmdFile.toPath()),
			Charsets.UTF8
		);
		return new MainOptions(cmd.split(" ")).getConvertOptions();
	}

	private static boolean compare(File inputFile, File expectedOutputFile,
		int indentLevel, ConvertOptions opts) {

		try {
			String input = new String(
				Files.readAllBytes(inputFile.toPath()),
				Charsets.UTF8
			);
			String expectedOutput = new String(
				Files.readAllBytes(expectedOutputFile.toPath()),
				Charsets.UTF8
			);

			DebuggableStrings dInput = currentConverter.convert(input);
			formatter.format(dInput.getDecorableList());
			String actualOutput = dInput.toString(
				opts == null
					? ConvertOptions.DEFAULT_DEBUGGING_FLAGS
					: opts.getDebuggingFlags(),
				opts == null
					? ConvertOptions.DEFAULT_STRICTNESS
					: opts.getStrictness()
			);
			out.print(inputFile.getName(), indentLevel);

			// introduce tolerance to newline discrepancies
			String cr = new String(Character.toChars(0xD));
			String lf = new String(Character.toChars(0xA));
			actualOutput = actualOutput.replace(cr + lf, lf).replace(cr, lf);

			if (expectedOutput.equals(actualOutput)) {
				out.println(" OK");
				return true;
			} else {
				out.println(" FAILURE");
				out.println("expected:", indentLevel + 1);
				out.println(expectedOutput, indentLevel + 2);
				out.println("instead of:", indentLevel + 1);
				out.println(actualOutput, indentLevel + 2);
				return false;
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return false;
	}

	private static Map<Boolean, Integer> compareAll(File inputDirectory,
		File expectedOutputDirectory, int indentLevel, ConvertOptions opts) {

		File[] inputs = listSortedFiles(inputDirectory);
		File[] expectedOutputs = listSortedFiles(expectedOutputDirectory);

		Map<Boolean, Integer> testResult = new HashMap<>();
		testResult.put(false, 0);
		testResult.put(true, 0);

		for (int i = 0; i < inputs.length && i < expectedOutputs.length; ++i) {
			boolean eq =
				compare(inputs[i], expectedOutputs[i], indentLevel, opts);
			testResult.put(eq, testResult.get(eq) + 1);
		}

		return testResult;
	}

	private static boolean isEncoding(File f, String encodingName) {
		return f == null
			? false
			: f.getName().equals(encodingName)
				|| isEncoding(f.getParentFile(), encodingName);
	}

	private static void setEncoding(File f, boolean detectEncoding,
		Replacements r) {

		if (detectEncoding) {
			currentConverter =
				ConverterFactory.getConverterInstance(Charsets.DETECT, r);