Implement basic Base64 and url-safe variant
diff --git a/libraries/stdlib/src/kotlin/io/encoding/Base64.kt b/libraries/stdlib/src/kotlin/io/encoding/Base64.kt
new file mode 100644
index 0000000..eb2b7a4
--- /dev/null
+++ b/libraries/stdlib/src/kotlin/io/encoding/Base64.kt
@@ -0,0 +1,324 @@
+/*
+ * Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
+ */
+
+package kotlin.io.encoding
+
+/**
+ * The "base64" encoding specified by [RFC 4648 section 4](https://www.rfc-editor.org/rfc/rfc4648#section-4), Base 64 Encoding.
+ *
+ * The character `'='` is used for padding.
+ */
+public open class Base64 private constructor(
+ private val encodeMap: ByteArray,
+ private val decodeMap: ByteArray,
+ // TODO: Take this parameter into consideration.
+ // https://www.rfc-editor.org/rfc/rfc4648#section-3.3
+ // It skips invalid symbols (including line separators) when decoding, and inserts line separators when encoding.
+ // Discuss line length and line separator string.
+ // The encoded output stream must be represented in lines of no more
+ // than 76 characters each. All line breaks or other characters not
+ // found in Table 1 must be ignored by decoding software.
+ @Suppress("unused") private val isMimeScheme: Boolean
+) {
+ init {
+ require(encodeMap.size == 64)
+ }
+
+ private fun encodeSize(sourceSize: Int): Int {
+ // includes padding chars
+ // TODO: Int overflow
+ return ((sourceSize + bytesPerGroup - 1) / bytesPerGroup) * symbolsPerGroup
+ }
+
+ private fun symbolAt(index: Int): Byte = encodeMap[index]
+
+ /**
+ * Encodes bytes from the specified [source] array.
+ */
+ public fun encodeToByteArray(source: ByteArray): ByteArray {
+ val result = ByteArray(encodeSize(source.size))
+ encodeToByteArray(source, result)
+ return result
+ }
+
+ /**
+ * Encodes bytes from the specified [source] array or its subrange and writes encoded symbols into the [destination] array.
+ * Returns the number of symbols written.
+ *
+ * @param source the array to encode bytes from.
+ * @param destination the array to write symbols into.
+ * @param destinationOffset the starting index in the [destination] array to write symbols to, 0 by default.
+ * @param startIndex the beginning (inclusive) of the subrange to encode, 0 by default.
+ * @param endIndex the end (exclusive) of the subrange to encode, size of the [source] array by default.
+ *
+ * @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of [source] array indices.
+ * @throws IllegalArgumentException when `startIndex > endIndex`.
+ * @throws IndexOutOfBoundsException when the resulting symbols don't fit into the [destination] array starting at the specified [destinationOffset],
+ * or when that index is out of the [destination] array indices range.
+ *
+ * @return the number of symbols written into [destination] array.
+ */
+ public fun encodeToByteArray(
+ source: ByteArray,
+ destination: ByteArray,
+ destinationOffset: Int = 0,
+ startIndex: Int = 0,
+ endIndex: Int = source.size
+ ): Int {
+ AbstractList.checkBoundsIndexes(startIndex, endIndex, source.size)
+ val encodeSize = encodeSize(endIndex - startIndex)
+ AbstractList.checkBoundsIndexes(destinationOffset, destinationOffset + encodeSize, destination.size)
+
+ var destinationIndex = destinationOffset
+
+ var payload = 0
+ var symbolStart = -bitsPerSymbol
+ for (byte in source) {
+ payload = (payload shl bitsPerByte) or (byte.toInt() and 0xFF)
+ symbolStart += bitsPerByte
+
+ while (symbolStart >= 0) {
+ destination[destinationIndex++] = symbolAt(payload ushr symbolStart)
+
+ payload = payload and ((1 shl symbolStart) - 1)
+ symbolStart -= bitsPerSymbol
+ }
+ }
+
+ if (symbolStart == -bitsPerSymbol) {
+ check(payload == 0)
+ check(destinationIndex == destinationOffset + encodeSize)
+ return encodeSize
+ }
+
+ destination[destinationIndex++] = symbolAt(payload shl -symbolStart)
+
+ while (destinationIndex < encodeSize) {
+ destination[destinationIndex++] = paddingSymbol
+ }
+
+ return encodeSize
+ }
+
+ /**
+ * Encodes bytes from the specified [source] array and returns a string with the resulting symbols.
+ */
+ public fun encode(source: ByteArray): String {
+ val byteResult = encodeToByteArray(source)
+ return buildString(byteResult.size) {
+ for (byte in byteResult) {
+ append(byte.toInt().toChar())
+ }
+ }
+ }
+
+ /**
+ * Encodes bytes from the specified [source] array or its subrange and appends encoded symbols to the [destination] appendable.
+ * Returns the number of symbols appended.
+ *
+ * @param source the array to encode bytes from.
+ * @param destination the appendable to append symbols to.
+ * @param startIndex the beginning (inclusive) of the subrange to encode, 0 by default.
+ * @param endIndex the end (exclusive) of the subrange to encode, size of the [source] array by default.
+ *
+ * @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of [source] array indices.
+ * @throws IllegalArgumentException when `startIndex > endIndex`.
+ *
+ * @return the number of symbols appended to the [destination] appendable.
+ */
+ public fun encode(
+ source: ByteArray,
+ destination: Appendable,
+ startIndex: Int = 0,
+ endIndex: Int = source.size
+ ): Int {
+ val byteResult = ByteArray(encodeSize(endIndex - startIndex))
+ val symbolsAppended = encodeToByteArray(source, byteResult, 0, startIndex, endIndex)
+ for (byte in byteResult) {
+ destination.append(byte.toInt().toChar())
+ }
+ return symbolsAppended
+ }
+
+ private fun paddingsCount(source: ByteArray, startIndex: Int, endIndex: Int): Int {
+ var paddingIndex = endIndex - 1
+ while (paddingIndex >= startIndex && source[paddingIndex] == paddingSymbol) {
+ paddingIndex--
+ }
+ return endIndex - paddingIndex - 1
+ }
+
+ private fun decodeSize(sourceSize: Int, paddings: Int): Int {
+ // TODO: Int overflow
+ return ((sourceSize - paddings) * bitsPerSymbol) / bitsPerByte
+ }
+
+ private fun throwIllegalSymbol(symbol: Int, index: Int): Nothing {
+ throw throw IllegalArgumentException("Invalid symbol '${symbol.toChar()}'(${symbol.toString(radix = 8)}) at index $index")
+ }
+
+ private fun decodeSymbol(symbol: Int, index: Int): Int {
+ if (symbol >= decodeMap.size || decodeMap[symbol] < 0) {
+ throwIllegalSymbol(symbol, index)
+ }
+ return decodeMap[symbol].toInt()
+ }
+
+ /**
+ * Decodes symbols from the specified [source] array.
+ */
+ public fun decodeFromByteArray(source: ByteArray): ByteArray {
+ val paddings = paddingsCount(source, 0, source.size)
+ val decodeSize = decodeSize(source.size, paddings)
+ val result = ByteArray(decodeSize)
+ decodeFromByteArray(source, result)
+ return result
+ }
+
+ /**
+ * Decodes symbols from the specified [source] array or its subrange and writes decoded bytes into the [destination] array.
+ * Returns the number of bytes written.
+ *
+ * @param destination the array to write bytes into.
+ * @param destinationOffset the starting index in the [destination] array to write bytes to, 0 by default.
+ * @param startIndex the beginning (inclusive) of the subrange to decode, 0 by default.
+ * @param endIndex the end (exclusive) of the subrange to decode, size of the [source] array by default.
+ *
+ * @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of [source] array indices.
+ * @throws IllegalArgumentException when `startIndex > endIndex`.
+ * @throws IndexOutOfBoundsException when the resulting bytes don't fit into the [destination] array starting at the specified [destinationOffset],
+ * or when that index is out of the [destination] array indices range.
+ *
+ * @return the number of bytes written into [destination] array.
+ */
+ public fun decodeFromByteArray(
+ source: ByteArray,
+ destination: ByteArray,
+ destinationOffset: Int = 0,
+ startIndex: Int = 0,
+ endIndex: Int = source.size
+ ): Int {
+ AbstractList.checkBoundsIndexes(startIndex, endIndex, source.size)
+ val paddings = paddingsCount(source, startIndex, endIndex)
+ val decodeSize = decodeSize(sourceSize = endIndex - startIndex, paddings)
+ AbstractList.checkBoundsIndexes(destinationOffset, destinationOffset + decodeSize, destination.size)
+
+ var destinationIndex = destinationOffset
+
+ var payload = 0
+ var byteStart = -bitsPerByte
+
+ for (sourceIndex in startIndex until endIndex - paddings) {
+ val symbol = source[sourceIndex].toInt() and 0xFF
+ val symbolBits = decodeSymbol(symbol, sourceIndex)
+ if (symbolBits < 0) {
+ continue
+ }
+
+ payload = (payload shl bitsPerSymbol) or symbolBits
+ byteStart += bitsPerSymbol
+
+ if (byteStart >= 0) {
+ destination[destinationIndex++] = (payload ushr byteStart).toByte()
+
+ payload = payload and ((1 shl byteStart) - 1)
+ byteStart -= bitsPerByte
+ }
+ }
+
+// check(payload == 0) // the padded bits are zeros
+ check(destinationIndex == destinationOffset + decodeSize)
+
+ return decodeSize
+ }
+
+ /**
+ * Decodes symbols from the specified [source] string.
+ */
+ public fun decode(source: String): ByteArray {
+ val byteSource = ByteArray(source.length) {
+ val symbol = source[it].code
+ if (symbol > Byte.MAX_VALUE) {
+ throwIllegalSymbol(symbol, it)
+ }
+ symbol.toByte()
+ }
+
+ return decodeFromByteArray(byteSource)
+ }
+
+ private fun isInAlphabet(value: Int): Boolean = value in decodeMap.indices
+ && value != paddingSymbol.toInt()
+ && decodeMap[value] > 0
+
+ /**
+ * Returns `true` if the given [value] is a valid symbol in this Base64.
+ */
+ public fun isInAlphabet(value: Byte): Boolean = isInAlphabet(value.toInt())
+
+ /**
+ * Returns `true` if the given [value] is a valid symbol in this Base64.
+ */
+ public fun isInAlphabet(value: Char): Boolean = isInAlphabet(value.code)
+
+ public companion object Default : Base64(base64EncodeMap, base64DecodeMap, isMimeScheme = false) {
+
+ private const val bitsPerByte = 8
+ private const val bitsPerSymbol = 6
+
+ private const val bytesPerGroup: Int = 3
+ private const val symbolsPerGroup: Int = 4
+
+ private const val paddingSymbol: Byte = 61 // '='
+
+ public val UrlSafe: Base64
+ get() = Base64(base64UrlEncodeMap, base64UrlDecodeMap, isMimeScheme = false)
+
+ public val Mime: Base64
+ get() = Base64(base64EncodeMap, base64DecodeMap, isMimeScheme = true)
+
+ @Suppress("UNUSED_PARAMETER")
+ public fun Mime(lineLength: Int, lineSeparator: String): Base64 = TODO()
+ }
+}
+
+
+// "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
+private val base64EncodeMap = byteArrayOf(
+ 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, /* 0 - 15 */
+ 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, /* 16 - 31 */
+ 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, /* 32 - 47 */
+ 119, 120, 121, 122, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 43, 47, /* 48 - 63 */
+)
+
+private val base64DecodeMap = byteArrayOf(
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0 - 15 */
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 16 - 31 */
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, /* 32 - 47 */
+ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, /* 48 - 63 */
+ -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, /* 64 - 79 */
+ 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, /* 80 - 95 */
+ -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, /* 96 - 111 */
+ 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, /* 112 - 127 */
+)
+
+// "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
+private val base64UrlEncodeMap = byteArrayOf(
+ 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, /* 0 - 15 */
+ 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 97, 98, 99, 100, 101, 102, /* 16 - 31 */
+ 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, /* 32 - 47 */
+ 119, 120, 121, 122, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 45, 95, /* 48 - 63 */
+)
+
+private val base64UrlDecodeMap = byteArrayOf(
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0 - 15 */
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 16 - 31 */
+ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, /* 32 - 47 */
+ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, /* 48 - 63 */
+ -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, /* 64 - 79 */
+ 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, 63, /* 80 - 95 */
+ -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, /* 96 - 111 */
+ 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, /* 112 - 127 */
+)
\ No newline at end of file
diff --git a/libraries/stdlib/test/io.encoding/Base64Test.kt b/libraries/stdlib/test/io.encoding/Base64Test.kt
new file mode 100644
index 0000000..9b8252d
--- /dev/null
+++ b/libraries/stdlib/test/io.encoding/Base64Test.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
+ */
+
+package test.io.encoding
+
+import kotlin.test.*
+import kotlin.io.encoding.Base64
+
+class Base64Test {
+
+ private fun testCoding(codec: Base64, text: String, encodedText: String) {
+ val bytes = ByteArray(text.length) { text[it].code.toByte() }
+ assertEquals(encodedText, codec.encode(bytes))
+
+ val encodedBytes = ByteArray(encodedText.length) { encodedText[it].code.toByte() }
+ assertContentEquals(encodedBytes, codec.encodeToByteArray(bytes))
+
+ assertContentEquals(bytes, codec.decode(encodedText))
+ assertContentEquals(bytes, codec.decodeFromByteArray(encodedBytes))
+ }
+
+ @Test
+ fun base64() {
+ fun testBase64(text: String, encodedText: String) {
+ testCoding(Base64, text, encodedText)
+ }
+
+ testBase64("", "")
+ testBase64("f", "Zg==")
+ testBase64("fo", "Zm8=")
+ testBase64("foo", "Zm9v")
+ testBase64("foob", "Zm9vYg==")
+ testBase64("fooba", "Zm9vYmE=")
+ testBase64("foobar", "Zm9vYmFy")
+ // 0b11111011, 0b11110000
+ testBase64("\u00FB\u00F0", "+/A=")
+
+ // the padded bits are allowed to be non-zero
+ assertEquals("fo", Base64.decode("Zm9=").decodeToString())
+ }
+
+ @Test
+ fun base64Url() {
+ fun testBase64(text: String, encodedText: String) {
+ testCoding(Base64.UrlSafe, text, encodedText)
+ }
+
+ testBase64("", "")
+ testBase64("f", "Zg==")
+ testBase64("fo", "Zm8=")
+ testBase64("foo", "Zm9v")
+ testBase64("foob", "Zm9vYg==")
+ testBase64("fooba", "Zm9vYmE=")
+ testBase64("foobar", "Zm9vYmFy")
+ // 0b11111011, 0b11110000
+ testBase64("\u00FB\u00F0", "-_A=")
+
+ // the padded bits are allowed to be non-zero
+ assertEquals("fo", Base64.UrlSafe.decode("Zm9=").decodeToString())
+ }
+}
\ No newline at end of file