From 788c82531e2be401b9fd1e16eecd96bc14c04962 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 19 Jul 2021 18:20:52 +0200 Subject: [PATCH] Wip: Adopt changes from the SOP specification version 2 --- .../cli/commands/EncryptDecryptTest.java | 1 - .../java/org/pgpainless/sop/ArmorImpl.java | 2 +- .../java/org/pgpainless/sop/DecryptImpl.java | 2 +- .../org/pgpainless/sop/GenerateKeyImpl.java | 2 +- .../picocli/SOPExceptionExitCodeMapper.java | 45 +++++ .../picocli/SOPExecutionExceptionHandler.java | 37 ++++ .../src/main/java/sop/cli/picocli/SopCLI.java | 2 + .../sop/cli/picocli/commands/DecryptCmd.java | 183 +++++++----------- .../sop/cli/picocli/commands/EncryptCmd.java | 64 ++---- .../cli/picocli/commands/ExtractCertCmd.java | 9 +- .../cli/picocli/commands/ArmorCmdTest.java | 4 +- .../cli/picocli/commands/DecryptCmdTest.java | 46 +++-- .../cli/picocli/commands/EncryptCmdTest.java | 22 +-- .../picocli/commands/GenerateKeyCmdTest.java | 2 +- .../sop/cli/picocli/commands/SignCmdTest.java | 2 +- .../cli/picocli/commands/VerifyCmdTest.java | 4 +- .../java/sop/exception/SOPGPException.java | 118 ++++++++++- 17 files changed, 328 insertions(+), 217 deletions(-) create mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/SOPExceptionExitCodeMapper.java create mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/SOPExecutionExceptionHandler.java diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/EncryptDecryptTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/EncryptDecryptTest.java index 96d7ef70..77e55830 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/EncryptDecryptTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/EncryptDecryptTest.java @@ -101,7 +101,6 @@ public class EncryptDecryptTest { msgAscOut.close(); File verifyFile = new File(tempDir, "verify.txt"); - assertTrue(verifyFile.createNewFile()); FileInputStream msgAscIn = new FileInputStream(msgAscFile); System.setIn(msgAscIn); diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java index 2d2b93c4..13bb1e04 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java @@ -38,7 +38,7 @@ public class ArmorImpl implements Armor { @Override public Armor label(ArmorLabel label) throws SOPGPException.UnsupportedOption { - throw new SOPGPException.UnsupportedOption(); + throw new SOPGPException.UnsupportedOption("Setting custom Armor labels not supported."); } @Override diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java index baae4ad6..58e4e128 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/DecryptImpl.java @@ -87,7 +87,7 @@ public class DecryptImpl implements Decrypt { @Override public DecryptImpl withSessionKey(SessionKey sessionKey) throws SOPGPException.UnsupportedOption { - throw new SOPGPException.UnsupportedOption(); + throw new SOPGPException.UnsupportedOption("Setting custom session key not supported."); } @Override diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java index 2ff2273a..029b502b 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/GenerateKeyImpl.java @@ -87,7 +87,7 @@ public class GenerateKeyImpl implements GenerateKey { } }; } catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException e) { - throw new SOPGPException.UnsupportedAsymmetricAlgo(e); + throw new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", e); } catch (PGPException e) { throw new RuntimeException(e); } diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/SOPExceptionExitCodeMapper.java b/sop-java-picocli/src/main/java/sop/cli/picocli/SOPExceptionExitCodeMapper.java new file mode 100644 index 00000000..5f41db49 --- /dev/null +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/SOPExceptionExitCodeMapper.java @@ -0,0 +1,45 @@ +/* + * Copyright 2021 Paul Schaub. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sop.cli.picocli; + +import picocli.CommandLine; +import sop.exception.SOPGPException; + +public class SOPExceptionExitCodeMapper implements CommandLine.IExitCodeExceptionMapper { + + @Override + public int getExitCode(Throwable exception) { + if (exception instanceof SOPGPException) { + return ((SOPGPException) exception).getExitCode(); + } + if (exception instanceof CommandLine.UnmatchedArgumentException) { + CommandLine.UnmatchedArgumentException ex = (CommandLine.UnmatchedArgumentException) exception; + // Unmatched option of subcommand (eg. `generate-key -k`) + if (ex.isUnknownOption()) { + return SOPGPException.UnsupportedOption.EXIT_CODE; + } + // Unmatched subcommand + return SOPGPException.UnsupportedSubcommand.EXIT_CODE; + } + // Invalid option (eg. `--label Invalid`) + if (exception instanceof CommandLine.ParameterException) { + return SOPGPException.UnsupportedOption.EXIT_CODE; + } + + // Others, like IOException etc. + return 1; + } +} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/SOPExecutionExceptionHandler.java b/sop-java-picocli/src/main/java/sop/cli/picocli/SOPExecutionExceptionHandler.java new file mode 100644 index 00000000..a2ced90e --- /dev/null +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/SOPExecutionExceptionHandler.java @@ -0,0 +1,37 @@ +/* + * Copyright 2021 Paul Schaub. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sop.cli.picocli; + +import picocli.CommandLine; + +public class SOPExecutionExceptionHandler implements CommandLine.IExecutionExceptionHandler { + + @Override + public int handleExecutionException(Exception ex, CommandLine commandLine, CommandLine.ParseResult parseResult) throws Exception { + int exitCode = commandLine.getExitCodeExceptionMapper() != null ? + commandLine.getExitCodeExceptionMapper().getExitCode(ex) : + commandLine.getCommandSpec().exitCodeOnExecutionException(); + CommandLine.Help.ColorScheme colorScheme = commandLine.getColorScheme(); + // CHECKSTYLE:OFF + if (ex.getMessage() != null) { + commandLine.getErr().println(colorScheme.errorText(ex.getMessage())); + } + ex.printStackTrace(commandLine.getErr()); + // CHECKSTYLE:ON + + return exitCode; + } +} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java b/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java index 64dc6236..745c7816 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java @@ -58,6 +58,8 @@ public class SopCLI { public static int execute(String[] args) { return new CommandLine(SopCLI.class) .setCommandName(EXECUTABLE_NAME) + .setExecutionExceptionHandler(new SOPExecutionExceptionHandler()) + .setExitCodeExceptionMapper(new SOPExceptionExitCodeMapper()) .setCaseInsensitiveEnumValuesAllowed(true) .execute(args); } diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DecryptCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DecryptCmd.java index 75ea2bca..b4c4bccd 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DecryptCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DecryptCmd.java @@ -32,7 +32,6 @@ import sop.ReadyWithResult; import sop.SessionKey; import sop.Verification; import sop.cli.picocli.DateParser; -import sop.cli.picocli.Print; import sop.cli.picocli.SopCLI; import sop.exception.SOPGPException; import sop.operation.Decrypt; @@ -93,9 +92,13 @@ public class DecryptCmd implements Runnable { @Override public void run() { - unlinkExistingVerifyOut(verifyOut); + throwIfVerifyOutExists(verifyOut); Decrypt decrypt = SopCLI.getSop().decrypt(); + if (decrypt == null) { + throw new SOPGPException.UnsupportedSubcommand("Subcommand 'decrypt' not implemented."); + } + setNotAfter(notAfter, decrypt); setNotBefore(notBefore, decrypt); setWithPasswords(withPassword, decrypt); @@ -104,65 +107,63 @@ public class DecryptCmd implements Runnable { setDecryptWith(keys, decrypt); if (verifyOut != null && certs.isEmpty()) { - Print.errln("--verify-out is requested, but no --verify-with was provided."); - System.exit(23); + throw new SOPGPException.IncompleteVerification("--verify-out is requested, but no --verify-with was provided."); } try { ReadyWithResult ready = decrypt.ciphertext(System.in); DecryptionResult result = ready.writeTo(System.out); - if (sessionKeyOut != null) { - if (sessionKeyOut.exists()) { - Print.errln("File " + sessionKeyOut.getAbsolutePath() + " already exists."); - Print.trace(new SOPGPException.OutputExists()); - System.exit(1); - } - - try (FileOutputStream outputStream = new FileOutputStream(sessionKeyOut)) { - if (!result.getSessionKey().isPresent()) { - Print.errln("Session key not extracted. Possibly the feature is not supported."); - System.exit(SOPGPException.UnsupportedOption.EXIT_CODE); - } else { - SessionKey sessionKey = result.getSessionKey().get(); - outputStream.write(sessionKey.getAlgorithm()); - outputStream.write(sessionKey.getKey()); - } - } - } - if (verifyOut != null) { - if (!verifyOut.createNewFile()) { - throw new IOException("Cannot create file " + verifyOut.getAbsolutePath()); - } - try (FileOutputStream outputStream = new FileOutputStream(verifyOut)) { - PrintWriter writer = new PrintWriter(outputStream); - for (Verification verification : result.getVerifications()) { - // CHECKSTYLE:OFF - writer.println(verification.toString()); - // CHECKSTYLE:ON - } - writer.flush(); - } - } + writeSessionKeyOut(result); + writeVerifyOut(result); } catch (SOPGPException.BadData badData) { - Print.errln("No valid OpenPGP message found on Standard Input."); - Print.trace(badData); - System.exit(badData.getExitCode()); - } catch (SOPGPException.MissingArg missingArg) { - Print.errln("Missing arguments."); - Print.trace(missingArg); - System.exit(missingArg.getExitCode()); - } catch (IOException e) { - Print.errln("IO Error."); - Print.trace(e); - System.exit(1); - } catch (SOPGPException.NoSignature noSignature) { - Print.errln("No verifiable signature found."); - Print.trace(noSignature); - System.exit(noSignature.getExitCode()); - } catch (SOPGPException.CannotDecrypt cannotDecrypt) { - Print.errln("Cannot decrypt."); - Print.trace(cannotDecrypt); - System.exit(cannotDecrypt.getExitCode()); + throw new SOPGPException.BadData("No valid OpenPGP message found on Standard Input.", badData); + } catch (IOException ioException) { + throw new RuntimeException(ioException); + } + } + + private void writeVerifyOut(DecryptionResult result) throws IOException { + if (verifyOut != null) { + if (!verifyOut.createNewFile()) { + throw new IOException("Cannot create file " + verifyOut.getAbsolutePath()); + } + try (FileOutputStream outputStream = new FileOutputStream(verifyOut)) { + PrintWriter writer = new PrintWriter(outputStream); + for (Verification verification : result.getVerifications()) { + // CHECKSTYLE:OFF + writer.println(verification.toString()); + // CHECKSTYLE:ON + } + writer.flush(); + } + } + } + + private void writeSessionKeyOut(DecryptionResult result) throws IOException { + if (sessionKeyOut != null) { + if (sessionKeyOut.exists()) { + throw new SOPGPException.OutputExists("Target " + sessionKeyOut.getAbsolutePath() + " of option --session-key-out already exists."); + } + + try (FileOutputStream outputStream = new FileOutputStream(sessionKeyOut)) { + if (!result.getSessionKey().isPresent()) { + throw new SOPGPException.UnsupportedOption("Session key not extracted. Possibly the feature --session-key-out is not supported."); + } else { + SessionKey sessionKey = result.getSessionKey().get(); + outputStream.write(sessionKey.getAlgorithm()); + outputStream.write(sessionKey.getKey()); + } + } + } + } + + private void throwIfVerifyOutExists(File verifyOut) throws SOPGPException.OutputExists { + if (verifyOut == null) { + return; + } + + if (verifyOut.exists()) { + throw new SOPGPException.OutputExists("Target " + verifyOut.getAbsolutePath() + " of option --verify-out already exists."); } } @@ -171,25 +172,13 @@ public class DecryptCmd implements Runnable { try (FileInputStream keyIn = new FileInputStream(key)) { decrypt.withKey(keyIn); } catch (SOPGPException.KeyIsProtected keyIsProtected) { - Print.errln("Key in file " + key.getAbsolutePath() + " is password protected."); - Print.trace(keyIsProtected); - System.exit(1); - } catch (SOPGPException.UnsupportedAsymmetricAlgo unsupportedAsymmetricAlgo) { - Print.errln("Key uses unsupported asymmetric algorithm."); - Print.trace(unsupportedAsymmetricAlgo); - System.exit(unsupportedAsymmetricAlgo.getExitCode()); + throw new SOPGPException.KeyIsProtected("Key in file " + key.getAbsolutePath() + " is password protected.", keyIsProtected); } catch (SOPGPException.BadData badData) { - Print.errln("File " + key.getAbsolutePath() + " does not contain a private key."); - Print.trace(badData); - System.exit(badData.getExitCode()); + throw new SOPGPException.BadData("File " + key.getAbsolutePath() + " does not contain a private key.", badData); } catch (FileNotFoundException e) { - Print.errln("File " + key.getAbsolutePath() + " does not exist."); - Print.trace(e); - System.exit(1); + throw new SOPGPException.MissingInput("File " + key.getAbsolutePath() + " does not exist.", e); } catch (IOException e) { - Print.errln("IO Error."); - Print.trace(e); - System.exit(1); + throw new RuntimeException(e); } } } @@ -199,30 +188,11 @@ public class DecryptCmd implements Runnable { try (FileInputStream certIn = new FileInputStream(cert)) { decrypt.verifyWithCert(certIn); } catch (FileNotFoundException e) { - Print.errln("File " + cert.getAbsolutePath() + " does not exist."); - Print.trace(e); - System.exit(1); - } catch (IOException e) { - Print.errln("IO Error."); - Print.trace(e); - System.exit(1); + throw new SOPGPException.MissingInput("File " + cert.getAbsolutePath() + " does not exist.", e); } catch (SOPGPException.BadData badData) { - Print.errln("File " + cert.getAbsolutePath() + " does not contain a valid certificate."); - Print.trace(badData); - System.exit(badData.getExitCode()); - } - } - } - - private void unlinkExistingVerifyOut(File verifyOut) { - if (verifyOut == null) { - return; - } - - if (verifyOut.exists()) { - if (!verifyOut.delete()) { - Print.errln("Cannot delete existing verification file" + verifyOut.getAbsolutePath()); - System.exit(1); + throw new SOPGPException.BadData("File " + cert.getAbsolutePath() + " does not contain a valid certificate.", badData); + } catch (IOException ioException) { + throw new RuntimeException(ioException); } } } @@ -231,9 +201,7 @@ public class DecryptCmd implements Runnable { Pattern sessionKeyPattern = Pattern.compile("^\\d+:[0-9A-F]+$"); for (String sessionKey : withSessionKey) { if (!sessionKeyPattern.matcher(sessionKey).matches()) { - Print.errln("Invalid session key format."); - Print.errln("Session keys are expected in the format 'ALGONUM:HEXKEY'"); - System.exit(1); + throw new IllegalArgumentException("Session keys are expected in the format 'ALGONUM:HEXKEY'."); } String[] split = sessionKey.split(":"); byte algorithm = (byte) Integer.parseInt(split[0]); @@ -242,10 +210,7 @@ public class DecryptCmd implements Runnable { try { decrypt.withSessionKey(new SessionKey(algorithm, key)); } catch (SOPGPException.UnsupportedOption unsupportedOption) { - Print.errln("Unsupported option '--with-session-key'."); - Print.trace(unsupportedOption); - System.exit(unsupportedOption.getExitCode()); - return; + throw new SOPGPException.UnsupportedOption("Unsupported option '--with-session-key'.", unsupportedOption); } } } @@ -254,14 +219,8 @@ public class DecryptCmd implements Runnable { for (String password : withPassword) { try { decrypt.withPassword(password); - } catch (SOPGPException.PasswordNotHumanReadable passwordNotHumanReadable) { - Print.errln("Password not human readable."); - Print.trace(passwordNotHumanReadable); - System.exit(passwordNotHumanReadable.getExitCode()); } catch (SOPGPException.UnsupportedOption unsupportedOption) { - Print.errln("Unsupported option '--with-password'."); - Print.trace(unsupportedOption); - System.exit(unsupportedOption.getExitCode()); + throw new SOPGPException.UnsupportedOption("Unsupported option '--with-password'.", unsupportedOption); } } } @@ -271,9 +230,7 @@ public class DecryptCmd implements Runnable { try { decrypt.verifyNotAfter(notAfterDate); } catch (SOPGPException.UnsupportedOption unsupportedOption) { - Print.errln("Option '--not-after' not supported."); - Print.trace(unsupportedOption); - System.exit(unsupportedOption.getExitCode()); + throw new SOPGPException.UnsupportedOption("Option '--not-after' not supported.", unsupportedOption); } } @@ -282,9 +239,7 @@ public class DecryptCmd implements Runnable { try { decrypt.verifyNotBefore(notBeforeDate); } catch (SOPGPException.UnsupportedOption unsupportedOption) { - Print.errln("Option '--not-before' not supported."); - Print.trace(unsupportedOption); - System.exit(unsupportedOption.getExitCode()); + throw new SOPGPException.UnsupportedOption("Option '--not-before' not supported.", unsupportedOption); } } } diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java index bdc6b1b4..fb18bc21 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java @@ -24,7 +24,6 @@ import java.util.List; import picocli.CommandLine; import sop.Ready; -import sop.cli.picocli.Print; import sop.cli.picocli.SopCLI; import sop.enums.EncryptAs; import sop.exception.SOPGPException; @@ -67,28 +66,19 @@ public class EncryptCmd implements Runnable { try { encrypt.mode(type); } catch (SOPGPException.UnsupportedOption unsupportedOption) { - Print.errln("Unsupported option '--as'."); - Print.trace(unsupportedOption); - System.exit(unsupportedOption.getExitCode()); + throw new SOPGPException.UnsupportedOption("Unsupported option '--as'.", unsupportedOption); } } if (withPassword.isEmpty() && certs.isEmpty()) { - Print.errln("At least one password or cert file required for encryption."); - System.exit(19); + throw new SOPGPException.MissingArg("At least one password or cert file required for encryption."); } for (String password : withPassword) { try { encrypt.withPassword(password); - } catch (SOPGPException.PasswordNotHumanReadable passwordNotHumanReadable) { - Print.errln("Password is not human-readable."); - Print.trace(passwordNotHumanReadable); - System.exit(passwordNotHumanReadable.getExitCode()); } catch (SOPGPException.UnsupportedOption unsupportedOption) { - Print.errln("Unsupported option '--with-password'."); - Print.trace(unsupportedOption); - System.exit(unsupportedOption.getExitCode()); + throw new SOPGPException.UnsupportedOption("Unsupported option '--with-password'.", unsupportedOption); } } @@ -96,29 +86,17 @@ public class EncryptCmd implements Runnable { try (FileInputStream keyIn = new FileInputStream(keyFile)) { encrypt.signWith(keyIn); } catch (FileNotFoundException e) { - Print.errln("Key file " + keyFile.getAbsolutePath() + " not found."); - Print.trace(e); - System.exit(1); + throw new SOPGPException.MissingInput("Key file " + keyFile.getAbsolutePath() + " not found.", e); } catch (IOException e) { - Print.errln("IO Error."); - Print.trace(e); - System.exit(1); + throw new RuntimeException(e); } catch (SOPGPException.KeyIsProtected keyIsProtected) { - Print.errln("Key from " + keyFile.getAbsolutePath() + " is password protected."); - Print.trace(keyIsProtected); - System.exit(1); + throw new SOPGPException.KeyIsProtected("Key from " + keyFile.getAbsolutePath() + " is password protected.", keyIsProtected); } catch (SOPGPException.UnsupportedAsymmetricAlgo unsupportedAsymmetricAlgo) { - Print.errln("Key from " + keyFile.getAbsolutePath() + " has unsupported asymmetric algorithm."); - Print.trace(unsupportedAsymmetricAlgo); - System.exit(unsupportedAsymmetricAlgo.getExitCode()); + throw new SOPGPException.UnsupportedAsymmetricAlgo("Key from " + keyFile.getAbsolutePath() + " has unsupported asymmetric algorithm.", unsupportedAsymmetricAlgo); } catch (SOPGPException.CertCannotSign certCannotSign) { - Print.errln("Key from " + keyFile.getAbsolutePath() + " cannot sign."); - Print.trace(certCannotSign); - System.exit(1); + throw new RuntimeException("Key from " + keyFile.getAbsolutePath() + " cannot sign.", certCannotSign); } catch (SOPGPException.BadData badData) { - Print.errln("Key file " + keyFile.getAbsolutePath() + " does not contain a valid OpenPGP private key."); - Print.trace(badData); - System.exit(badData.getExitCode()); + throw new SOPGPException.BadData("Key file " + keyFile.getAbsolutePath() + " does not contain a valid OpenPGP private key.", badData); } } @@ -126,25 +104,15 @@ public class EncryptCmd implements Runnable { try (FileInputStream certIn = new FileInputStream(certFile)) { encrypt.withCert(certIn); } catch (FileNotFoundException e) { - Print.errln("Certificate file " + certFile.getAbsolutePath() + " not found."); - Print.trace(e); - System.exit(1); + throw new SOPGPException.MissingInput("Certificate file " + certFile.getAbsolutePath() + " not found.", e); } catch (IOException e) { - Print.errln("IO Error."); - Print.trace(e); - System.exit(1); + throw new RuntimeException(e); } catch (SOPGPException.UnsupportedAsymmetricAlgo unsupportedAsymmetricAlgo) { - Print.errln("Certificate from " + certFile.getAbsolutePath() + " has unsupported asymmetric algorithm."); - Print.trace(unsupportedAsymmetricAlgo); - System.exit(unsupportedAsymmetricAlgo.getExitCode()); + throw new SOPGPException.UnsupportedAsymmetricAlgo("Certificate from " + certFile.getAbsolutePath() + " has unsupported asymmetric algorithm.", unsupportedAsymmetricAlgo); } catch (SOPGPException.CertCannotEncrypt certCannotEncrypt) { - Print.errln("Certificate from " + certFile.getAbsolutePath() + " is not capable of encryption."); - Print.trace(certCannotEncrypt); - System.exit(certCannotEncrypt.getExitCode()); + throw new SOPGPException.CertCannotEncrypt("Certificate from " + certFile.getAbsolutePath() + " is not capable of encryption.", certCannotEncrypt); } catch (SOPGPException.BadData badData) { - Print.errln("Certificate file " + certFile.getAbsolutePath() + " does not contain a valid OpenPGP certificate."); - Print.trace(badData); - System.exit(badData.getExitCode()); + throw new SOPGPException.BadData("Certificate file " + certFile.getAbsolutePath() + " does not contain a valid OpenPGP certificate.", badData); } } @@ -156,9 +124,7 @@ public class EncryptCmd implements Runnable { Ready ready = encrypt.plaintext(System.in); ready.writeTo(System.out); } catch (IOException e) { - Print.errln("IO Error."); - Print.trace(e); - System.exit(1); + throw new RuntimeException(e); } } } diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ExtractCertCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ExtractCertCmd.java index 16d9ac24..c8fbe1fc 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ExtractCertCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ExtractCertCmd.java @@ -19,7 +19,6 @@ import java.io.IOException; import picocli.CommandLine; import sop.Ready; -import sop.cli.picocli.Print; import sop.cli.picocli.SopCLI; import sop.exception.SOPGPException; import sop.operation.ExtractCert; @@ -45,13 +44,9 @@ public class ExtractCertCmd implements Runnable { Ready ready = extractCert.key(System.in); ready.writeTo(System.out); } catch (IOException e) { - Print.errln("IO Error."); - Print.trace(e); - System.exit(1); + throw new RuntimeException(e); } catch (SOPGPException.BadData badData) { - Print.errln("Standard Input does not contain valid OpenPGP private key material."); - Print.trace(badData); - System.exit(badData.getExitCode()); + throw new SOPGPException.BadData("Standard Input does not contain valid OpenPGP private key material.", badData); } } } diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ArmorCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ArmorCmdTest.java index e4a5d84a..0f7b232b 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ArmorCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ArmorCmdTest.java @@ -92,7 +92,7 @@ public class ArmorCmdTest { @Test @ExpectSystemExitWithStatus(37) public void ifLabelsUnsupportedExit37() throws SOPGPException.UnsupportedOption { - when(armor.label(any())).thenThrow(new SOPGPException.UnsupportedOption()); + when(armor.label(any())).thenThrow(new SOPGPException.UnsupportedOption("Custom Armor labels are not supported.")); SopCLI.main(new String[] {"armor", "--label", "Sig"}); } @@ -100,7 +100,7 @@ public class ArmorCmdTest { @Test @ExpectSystemExitWithStatus(37) public void ifAllowNestedUnsupportedExit37() throws SOPGPException.UnsupportedOption { - when(armor.allowNested()).thenThrow(new SOPGPException.UnsupportedOption()); + when(armor.allowNested()).thenThrow(new SOPGPException.UnsupportedOption("Allowing nested Armor not supported.")); SopCLI.main(new String[] {"armor", "--allow-nested"}); } diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DecryptCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DecryptCmdTest.java index 4e0b2497..2093ff59 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DecryptCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DecryptCmdTest.java @@ -27,7 +27,6 @@ import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; import java.io.OutputStream; @@ -115,7 +114,7 @@ public class DecryptCmdTest { @Test @ExpectSystemExitWithStatus(37) public void assertUnsupportedWithPasswordCausesExit37() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption { - when(decrypt.withPassword(any())).thenThrow(new SOPGPException.UnsupportedOption()); + when(decrypt.withPassword(any())).thenThrow(new SOPGPException.UnsupportedOption("Decrypting with password not supported.")); SopCLI.main(new String[] {"decrypt", "--with-password", "swordfish"}); } @@ -168,20 +167,20 @@ public class DecryptCmdTest { @Test @ExpectSystemExitWithStatus(37) public void assertUnsupportedNotAfterCausesExit37() throws SOPGPException.UnsupportedOption { - when(decrypt.verifyNotAfter(any())).thenThrow(new SOPGPException.UnsupportedOption()); + when(decrypt.verifyNotAfter(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting upper signature date boundary not supported.")); SopCLI.main(new String[] {"decrypt", "--not-after", "now"}); } @Test @ExpectSystemExitWithStatus(37) public void assertUnsupportedNotBeforeCausesExit37() throws SOPGPException.UnsupportedOption { - when(decrypt.verifyNotBefore(any())).thenThrow(new SOPGPException.UnsupportedOption()); + when(decrypt.verifyNotBefore(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting lower signature date boundary not supported.")); SopCLI.main(new String[] {"decrypt", "--not-before", "now"}); } @Test - @ExpectSystemExitWithStatus(1) - public void assertExistingSessionKeyOutFileCausesExit1() throws IOException { + @ExpectSystemExitWithStatus(59) + public void assertExistingSessionKeyOutFileCausesExit59() throws IOException { File tempFile = File.createTempFile("existing-session-key-", ".tmp"); tempFile.deleteOnExit(); SopCLI.main(new String[] {"decrypt", "--session-key-out", tempFile.getAbsolutePath()}); @@ -257,19 +256,28 @@ public class DecryptCmdTest { } @Test - @ExpectSystemExitWithStatus(1) - public void unexistentCertFileCausesExit1() { + @ExpectSystemExitWithStatus(61) + public void unexistentCertFileCausesExit61() { SopCLI.main(new String[] {"decrypt", "--verify-with", "invalid"}); } @Test - public void existingVerifyOutFileIsUnlinkedBeforeVerification() throws IOException, SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData { + @ExpectSystemExitWithStatus(59) + public void existingVerifyOutCausesExit59() throws IOException { File certFile = File.createTempFile("existing-verify-out-cert", ".asc"); File existingVerifyOut = File.createTempFile("existing-verify-out", ".tmp"); - byte[] data = "some data".getBytes(StandardCharsets.UTF_8); - try (FileOutputStream out = new FileOutputStream(existingVerifyOut)) { - out.write(data); + + SopCLI.main(new String[] {"decrypt", "--verify-out", existingVerifyOut.getAbsolutePath(), "--verify-with", certFile.getAbsolutePath()}); + } + + @Test + public void verifyOutIsProperlyWritten() throws IOException, SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData { + File certFile = File.createTempFile("verify-out-cert", ".asc"); + File verifyOut = new File(certFile.getParent(), "verify-out.txt"); + if (verifyOut.exists()) { + verifyOut.delete(); } + verifyOut.deleteOnExit(); Date date = UTCUtil.parseUTCDate("2021-07-11T20:58:23Z"); when(decrypt.ciphertext(any())).thenReturn(new ReadyWithResult() { @Override @@ -283,8 +291,8 @@ public class DecryptCmdTest { } }); - SopCLI.main(new String[] {"decrypt", "--verify-out", existingVerifyOut.getAbsolutePath(), "--verify-with", certFile.getAbsolutePath()}); - try (BufferedReader reader = new BufferedReader(new FileReader(existingVerifyOut))) { + SopCLI.main(new String[] {"decrypt", "--verify-out", verifyOut.getAbsolutePath(), "--verify-with", certFile.getAbsolutePath()}); + try (BufferedReader reader = new BufferedReader(new FileReader(verifyOut))) { String line = reader.readLine(); assertEquals("2021-07-11T20:58:23Z 1B66A707819A920925BC6777C3E0AFC0B2DFF862 C8CD564EBF8D7BBA90611D8D071773658BF6BF86", line); } @@ -317,14 +325,14 @@ public class DecryptCmdTest { } @Test - @ExpectSystemExitWithStatus(1) - public void assertKeyFileNotFoundCausesExit1() { + @ExpectSystemExitWithStatus(61) + public void assertKeyFileNotFoundCausesExit61() { SopCLI.main(new String[] {"decrypt", "nonexistent-key"}); } @Test - @ExpectSystemExitWithStatus(1) - public void assertProtectedKeyCausesExit1() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData { + @ExpectSystemExitWithStatus(67) + public void assertProtectedKeyCausesExit67() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData { when(decrypt.withKey(any())).thenThrow(new SOPGPException.KeyIsProtected()); File tempKeyFile = File.createTempFile("key-", ".tmp"); SopCLI.main(new String[] {"decrypt", tempKeyFile.getAbsolutePath()}); @@ -333,7 +341,7 @@ public class DecryptCmdTest { @Test @ExpectSystemExitWithStatus(13) public void assertUnsupportedAlgorithmExceptionCausesExit13() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData, IOException { - when(decrypt.withKey(any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo(new IOException())); + when(decrypt.withKey(any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new IOException())); File tempKeyFile = File.createTempFile("key-", ".tmp"); SopCLI.main(new String[] {"decrypt", tempKeyFile.getAbsolutePath()}); } diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/EncryptCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/EncryptCmdTest.java index ce56bbcd..1d61e2ea 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/EncryptCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/EncryptCmdTest.java @@ -65,7 +65,7 @@ public class EncryptCmdTest { @Test @ExpectSystemExitWithStatus(37) public void as_unsupportedEncryptAsCausesExit37() throws SOPGPException.UnsupportedOption { - when(encrypt.mode(any())).thenThrow(new SOPGPException.UnsupportedOption()); + when(encrypt.mode(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting encryption mode not supported.")); SopCLI.main(new String[] {"encrypt", "--as", "Binary"}); } @@ -95,7 +95,7 @@ public class EncryptCmdTest { @Test @ExpectSystemExitWithStatus(37) public void withPassword_unsupportedWithPasswordCausesExit37() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption { - when(encrypt.withPassword(any())).thenThrow(new SOPGPException.UnsupportedOption()); + when(encrypt.withPassword(any())).thenThrow(new SOPGPException.UnsupportedOption("Encrypting with password not supported.")); SopCLI.main(new String[] {"encrypt", "--with-password", "orange"}); } @@ -110,14 +110,14 @@ public class EncryptCmdTest { } @Test - @ExpectSystemExitWithStatus(1) - public void signWith_nonExistentKeyFileCausesExit1() { + @ExpectSystemExitWithStatus(61) + public void signWith_nonExistentKeyFileCausesExit61() { SopCLI.main(new String[] {"encrypt", "--with-password", "admin", "--sign-with", "nonExistent.asc"}); } @Test - @ExpectSystemExitWithStatus(1) - public void signWith_keyIsProtectedCausesExit1() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotSign, SOPGPException.BadData, IOException { + @ExpectSystemExitWithStatus(67) + public void signWith_keyIsProtectedCausesExit67() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotSign, SOPGPException.BadData, IOException { when(encrypt.signWith(any())).thenThrow(new SOPGPException.KeyIsProtected()); File keyFile = File.createTempFile("sign-with", ".asc"); SopCLI.main(new String[] {"encrypt", "--sign-with", keyFile.getAbsolutePath(), "--with-password", "starship"}); @@ -126,7 +126,7 @@ public class EncryptCmdTest { @Test @ExpectSystemExitWithStatus(13) public void signWith_unsupportedAsymmetricAlgoCausesExit13() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotSign, SOPGPException.BadData, IOException { - when(encrypt.signWith(any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo(new Exception())); + when(encrypt.signWith(any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new Exception())); File keyFile = File.createTempFile("sign-with", ".asc"); SopCLI.main(new String[] {"encrypt", "--with-password", "123456", "--sign-with", keyFile.getAbsolutePath()}); } @@ -148,15 +148,15 @@ public class EncryptCmdTest { } @Test - @ExpectSystemExitWithStatus(1) - public void cert_nonExistentCertFileCausesExit1() { + @ExpectSystemExitWithStatus(61) + public void cert_nonExistentCertFileCausesExit61() { SopCLI.main(new String[] {"encrypt", "invalid.asc"}); } @Test @ExpectSystemExitWithStatus(13) public void cert_unsupportedAsymmetricAlgorithmCausesExit13() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData { - when(encrypt.withCert(any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo(new Exception())); + when(encrypt.withCert(any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new Exception())); File certFile = File.createTempFile("cert", ".asc"); SopCLI.main(new String[] {"encrypt", certFile.getAbsolutePath()}); } @@ -164,7 +164,7 @@ public class EncryptCmdTest { @Test @ExpectSystemExitWithStatus(17) public void cert_certCannotEncryptCausesExit17() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData { - when(encrypt.withCert(any())).thenThrow(new SOPGPException.CertCannotEncrypt()); + when(encrypt.withCert(any())).thenThrow(new SOPGPException.CertCannotEncrypt("Certificate cannot encrypt.", new Exception())); File certFile = File.createTempFile("cert", ".asc"); SopCLI.main(new String[] {"encrypt", certFile.getAbsolutePath()}); } diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/GenerateKeyCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/GenerateKeyCmdTest.java index 4c061d43..a99bbab4 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/GenerateKeyCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/GenerateKeyCmdTest.java @@ -91,7 +91,7 @@ public class GenerateKeyCmdTest { @Test @ExpectSystemExitWithStatus(13) public void unsupportedAsymmetricAlgorithmCausesExit13() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException { - when(generateKey.generate()).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo(new Exception())); + when(generateKey.generate()).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new Exception())); SopCLI.main(new String[] {"generate-key", "Alice"}); } diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/SignCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/SignCmdTest.java index 98220c79..420af2e8 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/SignCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/SignCmdTest.java @@ -74,7 +74,7 @@ public class SignCmdTest { @Test @ExpectSystemExitWithStatus(37) public void as_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption { - when(sign.mode(any())).thenThrow(new SOPGPException.UnsupportedOption()); + when(sign.mode(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting signing mode not supported.")); SopCLI.main(new String[] {"sign", "--as", "binary", keyFile.getAbsolutePath()}); } diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VerifyCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VerifyCmdTest.java index 98d742e2..29b4388e 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VerifyCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VerifyCmdTest.java @@ -106,7 +106,7 @@ public class VerifyCmdTest { @Test @ExpectSystemExitWithStatus(37) public void notAfter_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption { - when(verify.notAfter(any())).thenThrow(new SOPGPException.UnsupportedOption()); + when(verify.notAfter(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting upper signature date boundary not supported.")); SopCLI.main(new String[] {"verify", "--not-after", "2019-10-29T18:36:45Z", signature.getAbsolutePath(), cert.getAbsolutePath()}); } @@ -133,7 +133,7 @@ public class VerifyCmdTest { @Test @ExpectSystemExitWithStatus(37) public void notBefore_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption { - when(verify.notBefore(any())).thenThrow(new SOPGPException.UnsupportedOption()); + when(verify.notBefore(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting lower signature date boundary not supported.")); SopCLI.main(new String[] {"verify", "--not-before", "2019-10-29T18:36:45Z", signature.getAbsolutePath(), cert.getAbsolutePath()}); } diff --git a/sop-java/src/main/java/sop/exception/SOPGPException.java b/sop-java/src/main/java/sop/exception/SOPGPException.java index c86d8d39..b1df995e 100644 --- a/sop-java/src/main/java/sop/exception/SOPGPException.java +++ b/sop-java/src/main/java/sop/exception/SOPGPException.java @@ -15,7 +15,7 @@ */ package sop.exception; -public class SOPGPException extends Exception { +public abstract class SOPGPException extends RuntimeException { public SOPGPException() { super(); @@ -29,10 +29,21 @@ public class SOPGPException extends Exception { super(e); } + public SOPGPException(String message, Throwable cause) { + super(message, cause); + } + + public abstract int getExitCode(); + public static class NoSignature extends SOPGPException { public static final int EXIT_CODE = 3; + public NoSignature() { + super("No verifiable signature found."); + } + + @Override public int getExitCode() { return EXIT_CODE; } @@ -42,10 +53,15 @@ public class SOPGPException extends Exception { public static final int EXIT_CODE = 13; + public UnsupportedAsymmetricAlgo(String message, Throwable e) { + super(message, e); + } + public UnsupportedAsymmetricAlgo(Throwable e) { super(e); } + @Override public int getExitCode() { return EXIT_CODE; } @@ -54,12 +70,17 @@ public class SOPGPException extends Exception { public static class CertCannotEncrypt extends SOPGPException { public static final int EXIT_CODE = 17; + public CertCannotEncrypt(String message, Throwable cause) { + super(message, cause); + } + + @Override public int getExitCode() { return EXIT_CODE; } } - public static class CertCannotSign extends SOPGPException { + public static class CertCannotSign extends Exception { } @@ -71,6 +92,7 @@ public class SOPGPException extends Exception { super(s); } + @Override public int getExitCode() { return EXIT_CODE; } @@ -80,6 +102,11 @@ public class SOPGPException extends Exception { public static final int EXIT_CODE = 23; + public IncompleteVerification(String message) { + super(message); + } + + @Override public int getExitCode() { return EXIT_CODE; } @@ -89,6 +116,7 @@ public class SOPGPException extends Exception { public static final int EXIT_CODE = 29; + @Override public int getExitCode() { return EXIT_CODE; } @@ -98,6 +126,7 @@ public class SOPGPException extends Exception { public static final int EXIT_CODE = 31; + @Override public int getExitCode() { return EXIT_CODE; } @@ -107,6 +136,15 @@ public class SOPGPException extends Exception { public static final int EXIT_CODE = 37; + public UnsupportedOption(String message) { + super(message); + } + + public UnsupportedOption(String message, Throwable cause) { + super(message, cause); + } + + @Override public int getExitCode() { return EXIT_CODE; } @@ -120,6 +158,11 @@ public class SOPGPException extends Exception { super(e); } + public BadData(String message, BadData badData) { + super(message, badData); + } + + @Override public int getExitCode() { return EXIT_CODE; } @@ -129,6 +172,7 @@ public class SOPGPException extends Exception { public static final int EXIT_CODE = 53; + @Override public int getExitCode() { return EXIT_CODE; } @@ -136,20 +180,80 @@ public class SOPGPException extends Exception { public static class OutputExists extends SOPGPException { + public static final int EXIT_CODE = 59; + + public OutputExists(String message) { + super(message); + } + + @Override + public int getExitCode() { + return EXIT_CODE; + } + } + + public static class MissingInput extends SOPGPException { + + public static final int EXIT_CODE = 61; + + public MissingInput(String message, Throwable cause) { + super(message, cause); + } + + @Override + public int getExitCode() { + return EXIT_CODE; + } } public static class KeyIsProtected extends SOPGPException { + public static final int EXIT_CODE = 67; + + public KeyIsProtected() { + super(); + } + + public KeyIsProtected(String message, Throwable cause) { + super(message, cause); + } + + @Override + public int getExitCode() { + return EXIT_CODE; + } } - public static class AmbiguousInput extends SOPGPException { - - } - - public static class NotImplemented extends SOPGPException { + public static class UnsupportedSubcommand extends SOPGPException { public static final int EXIT_CODE = 69; + public UnsupportedSubcommand(String message) { + super(message); + } + + @Override + public int getExitCode() { + return EXIT_CODE; + } + } + + public static class UnsupportedSpecialPrefix extends SOPGPException { + + public static final int EXIT_CODE = 71; + + @Override + public int getExitCode() { + return EXIT_CODE; + } + } + + + public static class AmbiguousInput extends SOPGPException { + + public static final int EXIT_CODE = 73; + + @Override public int getExitCode() { return EXIT_CODE; }