diff --git a/external-sop/src/main/java/sop/external/ExternalSOP.java b/external-sop/src/main/java/sop/external/ExternalSOP.java deleted file mode 100644 index 3819848..0000000 --- a/external-sop/src/main/java/sop/external/ExternalSOP.java +++ /dev/null @@ -1,469 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.external; - -import sop.Ready; -import sop.SOP; -import sop.exception.SOPGPException; -import sop.external.operation.ArmorExternal; -import sop.external.operation.ChangeKeyPasswordExternal; -import sop.external.operation.DearmorExternal; -import sop.external.operation.DecryptExternal; -import sop.external.operation.DetachedSignExternal; -import sop.external.operation.DetachedVerifyExternal; -import sop.external.operation.EncryptExternal; -import sop.external.operation.ExtractCertExternal; -import sop.external.operation.GenerateKeyExternal; -import sop.external.operation.InlineDetachExternal; -import sop.external.operation.InlineSignExternal; -import sop.external.operation.InlineVerifyExternal; -import sop.external.operation.ListProfilesExternal; -import sop.external.operation.RevokeKeyExternal; -import sop.external.operation.VersionExternal; -import sop.operation.Armor; -import sop.operation.ChangeKeyPassword; -import sop.operation.Dearmor; -import sop.operation.Decrypt; -import sop.operation.DetachedSign; -import sop.operation.DetachedVerify; -import sop.operation.Encrypt; -import sop.operation.ExtractCert; -import sop.operation.GenerateKey; -import sop.operation.InlineDetach; -import sop.operation.InlineSign; -import sop.operation.InlineVerify; -import sop.operation.ListProfiles; -import sop.operation.RevokeKey; -import sop.operation.Version; - -import javax.annotation.Nonnull; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.file.Files; -import java.nio.file.attribute.FileAttribute; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - -/** - * Implementation of the {@link SOP} API using an external SOP binary. - */ -public class ExternalSOP implements SOP { - - private final String binaryName; - private final Properties properties; - private final TempDirProvider tempDirProvider; - - /** - * Instantiate an {@link ExternalSOP} object for the given binary and pass it empty environment variables, - * as well as a default {@link TempDirProvider}. - * - * @param binaryName name / path of the SOP binary - */ - public ExternalSOP(@Nonnull String binaryName) { - this(binaryName, new Properties()); - } - - /** - * Instantiate an {@link ExternalSOP} object for the given binary, and pass it the given properties as - * environment variables, as well as a default {@link TempDirProvider}. - * - * @param binaryName name / path of the SOP binary - * @param properties environment variables - */ - public ExternalSOP(@Nonnull String binaryName, @Nonnull Properties properties) { - this(binaryName, properties, defaultTempDirProvider()); - } - - /** - * Instantiate an {@link ExternalSOP} object for the given binary and the given {@link TempDirProvider} - * using empty environment variables. - * - * @param binaryName name / path of the SOP binary - * @param tempDirProvider custom tempDirProvider - */ - public ExternalSOP(@Nonnull String binaryName, @Nonnull TempDirProvider tempDirProvider) { - this(binaryName, new Properties(), tempDirProvider); - } - - /** - * Instantiate an {@link ExternalSOP} object for the given binary using the given properties and - * custom {@link TempDirProvider}. - * - * @param binaryName name / path of the SOP binary - * @param properties environment variables - * @param tempDirProvider tempDirProvider - */ - public ExternalSOP(@Nonnull String binaryName, @Nonnull Properties properties, @Nonnull TempDirProvider tempDirProvider) { - this.binaryName = binaryName; - this.properties = properties; - this.tempDirProvider = tempDirProvider; - } - - @Override - @Nonnull - public Version version() { - return new VersionExternal(binaryName, properties); - } - - @Override - @Nonnull - public GenerateKey generateKey() { - return new GenerateKeyExternal(binaryName, properties); - } - - @Override - @Nonnull - public ExtractCert extractCert() { - return new ExtractCertExternal(binaryName, properties); - } - - @Override - @Nonnull - public DetachedSign detachedSign() { - return new DetachedSignExternal(binaryName, properties, tempDirProvider); - } - - @Override - @Nonnull - public InlineSign inlineSign() { - return new InlineSignExternal(binaryName, properties); - } - - @Override - @Nonnull - public DetachedVerify detachedVerify() { - return new DetachedVerifyExternal(binaryName, properties); - } - - @Override - @Nonnull - public InlineVerify inlineVerify() { - return new InlineVerifyExternal(binaryName, properties, tempDirProvider); - } - - @Override - @Nonnull - public InlineDetach inlineDetach() { - return new InlineDetachExternal(binaryName, properties, tempDirProvider); - } - - @Override - @Nonnull - public Encrypt encrypt() { - return new EncryptExternal(binaryName, properties, tempDirProvider); - } - - @Override - @Nonnull - public Decrypt decrypt() { - return new DecryptExternal(binaryName, properties, tempDirProvider); - } - - @Override - @Nonnull - public Armor armor() { - return new ArmorExternal(binaryName, properties); - } - - @Override - @Nonnull - public ListProfiles listProfiles() { - return new ListProfilesExternal(binaryName, properties); - } - - @Override - @Nonnull - public RevokeKey revokeKey() { - return new RevokeKeyExternal(binaryName, properties); - } - - @Override - @Nonnull - public ChangeKeyPassword changeKeyPassword() { - return new ChangeKeyPasswordExternal(binaryName, properties); - } - - @Override - @Nonnull - public Dearmor dearmor() { - return new DearmorExternal(binaryName, properties); - } - - public static void finish(@Nonnull Process process) throws IOException { - try { - mapExitCodeOrException(process); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } - - /** - * Wait for the {@link Process} to finish and read out its exit code. - * If the exit code is {@value "0"}, this method just returns. - * Otherwise, the exit code gets mapped to a {@link SOPGPException} which then gets thrown. - * If the exit code does not match any of the known exit codes defined in the SOP specification, - * this method throws a {@link RuntimeException} instead. - * - * @param process process - * @throws InterruptedException if the thread is interrupted before the process could exit - * @throws IOException in case of an IO error - */ - private static void mapExitCodeOrException(@Nonnull Process process) throws InterruptedException, IOException { - // wait for process termination - int exitCode = process.waitFor(); - - if (exitCode == 0) { - // we're good, bye - return; - } - - // Read error message - InputStream errIn = process.getErrorStream(); - String errorMessage = readString(errIn); - - switch (exitCode) { - case SOPGPException.NoSignature.EXIT_CODE: - throw new SOPGPException.NoSignature("External SOP backend reported error NoSignature (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.UnsupportedAsymmetricAlgo.EXIT_CODE: - throw new UnsupportedOperationException("External SOP backend reported error UnsupportedAsymmetricAlgo (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.CertCannotEncrypt.EXIT_CODE: - throw new SOPGPException.CertCannotEncrypt("External SOP backend reported error CertCannotEncrypt (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.MissingArg.EXIT_CODE: - throw new SOPGPException.MissingArg("External SOP backend reported error MissingArg (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.IncompleteVerification.EXIT_CODE: - throw new SOPGPException.IncompleteVerification("External SOP backend reported error IncompleteVerification (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.CannotDecrypt.EXIT_CODE: - throw new SOPGPException.CannotDecrypt("External SOP backend reported error CannotDecrypt (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.PasswordNotHumanReadable.EXIT_CODE: - throw new SOPGPException.PasswordNotHumanReadable("External SOP backend reported error PasswordNotHumanReadable (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.UnsupportedOption.EXIT_CODE: - throw new SOPGPException.UnsupportedOption("External SOP backend reported error UnsupportedOption (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.BadData.EXIT_CODE: - throw new SOPGPException.BadData("External SOP backend reported error BadData (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.ExpectedText.EXIT_CODE: - throw new SOPGPException.ExpectedText("External SOP backend reported error ExpectedText (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.OutputExists.EXIT_CODE: - throw new SOPGPException.OutputExists("External SOP backend reported error OutputExists (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.MissingInput.EXIT_CODE: - throw new SOPGPException.MissingInput("External SOP backend reported error MissingInput (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.KeyIsProtected.EXIT_CODE: - throw new SOPGPException.KeyIsProtected("External SOP backend reported error KeyIsProtected (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.UnsupportedSubcommand.EXIT_CODE: - throw new SOPGPException.UnsupportedSubcommand("External SOP backend reported error UnsupportedSubcommand (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.UnsupportedSpecialPrefix.EXIT_CODE: - throw new SOPGPException.UnsupportedSpecialPrefix("External SOP backend reported error UnsupportedSpecialPrefix (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.AmbiguousInput.EXIT_CODE: - throw new SOPGPException.AmbiguousInput("External SOP backend reported error AmbiguousInput (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.KeyCannotSign.EXIT_CODE: - throw new SOPGPException.KeyCannotSign("External SOP backend reported error KeyCannotSign (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.IncompatibleOptions.EXIT_CODE: - throw new SOPGPException.IncompatibleOptions("External SOP backend reported error IncompatibleOptions (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.UnsupportedProfile.EXIT_CODE: - throw new SOPGPException.UnsupportedProfile("External SOP backend reported error UnsupportedProfile (" + - exitCode + "):\n" + errorMessage); - - default: - // Did you forget to add a case for a new exception type? - throw new RuntimeException("External SOP backend reported unknown exit code (" + - exitCode + "):\n" + errorMessage); - } - } - - /** - * Return all key-value pairs from the given {@link Properties} object as a list with items of the form - *
key=value
. - * - * @param properties properties - * @return list of key=value strings - */ - public static List propertiesToEnv(@Nonnull Properties properties) { - List env = new ArrayList<>(); - for (Object key : properties.keySet()) { - env.add(key + "=" + properties.get(key)); - } - return env; - } - - /** - * Read the contents of the {@link InputStream} and return them as a {@link String}. - * - * @param inputStream input stream - * @return string - * @throws IOException in case of an IO error - */ - public static String readString(@Nonnull InputStream inputStream) throws IOException { - ByteArrayOutputStream bOut = new ByteArrayOutputStream(); - byte[] buf = new byte[4096]; - int r; - while ((r = inputStream.read(buf)) > 0) { - bOut.write(buf, 0, r); - } - return bOut.toString(); - } - - /** - * Execute the given command on the given {@link Runtime} with the given list of environment variables. - * This command does not transform any input data, and instead is purely a producer. - * - * @param runtime runtime - * @param commandList command - * @param envList environment variables - * @return ready to read the result from - */ - public static Ready executeProducingOperation(@Nonnull Runtime runtime, - @Nonnull List commandList, - @Nonnull List envList) { - String[] command = commandList.toArray(new String[0]); - String[] env = envList.toArray(new String[0]); - - try { - Process process = runtime.exec(command, env); - InputStream stdIn = process.getInputStream(); - - return new Ready() { - @Override - public void writeTo(@Nonnull OutputStream outputStream) throws IOException { - byte[] buf = new byte[4096]; - int r; - while ((r = stdIn.read(buf)) >= 0) { - outputStream.write(buf, 0, r); - } - - outputStream.flush(); - outputStream.close(); - - ExternalSOP.finish(process); - } - }; - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - /** - * Execute the given command on the given runtime using the given environment variables. - * The given input stream provides input for the process. - * This command is a transformation, meaning it is given input data and transforms it into output data. - * - * @param runtime runtime - * @param commandList command - * @param envList environment variables - * @param standardIn stream of input data for the process - * @return ready to read the result from - */ - public static Ready executeTransformingOperation(@Nonnull Runtime runtime, @Nonnull List commandList, @Nonnull List envList, @Nonnull InputStream standardIn) { - String[] command = commandList.toArray(new String[0]); - String[] env = envList.toArray(new String[0]); - try { - Process process = runtime.exec(command, env); - OutputStream processOut = process.getOutputStream(); - InputStream processIn = process.getInputStream(); - - return new Ready() { - @Override - public void writeTo(@Nonnull OutputStream outputStream) throws IOException { - byte[] buf = new byte[4096]; - int r; - while ((r = standardIn.read(buf)) > 0) { - processOut.write(buf, 0, r); - } - standardIn.close(); - - try { - processOut.flush(); - processOut.close(); - } catch (IOException e) { - // Perhaps the stream is already closed, in which case we ignore the exception. - if (!"Stream closed".equals(e.getMessage())) { - throw e; - } - } - - while ((r = processIn.read(buf)) > 0) { - outputStream.write(buf, 0 , r); - } - processIn.close(); - - outputStream.flush(); - outputStream.close(); - - finish(process); - } - }; - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - /** - * This interface can be used to provide a directory in which external SOP binaries can temporarily store - * additional results of OpenPGP operations such that the binding classes can parse them out from there. - * Unfortunately, on Java you cannot open {@link java.io.FileDescriptor FileDescriptors} arbitrarily, so we - * have to rely on temporary files to pass results. - * An example: - *
sop decrypt
can emit signature verifications via
--verify-out=/path/to/tempfile
. - * {@link DecryptExternal} will then parse the temp file to make the result available to consumers. - * Temporary files are deleted after being read, yet creating temp files for sensitive information on disk - * might pose a security risk. Use with care! - */ - public interface TempDirProvider { - File provideTempDirectory() throws IOException; - } - - /** - * Default implementation of the {@link TempDirProvider} which stores temporary files in the systems temp dir - * ({@link Files#createTempDirectory(String, FileAttribute[])}). - * - * @return default implementation - */ - public static TempDirProvider defaultTempDirProvider() { - return new TempDirProvider() { - @Override - public File provideTempDirectory() throws IOException { - return Files.createTempDirectory("ext-sop").toFile(); - } - }; - } -} diff --git a/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt b/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt new file mode 100644 index 0000000..3a0ef52 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt @@ -0,0 +1,318 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external + +import java.io.* +import java.nio.file.Files +import java.util.* +import javax.annotation.Nonnull +import sop.Ready +import sop.SOP +import sop.exception.SOPGPException.* +import sop.external.ExternalSOP.TempDirProvider +import sop.external.operation.* +import sop.operation.* + +/** + * Implementation of the {@link SOP} API using an external SOP binary. + * + * Instantiate an [ExternalSOP] object for the given binary and the given [TempDirProvider] using + * empty environment variables. + * + * @param binaryName name / path of the SOP binary + * @param tempDirProvider custom tempDirProvider + */ +class ExternalSOP( + private val binaryName: String, + private val properties: Properties = Properties(), + private val tempDirProvider: TempDirProvider = defaultTempDirProvider() +) : SOP { + + constructor( + binaryName: String, + properties: Properties + ) : this(binaryName, properties, defaultTempDirProvider()) + + override fun version(): Version = VersionExternal(binaryName, properties) + + override fun generateKey(): GenerateKey = GenerateKeyExternal(binaryName, properties) + + override fun extractCert(): ExtractCert = ExtractCertExternal(binaryName, properties) + + override fun detachedSign(): DetachedSign = + DetachedSignExternal(binaryName, properties, tempDirProvider) + + override fun inlineSign(): InlineSign = InlineSignExternal(binaryName, properties) + + override fun detachedVerify(): DetachedVerify = DetachedVerifyExternal(binaryName, properties) + + override fun inlineVerify(): InlineVerify = + InlineVerifyExternal(binaryName, properties, tempDirProvider) + + override fun inlineDetach(): InlineDetach = + InlineDetachExternal(binaryName, properties, tempDirProvider) + + override fun encrypt(): Encrypt = EncryptExternal(binaryName, properties, tempDirProvider) + + override fun decrypt(): Decrypt = DecryptExternal(binaryName, properties, tempDirProvider) + + override fun armor(): Armor = ArmorExternal(binaryName, properties) + + override fun dearmor(): Dearmor = DearmorExternal(binaryName, properties) + + override fun listProfiles(): ListProfiles = ListProfilesExternal(binaryName, properties) + + override fun revokeKey(): RevokeKey = RevokeKeyExternal(binaryName, properties) + + override fun changeKeyPassword(): ChangeKeyPassword = + ChangeKeyPasswordExternal(binaryName, properties) + + /** + * This interface can be used to provide a directory in which external SOP binaries can + * temporarily store additional results of OpenPGP operations such that the binding classes can + * parse them out from there. Unfortunately, on Java you cannot open + * [FileDescriptors][java.io.FileDescriptor] arbitrarily, so we have to rely on temporary files + * to pass results. An example: `sop decrypt` can emit signature verifications via + * `--verify-out=/path/to/tempfile`. [DecryptExternal] will then parse the temp file to make the + * result available to consumers. Temporary files are deleted after being read, yet creating + * temp files for sensitive information on disk might pose a security risk. Use with care! + */ + fun interface TempDirProvider { + + @Throws(IOException::class) fun provideTempDirectory(): File + } + + companion object { + + @JvmStatic + @Throws(IOException::class) + fun finish(process: Process) { + try { + mapExitCodeOrException(process) + } catch (e: InterruptedException) { + throw RuntimeException(e) + } + } + + @JvmStatic + @Throws(InterruptedException::class, IOException::class) + private fun mapExitCodeOrException(process: Process) { + // wait for process termination + val exitCode = process.waitFor() + + if (exitCode == 0) { + // we're good, bye + return + } + + // Read error message + val errIn = process.errorStream + val errorMessage = readString(errIn) + + when (exitCode) { + NoSignature.EXIT_CODE -> + throw NoSignature( + "External SOP backend reported error NoSignature ($exitCode):\n$errorMessage") + UnsupportedAsymmetricAlgo.EXIT_CODE -> + throw UnsupportedOperationException( + "External SOP backend reported error UnsupportedAsymmetricAlgo ($exitCode):\n$errorMessage") + CertCannotEncrypt.EXIT_CODE -> + throw CertCannotEncrypt( + "External SOP backend reported error CertCannotEncrypt ($exitCode):\n$errorMessage") + MissingArg.EXIT_CODE -> + throw MissingArg( + "External SOP backend reported error MissingArg ($exitCode):\n$errorMessage") + IncompleteVerification.EXIT_CODE -> + throw IncompleteVerification( + "External SOP backend reported error IncompleteVerification ($exitCode):\n$errorMessage") + CannotDecrypt.EXIT_CODE -> + throw CannotDecrypt( + "External SOP backend reported error CannotDecrypt ($exitCode):\n$errorMessage") + PasswordNotHumanReadable.EXIT_CODE -> + throw PasswordNotHumanReadable( + "External SOP backend reported error PasswordNotHumanReadable ($exitCode):\n$errorMessage") + UnsupportedOption.EXIT_CODE -> + throw UnsupportedOption( + "External SOP backend reported error UnsupportedOption ($exitCode):\n$errorMessage") + BadData.EXIT_CODE -> + throw BadData( + "External SOP backend reported error BadData ($exitCode):\n$errorMessage") + ExpectedText.EXIT_CODE -> + throw ExpectedText( + "External SOP backend reported error ExpectedText ($exitCode):\n$errorMessage") + OutputExists.EXIT_CODE -> + throw OutputExists( + "External SOP backend reported error OutputExists ($exitCode):\n$errorMessage") + MissingInput.EXIT_CODE -> + throw MissingInput( + "External SOP backend reported error MissingInput ($exitCode):\n$errorMessage") + KeyIsProtected.EXIT_CODE -> + throw KeyIsProtected( + "External SOP backend reported error KeyIsProtected ($exitCode):\n$errorMessage") + UnsupportedSubcommand.EXIT_CODE -> + throw UnsupportedSubcommand( + "External SOP backend reported error UnsupportedSubcommand ($exitCode):\n$errorMessage") + UnsupportedSpecialPrefix.EXIT_CODE -> + throw UnsupportedSpecialPrefix( + "External SOP backend reported error UnsupportedSpecialPrefix ($exitCode):\n$errorMessage") + AmbiguousInput.EXIT_CODE -> + throw AmbiguousInput( + "External SOP backend reported error AmbiguousInput ($exitCode):\n$errorMessage") + KeyCannotSign.EXIT_CODE -> + throw KeyCannotSign( + "External SOP backend reported error KeyCannotSign ($exitCode):\n$errorMessage") + IncompatibleOptions.EXIT_CODE -> + throw IncompatibleOptions( + "External SOP backend reported error IncompatibleOptions ($exitCode):\n$errorMessage") + UnsupportedProfile.EXIT_CODE -> + throw UnsupportedProfile( + "External SOP backend reported error UnsupportedProfile ($exitCode):\n$errorMessage") + + // Did you forget to add a case for a new exception type? + else -> + throw RuntimeException( + "External SOP backend reported unknown exit code ($exitCode):\n$errorMessage") + } + } + + /** + * Return all key-value pairs from the given [Properties] object as a list with items of the + * form `key=value`. + * + * @param properties properties + * @return list of key=value strings + */ + @JvmStatic + fun propertiesToEnv(properties: Properties): List = + properties.map { "${it.key}=${it.value}" } + + /** + * Read the contents of the [InputStream] and return them as a [String]. + * + * @param inputStream input stream + * @return string + * @throws IOException in case of an IO error + */ + @JvmStatic + @Throws(IOException::class) + fun readString(inputStream: InputStream): String { + val bOut = ByteArrayOutputStream() + val buf = ByteArray(4096) + var r: Int + while (inputStream.read(buf).also { r = it } > 0) { + bOut.write(buf, 0, r) + } + return bOut.toString() + } + + /** + * Execute the given command on the given [Runtime] with the given list of environment + * variables. This command does not transform any input data, and instead is purely a + * producer. + * + * @param runtime runtime + * @param commandList command + * @param envList environment variables + * @return ready to read the result from + */ + @JvmStatic + fun executeProducingOperation( + runtime: Runtime, + commandList: List, + envList: List + ): Ready { + try { + val process = runtime.exec(commandList.toTypedArray(), envList.toTypedArray()) + val stdIn = process.inputStream + + return object : Ready() { + @Throws(IOException::class) + override fun writeTo(@Nonnull outputStream: OutputStream) { + val buf = ByteArray(4096) + var r: Int + while (stdIn.read(buf).also { r = it } >= 0) { + outputStream.write(buf, 0, r) + } + outputStream.flush() + outputStream.close() + finish(process) + } + } + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + /** + * Execute the given command on the given runtime using the given environment variables. The + * given input stream provides input for the process. This command is a transformation, + * meaning it is given input data and transforms it into output data. + * + * @param runtime runtime + * @param commandList command + * @param envList environment variables + * @param standardIn stream of input data for the process + * @return ready to read the result from + */ + @JvmStatic + fun executeTransformingOperation( + runtime: Runtime, + commandList: List, + envList: List, + standardIn: InputStream + ): Ready { + try { + val process = runtime.exec(commandList.toTypedArray(), envList.toTypedArray()) + val processOut = process.outputStream + val processIn = process.inputStream + + return object : Ready() { + override fun writeTo(outputStream: OutputStream) { + val buf = ByteArray(4096) + var r: Int + while (standardIn.read(buf).also { r = it } > 0) { + processOut.write(buf, 0, r) + } + standardIn.close() + + try { + processOut.flush() + processOut.close() + } catch (e: IOException) { + // Perhaps the stream is already closed, in which case we ignore the + // exception. + if ("Stream closed" != e.message) { + throw e + } + } + + while (processIn.read(buf).also { r = it } > 0) { + outputStream.write(buf, 0, r) + } + processIn.close() + + outputStream.flush() + outputStream.close() + + finish(process) + } + } + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + /** + * Default implementation of the [TempDirProvider] which stores temporary files in the + * systems temp dir ([Files.createTempDirectory]). + * + * @return default implementation + */ + @JvmStatic + fun defaultTempDirProvider(): TempDirProvider { + return TempDirProvider { Files.createTempDirectory("ext-sop").toFile() } + } + } +}