Skip to content

Commit 1b03318

Browse files
committed
[PEM] Improve tests to test all encode/decode variations
1 parent 9116c4c commit 1b03318

File tree

2 files changed

+149
-109
lines changed

2 files changed

+149
-109
lines changed

cryptography-serialization/pem/src/commonMain/kotlin/PemDocument.kt

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -52,15 +52,15 @@ public class PemDocument(
5252

5353
// will decode only the first one, even if there is something else after it
5454
public fun decode(text: String): PemDocument {
55-
return tryDecodeFromString(text, startIndex = 0, saveEndIndex = {}) ?: error("Invalid PEM format: missing BEGIN label")
55+
return tryDecodeFromString(text, startIndex = 0, saveEndIndex = {}) ?: throwPemMissingBeginLabel()
5656
}
5757

5858
public fun decodeToSequence(text: String): Sequence<PemDocument> = sequence {
5959
var startIndex = 0
6060
while (startIndex < text.length) {
6161
yield(tryDecodeFromString(text, startIndex) { startIndex = it } ?: break)
6262
}
63-
if (startIndex == 0) error("Invalid PEM format: missing BEGIN label")
63+
if (startIndex == 0) throwPemMissingBeginLabel()
6464
}
6565

6666
@OptIn(UnsafeByteStringApi::class)
@@ -74,19 +74,19 @@ public class PemDocument(
7474
}
7575

7676
public fun decode(bytes: ByteString): PemDocument {
77-
return tryDecodeFromByteString(bytes, startIndex = 0, saveEndIndex = {}) ?: error("Invalid PEM format: missing BEGIN label")
77+
return tryDecodeFromByteString(bytes, startIndex = 0, saveEndIndex = {}) ?: throwPemMissingBeginLabel()
7878
}
7979

8080
public fun decodeToSequence(bytes: ByteString): Sequence<PemDocument> = sequence {
8181
var startIndex = 0
8282
while (startIndex < bytes.size) {
8383
yield(tryDecodeFromByteString(bytes, startIndex) { startIndex = it } ?: break)
8484
}
85-
if (startIndex == 0) error("Invalid PEM format: missing BEGIN label")
85+
if (startIndex == 0) throwPemMissingBeginLabel()
8686
}
8787

8888
public fun decode(source: Source): PemDocument {
89-
return tryDecodeFromSource(source) ?: error("Invalid PEM format: missing BEGIN label")
89+
return tryDecodeFromSource(source) ?: throwPemMissingBeginLabel()
9090
}
9191

9292
public fun decodeToSequence(source: Source): Sequence<PemDocument> = sequence {
@@ -95,7 +95,7 @@ public class PemDocument(
9595
yield(tryDecodeFromSource(source) ?: break)
9696
hasAtLeastOneBeginLabel = true
9797
}
98-
if (!hasAtLeastOneBeginLabel) error("Invalid PEM format: missing BEGIN label")
98+
if (!hasAtLeastOneBeginLabel) throwPemMissingBeginLabel()
9999
}
100100
}
101101
}
@@ -180,20 +180,20 @@ private inline fun tryDecodeFromString(
180180
val beginIndex = text.indexOf(BEGIN_PREFIX, startIndex)
181181
if (beginIndex == -1) return null
182182
val beginLineEndIndex = text.indexOf(NEW_LINE, beginIndex + BEGIN_PREFIX.length)
183-
if (beginLineEndIndex == -1) error("Invalid PEM format: missing new line after BEGIN label")
183+
if (beginLineEndIndex == -1) throwPemMissingNewLineAfterBeginLabel()
184184
val beginSuffixIndex = text.indexOf(SUFFIX, beginIndex + BEGIN_PREFIX.length)
185-
if (beginSuffixIndex == -1 || beginSuffixIndex > beginLineEndIndex) error("Invalid PEM format: missing BEGIN label suffix")
185+
if (beginSuffixIndex == -1 || beginSuffixIndex > beginLineEndIndex) throwPemMissingBeginLabelSuffix()
186186

187187
val beginLabel = text.substring(beginIndex + BEGIN_PREFIX.length, beginSuffixIndex)
188188

189189
val endIndex = text.indexOf(END_PREFIX, beginLineEndIndex)
190-
if (endIndex == -1) error("Invalid PEM format: missing END label")
190+
if (endIndex == -1) throwPemMissingEndLabel()
191191
val endLineEndIndex = text.indexOf(NEW_LINE, endIndex + END_PREFIX.length)
192192
val endSuffixIndex = text.indexOf(SUFFIX, endIndex + END_PREFIX.length)
193-
if (endSuffixIndex == -1 || (endLineEndIndex != -1 && endSuffixIndex > endLineEndIndex)) error("Invalid PEM format: missing END label suffix")
193+
if (endSuffixIndex == -1 || (endLineEndIndex != -1 && endSuffixIndex > endLineEndIndex)) throwPemMissingEndLabelSuffix()
194194

195195
val endLabel = text.substring(endIndex + END_PREFIX.length, endSuffixIndex)
196-
if (endLabel != beginLabel) error("Invalid PEM format: BEGIN=`$beginLabel`, END=`$endLabel`")
196+
if (endLabel != beginLabel) throwPemBeginEndLabelMismatch(beginLabel, endLabel)
197197

198198
saveEndIndex(
199199
if (endLineEndIndex == -1) {
@@ -222,20 +222,20 @@ private inline fun tryDecodeFromByteString(
222222
val beginIndex = bytes.indexOf(BEGIN_BYTES, startIndex)
223223
if (beginIndex == -1) return null
224224
val beginLineEndIndex = bytes.indexOf(NEW_LINE_BYTE, beginIndex + BEGIN_BYTES.size)
225-
if (beginLineEndIndex == -1) error("Invalid PEM format: missing new line after BEGIN label")
225+
if (beginLineEndIndex == -1) throwPemMissingNewLineAfterBeginLabel()
226226
val beginSuffixIndex = bytes.indexOf(SUFFIX_BYTES, beginIndex + BEGIN_BYTES.size)
227-
if (beginSuffixIndex == -1 || beginSuffixIndex > beginLineEndIndex) error("Invalid PEM format: missing BEGIN label suffix")
227+
if (beginSuffixIndex == -1 || beginSuffixIndex > beginLineEndIndex) throwPemMissingBeginLabelSuffix()
228228

229229
val beginLabel = bytes.substring(beginIndex + BEGIN_BYTES.size, beginSuffixIndex)
230230

231231
val endIndex = bytes.indexOf(END_BYTES, beginLineEndIndex)
232-
if (endIndex == -1) error("Invalid PEM format: missing END label")
232+
if (endIndex == -1) throwPemMissingEndLabel()
233233
val endLineEndIndex = bytes.indexOf(NEW_LINE_BYTE, endIndex + END_BYTES.size)
234234
val endSuffixIndex = bytes.indexOf(SUFFIX_BYTES, endIndex + END_BYTES.size)
235-
if (endSuffixIndex == -1 || (endLineEndIndex != -1 && endSuffixIndex > endLineEndIndex)) error("Invalid PEM format: missing END label suffix")
235+
if (endSuffixIndex == -1 || (endLineEndIndex != -1 && endSuffixIndex > endLineEndIndex)) throwPemMissingEndLabelSuffix()
236236

237237
val endLabel = bytes.substring(endIndex + END_BYTES.size, endSuffixIndex)
238-
if (endLabel != beginLabel) error("Invalid PEM format: BEGIN=`${beginLabel.decodeToString()}`, END=`${endLabel.decodeToString()}`")
238+
if (endLabel != beginLabel) throwPemBeginEndLabelMismatch(beginLabel.decodeToString(), endLabel.decodeToString())
239239

240240
saveEndIndex(
241241
if (endLineEndIndex == -1) {
@@ -272,22 +272,22 @@ private fun tryDecodeFromSource(source: Source): PemDocument? {
272272
source.skip(beginIndex + BEGIN_BYTES.size)
273273

274274
val beginLineEndIndex = source.indexOf(NEW_LINE_BYTE)
275-
if (beginLineEndIndex == -1L) error("Invalid PEM format: missing new line after BEGIN label")
275+
if (beginLineEndIndex == -1L) throwPemMissingNewLineAfterBeginLabel()
276276
val beginSuffixIndex = source.indexOf(SUFFIX_BYTES)
277-
if (beginSuffixIndex == -1L || beginSuffixIndex > beginLineEndIndex) error("Invalid PEM format: missing BEGIN label suffix")
277+
if (beginSuffixIndex == -1L || beginSuffixIndex > beginLineEndIndex) throwPemMissingBeginLabelSuffix()
278278

279279
val beginLabel = source.readByteString(beginSuffixIndex.toInt())
280280
source.skip(beginLineEndIndex + 1 - beginSuffixIndex) // skip suffix & new line
281281

282282
val endIndex = source.indexOf(END_BYTES)
283-
if (endIndex == -1L) error("Invalid PEM format: missing END label")
283+
if (endIndex == -1L) throwPemMissingEndLabel()
284284

285285
val base64Content = source.readByteString(endIndex.toInt())
286286
source.skip(END_BYTES.size.toLong())
287287

288288
val endLineEndIndex = source.indexOf(NEW_LINE_BYTE)
289289
val endSuffixIndex = source.indexOf(SUFFIX_BYTES)
290-
if (endSuffixIndex == -1L || (endLineEndIndex != -1L && endSuffixIndex > endLineEndIndex)) error("Invalid PEM format: missing END label suffix")
290+
if (endSuffixIndex == -1L || (endLineEndIndex != -1L && endSuffixIndex > endLineEndIndex)) throwPemMissingEndLabelSuffix()
291291

292292
val endLabel = source.readByteString(endSuffixIndex.toInt())
293293
if (endLineEndIndex == -1L) {
@@ -296,10 +296,19 @@ private fun tryDecodeFromSource(source: Source): PemDocument? {
296296
source.skip(endLineEndIndex + 1 - endSuffixIndex)
297297
}
298298

299-
if (endLabel != beginLabel) error("Invalid PEM format: BEGIN=`${beginLabel.decodeToString()}`, END=`${endLabel.decodeToString()}`")
299+
if (endLabel != beginLabel) throwPemBeginEndLabelMismatch(beginLabel.decodeToString(), endLabel.decodeToString())
300300

301301
return PemDocument(
302302
label = PemLabel(beginLabel.decodeToString()),
303303
content = Base64.Pem.decodeToByteString(base64Content)
304304
)
305305
}
306+
307+
private fun throwPemInvalid(message: String): Nothing = throw IllegalArgumentException("Invalid PEM format: $message")
308+
private fun throwPemMissingBeginLabel(): Nothing = throwPemInvalid("missing BEGIN label")
309+
private fun throwPemMissingNewLineAfterBeginLabel(): Nothing = throwPemInvalid("missing new line after BEGIN label")
310+
private fun throwPemMissingBeginLabelSuffix(): Nothing = throwPemInvalid("missing BEGIN label suffix")
311+
private fun throwPemMissingEndLabel(): Nothing = throwPemInvalid("missing END label")
312+
private fun throwPemMissingEndLabelSuffix(): Nothing = throwPemInvalid("missing END label suffix")
313+
private fun throwPemBeginEndLabelMismatch(beginLabel: String, endLabel: String): Nothing =
314+
throwPemInvalid("BEGIN($beginLabel) and END($endLabel) labels mismatch")

cryptography-serialization/pem/src/commonTest/kotlin/PemTest.kt

Lines changed: 119 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -4,118 +4,149 @@
44

55
package dev.whyoleg.cryptography.serialization.pem
66

7+
import kotlinx.io.*
78
import kotlinx.io.bytestring.*
89
import kotlin.test.*
910

1011
class PemTest {
1112

1213
@Test
13-
fun testEncoding() {
14-
assertEquals(
15-
"""
16-
-----BEGIN UNKNOWN-----
17-
SGVsbG8gV29ybGQ=
18-
-----END UNKNOWN-----
19-
20-
""".trimIndent(),
21-
PemDocument(
22-
PemLabel("UNKNOWN"),
23-
"Hello World".encodeToByteArray()
24-
).encodeToString()
25-
)
26-
}
14+
fun testHelloWorld() = testPem(
15+
label = "UNKNOWN",
16+
content = "Hello World".encodeToByteString(),
17+
document = """
18+
-----BEGIN UNKNOWN-----
19+
SGVsbG8gV29ybGQ=
20+
-----END UNKNOWN-----
21+
""".trimIndent()
22+
)
2723

2824
@Test
29-
fun testDecoding() {
30-
val content = PemDocument.decode(
31-
"""
32-
-----BEGIN UNKNOWN-----
33-
SGVsbG8gV29ybGQ=
34-
-----END UNKNOWN-----
35-
36-
""".trimIndent(),
37-
)
25+
fun testMultiLine() = testPem(
26+
label = "UNKNOWN CHUNKED",
27+
content = "Hello World".repeat(10).encodeToByteString(),
28+
document = """
29+
-----BEGIN UNKNOWN CHUNKED-----
30+
SGVsbG8gV29ybGRIZWxsbyBXb3JsZEhlbGxvIFdvcmxkSGVsbG8gV29ybGRIZWxs
31+
byBXb3JsZEhlbGxvIFdvcmxkSGVsbG8gV29ybGRIZWxsbyBXb3JsZEhlbGxvIFdv
32+
cmxkSGVsbG8gV29ybGQ=
33+
-----END UNKNOWN CHUNKED-----
34+
""".trimIndent()
35+
)
3836

39-
assertEquals(PemLabel("UNKNOWN"), content.label)
40-
assertEquals("Hello World", content.content.decodeToString())
41-
}
37+
@Test
38+
fun testDecodingWithComment() = testPemDecode(
39+
label = "UNKNOWN",
40+
content = "Hello World".encodeToByteString(),
41+
// because of comments we test only if's decoded correctly
42+
document = """
43+
Here is some description for pem
44+
it should not affect anything
45+
-----BEGIN UNKNOWN-----
46+
SGVsbG8gV29ybGQ=
47+
-----END UNKNOWN-----
48+
Here is some comments in the end
49+
"""
50+
)
4251

4352
@Test
44-
fun testChunkedEncoding() {
45-
assertEquals(
46-
"""
47-
-----BEGIN UNKNOWN CHUNKED-----
48-
SGVsbG8gV29ybGRIZWxsbyBXb3JsZEhlbGxvIFdvcmxkSGVsbG8gV29ybGRIZWxs
49-
byBXb3JsZEhlbGxvIFdvcmxkSGVsbG8gV29ybGRIZWxsbyBXb3JsZEhlbGxvIFdv
50-
cmxkSGVsbG8gV29ybGQ=
51-
-----END UNKNOWN CHUNKED-----
52-
53-
""".trimIndent(),
54-
PemDocument(
55-
PemLabel("UNKNOWN CHUNKED"),
56-
"Hello World".repeat(10).encodeToByteArray()
57-
).encodeToString().lines().joinToString("\n")
58-
)
53+
fun testDecodingWithNoBeginLabel() = testPemDecodeFailure(
54+
document = "SGVsbG8gV29ybGQ=\n-----END UNKNOWN-----"
55+
) {
56+
assertIs<IllegalArgumentException>(it)
57+
assertEquals("Invalid PEM format: missing BEGIN label", it.message)
5958
}
6059

6160
@Test
62-
fun testChunkedDecoding() {
63-
val content = PemDocument.decode(
64-
"""
65-
-----BEGIN UNKNOWN CHUNKED-----
66-
SGVsbG8gV29ybGRIZWxsbyBXb3JsZEhlbGxvIFdvcmxkSGVsbG8gV29ybGRIZWxs
67-
byBXb3JsZEhlbGxvIFdvcmxkSGVsbG8gV29ybGRIZWxsbyBXb3JsZEhlbGxvIFdv
68-
cmxkSGVsbG8gV29ybGQ=
69-
-----END UNKNOWN CHUNKED-----
70-
71-
""".trimIndent(),
72-
)
73-
74-
assertEquals(PemLabel("UNKNOWN CHUNKED"), content.label)
75-
assertEquals("Hello World".repeat(10), content.content.decodeToString())
61+
fun testDecodingWithNoEndLabel() = testPemDecodeFailure(
62+
document = "-----BEGIN UNKNOWN-----\nSGVsbG8gV29ybGQ="
63+
) {
64+
assertIs<IllegalArgumentException>(it)
65+
assertEquals("Invalid PEM format: missing END label", it.message)
7666
}
7767

7868
@Test
79-
fun testDecodingWithComment() {
80-
val content = PemDocument.decode(
81-
"""
82-
Here is some description for pem
83-
it should not affect anything
84-
-----BEGIN UNKNOWN-----
85-
SGVsbG8gV29ybGQ=
86-
-----END UNKNOWN-----
87-
""".trimIndent(),
88-
)
69+
fun testDecodingWithDifferentLabels() = testPemDecodeFailure(
70+
document = """
71+
-----BEGIN UNKNOWN1-----
72+
SGVsbG8gV29ybGQ=
73+
-----END UNKNOWN2-----
74+
""".trimIndent()
75+
) {
76+
assertIs<IllegalArgumentException>(it)
77+
assertEquals("Invalid PEM format: BEGIN(UNKNOWN1) and END(UNKNOWN2) labels mismatch", it.message)
78+
}
8979

90-
assertEquals(PemLabel("UNKNOWN"), content.label)
91-
assertEquals("Hello World", content.content.decodeToString())
80+
private fun testPem(
81+
label: String,
82+
content: ByteString,
83+
document: String,
84+
) {
85+
testPemDecode(label, content, document)
86+
testPemEncode(label, content, document)
9287
}
9388

94-
@Test
95-
fun testDecodingWithNoBeginLabel() {
96-
assertFails {
97-
PemDocument.decode("SGVsbG8gV29ybGQ=\n-----END UNKNOWN-----")
98-
}
89+
private fun testPemDecode(
90+
label: String,
91+
content: ByteString,
92+
document: String,
93+
) {
94+
val expectedDocument = PemDocument(PemLabel(label), content)
95+
96+
assertEquals(expectedDocument, PemDocument.decode(document))
97+
assertEquals(expectedDocument, PemDocument.decode(document.encodeToByteString()))
98+
assertEquals(expectedDocument, PemDocument.decode(document.encodeToByteArray()))
99+
assertEquals(expectedDocument, PemDocument.decode(Buffer().also {
100+
it.write(document.encodeToByteArray())
101+
}))
102+
assertEquals(expectedDocument, PemDocument.decode((Buffer().also {
103+
it.write(document.encodeToByteArray())
104+
} as Source).buffered()))
99105
}
100106

101-
@Test
102-
fun testDecodingWithNoEndLabel() {
103-
assertFails {
104-
PemDocument.decode("-----BEGIN UNKNOWN-----\nSGVsbG8gV29ybGQ=")
105-
}
107+
private fun testPemDecodeFailure(
108+
document: String,
109+
assertThrowable: (Throwable) -> Unit,
110+
) {
111+
assertThrowable(assertFails { PemDocument.decode(document) })
112+
assertThrowable(assertFails { PemDocument.decode(document.encodeToByteString()) })
113+
assertThrowable(assertFails { PemDocument.decode(document.encodeToByteArray()) })
114+
assertThrowable(assertFails {
115+
PemDocument.decode(Buffer().also {
116+
it.write(document.encodeToByteArray())
117+
})
118+
})
119+
assertThrowable(assertFails {
120+
PemDocument.decode((Buffer().also {
121+
it.write(document.encodeToByteArray())
122+
} as Source).buffered())
123+
})
106124
}
107125

108-
@Test
109-
fun testDecodingWithDifferentLabels() {
110-
assertFails {
111-
PemDocument.decode(
112-
"""
113-
-----BEGIN UNKNOWN1-----
114-
SGVsbG8gV29ybGQ=
115-
-----END UNKNOWN2-----
116-
""".trimIndent(),
117-
)
118-
}
126+
private fun testPemEncode(
127+
label: String,
128+
content: ByteString,
129+
document: String,
130+
) {
131+
val expectedDocument = PemDocument(PemLabel(label), content)
132+
133+
assertLinesEquals(document, expectedDocument.encodeToString())
134+
assertLinesEquals(document, expectedDocument.encodeToByteArray().decodeToString())
135+
assertLinesEquals(document, expectedDocument.encodeToByteString().decodeToString())
136+
assertLinesEquals(document, Buffer().also {
137+
expectedDocument.encodeToSink(it)
138+
}.readString())
139+
assertLinesEquals(document, Buffer().also {
140+
val sink = (it as Sink).buffered()
141+
expectedDocument.encodeToSink(sink)
142+
sink.flush()
143+
}.readString())
119144
}
120145

146+
private fun assertLinesEquals(expected: String, actual: String) {
147+
assertEquals(
148+
expected.lines().dropLastWhile { it.isBlank() },
149+
actual.lines().dropLastWhile { it.isBlank() },
150+
)
151+
}
121152
}

0 commit comments

Comments
 (0)