@@ -9,24 +9,111 @@ import kotlinx.io.bytestring.*
99import kotlinx.io.bytestring.unsafe.*
1010import kotlin.io.encoding.*
1111
12+ /* *
13+ * Represents a single PEM (Privacy-Enhanced Mail) document as defined by [RFC 7468](https://datatracker.ietf.org/doc/html/rfc7468),
14+ * consisting of a textual [label], and the binary [content] it encapsulates
15+ *
16+ * To encode the document into a PEM-encoded string use one of
17+ * [encodeToString], [encodeToByteArray], [encodeToByteString], or [encodeToSink] depending on the desired output type
18+ *
19+ * Encoding produces the canonical PEM form:
20+ *
21+ * ```text
22+ * -----BEGIN {label}-----
23+ * Base64-encoded {content} with line breaks every 64 characters
24+ * -----END {label}-----
25+ * ```
26+ *
27+ * Creation of document instances can be done using one of the following methods:
28+ *
29+ * - using constructor, accepting [label] and [content]
30+ * - using [PemDocument.decode] for decoding from a string or binary input
31+ * - using [PemDocument.decodeToSequence] for decoding multiple documents from a single string or binary input
32+ *
33+ * [PemDocument] is an **immutable**, **thread-safe** value object, with structural equality and hash code that uses both [label] and [content].
34+ *
35+ * @constructor Creates a new [PemDocument] with the provided [label] and [content]
36+ * @property label Case-sensitive encapsulation label (for example, `"CERTIFICATE"` or `"PRIVATE KEY"`)
37+ * @property content Raw binary payload (commonly DER or any arbitrary bytes) that is base64-armored when encoded to PEM
38+ */
1239public class PemDocument (
1340 public val label : PemLabel ,
1441 public val content : ByteString ,
1542) {
43+ /* *
44+ * Creates a new [PemDocument] with the provided [label] and [content]
45+ */
1646 public constructor (
1747 label: PemLabel ,
1848 content: ByteArray ,
1949 ) : this (label, ByteString (content))
2050
51+ /* *
52+ * Encodes this document into a [String] in [PEM](https://datatracker.ietf.org/doc/html/rfc7468) format
53+ *
54+ * The output uses the form:
55+ *
56+ * ```text
57+ * -----BEGIN {label}-----
58+ * Base64-encoded {content} with line breaks every 64 characters
59+ * -----END {label}-----
60+ * ```
61+ *
62+ * @return the PEM-encoded string
63+ */
2164 public fun encodeToString (): String = encodeToByteArrayImpl().decodeToString()
2265
66+ /* *
67+ * Encodes this document into a [ByteArray] as a string in [PEM](https://datatracker.ietf.org/doc/html/rfc7468) format
68+ *
69+ * The output uses the form:
70+ *
71+ * ```text
72+ * -----BEGIN {label}-----
73+ * Base64-encoded {content} with line breaks every 64 characters
74+ * -----END {label}-----
75+ * ```
76+ *
77+ * @return the bytes representing PEM-encoded string
78+ */
2379 public fun encodeToByteArray (): ByteArray = encodeToByteArrayImpl()
2480
81+ /* *
82+ * Encodes this document into a [ByteString] as a string in [PEM](https://datatracker.ietf.org/doc/html/rfc7468) format
83+ *
84+ * The output uses the form:
85+ *
86+ * ```text
87+ * -----BEGIN {label}-----
88+ * Base64-encoded {content} with line breaks every 64 characters
89+ * -----END {label}-----
90+ * ```
91+ *
92+ * @return the bytes representing PEM-encoded string
93+ */
2594 @OptIn(UnsafeByteStringApi ::class )
2695 public fun encodeToByteString (): ByteString = UnsafeByteStringOperations .wrapUnsafe(encodeToByteArrayImpl())
2796
97+ /* *
98+ * Encodes this document to the provided [sink] as a string in [PEM](https://datatracker.ietf.org/doc/html/rfc7468) format
99+ *
100+ * The output uses the form:
101+ *
102+ * ```text
103+ * -----BEGIN {label}-----
104+ * Base64-encoded {content} with line breaks every 64 characters
105+ * -----END {label}-----
106+ * ```
107+ *
108+ * @param sink the destination to write bytes representing PEM-encoded string into
109+ */
28110 public fun encodeToSink (sink : Sink ): Unit = sink.write(encodeToByteArrayImpl())
29111
112+ /* *
113+ * Returns `true` if [other] is a [PemDocument] with the same [label] and [content]
114+ *
115+ * [content] should contain exactly the same byte sequence
116+ */
30117 override fun equals (other : Any? ): Boolean {
31118 if (this == = other) return true
32119 if (other !is PemDocument ) return false
@@ -37,56 +124,236 @@ public class PemDocument(
37124 return true
38125 }
39126
127+ /* *
128+ * Returns a hash code consistent with [equals], computed from [label] and [content]
129+ */
40130 override fun hashCode (): Int {
41131 var result = label.hashCode()
42132 result = 31 * result + content.hashCode()
43133 return result
44134 }
45135
136+ /* *
137+ * Returns a concise debug representation of this document including its [label] and [content]
138+ *
139+ * **Avoid logging if the [content] is sensitive**
140+ */
46141 override fun toString (): String {
47142 return " PemDocument(label=$label , content=$content )"
48143 }
49144
50145 public companion object {
51- // decode will skip comments and everything else which is not label or content
52-
53- // will decode only the first one, even if there is something else after it
146+ /* *
147+ * Decodes the first [PEM](https://datatracker.ietf.org/doc/html/rfc7468) document found in [text]
148+ *
149+ * The input should be in the form:
150+ *
151+ * ```text
152+ * -----BEGIN {label}-----
153+ * Base64-encoded {content} with line breaks every 64 characters
154+ * -----END {label}-----
155+ * ```
156+ *
157+ * Any content before `-----BEGIN {label}-----` and after `-----END {label}-----` is ignored
158+ *
159+ * Only the first complete document is decoded. For decoding of multiple documents, use [PemDocument.decodeToSequence]
160+ *
161+ * @param text the textual input that may contain a PEM document
162+ * @return the decoded [PemDocument]
163+ * @throws IllegalArgumentException if no PEM documents present in [text], or the PEM encoding is invalid
164+ */
54165 public fun decode (text : String ): PemDocument {
55166 return tryDecodeFromString(text, startIndex = 0 , saveEndIndex = {}) ? : throwPemMissingBeginLabel()
56167 }
57168
169+ /* *
170+ * Lazily decodes all [PEM](https://datatracker.ietf.org/doc/html/rfc7468) documents found in [text]
171+ *
172+ * The input should be in the form:
173+ *
174+ * ```text
175+ * -----BEGIN {LABEL1}-----
176+ * Base64-encoded {content} with line breaks every 64 characters
177+ * -----END {LABEL1}-----
178+ *
179+ * -----BEGIN {LABEL2}-----
180+ * Base64-encoded {content} with line breaks every 64 characters
181+ * -----END {LABEL2}-----
182+ * ```
183+ *
184+ * Any content before the first `-----BEGIN {label}-----` and after the last `-----END {label}-----` is ignored,
185+ * as well as any content after each document and before the next `-----BEGIN {label}-----`.
186+ * The sequence yields each discovered [PemDocument] in order
187+ *
188+ * @param text the textual input that may contain multiple PEM documents
189+ * @return a sequence of decoded [PemDocument]s, empty sequence if no PEM documents present
190+ * @throws IllegalArgumentException if the PEM encoding of any document is invalid
191+ */
58192 public fun decodeToSequence (text : String ): Sequence <PemDocument > = sequence {
59193 var startIndex = 0
60194 while (startIndex < text.length) {
61195 yield (tryDecodeFromString(text, startIndex) { startIndex = it } ? : break )
62196 }
63197 }
64198
199+ /* *
200+ * Decodes the first [PEM](https://datatracker.ietf.org/doc/html/rfc7468) document found in [bytes].
201+ * The [bytes] are treated as an encoded string
202+ *
203+ * The input should be in the form:
204+ *
205+ * ```text
206+ * -----BEGIN {label}-----
207+ * Base64-encoded {content} with line breaks every 64 characters
208+ * -----END {label}-----
209+ * ```
210+ *
211+ * Any content before `-----BEGIN {label}-----` and after `-----END {label}-----` is ignored
212+ *
213+ * Only the first complete document is decoded. For decoding of multiple documents, use [PemDocument.decodeToSequence]
214+ *
215+ * @param bytes the byte array that may contain a PEM document
216+ * @return the decoded [PemDocument]
217+ * @throws IllegalArgumentException if no PEM documents present in [bytes], or the PEM encoding is invalid
218+ */
65219 @OptIn(UnsafeByteStringApi ::class )
66220 public fun decode (bytes : ByteArray ): PemDocument {
67221 return decode(UnsafeByteStringOperations .wrapUnsafe(bytes))
68222 }
69223
224+ /* *
225+ * Lazily decodes all [PEM](https://datatracker.ietf.org/doc/html/rfc7468) documents found in [bytes].
226+ * The [bytes] are treated as an encoded string
227+ *
228+ * The input should be in the form:
229+ *
230+ * ```text
231+ * -----BEGIN {LABEL1}-----
232+ * Base64-encoded {content} with line breaks every 64 characters
233+ * -----END {LABEL1}-----
234+ *
235+ * -----BEGIN {LABEL2}-----
236+ * Base64-encoded {content} with line breaks every 64 characters
237+ * -----END {LABEL2}-----
238+ * ```
239+ *
240+ * Any content before the first `-----BEGIN {label}-----` and after the last `-----END {label}-----` is ignored,
241+ * as well as any content after each document and before the next `-----BEGIN {label}-----`.
242+ * The sequence yields each discovered [PemDocument] in order
243+ *
244+ * @param bytes the byte array that may contain multiple PEM documents
245+ * @return a sequence of decoded [PemDocument]s, empty sequence if no PEM documents present
246+ * @throws IllegalArgumentException if the PEM encoding of any document is invalid
247+ */
70248 @OptIn(UnsafeByteStringApi ::class )
71249 public fun decodeToSequence (bytes : ByteArray ): Sequence <PemDocument > {
72250 return decodeToSequence(UnsafeByteStringOperations .wrapUnsafe(bytes))
73251 }
74252
253+ /* *
254+ * Decodes the first [PEM](https://datatracker.ietf.org/doc/html/rfc7468) document found in [bytes].
255+ * The [bytes] are treated as an encoded string
256+ *
257+ * The input should be in the form:
258+ *
259+ * ```text
260+ * -----BEGIN {label}-----
261+ * Base64-encoded {content} with line breaks every 64 characters
262+ * -----END {label}-----
263+ * ```
264+ *
265+ * Any content before `-----BEGIN {label}-----` and after `-----END {label}-----` is ignored
266+ *
267+ * Only the first complete document is decoded. For decoding of multiple documents, use [PemDocument.decodeToSequence]
268+ *
269+ * @param bytes the byte array that may contain a PEM document
270+ * @return the decoded [PemDocument]
271+ * @throws IllegalArgumentException if no PEM documents present in [bytes], or the PEM encoding is invalid
272+ */
75273 public fun decode (bytes : ByteString ): PemDocument {
76274 return tryDecodeFromByteString(bytes, startIndex = 0 , saveEndIndex = {}) ? : throwPemMissingBeginLabel()
77275 }
78276
277+ /* *
278+ * Lazily decodes all [PEM](https://datatracker.ietf.org/doc/html/rfc7468) documents found in [bytes].
279+ * The [bytes] are treated as an encoded string
280+ *
281+ * The input should be in the form:
282+ *
283+ * ```text
284+ * -----BEGIN {LABEL1}-----
285+ * Base64-encoded {content} with line breaks every 64 characters
286+ * -----END {LABEL1}-----
287+ *
288+ * -----BEGIN {LABEL2}-----
289+ * Base64-encoded {content} with line breaks every 64 characters
290+ * -----END {LABEL2}-----
291+ * ```
292+ *
293+ * Any content before the first `-----BEGIN {label}-----` and after the last `-----END {label}-----` is ignored,
294+ * as well as any content after each document and before the next `-----BEGIN {label}-----`.
295+ * The sequence yields each discovered [PemDocument] in order
296+ *
297+ * @param bytes the byte array that may contain multiple PEM documents
298+ * @return a sequence of decoded [PemDocument]s, empty sequence if no PEM documents present
299+ * @throws IllegalArgumentException if the PEM encoding of any document is invalid
300+ */
79301 public fun decodeToSequence (bytes : ByteString ): Sequence <PemDocument > = sequence {
80302 var startIndex = 0
81303 while (startIndex < bytes.size) {
82304 yield (tryDecodeFromByteString(bytes, startIndex) { startIndex = it } ? : break )
83305 }
84306 }
85307
308+ /* *
309+ * Decodes the first [PEM](https://datatracker.ietf.org/doc/html/rfc7468) document found in [source].
310+ * The [source] is treated as an encoded string and is consumed up to and including the decoded document
311+ *
312+ *
313+ * The input should be in the form:
314+ *
315+ * ```text
316+ * -----BEGIN {label}-----
317+ * Base64-encoded {content} with line breaks every 64 characters
318+ * -----END {label}-----
319+ * ```
320+ *
321+ * Any content before `-----BEGIN {label}-----` and after `-----END {label}-----` is ignored
322+ *
323+ * Only the first complete document is decoded. For decoding of multiple documents, use [PemDocument.decodeToSequence]
324+ *
325+ * @param source the source to read from
326+ * @return the decoded [PemDocument]
327+ * @throws IllegalArgumentException if no PEM documents present in [source], or the PEM encoding is invalid
328+ */
86329 public fun decode (source : Source ): PemDocument {
87330 return tryDecodeFromSource(source) ? : throwPemMissingBeginLabel()
88331 }
89332
333+ /* *
334+ * Lazily decodes all [PEM](https://datatracker.ietf.org/doc/html/rfc7468) documents found in [source].
335+ * The [source] is treated as an encoded string and is consumed up to and including the last decoded document, which was consumed from the sequence
336+ *
337+ * The input should be in the form:
338+ *
339+ * ```text
340+ * -----BEGIN {LABEL1}-----
341+ * Base64-encoded {content} with line breaks every 64 characters
342+ * -----END {LABEL1}-----
343+ *
344+ * -----BEGIN {LABEL2}-----
345+ * Base64-encoded {content} with line breaks every 64 characters
346+ * -----END {LABEL2}-----
347+ * ```
348+ *
349+ * Any content before the first `-----BEGIN {label}-----` and after the last `-----END {label}-----` is ignored,
350+ * as well as any content after each document and before the next `-----BEGIN {label}-----`.
351+ * The sequence yields each discovered [PemDocument] in order
352+ *
353+ * @param source the source to read from
354+ * @return a sequence of decoded [PemDocument]s, empty sequence if no PEM documents present
355+ * @throws IllegalArgumentException if the PEM encoding of any document is invalid
356+ */
90357 public fun decodeToSequence (source : Source ): Sequence <PemDocument > = sequence {
91358 while (! source.exhausted()) {
92359 yield (tryDecodeFromSource(source) ? : break )
0 commit comments