diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopVCLI.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopVCLI.kt index 311a446..fa9683f 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopVCLI.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopVCLI.kt @@ -45,8 +45,7 @@ class SopVCLI { @JvmField var EXECUTABLE_NAME = "sopv" @JvmField - @CommandLine.Option( - names = ["--stacktrace", "--debug"], scope = CommandLine.ScopeType.INHERIT) + @CommandLine.Option(names = ["--stacktrace", "--debug"], scope = CommandLine.ScopeType.INHERIT) var stacktrace = false @JvmStatic diff --git a/sop-java/src/main/kotlin/sop/Verification.kt b/sop-java/src/main/kotlin/sop/Verification.kt index 982e691..a8db800 100644 --- a/sop-java/src/main/kotlin/sop/Verification.kt +++ b/sop-java/src/main/kotlin/sop/Verification.kt @@ -10,15 +10,6 @@ import sop.enums.SignatureMode import sop.util.Optional import sop.util.UTCUtil -/** - * Metadata about a verified signature. - * - * @param creationTime creation time of the signature - * @param signingKeyFingerprint fingerprint of the (sub-)key that issued the signature - * @param signingCertFingerprint fingerprint of the certificate that contains the signing key - * @param signatureMode optional signature mode (text/binary) - * @param jsonOrDescription arbitrary text or JSON data - */ data class Verification( val creationTime: Date, val signingKeyFingerprint: String, @@ -56,28 +47,27 @@ data class Verification( Optional.ofNullable(signatureMode), Optional.of(jsonSerializer.serialize(json))) - @Deprecated("Replaced by jsonOrDescription", replaceWith = ReplaceWith("jsonOrDescription")) + @Deprecated("Replaced by jsonOrDescription", + replaceWith = ReplaceWith("jsonOrDescription") + ) val description = jsonOrDescription - /** This value is `true` if the [Verification] contains extension JSON. */ - val containsJson: Boolean = - jsonOrDescription.get()?.trim()?.let { it.startsWith("{") && it.endsWith("}") } ?: false - /** - * Attempt to parse the [jsonOrDescription] field using the provided [JSONParser] and return the - * result. This method returns `null` if parsing fails. + * Attempt to parse the [jsonOrDescription] field using the provided [JSONParser] and return the result. + * This method returns `null` if parsing fails. * * @param parser [JSONParser] implementation * @return successfully parsed [JSON] POJO or `null`. */ fun getJson(parser: JSONParser): JSON? { - return jsonOrDescription.get()?.let { - try { - parser.parse(it) - } catch (e: ParseException) { - null + return jsonOrDescription.get() + ?.let { + try { + parser.parse(it) + } catch (e: ParseException) { + null + } } - } } override fun toString(): String = @@ -126,37 +116,35 @@ data class Verification( /** * POJO data class representing JSON metadata. * - * @param signers list of supplied CERTS objects that could have issued the signature, - * identified by the name given on the command line. + * @param signers list of supplied CERTS objects that could have issued the signature, identified by + * the name given on the command line. * @param comment a freeform UTF-8 encoded text describing the verification * @param ext an extension object containing arbitrary, implementation-specific data */ - data class JSON(val signers: List, val comment: String?, val ext: Any?) { + data class JSON( + val signers: List, + val comment: String?, + val ext: Any?) - /** Create a JSON object with only a list of signers. */ - constructor(signers: List) : this(signers, null, null) - - /** Create a JSON object with only a single signer. */ - constructor(signer: String) : this(listOf(signer)) - } - - /** Interface abstracting a JSON parser that parses [JSON] POJOs from single-line strings. */ + /** + * Interface abstracting a JSON parser that parses [JSON] POJOs from single-line strings. + */ fun interface JSONParser { /** - * Parse a [JSON] POJO from the given single-line [string]. If the string does not represent - * a JSON object matching the [JSON] definition, this method throws a [ParseException]. + * Parse a [JSON] POJO from the given single-line [string]. + * If the string does not represent a JSON object matching the [JSON] definition, + * this method throws a [ParseException]. * * @param string [String] representation of the [JSON] object. * @return parsed [JSON] POJO - * @throws ParseException if the [string] is not a JSON string representing the [JSON] - * object. + * @throws ParseException if the [string] is not a JSON string representing the [JSON] object. */ - @Throws(ParseException::class) fun parse(string: String): JSON + @Throws(ParseException::class) + fun parse(string: String): JSON } /** - * Interface abstracting a JSON serializer that converts [JSON] POJOs into single-line JSON - * strings. + * Interface abstracting a JSON serializer that converts [JSON] POJOs into single-line JSON strings. */ fun interface JSONSerializer { diff --git a/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt b/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt new file mode 100644 index 0000000..4ba24b8 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.util + +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.OutputStream + +/** + * [OutputStream] that buffers data being written into it, until its underlying output stream is + * being replaced. At that point, first all the buffered data is being written to the underlying + * stream, followed by any successive data that may get written to the [ProxyOutputStream]. This + * class is useful if we need to provide an [OutputStream] at one point in time when the final + * target output stream is not yet known. + */ +@Deprecated("Marked for removal.") +// TODO: Remove in 11.X +class ProxyOutputStream : OutputStream() { + private val buffer = ByteArrayOutputStream() + private var swapped: OutputStream? = null + + @Synchronized + fun replaceOutputStream(underlying: OutputStream) { + this.swapped = underlying + swapped!!.write(buffer.toByteArray()) + } + + @Synchronized + @Throws(IOException::class) + override fun write(b: ByteArray) { + if (swapped == null) { + buffer.write(b) + } else { + swapped!!.write(b) + } + } + + @Synchronized + @Throws(IOException::class) + override fun write(b: ByteArray, off: Int, len: Int) { + if (swapped == null) { + buffer.write(b, off, len) + } else { + swapped!!.write(b, off, len) + } + } + + @Synchronized + @Throws(IOException::class) + override fun flush() { + buffer.flush() + if (swapped != null) { + swapped!!.flush() + } + } + + @Synchronized + @Throws(IOException::class) + override fun close() { + buffer.close() + if (swapped != null) { + swapped!!.close() + } + } + + @Synchronized + @Throws(IOException::class) + override fun write(i: Int) { + if (swapped == null) { + buffer.write(i) + } else { + swapped!!.write(i) + } + } +} diff --git a/sop-java/src/test/java/sop/VerificationJSONTest.java b/sop-java/src/test/java/sop/VerificationJSONTest.java deleted file mode 100644 index 10253d8..0000000 --- a/sop-java/src/test/java/sop/VerificationJSONTest.java +++ /dev/null @@ -1,163 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.Test; -import sop.enums.SignatureMode; - -import java.text.ParseException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -public class VerificationJSONTest { - - // A hacky self-made "JSON parser" stand-in. - // Only used for testing, do not use in production! - private Verification.JSONParser dummyParser = new Verification.JSONParser() { - @NotNull - @Override - public Verification.JSON parse(@NotNull String string) throws ParseException { - if (!string.startsWith("{")) { - throw new ParseException("Alleged JSON String does not begin with '{'", 0); - } - if (!string.endsWith("}")) { - throw new ParseException("Alleged JSON String does not end with '}'", string.length() - 1); - } - - List signersList = new ArrayList<>(); - Matcher signersMat = Pattern.compile("\"signers\": \\[(.*?)\\]").matcher(string); - if (signersMat.find()) { - String signersCat = signersMat.group(1); - String[] split = signersCat.split(","); - for (String s : split) { - s = s.trim(); - signersList.add(s.substring(1, s.length() - 1)); - } - } - - String comment = null; - Matcher commentMat = Pattern.compile("\"comment\": \"(.*?)\"").matcher(string); - if (commentMat.find()) { - comment = commentMat.group(1); - } - - String ext = null; - Matcher extMat = Pattern.compile("\"ext\": (.*?})}").matcher(string); - if (extMat.find()) { - ext = extMat.group(1); - } - - return new Verification.JSON(signersList, comment, ext); - } - }; - - // A just as hacky "JSON Serializer" lookalike. - // Also don't use in production, for testing only! - private Verification.JSONSerializer dummySerializer = new Verification.JSONSerializer() { - @NotNull - @Override - public String serialize(@NotNull Verification.JSON json) { - if (json.getSigners().isEmpty() && json.getComment() == null && json.getExt() == null) { - return ""; - } - StringBuilder sb = new StringBuilder("{"); - boolean comma = false; - - if (!json.getSigners().isEmpty()) { - comma = true; - sb.append("\"signers\": ["); - for (Iterator iterator = json.getSigners().iterator(); iterator.hasNext(); ) { - String signer = iterator.next(); - sb.append("\"").append(signer).append("\""); - if (iterator.hasNext()) { - sb.append(", "); - } - } - sb.append("]"); - } - - if (json.getComment() != null) { - if (comma) { - sb.append(", "); - } - comma = true; - sb.append("\"comment\": \"").append(json.getComment()).append("\""); - } - - if (json.getExt() != null) { - if (comma) { - sb.append(", "); - } - comma = true; - sb.append("\"ext\": ").append(json.getExt().toString()); - } - return sb.append("}").toString(); - } - }; - - @Test - public void testSimpleSerializeParse() throws ParseException { - String signer = "alice.pub"; - Verification.JSON json = new Verification.JSON(signer); - - String string = dummySerializer.serialize(json); - assertEquals("{\"signers\": [\"alice.pub\"]}", string); - - Verification.JSON parsed = dummyParser.parse(string); - assertEquals(signer, parsed.getSigners().get(0)); - assertEquals(1, parsed.getSigners().size()); - assertNull(parsed.getComment()); - assertNull(parsed.getExt()); - } - - @Test - public void testAdvancedSerializeParse() throws ParseException { - Verification.JSON json = new Verification.JSON( - Arrays.asList("../certs/alice.pub", "/etc/pgp/certs.pgp"), - "This is a comment", - "{\"Foo\": \"Bar\"}"); - - String serialized = dummySerializer.serialize(json); - assertEquals("{\"signers\": [\"../certs/alice.pub\", \"/etc/pgp/certs.pgp\"], \"comment\": \"This is a comment\", \"ext\": {\"Foo\": \"Bar\"}}", - serialized); - - Verification.JSON parsed = dummyParser.parse(serialized); - assertEquals(json.getSigners(), parsed.getSigners()); - assertEquals(json.getComment(), parsed.getComment()); - assertEquals(json.getExt(), parsed.getExt()); - } - - @Test - public void testVerificationWithSimpleJson() { - String string = "2019-10-29T18:36:45Z EB85BB5FA33A75E15E944E63F231550C4F47E38E EB85BB5FA33A75E15E944E63F231550C4F47E38E mode:text {\"signers\": [\"alice.pgp\"]}"; - Verification verification = Verification.fromString(string); - - assertTrue(verification.getContainsJson()); - assertEquals("EB85BB5FA33A75E15E944E63F231550C4F47E38E", verification.getSigningKeyFingerprint()); - assertEquals("EB85BB5FA33A75E15E944E63F231550C4F47E38E", verification.getSigningCertFingerprint()); - assertEquals(SignatureMode.text, verification.getSignatureMode().get()); - - Verification.JSON json = verification.getJson(dummyParser); - assertNotNull(json, "The verification string MUST contain valid extension json"); - - assertEquals(Collections.singletonList("alice.pgp"), json.getSigners()); - assertNull(json.getComment()); - assertNull(json.getExt()); - - verification = new Verification(verification.getCreationTime(), verification.getSigningKeyFingerprint(), verification.getSigningCertFingerprint(), verification.getSignatureMode().get(), json, dummySerializer); - assertEquals(string, verification.toString()); - } -} diff --git a/sop-java/src/test/java/sop/VerificationTest.java b/sop-java/src/test/java/sop/VerificationTest.java index 1e10f61..e956435 100644 --- a/sop-java/src/test/java/sop/VerificationTest.java +++ b/sop-java/src/test/java/sop/VerificationTest.java @@ -13,7 +13,6 @@ import java.text.ParseException; import java.util.Date; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; public class VerificationTest { @@ -26,8 +25,6 @@ public class VerificationTest { Verification verification = new Verification(signDate, keyFP, certFP); assertEquals("2022-11-07T15:01:24Z F9E6F53F7201C60A87064EAB0B27F2B0760A1209 4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B", verification.toString()); - assertFalse(verification.getContainsJson()); - VerificationAssert.assertThatVerification(verification) .issuedBy(certFP) .isBySigningKey(keyFP) diff --git a/sop-java/src/test/java/sop/util/ProxyOutputStreamTest.java b/sop-java/src/test/java/sop/util/ProxyOutputStreamTest.java new file mode 100644 index 0000000..9d99fd4 --- /dev/null +++ b/sop-java/src/test/java/sop/util/ProxyOutputStreamTest.java @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; + +public class ProxyOutputStreamTest { + + @Test + public void replaceOutputStreamThrowsNPEForNull() { + ProxyOutputStream proxy = new ProxyOutputStream(); + assertThrows(NullPointerException.class, () -> proxy.replaceOutputStream(null)); + } + + @Test + public void testSwappingStreamPreservesWrittenBytes() throws IOException { + byte[] firstSection = "Foo\nBar\n".getBytes(StandardCharsets.UTF_8); + byte[] secondSection = "Baz\n".getBytes(StandardCharsets.UTF_8); + + ProxyOutputStream proxy = new ProxyOutputStream(); + proxy.write(firstSection); + + ByteArrayOutputStream swappedStream = new ByteArrayOutputStream(); + proxy.replaceOutputStream(swappedStream); + + proxy.write(secondSection); + proxy.close(); + + assertEquals("Foo\nBar\nBaz\n", swappedStream.toString()); + } +}