From 666d51384b442ffa8621299b7874a1fabba6c020 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 4 Nov 2023 17:20:00 +0100 Subject: [PATCH] Kotlin conversion: AbstractSopCmd --- .../cli/picocli/commands/AbstractSopCmd.java | 282 ------------------ .../cli/picocli/commands/AbstractSopCmd.kt | 248 +++++++++++++++ .../picocli/commands/AbstractSopCmdTest.java | 2 +- 3 files changed, 249 insertions(+), 283 deletions(-) delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/AbstractSopCmd.java create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/AbstractSopCmd.kt diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/AbstractSopCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/AbstractSopCmd.java deleted file mode 100644 index 9aec5a7..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/AbstractSopCmd.java +++ /dev/null @@ -1,282 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import sop.exception.SOPGPException; -import sop.util.UTCUtil; -import sop.util.UTF8Util; - -import javax.annotation.Nonnull; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.text.ParseException; -import java.util.Collection; -import java.util.Date; -import java.util.Locale; -import java.util.ResourceBundle; -import java.util.regex.Pattern; - -/** - * Abstract super class of SOP subcommands. - */ -public abstract class AbstractSopCmd implements Runnable { - - /** - * Interface to modularize resolving of environment variables. - */ - public interface EnvironmentVariableResolver { - /** - * Resolve the value of the given environment variable. - * Return null if the variable is not present. - * - * @param name name of the variable - * @return variable value or null - */ - String resolveEnvironmentVariable(String name); - } - - public static final String PRFX_ENV = "@ENV:"; - public static final String PRFX_FD = "@FD:"; - public static final Date BEGINNING_OF_TIME = new Date(0); - public static final Date END_OF_TIME = new Date(8640000000000000L); - - public static final Pattern PATTERN_FD = Pattern.compile("^\\d{1,20}$"); - - protected final ResourceBundle messages; - protected EnvironmentVariableResolver envResolver = System::getenv; - - public AbstractSopCmd() { - this(Locale.getDefault()); - } - - public AbstractSopCmd(@Nonnull Locale locale) { - messages = ResourceBundle.getBundle("msg_sop", locale); - } - - void throwIfOutputExists(String output) { - if (output == null) { - return; - } - - File outputFile = new File(output); - if (outputFile.exists()) { - String errorMsg = getMsg("sop.error.indirect_data_type.output_file_already_exists", outputFile.getAbsolutePath()); - throw new SOPGPException.OutputExists(errorMsg); - } - } - - public String getMsg(String key) { - return messages.getString(key); - } - - public String getMsg(String key, String arg1) { - return String.format(messages.getString(key), arg1); - } - - public String getMsg(String key, String arg1, String arg2) { - return String.format(messages.getString(key), arg1, arg2); - } - - void throwIfMissingArg(Object arg, String argName) { - if (arg == null) { - String errorMsg = getMsg("sop.error.usage.argument_required", argName); - throw new SOPGPException.MissingArg(errorMsg); - } - } - - void throwIfEmptyParameters(Collection arg, String parmName) { - if (arg.isEmpty()) { - String errorMsg = getMsg("sop.error.usage.parameter_required", parmName); - throw new SOPGPException.MissingArg(errorMsg); - } - } - - T throwIfUnsupportedSubcommand(T subcommand, String subcommandName) { - if (subcommand == null) { - String errorMsg = getMsg("sop.error.feature_support.subcommand_not_supported", subcommandName); - throw new SOPGPException.UnsupportedSubcommand(errorMsg); - } - return subcommand; - } - - void setEnvironmentVariableResolver(EnvironmentVariableResolver envResolver) { - if (envResolver == null) { - throw new NullPointerException("Variable envResolver cannot be null."); - } - this.envResolver = envResolver; - } - - public InputStream getInput(String indirectInput) throws IOException { - if (indirectInput == null) { - throw new IllegalArgumentException("Input cannot not be null."); - } - - String trimmed = indirectInput.trim(); - if (trimmed.isEmpty()) { - throw new IllegalArgumentException("Input cannot be blank."); - } - - if (trimmed.startsWith(PRFX_ENV)) { - if (new File(trimmed).exists()) { - String errorMsg = getMsg("sop.error.indirect_data_type.ambiguous_filename", trimmed); - throw new SOPGPException.AmbiguousInput(errorMsg); - } - - String envName = trimmed.substring(PRFX_ENV.length()); - String envValue = envResolver.resolveEnvironmentVariable(envName); - if (envValue == null) { - String errorMsg = getMsg("sop.error.indirect_data_type.environment_variable_not_set", envName); - throw new IllegalArgumentException(errorMsg); - } - - if (envValue.trim().isEmpty()) { - String errorMsg = getMsg("sop.error.indirect_data_type.environment_variable_empty", envName); - throw new IllegalArgumentException(errorMsg); - } - - return new ByteArrayInputStream(envValue.getBytes("UTF8")); - - } else if (trimmed.startsWith(PRFX_FD)) { - - if (new File(trimmed).exists()) { - String errorMsg = getMsg("sop.error.indirect_data_type.ambiguous_filename", trimmed); - throw new SOPGPException.AmbiguousInput(errorMsg); - } - - File fdFile = fileDescriptorFromString(trimmed); - try { - FileInputStream fileIn = new FileInputStream(fdFile); - return fileIn; - } catch (FileNotFoundException e) { - String errorMsg = getMsg("sop.error.indirect_data_type.file_descriptor_not_found", fdFile.getAbsolutePath()); - throw new IOException(errorMsg, e); - } - } else { - File file = new File(trimmed); - if (!file.exists()) { - String errorMsg = getMsg("sop.error.indirect_data_type.input_file_does_not_exist", file.getAbsolutePath()); - throw new SOPGPException.MissingInput(errorMsg); - } - - if (!file.isFile()) { - String errorMsg = getMsg("sop.error.indirect_data_type.input_not_a_file", file.getAbsolutePath()); - throw new SOPGPException.MissingInput(errorMsg); - } - - return new FileInputStream(file); - } - } - - public OutputStream getOutput(String indirectOutput) throws IOException { - if (indirectOutput == null) { - throw new IllegalArgumentException("Output cannot be null."); - } - - String trimmed = indirectOutput.trim(); - if (trimmed.isEmpty()) { - throw new IllegalArgumentException("Output cannot be blank."); - } - - // @ENV not allowed for output - if (trimmed.startsWith(PRFX_ENV)) { - String errorMsg = getMsg("sop.error.indirect_data_type.illegal_use_of_env_designator"); - throw new SOPGPException.UnsupportedSpecialPrefix(errorMsg); - } - - // File Descriptor - if (trimmed.startsWith(PRFX_FD)) { - File fdFile = fileDescriptorFromString(trimmed); - try { - FileOutputStream fout = new FileOutputStream(fdFile); - return fout; - } catch (FileNotFoundException e) { - String errorMsg = getMsg("sop.error.indirect_data_type.file_descriptor_not_found", fdFile.getAbsolutePath()); - throw new IOException(errorMsg, e); - } - } - - File file = new File(trimmed); - if (file.exists()) { - String errorMsg = getMsg("sop.error.indirect_data_type.output_file_already_exists", file.getAbsolutePath()); - throw new SOPGPException.OutputExists(errorMsg); - } - - if (!file.createNewFile()) { - String errorMsg = getMsg("sop.error.indirect_data_type.output_file_cannot_be_created", file.getAbsolutePath()); - throw new IOException(errorMsg); - } - - return new FileOutputStream(file); - } - - public File fileDescriptorFromString(String fdString) { - File fdDir = new File("/dev/fd/"); - if (!fdDir.exists()) { - String errorMsg = getMsg("sop.error.indirect_data_type.designator_fd_not_supported"); - throw new SOPGPException.UnsupportedSpecialPrefix(errorMsg); - } - String fdNumber = fdString.substring(PRFX_FD.length()); - if (!PATTERN_FD.matcher(fdNumber).matches()) { - throw new IllegalArgumentException("File descriptor must be a positive number."); - } - File descriptor = new File(fdDir, fdNumber); - return descriptor; - } - - public static String stringFromInputStream(InputStream inputStream) throws IOException { - try { - ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); - byte[] buf = new byte[4096]; int read; - while ((read = inputStream.read(buf)) != -1) { - byteOut.write(buf, 0, read); - } - // TODO: For decrypt operations we MUST accept non-UTF8 passwords - return UTF8Util.decodeUTF8(byteOut.toByteArray()); - } finally { - inputStream.close(); - } - } - - public Date parseNotAfter(String notAfter) { - if (notAfter.equals("now")) { - return new Date(); - } - - if (notAfter.equals("-")) { - return END_OF_TIME; - } - - try { - return UTCUtil.parseUTCDate(notAfter); - } catch (ParseException e) { - String errorMsg = getMsg("sop.error.input.malformed_not_after"); - throw new IllegalArgumentException(errorMsg); - } - } - - public Date parseNotBefore(String notBefore) { - if (notBefore.equals("now")) { - return new Date(); - } - - if (notBefore.equals("-")) { - return BEGINNING_OF_TIME; - } - - try { - return UTCUtil.parseUTCDate(notBefore); - } catch (ParseException e) { - String errorMsg = getMsg("sop.error.input.malformed_not_before"); - throw new IllegalArgumentException(errorMsg); - } - } -} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/AbstractSopCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/AbstractSopCmd.kt new file mode 100644 index 0000000..4629e57 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/AbstractSopCmd.kt @@ -0,0 +1,248 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.* +import java.text.ParseException +import java.util.* +import sop.cli.picocli.commands.AbstractSopCmd.EnvironmentVariableResolver +import sop.exception.SOPGPException.* +import sop.util.UTCUtil.Companion.parseUTCDate +import sop.util.UTF8Util.Companion.decodeUTF8 + +/** Abstract super class of SOP subcommands. */ +abstract class AbstractSopCmd(locale: Locale = Locale.getDefault()) : Runnable { + + private val messages: ResourceBundle = ResourceBundle.getBundle("msg_sop", locale) + var environmentVariableResolver = EnvironmentVariableResolver { name: String -> + System.getenv(name) + } + + /** Interface to modularize resolving of environment variables. */ + fun interface EnvironmentVariableResolver { + + /** + * Resolve the value of the given environment variable. Return null if the variable is not + * present. + * + * @param name name of the variable + * @return variable value or null + */ + fun resolveEnvironmentVariable(name: String): String? + } + + fun throwIfOutputExists(output: String?) { + output + ?.let { File(it) } + ?.let { + if (it.exists()) { + val errorMsg: String = + getMsg( + "sop.error.indirect_data_type.output_file_already_exists", + it.absolutePath) + throw OutputExists(errorMsg) + } + } + } + + fun getMsg(key: String): String = messages.getString(key) + + fun getMsg(key: String, vararg args: String): String { + val msg = messages.getString(key) + return String.format(msg, *args) + } + + fun throwIfMissingArg(arg: Any?, argName: String) { + if (arg == null) { + val errorMsg = getMsg("sop.error.usage.argument_required", argName) + throw MissingArg(errorMsg) + } + } + + fun throwIfEmptyParameters(arg: Collection<*>, parmName: String) { + if (arg.isEmpty()) { + val errorMsg = getMsg("sop.error.usage.parameter_required", parmName) + throw MissingArg(errorMsg) + } + } + + fun throwIfUnsupportedSubcommand(subcommand: T?, subcommandName: String): T { + if (subcommand == null) { + val errorMsg = + getMsg("sop.error.feature_support.subcommand_not_supported", subcommandName) + throw UnsupportedSubcommand(errorMsg) + } + return subcommand + } + + @Throws(IOException::class) + fun getInput(indirectInput: String): InputStream { + val trimmed = indirectInput.trim() + require(trimmed.isNotBlank()) { "Input cannot be blank." } + + if (trimmed.startsWith(PRFX_ENV)) { + if (File(trimmed).exists()) { + val errorMsg = getMsg("sop.error.indirect_data_type.ambiguous_filename", trimmed) + throw AmbiguousInput(errorMsg) + } + + val envName = trimmed.substring(PRFX_ENV.length) + val envValue = environmentVariableResolver.resolveEnvironmentVariable(envName) + requireNotNull(envValue) { + getMsg("sop.error.indirect_data_type.environment_variable_not_set", envName) + } + + require(envValue.trim().isNotEmpty()) { + getMsg("sop.error.indirect_data_type.environment_variable_empty", envName) + } + + return envValue.byteInputStream() + } else if (trimmed.startsWith(PRFX_FD)) { + + if (File(trimmed).exists()) { + val errorMsg = getMsg("sop.error.indirect_data_type.ambiguous_filename", trimmed) + throw AmbiguousInput(errorMsg) + } + + val fdFile: File = fileDescriptorFromString(trimmed) + return try { + fdFile.inputStream() + } catch (e: FileNotFoundException) { + val errorMsg = + getMsg( + "sop.error.indirect_data_type.file_descriptor_not_found", + fdFile.absolutePath) + throw IOException(errorMsg, e) + } + } else { + + val file = File(trimmed) + if (!file.exists()) { + val errorMsg = + getMsg( + "sop.error.indirect_data_type.input_file_does_not_exist", file.absolutePath) + throw MissingInput(errorMsg) + } + if (!file.isFile()) { + val errorMsg = + getMsg("sop.error.indirect_data_type.input_not_a_file", file.absolutePath) + throw MissingInput(errorMsg) + } + return file.inputStream() + } + } + + @Throws(IOException::class) + fun getOutput(indirectOutput: String?): OutputStream { + requireNotNull(indirectOutput) { "Output cannot be null." } + val trimmed = indirectOutput.trim() + require(trimmed.isNotEmpty()) { "Output cannot be blank." } + + // @ENV not allowed for output + if (trimmed.startsWith(PRFX_ENV)) { + val errorMsg = getMsg("sop.error.indirect_data_type.illegal_use_of_env_designator") + throw UnsupportedSpecialPrefix(errorMsg) + } + + // File Descriptor + if (trimmed.startsWith(PRFX_FD)) { + val fdFile = fileDescriptorFromString(trimmed) + return try { + fdFile.outputStream() + } catch (e: FileNotFoundException) { + val errorMsg = + getMsg( + "sop.error.indirect_data_type.file_descriptor_not_found", + fdFile.absolutePath) + throw IOException(errorMsg, e) + } + } + val file = File(trimmed) + if (file.exists()) { + val errorMsg = + getMsg("sop.error.indirect_data_type.output_file_already_exists", file.absolutePath) + throw OutputExists(errorMsg) + } + if (!file.createNewFile()) { + val errorMsg = + getMsg( + "sop.error.indirect_data_type.output_file_cannot_be_created", file.absolutePath) + throw IOException(errorMsg) + } + return file.outputStream() + } + + fun fileDescriptorFromString(fdString: String): File { + val fdDir = File("/dev/fd/") + if (!fdDir.exists()) { + val errorMsg = getMsg("sop.error.indirect_data_type.designator_fd_not_supported") + throw UnsupportedSpecialPrefix(errorMsg) + } + val fdNumber = fdString.substring(PRFX_FD.length) + require(PATTERN_FD.matcher(fdNumber).matches()) { + "File descriptor must be a positive number." + } + return File(fdDir, fdNumber) + } + + fun parseNotAfter(notAfter: String): Date { + return when (notAfter) { + "now" -> Date() + "-" -> END_OF_TIME + else -> + try { + parseUTCDate(notAfter) + } catch (e: ParseException) { + val errorMsg = getMsg("sop.error.input.malformed_not_after") + throw IllegalArgumentException(errorMsg) + } + } + } + + fun parseNotBefore(notBefore: String): Date { + return when (notBefore) { + "now" -> Date() + "-" -> DAWN_OF_TIME + else -> + try { + parseUTCDate(notBefore) + } catch (e: ParseException) { + val errorMsg = getMsg("sop.error.input.malformed_not_before") + throw IllegalArgumentException(errorMsg) + } + } + } + + companion object { + const val PRFX_ENV = "@ENV:" + + const val PRFX_FD = "@FD:" + + @JvmField val DAWN_OF_TIME = Date(0) + + @JvmField + @Deprecated("Replace with DAWN_OF_TIME", ReplaceWith("DAWN_OF_TIME")) + val BEGINNING_OF_TIME = DAWN_OF_TIME + + @JvmField val END_OF_TIME = Date(8640000000000000L) + + @JvmField val PATTERN_FD = "^\\d{1,20}$".toPattern() + + @Throws(IOException::class) + @JvmStatic + fun stringFromInputStream(inputStream: InputStream): String { + return inputStream.use { input -> + val byteOut = ByteArrayOutputStream() + val buf = ByteArray(4096) + var read: Int + while (input.read(buf).also { read = it } != -1) { + byteOut.write(buf, 0, read) + } + // TODO: For decrypt operations we MUST accept non-UTF8 passwords + decodeUTF8(byteOut.toByteArray()) + } + } + } +} diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/AbstractSopCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/AbstractSopCmdTest.java index aed420b..396bc7f 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/AbstractSopCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/AbstractSopCmdTest.java @@ -36,7 +36,7 @@ public class AbstractSopCmdTest { @Test public void getInput_NullInvalid() { - assertThrows(IllegalArgumentException.class, () -> abstractCmd.getInput(null)); + assertThrows(NullPointerException.class, () -> abstractCmd.getInput(null)); } @Test