diff --git a/js/js.translator/testData/box/multiModule/privateNameClash.kt b/js/js.translator/testData/box/multiModule/privateNameClash.kt
index c9f3cfb49fd..c024d9e1f9c 100644
--- a/js/js.translator/testData/box/multiModule/privateNameClash.kt
+++ b/js/js.translator/testData/box/multiModule/privateNameClash.kt
@@ -2,7 +2,7 @@
// KJS_WITH_FULL_RUNTIME
// IGNORE_BACKEND: JS_IR
// IGNORE_BACKEND: JS_IR_ES6
-// EXPECTED_REACHABLE_NODES: 1805
+// EXPECTED_REACHABLE_NODES: 1992
// MODULE: lib
// FILE: lib.kt
package lib
diff --git a/libraries/stdlib/api/js-v1/kotlin.io.encoding.kt b/libraries/stdlib/api/js-v1/kotlin.io.encoding.kt
new file mode 100644
index 00000000000..3a1c04341ea
--- /dev/null
+++ b/libraries/stdlib/api/js-v1/kotlin.io.encoding.kt
@@ -0,0 +1,25 @@
+@kotlin.SinceKotlin(version = "1.8")
+@kotlin.ExperimentalStdlibApi
+public open class Base64 {
+ public final fun decode(source: kotlin.ByteArray, startIndex: kotlin.Int = ..., endIndex: kotlin.Int = ...): kotlin.ByteArray
+
+ public final fun decode(source: kotlin.CharSequence, startIndex: kotlin.Int = ..., endIndex: kotlin.Int = ...): kotlin.ByteArray
+
+ public final fun decodeIntoByteArray(source: kotlin.ByteArray, destination: kotlin.ByteArray, destinationOffset: kotlin.Int = ..., startIndex: kotlin.Int = ..., endIndex: kotlin.Int = ...): kotlin.Int
+
+ public final fun decodeIntoByteArray(source: kotlin.CharSequence, destination: kotlin.ByteArray, destinationOffset: kotlin.Int = ..., startIndex: kotlin.Int = ..., endIndex: kotlin.Int = ...): kotlin.Int
+
+ public final fun encode(source: kotlin.ByteArray, startIndex: kotlin.Int = ..., endIndex: kotlin.Int = ...): kotlin.String
+
+ public final fun encodeIntoByteArray(source: kotlin.ByteArray, destination: kotlin.ByteArray, destinationOffset: kotlin.Int = ..., startIndex: kotlin.Int = ..., endIndex: kotlin.Int = ...): kotlin.Int
+
+ public final fun encodeToAppendable(source: kotlin.ByteArray, destination: A, startIndex: kotlin.Int = ..., endIndex: kotlin.Int = ...): A
+
+ public final fun encodeToByteArray(source: kotlin.ByteArray, startIndex: kotlin.Int = ..., endIndex: kotlin.Int = ...): kotlin.ByteArray
+
+ public companion object of Base64 Default : kotlin.io.encoding.Base64 {
+ public final val Mime: kotlin.io.encoding.Base64 { get; }
+
+ public final val UrlSafe: kotlin.io.encoding.Base64 { get; }
+ }
+}
\ No newline at end of file
diff --git a/libraries/stdlib/api/js/kotlin.io.encoding.kt b/libraries/stdlib/api/js/kotlin.io.encoding.kt
new file mode 100644
index 00000000000..3a1c04341ea
--- /dev/null
+++ b/libraries/stdlib/api/js/kotlin.io.encoding.kt
@@ -0,0 +1,25 @@
+@kotlin.SinceKotlin(version = "1.8")
+@kotlin.ExperimentalStdlibApi
+public open class Base64 {
+ public final fun decode(source: kotlin.ByteArray, startIndex: kotlin.Int = ..., endIndex: kotlin.Int = ...): kotlin.ByteArray
+
+ public final fun decode(source: kotlin.CharSequence, startIndex: kotlin.Int = ..., endIndex: kotlin.Int = ...): kotlin.ByteArray
+
+ public final fun decodeIntoByteArray(source: kotlin.ByteArray, destination: kotlin.ByteArray, destinationOffset: kotlin.Int = ..., startIndex: kotlin.Int = ..., endIndex: kotlin.Int = ...): kotlin.Int
+
+ public final fun decodeIntoByteArray(source: kotlin.CharSequence, destination: kotlin.ByteArray, destinationOffset: kotlin.Int = ..., startIndex: kotlin.Int = ..., endIndex: kotlin.Int = ...): kotlin.Int
+
+ public final fun encode(source: kotlin.ByteArray, startIndex: kotlin.Int = ..., endIndex: kotlin.Int = ...): kotlin.String
+
+ public final fun encodeIntoByteArray(source: kotlin.ByteArray, destination: kotlin.ByteArray, destinationOffset: kotlin.Int = ..., startIndex: kotlin.Int = ..., endIndex: kotlin.Int = ...): kotlin.Int
+
+ public final fun encodeToAppendable(source: kotlin.ByteArray, destination: A, startIndex: kotlin.Int = ..., endIndex: kotlin.Int = ...): A
+
+ public final fun encodeToByteArray(source: kotlin.ByteArray, startIndex: kotlin.Int = ..., endIndex: kotlin.Int = ...): kotlin.ByteArray
+
+ public companion object of Base64 Default : kotlin.io.encoding.Base64 {
+ public final val Mime: kotlin.io.encoding.Base64 { get; }
+
+ public final val UrlSafe: kotlin.io.encoding.Base64 { get; }
+ }
+}
\ No newline at end of file
diff --git a/libraries/stdlib/js-ir-minimal-for-test/build.gradle.kts b/libraries/stdlib/js-ir-minimal-for-test/build.gradle.kts
index 6784810e87a..e673452a031 100644
--- a/libraries/stdlib/js-ir-minimal-for-test/build.gradle.kts
+++ b/libraries/stdlib/js-ir-minimal-for-test/build.gradle.kts
@@ -44,6 +44,7 @@ val commonMainSources by task {
"libraries/stdlib/common/src/kotlin/collections/**",
"libraries/stdlib/common/src/kotlin/ioH.kt",
"libraries/stdlib/src/kotlin/collections/**",
+ "libraries/stdlib/src/kotlin/io/**",
"libraries/stdlib/src/kotlin/properties/Delegates.kt",
"libraries/stdlib/src/kotlin/random/URandom.kt",
"libraries/stdlib/src/kotlin/text/**",
@@ -87,6 +88,7 @@ val jsMainSources by task {
"libraries/stdlib/js/src/kotlin/date.kt",
"libraries/stdlib/js/src/kotlin/grouping.kt",
"libraries/stdlib/js/src/kotlin/ItemArrayLike.kt",
+ "libraries/stdlib/js/src/kotlin/io/**",
"libraries/stdlib/js/src/kotlin/json.kt",
"libraries/stdlib/js/src/kotlin/promise.kt",
"libraries/stdlib/js/src/kotlin/regexp.kt",
diff --git a/libraries/stdlib/js/src/kotlin/io/encoding/Base64Js.kt b/libraries/stdlib/js/src/kotlin/io/encoding/Base64Js.kt
new file mode 100644
index 00000000000..6bc4d295aee
--- /dev/null
+++ b/libraries/stdlib/js/src/kotlin/io/encoding/Base64Js.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2010-2023 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
+
+@SinceKotlin("1.8")
+@ExperimentalStdlibApi
+@kotlin.internal.InlineOnly
+internal actual inline fun Base64.platformCharsToBytes(source: CharSequence, startIndex: Int, endIndex: Int): ByteArray {
+ return charsToBytesImpl(source, startIndex, endIndex)
+}
+
+
+@SinceKotlin("1.8")
+@ExperimentalStdlibApi
+@kotlin.internal.InlineOnly
+internal actual inline fun Base64.platformEncodeToString(source: ByteArray, startIndex: Int, endIndex: Int): String {
+ val byteResult = encodeToByteArrayImpl(source, startIndex, endIndex)
+ return bytesToStringImpl(byteResult)
+}
+
+@SinceKotlin("1.8")
+@ExperimentalStdlibApi
+@kotlin.internal.InlineOnly
+internal actual inline fun Base64.platformEncodeIntoByteArray(
+ source: ByteArray,
+ destination: ByteArray,
+ destinationOffset: Int,
+ startIndex: Int,
+ endIndex: Int
+): Int {
+ return encodeIntoByteArrayImpl(source, destination, destinationOffset, startIndex, endIndex)
+}
+
+@SinceKotlin("1.8")
+@ExperimentalStdlibApi
+@kotlin.internal.InlineOnly
+internal actual inline fun Base64.platformEncodeToByteArray(
+ source: ByteArray,
+ startIndex: Int,
+ endIndex: Int
+): ByteArray {
+ return encodeToByteArrayImpl(source, startIndex, endIndex)
+}
\ No newline at end of file
diff --git a/libraries/stdlib/jvm/java9/module-info.java b/libraries/stdlib/jvm/java9/module-info.java
index b083bd95918..df69f1a693b 100644
--- a/libraries/stdlib/jvm/java9/module-info.java
+++ b/libraries/stdlib/jvm/java9/module-info.java
@@ -13,6 +13,7 @@ module kotlin.stdlib {
exports kotlin.coroutines.jvm.internal;
exports kotlin.enums;
exports kotlin.io;
+ exports kotlin.io.encoding;
exports kotlin.jvm;
exports kotlin.jvm.functions;
exports kotlin.math;
diff --git a/libraries/stdlib/jvm/src/kotlin/io/encoding/Base64IOStream.kt b/libraries/stdlib/jvm/src/kotlin/io/encoding/Base64IOStream.kt
new file mode 100644
index 00000000000..89f4e06bc77
--- /dev/null
+++ b/libraries/stdlib/jvm/src/kotlin/io/encoding/Base64IOStream.kt
@@ -0,0 +1,343 @@
+/*
+ * 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.
+ */
+
+@file:JvmMultifileClass
+@file:JvmName("StreamEncodingKt")
+
+package kotlin.io.encoding
+
+import java.io.IOException
+import java.io.InputStream
+import java.io.OutputStream
+import kotlin.io.encoding.Base64.Default.bytesPerGroup
+import kotlin.io.encoding.Base64.Default.mimeLineLength
+import kotlin.io.encoding.Base64.Default.mimeLineSeparatorSymbols
+import kotlin.io.encoding.Base64.Default.padSymbol
+import kotlin.io.encoding.Base64.Default.symbolsPerGroup
+
+/**
+ * Returns an input stream that decodes symbols from this input stream using the specified [base64] encoding.
+ *
+ * Reading from the returned input stream leads to reading some symbols from the underlying input stream.
+ * The symbols are decoded using the specified [base64] encoding and the resulting bytes are returned.
+ * Symbols are decoded in 4-symbol blocks.
+ *
+ * The symbols for decoding are not required to be padded.
+ * However, if there is a padding character present, the correct amount of padding character(s) must be present.
+ * The padding character `'='` is interpreted as the end of the symbol stream. Subsequent symbols are not read even if
+ * the end of the underlying input stream is not reached.
+ *
+ * The returned input stream should be closed in a timely manner. We suggest you try the [use] function,
+ * which closes the resource after a given block of code is executed.
+ * The close operation discards leftover bytes.
+ * Closing the returned input stream will close the underlying input stream.
+ */
+@SinceKotlin("1.8")
+@ExperimentalStdlibApi
+public fun InputStream.decodingWith(base64: Base64): InputStream {
+ return DecodeInputStream(this, base64)
+}
+
+/**
+ * Returns an output stream that encodes bytes using the specified [base64] encoding
+ * and writes the result to this output stream.
+ *
+ * The byte data written to the returned output stream is encoded using the specified [base64] encoding
+ * and the resulting symbols are written to the underlying output stream.
+ * Bytes are encoded in 3-byte blocks.
+ *
+ * The returned output stream should be closed in a timely manner. We suggest you try the [use] function,
+ * which closes the resource after a given block of code is executed.
+ * The close operation writes properly padded leftover symbols to the underlying output stream.
+ * Closing the returned output stream will close the underlying output stream.
+ */
+@SinceKotlin("1.8")
+@ExperimentalStdlibApi
+public fun OutputStream.encodingWith(base64: Base64): OutputStream {
+ return EncodeOutputStream(this, base64)
+}
+
+
+@ExperimentalStdlibApi
+private class DecodeInputStream(
+ private val input: InputStream,
+ private val base64: Base64
+) : InputStream() {
+ private var isClosed = false
+ private var isEOF = false
+ private val singleByteBuffer = ByteArray(1)
+
+ private val symbolBuffer = ByteArray(1024) // a multiple of symbolsPerGroup
+
+ private val byteBuffer = ByteArray(1024)
+ private var byteBufferStartIndex = 0
+ private var byteBufferEndIndex = 0
+ private val byteBufferLength: Int
+ get() = byteBufferEndIndex - byteBufferStartIndex
+
+ override fun read(): Int {
+ if (byteBufferStartIndex < byteBufferEndIndex) {
+ val byte = byteBuffer[byteBufferStartIndex].toInt() and 0xFF
+ byteBufferStartIndex += 1
+ resetByteBufferIfEmpty()
+ return byte
+ }
+ return when (read(singleByteBuffer, 0, 1)) {
+ -1 -> -1
+ 1 -> singleByteBuffer[0].toInt() and 0xFF
+ else -> error("Unreachable")
+ }
+ }
+
+ override fun read(destination: ByteArray, offset: Int, length: Int): Int {
+ if (offset < 0 || length < 0 || offset + length > destination.size) {
+ throw IndexOutOfBoundsException("offset: $offset, length: $length, buffer size: ${destination.size}")
+ }
+ if (isClosed) {
+ throw IOException("The input stream is closed.")
+ }
+ if (isEOF) {
+ return -1
+ }
+ if (length == 0) {
+ return 0
+ }
+
+ if (byteBufferLength >= length) {
+ copyByteBufferInto(destination, offset, length)
+ return length
+ }
+
+ val bytesNeeded = length - byteBufferLength
+ val groupsNeeded = (bytesNeeded + bytesPerGroup - 1) / bytesPerGroup
+ var symbolsNeeded = groupsNeeded * symbolsPerGroup
+
+ var dstOffset = offset
+
+ while (!isEOF && symbolsNeeded > 0) {
+ var symbolBufferLength = 0
+ val symbolsToRead = minOf(symbolBuffer.size, symbolsNeeded)
+
+ while (!isEOF && symbolBufferLength < symbolsToRead) {
+ when (val symbol = readNextSymbol()) {
+ -1 ->
+ isEOF = true
+ padSymbol.toInt() -> {
+ symbolBufferLength = handlePaddingSymbol(symbolBufferLength)
+ isEOF = true
+ }
+ else -> {
+ symbolBuffer[symbolBufferLength] = symbol.toByte()
+ symbolBufferLength += 1
+ }
+ }
+ }
+
+ check(isEOF || symbolBufferLength == symbolsToRead)
+
+ symbolsNeeded -= symbolBufferLength
+
+ dstOffset += decodeSymbolBufferInto(destination, dstOffset, length + offset, symbolBufferLength)
+ }
+
+ return if (dstOffset == offset && isEOF) -1 else dstOffset - offset
+ }
+
+ override fun close() {
+ if (!isClosed) {
+ isClosed = true
+ input.close()
+ }
+ }
+
+ // private functions
+
+ private fun decodeSymbolBufferInto(dst: ByteArray, dstOffset: Int, dstEndIndex: Int, symbolBufferLength: Int): Int {
+ byteBufferEndIndex += base64.decodeIntoByteArray(
+ symbolBuffer,
+ byteBuffer,
+ destinationOffset = byteBufferEndIndex,
+ startIndex = 0,
+ endIndex = symbolBufferLength
+ )
+
+ val bytesToCopy = minOf(byteBufferLength, dstEndIndex - dstOffset)
+ copyByteBufferInto(dst, dstOffset, bytesToCopy)
+ shiftByteBufferToStartIfNeeded()
+ return bytesToCopy
+ }
+
+ private fun copyByteBufferInto(dst: ByteArray, dstOffset: Int, length: Int) {
+ byteBuffer.copyInto(
+ dst,
+ dstOffset,
+ startIndex = byteBufferStartIndex,
+ endIndex = byteBufferStartIndex + length
+ )
+ byteBufferStartIndex += length
+ resetByteBufferIfEmpty()
+ }
+
+ private fun resetByteBufferIfEmpty() {
+ if (byteBufferStartIndex == byteBufferEndIndex) {
+ byteBufferStartIndex = 0
+ byteBufferEndIndex = 0
+ }
+ }
+
+ private fun shiftByteBufferToStartIfNeeded() {
+ // byte buffer should always have enough capacity to accommodate all symbols from symbol buffer
+ val byteBufferCapacity = byteBuffer.size - byteBufferEndIndex
+ val symbolBufferCapacity = symbolBuffer.size / symbolsPerGroup * bytesPerGroup
+ if (symbolBufferCapacity > byteBufferCapacity) {
+ byteBuffer.copyInto(byteBuffer, 0, byteBufferStartIndex, byteBufferEndIndex)
+ byteBufferEndIndex -= byteBufferStartIndex
+ byteBufferStartIndex = 0
+ }
+ }
+
+ private fun handlePaddingSymbol(symbolBufferLength: Int): Int {
+ symbolBuffer[symbolBufferLength] = padSymbol
+
+ return when (symbolBufferLength and 3) { // pads expected
+ 2 -> { // xx=
+ val secondPad = readNextSymbol()
+ if (secondPad >= 0) {
+ symbolBuffer[symbolBufferLength + 1] = secondPad.toByte()
+ }
+ symbolBufferLength + 2
+ }
+ else ->
+ symbolBufferLength + 1
+ }
+ }
+
+ private fun readNextSymbol(): Int {
+ if (!base64.isMimeScheme) {
+ return input.read()
+ }
+
+ var read: Int
+ do {
+ read = input.read()
+ } while (read != -1 && !isInMimeAlphabet(read))
+
+ return read
+ }
+}
+
+@ExperimentalStdlibApi
+private class EncodeOutputStream(
+ private val output: OutputStream,
+ private val base64: Base64
+) : OutputStream() {
+ private var isClosed = false
+
+ private var lineLength = if (base64.isMimeScheme) mimeLineLength else -1
+
+ private val symbolBuffer = ByteArray(1024)
+
+ private val byteBuffer = ByteArray(bytesPerGroup)
+ private var byteBufferLength = 0
+
+ override fun write(b: Int) {
+ checkOpen()
+ byteBuffer[byteBufferLength++] = b.toByte()
+ if (byteBufferLength == bytesPerGroup) {
+ encodeByteBufferIntoOutput()
+ }
+ }
+
+ override fun write(source: ByteArray, offset: Int, length: Int) {
+ checkOpen()
+ if (offset < 0 || length < 0 || offset + length > source.size) {
+ throw IndexOutOfBoundsException("offset: $offset, length: $length, source size: ${source.size}")
+ }
+ if (length == 0) {
+ return
+ }
+
+ check(byteBufferLength < bytesPerGroup)
+
+ var startIndex = offset
+ val endIndex = startIndex + length
+
+ if (byteBufferLength != 0) {
+ startIndex += copyIntoByteBuffer(source, startIndex, endIndex)
+ if (byteBufferLength != 0) {
+ return
+ }
+ }
+
+ while (startIndex + bytesPerGroup <= endIndex) {
+ val groupCapacity = (if (base64.isMimeScheme) lineLength else symbolBuffer.size) / symbolsPerGroup
+ val groupsToEncode = minOf(groupCapacity, (endIndex - startIndex) / bytesPerGroup)
+ val bytesToEncode = groupsToEncode * bytesPerGroup
+
+ val symbolsEncoded = encodeIntoOutput(source, startIndex, startIndex + bytesToEncode)
+ check(symbolsEncoded == groupsToEncode * symbolsPerGroup)
+
+ startIndex += bytesToEncode
+ }
+
+ source.copyInto(byteBuffer, destinationOffset = 0, startIndex, endIndex)
+ byteBufferLength = endIndex - startIndex
+ }
+
+ override fun flush() {
+ checkOpen()
+ output.flush()
+ }
+
+ override fun close() {
+ if (!isClosed) {
+ isClosed = true
+ if (byteBufferLength != 0) {
+ encodeByteBufferIntoOutput()
+ }
+ output.close()
+ }
+ }
+
+ // private functions
+
+ private fun copyIntoByteBuffer(source: ByteArray, startIndex: Int, endIndex: Int): Int {
+ val bytesToCopy = minOf(bytesPerGroup - byteBufferLength, endIndex - startIndex)
+ source.copyInto(byteBuffer, destinationOffset = byteBufferLength, startIndex, startIndex + bytesToCopy)
+ byteBufferLength += bytesToCopy
+ if (byteBufferLength == bytesPerGroup) {
+ encodeByteBufferIntoOutput()
+ }
+ return bytesToCopy
+ }
+
+ private fun encodeByteBufferIntoOutput() {
+ val symbolsEncoded = encodeIntoOutput(byteBuffer, 0, byteBufferLength)
+ check(symbolsEncoded == symbolsPerGroup)
+ byteBufferLength = 0
+ }
+
+ private fun encodeIntoOutput(source: ByteArray, startIndex: Int, endIndex: Int): Int {
+ val symbolsEncoded = base64.encodeIntoByteArray(
+ source,
+ symbolBuffer,
+ destinationOffset = 0,
+ startIndex,
+ endIndex
+ )
+ if (lineLength == 0) {
+ output.write(mimeLineSeparatorSymbols)
+ lineLength = mimeLineLength
+ check(symbolsEncoded <= mimeLineLength)
+ }
+ output.write(symbolBuffer, 0, symbolsEncoded)
+ lineLength -= symbolsEncoded
+ return symbolsEncoded
+ }
+
+ private fun checkOpen() {
+ if (isClosed) throw IOException("The output stream is closed.")
+ }
+}
\ No newline at end of file
diff --git a/libraries/stdlib/jvm/src/kotlin/io/encoding/Base64JVM.kt b/libraries/stdlib/jvm/src/kotlin/io/encoding/Base64JVM.kt
new file mode 100644
index 00000000000..2dc3e687407
--- /dev/null
+++ b/libraries/stdlib/jvm/src/kotlin/io/encoding/Base64JVM.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2010-2023 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
+
+@SinceKotlin("1.8")
+@ExperimentalStdlibApi
+@kotlin.internal.InlineOnly
+internal actual inline fun Base64.platformCharsToBytes(source: CharSequence, startIndex: Int, endIndex: Int): ByteArray {
+ return if (source is String) {
+ checkSourceBounds(source.length, startIndex, endIndex)
+ // up to 10x faster than the Common implementation
+ source.substring(startIndex, endIndex).toByteArray(Charsets.ISO_8859_1)
+ } else {
+ charsToBytesImpl(source, startIndex, endIndex)
+ }
+}
+
+
+@SinceKotlin("1.8")
+@ExperimentalStdlibApi
+@kotlin.internal.InlineOnly
+internal actual inline fun Base64.platformEncodeToString(source: ByteArray, startIndex: Int, endIndex: Int): String {
+// val subArray = if (startIndex == 0 && endIndex == source.size) {
+// source
+// } else {
+// source.copyOfRange(startIndex, endIndex)
+// }
+// return javaEncoder().encodeToString(subArray)
+ // TODO: Move to kotlin-stdlib-jdk8 and use the commented-out implementation above when KT-54970 gets fixed.
+ val byteResult = encodeToByteArrayImpl(source, startIndex, endIndex)
+ return String(byteResult, Charsets.ISO_8859_1)
+}
+
+@SinceKotlin("1.8")
+@ExperimentalStdlibApi
+@kotlin.internal.InlineOnly
+internal actual inline fun Base64.platformEncodeIntoByteArray(
+ source: ByteArray,
+ destination: ByteArray,
+ destinationOffset: Int,
+ startIndex: Int,
+ endIndex: Int
+): Int {
+// return if (destinationOffset == 0 && startIndex == 0 && endIndex == source.size) {
+// // up to 2x faster than the Common implementation
+// javaEncoder().encode(source, destination)
+// } else {
+// encodeIntoByteArrayImpl(source, destination, destinationOffset, startIndex, endIndex)
+// }
+ // TODO: Move to kotlin-stdlib-jdk8 and use the commented-out implementation above when KT-54970 gets fixed.
+ return encodeIntoByteArrayImpl(source, destination, destinationOffset, startIndex, endIndex)
+}
+
+@SinceKotlin("1.8")
+@ExperimentalStdlibApi
+@kotlin.internal.InlineOnly
+internal actual inline fun Base64.platformEncodeToByteArray(
+ source: ByteArray,
+ startIndex: Int,
+ endIndex: Int
+): ByteArray {
+// return if (startIndex == 0 && endIndex == source.size) {
+// // up to 2x faster than the Common implementation
+// javaEncoder().encode(source)
+// } else {
+// encodeToByteArrayImpl(source, startIndex, endIndex)
+// }
+ // TODO: Move to kotlin-stdlib-jdk8 and use the commented-out implementation above when KT-54970 gets fixed.
+ return encodeToByteArrayImpl(source, startIndex, endIndex)
+}
+
+//@SinceKotlin("1.8")
+//@ExperimentalStdlibApi
+//private fun Base64.javaEncoder(): java.util.Base64.Encoder {
+// return if (isMimeScheme) {
+// java.util.Base64.getMimeEncoder(Base64.mimeLineLength, Base64.mimeLineSeparatorSymbols)
+// } else if (isUrlSafe) {
+// java.util.Base64.getUrlEncoder()
+// } else {
+// java.util.Base64.getEncoder()
+// }
+//}
\ No newline at end of file
diff --git a/libraries/stdlib/jvm/test/io/Base64IOStreamTest.kt b/libraries/stdlib/jvm/test/io/Base64IOStreamTest.kt
new file mode 100644
index 00000000000..88cd242c16e
--- /dev/null
+++ b/libraries/stdlib/jvm/test/io/Base64IOStreamTest.kt
@@ -0,0 +1,371 @@
+/*
+ * 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
+
+import java.io.ByteArrayOutputStream
+import java.io.IOException
+import java.io.InputStream
+import java.io.OutputStream
+import kotlin.io.encoding.Base64
+import kotlin.io.encoding.decodingWith
+import kotlin.io.encoding.encodingWith
+import kotlin.test.*
+
+class Base64IOStreamTest {
+
+ private fun testCoding(base64: Base64, text: String, encodedText: String) {
+ val encodedBytes = ByteArray(encodedText.length) { encodedText[it].code.toByte() }
+ val bytes = ByteArray(text.length) { text[it].code.toByte() }
+ encodedBytes.inputStream().decodingWith(base64).use { inputStream ->
+ assertEquals(text, inputStream.reader().readText())
+ }
+ encodedBytes.inputStream().decodingWith(base64).use { inputStream ->
+ assertContentEquals(bytes, inputStream.readBytes())
+ }
+ ByteArrayOutputStream().let { outputStream ->
+ outputStream.encodingWith(base64).use {
+ it.write(bytes)
+ }
+ assertContentEquals(encodedBytes, outputStream.toByteArray())
+ }
+ }
+
+ @Test
+ fun base64() {
+ fun testBase64(text: String, encodedText: String) {
+ testCoding(Base64, text, encodedText)
+ testCoding(Base64.Mime, text, encodedText)
+ }
+
+ testBase64("", "")
+ testBase64("f", "Zg==")
+ testBase64("fo", "Zm8=")
+ testBase64("foo", "Zm9v")
+ testBase64("foob", "Zm9vYg==")
+ testBase64("fooba", "Zm9vYmE=")
+ testBase64("foobar", "Zm9vYmFy")
+ }
+
+ @Test
+ fun readDifferentOffsetAndLength() {
+ val repeat = 10_000
+ val symbols = "Zm9vYmFy".repeat(repeat) + "Zm8="
+ val expected = "foobar".repeat(repeat) + "fo"
+
+ val bytes = ByteArray(expected.length)
+
+ symbols.byteInputStream().decodingWith(Base64).use { input ->
+ var read = 0
+ repeat(6) {
+ bytes[read++] = input.read().toByte()
+ }
+
+ var toRead = 1
+ while (read < bytes.size) {
+ val length = minOf(toRead, bytes.size - read)
+ val result = input.read(bytes, read, length)
+
+ assertEquals(length, result)
+
+ read += result
+ toRead += toRead * 10 / 9
+ }
+
+ assertEquals(-1, input.read(bytes))
+ assertEquals(-1, input.read())
+ assertEquals(expected, bytes.decodeToString())
+ }
+ }
+
+ @Test
+ fun readDifferentOffsetAndLengthMime() {
+ val repeat = 10_000
+ val symbols = ("Zm9vYmFy".repeat(repeat) + "Zm8=").chunked(76).joinToString(separator = "\r\n")
+ val expected = "foobar".repeat(repeat) + "fo"
+
+ val bytes = ByteArray(expected.length)
+
+ symbols.byteInputStream().decodingWith(Base64.Mime).use { input ->
+ var read = 0
+ repeat(6) {
+ bytes[read++] = input.read().toByte()
+ }
+
+ var toRead = 1
+ while (read < bytes.size) {
+ val length = minOf(toRead, bytes.size - read)
+ val result = input.read(bytes, read, length)
+
+ assertEquals(length, result)
+
+ read += result
+ toRead += toRead * 10 / 9
+ }
+
+ assertEquals(-1, input.read(bytes))
+ assertEquals(-1, input.read())
+ assertEquals(expected, bytes.decodeToString())
+ }
+ }
+
+ @Test
+ fun writeDifferentOffsetAndLength() {
+ val repeat = 10_000
+ val bytes = ("foobar".repeat(repeat) + "fo").encodeToByteArray()
+ val expected = "Zm9vYmFy".repeat(repeat) + "Zm8="
+
+ val underlying = ByteArrayOutputStream()
+
+ underlying.encodingWith(Base64).use { output ->
+ var written = 0
+ repeat(8) {
+ output.write(bytes[written++].toInt())
+ }
+ var toWrite = 1
+ while (written < bytes.size) {
+ val length = minOf(toWrite, bytes.size - written)
+ output.write(bytes, written, length)
+
+ written += length
+ toWrite += toWrite * 10 / 9
+ }
+ }
+
+ assertEquals(expected, underlying.toString())
+ }
+
+ @Test
+ fun writeDifferentOffsetAndLengthMime() {
+ val repeat = 10_000
+ val bytes = ("foobar".repeat(repeat) + "fo").encodeToByteArray()
+ val expected = ("Zm9vYmFy".repeat(repeat) + "Zm8=").chunked(76).joinToString(separator = "\r\n")
+
+ val underlying = ByteArrayOutputStream()
+
+ underlying.encodingWith(Base64.Mime).use { output ->
+ var written = 0
+ repeat(8) {
+ output.write(bytes[written++].toInt())
+ }
+ var toWrite = 1
+ while (written < bytes.size) {
+ val length = minOf(toWrite, bytes.size - written)
+ output.write(bytes, written, length)
+
+ written += length
+ toWrite += toWrite * 10 / 9
+ }
+ }
+
+ assertEquals(expected, underlying.toString())
+ }
+
+
+ @Test
+ fun inputStreamClosesUnderlying() {
+ val underlying = object : InputStream() {
+ var isClosed: Boolean = false
+
+ override fun close() {
+ isClosed = true
+ super.close()
+ }
+
+ override fun read(): Int {
+ return 0
+ }
+ }
+ val wrapper = underlying.decodingWith(Base64)
+ wrapper.close()
+ assertTrue(underlying.isClosed)
+ }
+
+ @Test
+ fun outputStreamClosesUnderlying() {
+ val underlying = object : OutputStream() {
+ var isClosed: Boolean = false
+
+ override fun close() {
+ isClosed = true
+ super.close()
+ }
+
+ override fun write(b: Int) {
+ // ignore
+ }
+ }
+ val wrapper = underlying.encodingWith(Base64)
+ wrapper.close()
+ assertTrue(underlying.isClosed)
+ }
+
+
+ @Test
+ fun correctPadding() {
+ val inputStream = "Zg==Zg==".byteInputStream()
+ val wrapper = inputStream.decodingWith(Base64)
+
+ wrapper.use {
+ assertEquals('f'.code, it.read())
+ assertEquals(-1, it.read())
+ assertEquals(-1, it.read())
+
+ // in the wrapped IS the chars after the padding are not consumed
+ assertContentEquals("Zg==".toByteArray(), inputStream.readBytes())
+ }
+
+ assertFailsWith {
+ wrapper.read()
+ }
+ }
+
+
+ @Test
+ fun correctPaddingMime() {
+ val inputStream = "Zg==Zg==".byteInputStream()
+ val wrapper = inputStream.decodingWith(Base64.Mime)
+
+ wrapper.use {
+ assertEquals('f'.code, it.read())
+ assertEquals(-1, it.read())
+ assertEquals(-1, it.read())
+
+ // in the wrapped IS the chars after the padding are not consumed
+ assertContentEquals("Zg==".toByteArray(), inputStream.readBytes())
+ }
+
+ // closed
+ assertFailsWith {
+ wrapper.read()
+ }
+ }
+
+ @Test
+ fun illegalSymbol() {
+ val inputStream = "Zm\u00FF9vYg==".byteInputStream()
+ val wrapper = inputStream.decodingWith(Base64)
+
+ wrapper.use {
+ // one group of 4 symbols is read for decoding, that group includes illegal '\u00FF'
+ assertFailsWith {
+ it.read()
+ }
+ }
+
+ // closed
+ assertFailsWith {
+ wrapper.read()
+ }
+ }
+
+ @Test
+ fun illegalSymbolMime() {
+ val inputStream = "Zm\u00FF9vYg==".byteInputStream()
+ val wrapper = inputStream.decodingWith(Base64.Mime)
+
+ wrapper.use {
+ assertEquals('f'.code, it.read())
+ assertEquals('o'.code, it.read())
+ assertEquals('o'.code, it.read())
+ assertEquals('b'.code, it.read())
+ assertEquals(-1, it.read())
+ assertEquals(-1, it.read())
+ }
+
+ // closed
+ assertFailsWith {
+ wrapper.read()
+ }
+ }
+
+ @Test
+ fun incorrectPadding() {
+ for (base64 in listOf(Base64, Base64.Mime)) {
+ val inputStream = "Zm9vZm=9v".byteInputStream()
+ val wrapper = inputStream.decodingWith(base64)
+
+ wrapper.use {
+ assertEquals('f'.code, it.read())
+ assertEquals('o'.code, it.read())
+ assertEquals('o'.code, it.read())
+
+ // the second group is incorrectly padded
+ assertFailsWith {
+ it.read()
+ }
+ }
+
+ // closed
+ assertFailsWith {
+ wrapper.read()
+ }
+ }
+ }
+
+ @Test
+ fun withoutPadding() {
+ for (base64 in listOf(Base64, Base64.Mime)) {
+ val inputStream = "Zm9vYg".byteInputStream()
+ val wrapper = inputStream.decodingWith(base64)
+
+ wrapper.use {
+ assertEquals('f'.code, it.read())
+ assertEquals('o'.code, it.read())
+ assertEquals('o'.code, it.read())
+ assertEquals('b'.code, it.read())
+ assertEquals(-1, it.read())
+ assertEquals(-1, it.read())
+ }
+
+ // closed
+ assertFailsWith {
+ wrapper.read()
+ }
+ }
+ }
+
+ @Test
+ fun separatedPadSymbols() {
+ val inputStream = "Zm9vYg=[,.|^&*@#]=".byteInputStream()
+ val wrapper = inputStream.decodingWith(Base64)
+
+ wrapper.use {
+ assertEquals('f'.code, it.read())
+ assertEquals('o'.code, it.read())
+ assertEquals('o'.code, it.read())
+
+ // the second group contains illegal symbols
+ assertFailsWith {
+ it.read()
+ }
+ }
+
+ // closed
+ assertFailsWith {
+ wrapper.read()
+ }
+ }
+
+ @Test
+ fun separatedPadSymbolsMime() {
+ val inputStream = "Zm9vYg=[,.|^&*@#]=".byteInputStream()
+ val wrapper = inputStream.decodingWith(Base64.Mime)
+
+ wrapper.use {
+ assertEquals('f'.code, it.read())
+ assertEquals('o'.code, it.read())
+ assertEquals('o'.code, it.read())
+ assertEquals('b'.code, it.read())
+ assertEquals(-1, it.read())
+ assertEquals(-1, it.read())
+ }
+
+ // closed
+ assertFailsWith {
+ wrapper.read()
+ }
+ }
+}
diff --git a/libraries/stdlib/native-wasm/src/kotlin/io/encoding/Base64.kt b/libraries/stdlib/native-wasm/src/kotlin/io/encoding/Base64.kt
new file mode 100644
index 00000000000..6bc4d295aee
--- /dev/null
+++ b/libraries/stdlib/native-wasm/src/kotlin/io/encoding/Base64.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2010-2023 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
+
+@SinceKotlin("1.8")
+@ExperimentalStdlibApi
+@kotlin.internal.InlineOnly
+internal actual inline fun Base64.platformCharsToBytes(source: CharSequence, startIndex: Int, endIndex: Int): ByteArray {
+ return charsToBytesImpl(source, startIndex, endIndex)
+}
+
+
+@SinceKotlin("1.8")
+@ExperimentalStdlibApi
+@kotlin.internal.InlineOnly
+internal actual inline fun Base64.platformEncodeToString(source: ByteArray, startIndex: Int, endIndex: Int): String {
+ val byteResult = encodeToByteArrayImpl(source, startIndex, endIndex)
+ return bytesToStringImpl(byteResult)
+}
+
+@SinceKotlin("1.8")
+@ExperimentalStdlibApi
+@kotlin.internal.InlineOnly
+internal actual inline fun Base64.platformEncodeIntoByteArray(
+ source: ByteArray,
+ destination: ByteArray,
+ destinationOffset: Int,
+ startIndex: Int,
+ endIndex: Int
+): Int {
+ return encodeIntoByteArrayImpl(source, destination, destinationOffset, startIndex, endIndex)
+}
+
+@SinceKotlin("1.8")
+@ExperimentalStdlibApi
+@kotlin.internal.InlineOnly
+internal actual inline fun Base64.platformEncodeToByteArray(
+ source: ByteArray,
+ startIndex: Int,
+ endIndex: Int
+): ByteArray {
+ return encodeToByteArrayImpl(source, startIndex, endIndex)
+}
\ No newline at end of file
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 00000000000..ae005965f0e
--- /dev/null
+++ b/libraries/stdlib/src/kotlin/io/encoding/Base64.kt
@@ -0,0 +1,647 @@
+/*
+ * 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
+
+import kotlin.native.concurrent.SharedImmutable
+
+/**
+ * Provides Base64 encoding and decoding functionality.
+ *
+ * This class is not supposed to be instantiated or inherited.
+ * However, predefined instances of this class are available for use.
+ * The companion object [Base64.Default] is the default instance of [Base64].
+ * There are also [Base64.UrlSafe] and [Base64.Mime] instances.
+ */
+@SinceKotlin("1.8")
+@ExperimentalStdlibApi
+public open class Base64 private constructor(
+ internal val isUrlSafe: Boolean,
+ internal val isMimeScheme: Boolean
+) {
+ init {
+ require(!isUrlSafe || !isMimeScheme)
+ }
+
+ /**
+ * Encodes bytes from the specified [source] array or its subrange.
+ * Returns a [ByteArray] containing the resulting symbols.
+ *
+ * If the size of the [source] array or its subrange is not an integral multiple of 3,
+ * the result is padded with `'='` to an integral multiple of 4 symbols.
+ *
+ * Each resulting symbol occupies one byte in the returned byte array.
+ *
+ * Use [encode] to get the output in string form.
+ *
+ * @param source the array to encode bytes from.
+ * @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 a [ByteArray] with the resulting symbols.
+ */
+ public fun encodeToByteArray(source: ByteArray, startIndex: Int = 0, endIndex: Int = source.size): ByteArray {
+ return platformEncodeToByteArray(source, startIndex, endIndex)
+ }
+
+ /**
+ * Encodes bytes from the specified [source] array or its subrange and writes resulting symbols into the [destination] array.
+ * Returns the number of symbols written.
+ *
+ * If the size of the [source] array or its subrange is not an integral multiple of 3,
+ * the result is padded with `'='` to an integral multiple of 4 symbols.
+ *
+ * @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 encodeIntoByteArray(
+ source: ByteArray,
+ destination: ByteArray,
+ destinationOffset: Int = 0,
+ startIndex: Int = 0,
+ endIndex: Int = source.size
+ ): Int {
+ return platformEncodeIntoByteArray(source, destination, destinationOffset, startIndex, endIndex)
+ }
+
+ /**
+ * Encodes bytes from the specified [source] array or its subrange.
+ * Returns a string with the resulting symbols.
+ *
+ * If the size of the [source] array or its subrange is not an integral multiple of 3,
+ * the result is padded with `'='` to an integral multiple of 4 symbols.
+ *
+ * Use [encodeToByteArray] to get the output in [ByteArray] form.
+ *
+ * @param source the array to encode bytes from.
+ * @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 a string with the resulting symbols.
+ */
+ public fun encode(source: ByteArray, startIndex: Int = 0, endIndex: Int = source.size): String {
+ return platformEncodeToString(source, startIndex, endIndex)
+ }
+
+ /**
+ * Encodes bytes from the specified [source] array or its subrange and appends resulting symbols to the [destination] appendable.
+ * Returns the destination appendable.
+ *
+ * If the size of the [source] array or its subrange is not an integral multiple of 3,
+ * the result is padded with `'='` to an integral multiple of 4 symbols.
+ *
+ * @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 destination appendable.
+ */
+ public fun encodeToAppendable(
+ source: ByteArray,
+ destination: A,
+ startIndex: Int = 0,
+ endIndex: Int = source.size
+ ): A {
+ val stringResult = platformEncodeToString(source, startIndex, endIndex)
+ destination.append(stringResult)
+ return destination
+ }
+
+ /**
+ * Decodes symbols from the specified [source] array or its subrange.
+ * Returns a [ByteArray] containing the resulting bytes.
+ *
+ * The symbols for decoding are not required to be padded.
+ * However, if there is a padding character present, the correct amount of padding character(s) must be present.
+ * The padding character `'='` is interpreted as the end of the encoded byte data. Subsequent symbols are prohibited.
+ *
+ * @param source the array to decode symbols from.
+ * @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 IllegalArgumentException when the symbols for decoding are padded incorrectly or there are extra symbols after the padding.
+ *
+ * @return a [ByteArray] with the resulting bytes.
+ */
+ public fun decode(source: ByteArray, startIndex: Int = 0, endIndex: Int = source.size): ByteArray {
+ checkSourceBounds(source.size, startIndex, endIndex)
+
+ val decodeSize = decodeSize(source, startIndex, endIndex)
+ val destination = ByteArray(decodeSize)
+
+ val bytesWritten = decodeImpl(source, destination, 0, startIndex, endIndex)
+
+ check(bytesWritten == destination.size)
+
+ return destination
+ }
+
+ /**
+ * Decodes symbols from the specified [source] array or its subrange and writes resulting bytes into the [destination] array.
+ * Returns the number of bytes written.
+ *
+ * The symbols for decoding are not required to be padded.
+ * However, if there is a padding character present, the correct amount of padding character(s) must be present.
+ * The padding character `'='` is interpreted as the end of the encoded byte data. Subsequent symbols are prohibited.
+ *
+ * @param source the array to decode symbols from.
+ * @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.
+ * @throws IllegalArgumentException when the symbols for decoding are padded incorrectly or there are extra symbols after the padding.
+ *
+ * @return the number of bytes written into [destination] array.
+ */
+ public fun decodeIntoByteArray(
+ source: ByteArray,
+ destination: ByteArray,
+ destinationOffset: Int = 0,
+ startIndex: Int = 0,
+ endIndex: Int = source.size
+ ): Int {
+ checkSourceBounds(source.size, startIndex, endIndex)
+ checkDestinationBounds(destination.size, destinationOffset, decodeSize(source, startIndex, endIndex))
+
+ return decodeImpl(source, destination, destinationOffset, startIndex, endIndex)
+ }
+
+ /**
+ * Decodes symbols from the specified [source] char sequence or its substring.
+ * Returns a [ByteArray] containing the resulting bytes.
+ *
+ * The symbols for decoding are not required to be padded.
+ * However, if there is a padding character present, the correct amount of padding character(s) must be present.
+ * The padding character `'='` is interpreted as the end of the encoded byte data. Subsequent symbols are prohibited.
+ *
+ * @param source the char sequence to decode symbols from.
+ * @param startIndex the beginning (inclusive) of the substring to decode, 0 by default.
+ * @param endIndex the end (exclusive) of the substring to decode, length of the [source] by default.
+ *
+ * @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of [source] indices.
+ * @throws IllegalArgumentException when `startIndex > endIndex`.
+ * @throws IllegalArgumentException when the symbols for decoding are padded incorrectly or there are extra symbols after the padding.
+ *
+ * @return a [ByteArray] with the resulting bytes.
+ */
+ public fun decode(source: CharSequence, startIndex: Int = 0, endIndex: Int = source.length): ByteArray {
+ val byteSource = platformCharsToBytes(source, startIndex, endIndex)
+ return decode(byteSource)
+ }
+
+ /**
+ * Decodes symbols from the specified [source] char sequence or its substring and writes resulting bytes into the [destination] array.
+ * Returns the number of bytes written.
+ *
+ * The symbols for decoding are not required to be padded.
+ * However, if there is a padding character present, the correct amount of padding character(s) must be present.
+ * The padding character `'='` is interpreted as the end of the encoded byte data. Subsequent symbols are prohibited.
+ *
+ * @param source the char sequence to decode symbols from.
+ * @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 substring to decode, 0 by default.
+ * @param endIndex the end (exclusive) of the substring to decode, length of the [source] by default.
+ *
+ * @throws IndexOutOfBoundsException when [startIndex] or [endIndex] is out of range of [source] 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.
+ * @throws IllegalArgumentException when the symbols for decoding are padded incorrectly or there are extra symbols after the padding.
+ *
+ * @return the number of bytes written into [destination] array.
+ */
+ public fun decodeIntoByteArray(
+ source: CharSequence,
+ destination: ByteArray,
+ destinationOffset: Int = 0,
+ startIndex: Int = 0,
+ endIndex: Int = source.length
+ ): Int {
+ val byteSource = platformCharsToBytes(source, startIndex, endIndex)
+ return decodeIntoByteArray(byteSource, destination, destinationOffset)
+ }
+
+ // internal functions
+
+ internal fun encodeToByteArrayImpl(source: ByteArray, startIndex: Int, endIndex: Int): ByteArray {
+ checkSourceBounds(source.size, startIndex, endIndex)
+
+ val encodeSize = encodeSize(endIndex - startIndex)
+ val destination = ByteArray(encodeSize)
+ encodeIntoByteArrayImpl(source, destination, 0, startIndex, endIndex)
+ return destination
+ }
+
+ internal fun encodeIntoByteArrayImpl(
+ source: ByteArray,
+ destination: ByteArray,
+ destinationOffset: Int,
+ startIndex: Int,
+ endIndex: Int
+ ): Int {
+ checkSourceBounds(source.size, startIndex, endIndex)
+ checkDestinationBounds(destination.size, destinationOffset, encodeSize(endIndex - startIndex))
+
+ val encodeMap = if (isUrlSafe) base64UrlEncodeMap else base64EncodeMap
+ var sourceIndex = startIndex
+ var destinationIndex = destinationOffset
+ val groupsPerLine = if (isMimeScheme) mimeGroupsPerLine else Int.MAX_VALUE
+
+ while (sourceIndex + 2 < endIndex) {
+ val groups = minOf((endIndex - sourceIndex) / bytesPerGroup, groupsPerLine)
+ for (i in 0 until groups) {
+ val byte1 = source[sourceIndex++].toInt() and 0xFF
+ val byte2 = source[sourceIndex++].toInt() and 0xFF
+ val byte3 = source[sourceIndex++].toInt() and 0xFF
+ val bits = (byte1 shl 16) or (byte2 shl 8) or byte3
+ destination[destinationIndex++] = encodeMap[bits ushr 18]
+ destination[destinationIndex++] = encodeMap[(bits ushr 12) and 0x3F]
+ destination[destinationIndex++] = encodeMap[(bits ushr 6) and 0x3F]
+ destination[destinationIndex++] = encodeMap[bits and 0x3F]
+ }
+ if (groups == groupsPerLine && sourceIndex != endIndex) {
+ destination[destinationIndex++] = mimeLineSeparatorSymbols[0]
+ destination[destinationIndex++] = mimeLineSeparatorSymbols[1]
+ }
+ }
+
+ when (endIndex - sourceIndex) {
+ 1 -> {
+ val byte1 = source[sourceIndex++].toInt() and 0xFF
+ val bits = byte1 shl 4
+ destination[destinationIndex++] = encodeMap[bits ushr 6]
+ destination[destinationIndex++] = encodeMap[bits and 0x3F]
+ destination[destinationIndex++] = padSymbol
+ destination[destinationIndex++] = padSymbol
+ }
+ 2 -> {
+ val byte1 = source[sourceIndex++].toInt() and 0xFF
+ val byte2 = source[sourceIndex++].toInt() and 0xFF
+ val bits = (byte1 shl 10) or (byte2 shl 2)
+ destination[destinationIndex++] = encodeMap[bits ushr 12]
+ destination[destinationIndex++] = encodeMap[(bits ushr 6) and 0x3F]
+ destination[destinationIndex++] = encodeMap[bits and 0x3F]
+ destination[destinationIndex++] = padSymbol
+ }
+ }
+
+ check(sourceIndex == endIndex)
+
+ return destinationIndex - destinationOffset
+ }
+
+ private fun encodeSize(sourceSize: Int): Int {
+ // includes padding chars
+ val groups = (sourceSize + bytesPerGroup - 1) / bytesPerGroup
+ val lineSeparators = if (isMimeScheme) (groups - 1) / mimeGroupsPerLine else 0
+ val size = groups * symbolsPerGroup + lineSeparators * 2
+ if (size < 0) { // Int overflow
+ throw IllegalArgumentException("Input is too big")
+ }
+ return size
+ }
+
+ private fun decodeImpl(
+ source: ByteArray,
+ destination: ByteArray,
+ destinationOffset: Int,
+ startIndex: Int,
+ endIndex: Int
+ ): Int {
+ val decodeMap = if (isUrlSafe) base64UrlDecodeMap else base64DecodeMap
+ var payload = 0
+ var byteStart = -bitsPerByte
+ var sourceIndex = startIndex
+ var destinationIndex = destinationOffset
+
+ while (sourceIndex < endIndex) {
+ if (byteStart == -bitsPerByte && sourceIndex + 3 < endIndex) {
+ val symbol1 = decodeMap[source[sourceIndex++].toInt() and 0xFF]
+ val symbol2 = decodeMap[source[sourceIndex++].toInt() and 0xFF]
+ val symbol3 = decodeMap[source[sourceIndex++].toInt() and 0xFF]
+ val symbol4 = decodeMap[source[sourceIndex++].toInt() and 0xFF]
+ val bits = (symbol1 shl 18) or (symbol2 shl 12) or (symbol3 shl 6) or symbol4
+ if (bits >= 0) { // all base64 symbols
+ destination[destinationIndex++] = (bits shr 16).toByte()
+ destination[destinationIndex++] = (bits shr 8).toByte()
+ destination[destinationIndex++] = bits.toByte()
+ continue
+ }
+ sourceIndex -= 4
+ }
+
+ val symbol = source[sourceIndex].toInt() and 0xFF
+ val symbolBits = decodeMap[symbol]
+ if (symbolBits < 0) {
+ if (symbolBits == -2) {
+ sourceIndex = handlePaddingSymbol(source, sourceIndex, endIndex, byteStart)
+ break
+ } else if (isMimeScheme) {
+ sourceIndex += 1
+ continue
+ } else {
+ throw IllegalArgumentException("Invalid symbol '${symbol.toChar()}'(${symbol.toString(radix = 8)}) at index $sourceIndex")
+ }
+ } else {
+ sourceIndex += 1
+ }
+
+ 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
+ }
+ }
+
+ // pad or end of input
+
+ if (byteStart == -bitsPerByte + bitsPerSymbol) { // dangling single symbol, incorrectly encoded
+ throw IllegalArgumentException("The last unit of input does not have enough bits")
+ }
+
+// check(payload == 0) // the padded bits are allowed to be non-zero
+
+ sourceIndex = skipIllegalSymbolsIfMime(source, sourceIndex, endIndex)
+ if (sourceIndex < endIndex) {
+ val symbol = source[sourceIndex].toInt() and 0xFF
+ throw IllegalArgumentException("Symbol '${symbol.toChar()}'(${symbol.toString(radix = 8)}) at index ${sourceIndex - 1} is prohibited after the pad character")
+ }
+
+ return destinationIndex - destinationOffset
+ }
+
+ private fun decodeSize(source: ByteArray, startIndex: Int, endIndex: Int): Int {
+ var symbols = endIndex - startIndex
+ if (symbols == 0) {
+ return 0
+ }
+ if (symbols == 1) {
+ throw IllegalArgumentException("Input should have at list 2 symbols for Base64 decoding, startIndex: $startIndex, endIndex: $endIndex")
+ }
+ if (isMimeScheme) {
+ for (index in startIndex until endIndex) {
+ val symbol = source[index].toInt() and 0xFF
+ val symbolBits = base64DecodeMap[symbol]
+ if (symbolBits < 0) {
+ if (symbolBits == -2) {
+ symbols -= endIndex - index
+ break
+ }
+ symbols--
+ }
+ }
+ } else if (source[endIndex - 1] == padSymbol) {
+ symbols--
+ if (source[endIndex - 2] == padSymbol) {
+ symbols--
+ }
+ }
+ return ((symbols.toLong() * bitsPerSymbol) / bitsPerByte).toInt() // conversion due to possible Int overflow
+ }
+
+ internal fun charsToBytesImpl(source: CharSequence, startIndex: Int, endIndex: Int): ByteArray {
+ checkSourceBounds(source.length, startIndex, endIndex)
+
+ val byteArray = ByteArray(endIndex - startIndex)
+ var length = 0
+ for (index in startIndex until endIndex) {
+ val symbol = source[index].code
+ if (symbol <= 0xFF) {
+ byteArray[length++] = symbol.toByte()
+ } else {
+ // the replacement byte must be an illegal symbol
+ // so that mime skips it and basic throws with correct index
+ byteArray[length++] = 0x3F
+ }
+ }
+ return byteArray
+ }
+
+ internal fun bytesToStringImpl(source: ByteArray): String {
+ val stringBuilder = StringBuilder(source.size)
+ for (byte in source) {
+ stringBuilder.append(byte.toInt().toChar())
+ }
+ return stringBuilder.toString()
+ }
+
+ private fun handlePaddingSymbol(source: ByteArray, padIndex: Int, endIndex: Int, byteStart: Int): Int {
+ return when (byteStart) {
+ -bitsPerByte -> // =
+ throw IllegalArgumentException("Redundant pad character at index $padIndex")
+ -bitsPerByte + bitsPerSymbol -> // x=, dangling single symbol
+ padIndex + 1
+ -bitsPerByte + 2 * bitsPerSymbol - bitsPerByte -> { // xx=
+ val secondPadIndex = skipIllegalSymbolsIfMime(source, padIndex + 1, endIndex)
+ if (secondPadIndex == endIndex || source[secondPadIndex] != padSymbol) {
+ throw IllegalArgumentException("Missing one pad character at index $secondPadIndex")
+ }
+ secondPadIndex + 1
+ }
+ -bitsPerByte + 3 * bitsPerSymbol - 2 * bitsPerByte -> // xxx=
+ padIndex + 1
+ else ->
+ error("Unreachable")
+ }
+ }
+
+ private fun skipIllegalSymbolsIfMime(source: ByteArray, startIndex: Int, endIndex: Int): Int {
+ if (!isMimeScheme) {
+ return startIndex
+ }
+ var sourceIndex = startIndex
+ while (sourceIndex < endIndex) {
+ val symbol = source[sourceIndex].toInt() and 0xFF
+ if (base64DecodeMap[symbol] != -1) {
+ return sourceIndex
+ }
+ sourceIndex += 1
+ }
+ return sourceIndex
+ }
+
+ internal fun checkSourceBounds(sourceSize: Int, startIndex: Int, endIndex: Int) {
+ AbstractList.checkBoundsIndexes(startIndex, endIndex, sourceSize)
+ }
+
+ private fun checkDestinationBounds(destinationSize: Int, destinationOffset: Int, capacityNeeded: Int) {
+ if (destinationOffset < 0 || destinationOffset > destinationSize) {
+ throw IndexOutOfBoundsException("destination offset: $destinationOffset, destination size: $destinationSize")
+ }
+
+ val destinationEndIndex = destinationOffset + capacityNeeded
+ if (destinationEndIndex < 0 || destinationEndIndex > destinationSize) {
+ throw IndexOutOfBoundsException(
+ "The destination array does not have enough capacity, " +
+ "destination offset: $destinationOffset, destination size: $destinationSize, capacity needed: $capacityNeeded"
+ )
+ }
+ }
+
+ // companion object
+
+ /**
+ * The "base64" encoding specified by [`RFC 4648 section 4`](https://www.rfc-editor.org/rfc/rfc4648#section-4),
+ * Base 64 Encoding.
+ *
+ * Uses "The Base 64 Alphabet" as specified in Table 1 of RFC 4648 for encoding and decoding.
+ * Encode operation does not add any line separator character.
+ * Decode operation throws if it encounters a character outside the base64 alphabet.
+ *
+ * The character `'='` is used for padding.
+ */
+ public companion object Default : Base64(isUrlSafe = false, isMimeScheme = false) {
+
+ private const val bitsPerByte: Int = 8
+ private const val bitsPerSymbol: Int = 6
+
+ internal const val bytesPerGroup: Int = 3
+ internal const val symbolsPerGroup: Int = 4
+
+ internal const val padSymbol: Byte = 61 // '='
+
+ internal const val mimeLineLength: Int = 76
+ private const val mimeGroupsPerLine: Int = mimeLineLength / symbolsPerGroup
+ internal val mimeLineSeparatorSymbols: ByteArray = byteArrayOf('\r'.code.toByte(), '\n'.code.toByte())
+
+ /**
+ * The "base64url" encoding specified by [`RFC 4648 section 5`](https://www.rfc-editor.org/rfc/rfc4648#section-5),
+ * Base 64 Encoding with URL and Filename Safe Alphabet.
+ *
+ * Uses "The URL and Filename safe Base 64 Alphabet" as specified in Table 1 of RFC 4648 for encoding and decoding.
+ * Encode operation does not add any line separator character.
+ * Decode operation throws if it encounters a character outside the base64url alphabet.
+ *
+ * The character `'='` is used for padding.
+ */
+ public val UrlSafe: Base64 = Base64(isUrlSafe = true, isMimeScheme = false)
+
+ /**
+ * The encoding specified by [`RFC 2045 section 6.8`](https://www.rfc-editor.org/rfc/rfc2045#section-6.8),
+ * Base64 Content-Transfer-Encoding.
+ *
+ * Uses "The Base64 Alphabet" as specified in Table 1 of RFC 2045 for encoding and decoding.
+ * Encode operation adds CRLF every 76 symbols. No line separator is added to the end of the encoded output.
+ * Decode operation ignores all line separators and other characters outside the base64 alphabet.
+ *
+ * The character `'='` is used for padding.
+ */
+ public val Mime: Base64 = Base64(isUrlSafe = false, isMimeScheme = true)
+ }
+}
+
+
+// "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
+@SharedImmutable
+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 */
+)
+
+@ExperimentalStdlibApi
+@SharedImmutable
+private val base64DecodeMap = IntArray(256).apply {
+ this.fill(-1)
+ this[Base64.padSymbol.toInt()] = -2
+ base64EncodeMap.forEachIndexed { index, symbol ->
+ this[symbol.toInt()] = index
+ }
+}
+
+// "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
+@SharedImmutable
+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 */
+)
+
+@ExperimentalStdlibApi
+@SharedImmutable
+private val base64UrlDecodeMap = IntArray(256).apply {
+ this.fill(-1)
+ this[Base64.padSymbol.toInt()] = -2
+ base64UrlEncodeMap.forEachIndexed { index, symbol ->
+ this[symbol.toInt()] = index
+ }
+}
+
+
+@SinceKotlin("1.8")
+@ExperimentalStdlibApi
+internal fun isInMimeAlphabet(symbol: Int): Boolean {
+ return symbol in base64DecodeMap.indices && base64DecodeMap[symbol] != -1
+}
+
+
+@SinceKotlin("1.8")
+@ExperimentalStdlibApi
+internal expect fun Base64.platformCharsToBytes(
+ source: CharSequence,
+ startIndex: Int,
+ endIndex: Int
+): ByteArray
+
+
+@SinceKotlin("1.8")
+@ExperimentalStdlibApi
+internal expect fun Base64.platformEncodeToString(
+ source: ByteArray,
+ startIndex: Int,
+ endIndex: Int
+): String
+
+@SinceKotlin("1.8")
+@ExperimentalStdlibApi
+internal expect fun Base64.platformEncodeIntoByteArray(
+ source: ByteArray,
+ destination: ByteArray,
+ destinationOffset: Int,
+ startIndex: Int,
+ endIndex: Int
+): Int
+
+@SinceKotlin("1.8")
+@ExperimentalStdlibApi
+internal expect fun Base64.platformEncodeToByteArray(
+ source: ByteArray,
+ startIndex: Int,
+ endIndex: Int
+): ByteArray
\ 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 00000000000..a321f1d4450
--- /dev/null
+++ b/libraries/stdlib/test/io.encoding/Base64Test.kt
@@ -0,0 +1,275 @@
+/*
+ * 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 testEncode(codec: Base64, bytes: ByteArray, expected: String) {
+ assertEquals(expected, codec.encode(bytes))
+ assertContentEquals(expected.encodeToByteArray(), codec.encodeToByteArray(bytes))
+ }
+
+ private fun testDecode(codec: Base64, symbols: String, expected: ByteArray) {
+ assertContentEquals(expected, codec.decode(symbols))
+ assertContentEquals(expected, codec.decode(symbols.encodeToByteArray()))
+ }
+
+ private fun testCoding(codec: Base64, bytes: ByteArray, symbols: String) {
+ testEncode(codec, bytes, symbols)
+ testDecode(codec, symbols, bytes)
+ }
+
+ private fun bytes(vararg values: Int): ByteArray {
+ return ByteArray(values.size) { values[it].toByte() }
+ }
+
+ private val codecs = listOf(
+ Base64 to "Basic",
+ Base64.UrlSafe to "UrlSafe",
+ Base64.Mime to "Mime"
+ )
+
+ @Test
+ fun index() {
+ val bytes = bytes(0b0000_0100, 0b0010_0000, 0b1100_0100, 0b0001_0100, 0b0110_0001, 0b1100_1000)
+ val symbols = "BCDEFGHI"
+
+ // encode
+ for ((base64, scheme) in codecs) {
+ testEncode(base64, bytes, symbols)
+ assertFailsWith(scheme) { base64.encode(bytes, startIndex = -1) }
+ assertFailsWith(scheme) { base64.encode(bytes, endIndex = bytes.size + 1) }
+ assertFailsWith(scheme) { base64.encode(bytes, startIndex = bytes.size + 1) }
+ assertFailsWith(scheme) { base64.encode(bytes, startIndex = 3, endIndex = 0) }
+
+ assertEquals(symbols.substring(0, 4), base64.encode(bytes, endIndex = 3))
+ assertEquals(symbols.substring(4), base64.encode(bytes, startIndex = 3))
+
+ val destination = StringBuilder()
+ base64.encodeToAppendable(bytes, destination, endIndex = 3)
+ assertEquals(symbols.substring(0, 4), destination.toString())
+ base64.encodeToAppendable(bytes, destination, startIndex = 3)
+ assertEquals(symbols, destination.toString())
+ }
+
+ // encodeToByteArray
+ for ((base64, scheme) in codecs) {
+ assertFailsWith(scheme) { base64.encodeToByteArray(bytes, startIndex = -1) }
+ assertFailsWith(scheme) { base64.encodeToByteArray(bytes, endIndex = bytes.size + 1) }
+ assertFailsWith(scheme) { base64.encodeToByteArray(bytes, startIndex = bytes.size + 1) }
+ assertFailsWith(scheme) { base64.encodeToByteArray(bytes, startIndex = 3, endIndex = 0) }
+
+ assertContentEquals(symbols.encodeToByteArray(0, 4), base64.encodeToByteArray(bytes, endIndex = 3))
+ assertContentEquals(symbols.encodeToByteArray(4), base64.encodeToByteArray(bytes, startIndex = 3))
+
+ val destination = ByteArray(8)
+ assertFailsWith { base64.encodeIntoByteArray(bytes, destination, destinationOffset = -1) }
+ assertFailsWith { base64.encodeIntoByteArray(bytes, destination, destinationOffset = destination.size + 1) }
+ assertFailsWith { base64.encodeIntoByteArray(bytes, destination, destinationOffset = 1) }
+
+ assertTrue(destination.all { it == 0.toByte() })
+
+ var length = base64.encodeIntoByteArray(bytes, destination, endIndex = 3)
+ assertContentEquals(symbols.encodeToByteArray(0, 4), destination.copyOf(length))
+ length += base64.encodeIntoByteArray(bytes, destination, destinationOffset = length, startIndex = 3)
+ assertContentEquals(symbols.encodeToByteArray(), destination)
+ }
+
+ // decode(CharSequence)
+ for ((base64, scheme) in codecs) {
+ testDecode(base64, symbols, bytes)
+ assertFailsWith(scheme) { base64.decode(symbols, startIndex = -1) }
+ assertFailsWith(scheme) { base64.decode(symbols, endIndex = symbols.length + 1) }
+ assertFailsWith(scheme) { base64.decode(symbols, startIndex = symbols.length + 1) }
+ assertFailsWith(scheme) { base64.decode(symbols, startIndex = 4, endIndex = 0) }
+
+ assertContentEquals(bytes.copyOfRange(0, 3), base64.decode(symbols, endIndex = 4))
+ assertContentEquals(bytes.copyOfRange(3, bytes.size), base64.decode(symbols, startIndex = 4))
+
+ val destination = ByteArray(6)
+ assertFailsWith { base64.decodeIntoByteArray(symbols, destination, destinationOffset = -1) }
+ assertFailsWith { base64.decodeIntoByteArray(symbols, destination, destinationOffset = destination.size + 1) }
+ assertFailsWith { base64.decodeIntoByteArray(symbols, destination, destinationOffset = 1) }
+
+ assertTrue(destination.all { it == 0.toByte() })
+
+ var length = base64.decodeIntoByteArray(symbols, destination, endIndex = 4)
+ assertContentEquals(bytes.copyOfRange(0, 3), destination.copyOf(length))
+ length += base64.decodeIntoByteArray(symbols, destination, destinationOffset = length, startIndex = 4)
+ assertContentEquals(bytes, destination)
+ }
+
+ // decode(ByteArray)
+ val symbolBytes = symbols.encodeToByteArray()
+ for ((base64, scheme) in codecs) {
+ assertFailsWith(scheme) { base64.decode(symbolBytes, startIndex = -1) }
+ assertFailsWith(scheme) { base64.decode(symbolBytes, endIndex = symbolBytes.size + 1) }
+ assertFailsWith(scheme) { base64.decode(symbolBytes, startIndex = symbolBytes.size + 1) }
+ assertFailsWith(scheme) { base64.decode(symbolBytes, startIndex = 4, endIndex = 0) }
+
+ assertContentEquals(bytes.copyOfRange(0, 3), base64.decode(symbolBytes, endIndex = 4))
+ assertContentEquals(bytes.copyOfRange(3, bytes.size), base64.decode(symbolBytes, startIndex = 4))
+
+ val destination = ByteArray(6)
+ assertFailsWith { base64.decodeIntoByteArray(symbolBytes, destination, destinationOffset = -1) }
+ assertFailsWith { base64.decodeIntoByteArray(symbolBytes, destination, destinationOffset = destination.size + 1) }
+ assertFailsWith { base64.decodeIntoByteArray(symbolBytes, destination, destinationOffset = 1) }
+
+ assertTrue(destination.all { it == 0.toByte() })
+
+ var length = base64.decodeIntoByteArray(symbolBytes, destination, endIndex = 4)
+ assertContentEquals(bytes.copyOfRange(0, 3), destination.copyOf(length))
+ length += base64.decodeIntoByteArray(symbolBytes, destination, destinationOffset = length, startIndex = 4)
+ assertContentEquals(bytes, destination)
+ }
+ }
+
+ @Test
+ fun common() {
+ fun testEncode(bytes: ByteArray, symbols: String) {
+ testEncode(Base64, bytes, symbols)
+ testEncode(Base64.UrlSafe, bytes, symbols)
+ testEncode(Base64.Mime, bytes, symbols)
+ }
+
+ fun testDecode(symbols: String, bytes: ByteArray) {
+ testDecode(Base64, symbols, bytes)
+ testDecode(Base64.UrlSafe, symbols, bytes)
+ testDecode(Base64.Mime, symbols, bytes)
+ }
+
+ fun testCoding(text: String, symbols: String) {
+ val bytes = text.encodeToByteArray()
+ testEncode(bytes, symbols)
+ testDecode(symbols, bytes)
+ }
+
+ testCoding("", "")
+ testCoding("f", "Zg==")
+ testCoding("fo", "Zm8=")
+ testCoding("foo", "Zm9v")
+ testCoding("foob", "Zm9vYg==")
+ testCoding("fooba", "Zm9vYmE=")
+ testCoding("foobar", "Zm9vYmFy")
+
+ // the padded bits are allowed to be non-zero
+ testDecode("Zm9=", "fo".encodeToByteArray())
+
+ // paddings not required
+ testDecode("Zg", "f".encodeToByteArray())
+ testDecode("Zm9vYmE", "fooba".encodeToByteArray())
+
+ for ((codec, scheme) in codecs) {
+ // dangling single symbol at the end that does not have bits even for a byte
+ val lastDandlingSymbol = listOf("Z", "Z=", "Z==", "Z===", "Zm9vZ", "Zm9vZ=", "Zm9vZ==", "Zm9vZ===")
+ for (symbols in lastDandlingSymbol) {
+ assertFailsWith("$scheme <$symbols>") { codec.decode(symbols) }
+ }
+
+ // incorrect padding
+ assertFailsWith(scheme) { codec.decode("Zg=") }
+ assertFailsWith(scheme) { codec.decode("Zm9vYmE==") }
+
+ // padding in the middle
+ assertFailsWith(scheme) { codec.decode("Zg==Zg==") }
+ }
+ }
+
+ private val basicAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
+ private val urlSafeAlphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
+ private val alphabetBytes = ByteArray(48) {
+ val symbol = it / 3 * 4
+ when (it % 3) {
+ 0 -> (symbol shl 2) + ((symbol + 1) shr 4)
+ 1 -> ((symbol + 1) and 0xF shl 4) + ((symbol + 2) shr 2)
+ else -> ((symbol + 2) and 0x3 shl 6) + (symbol + 3)
+ }.toByte()
+ }
+
+ @Test
+ fun basic() {
+ testCoding(Base64, bytes(0b1111_1011, 0b1111_0000), "+/A=")
+
+ // all symbols from alphabet
+ testCoding(Base64, alphabetBytes, basicAlphabet)
+
+ // decode line separator
+ assertFailsWith { Base64.decode("Zm9v\r\nYg==") }
+ assertFailsWith { Base64.decode("Zm9v\nYg==") }
+ assertFailsWith { Base64.decode("Zm9\rvYg==") }
+
+ // decode illegal char
+ assertFailsWith { Base64.decode("Zm9vY(==") }
+ assertFailsWith { Base64.decode("Zm[@]9vYg==") }
+ assertFailsWith { Base64.decode("Zm9v-Yg==") }
+ assertFailsWith { Base64.decode("Zm9vYg=(%^)=") }
+ assertFailsWith { Base64.decode("Zm\u00FF9vYg==") }
+ assertFailsWith { Base64.decode("\uFFFFZm9vYg==") }
+ assertFailsWith { Base64.decode("Zm9vYg==\uD800\uDC00") }
+
+ // no line separator inserted
+ val expected = "Zm9vYmFy".repeat(76)
+ testEncode(Base64, "foobar".repeat(76).encodeToByteArray(), expected)
+ }
+
+ @Test
+ fun urlSafe() {
+ testCoding(Base64.UrlSafe, bytes(0b1111_1011, 0b1111_0000), "-_A=")
+
+ // all symbols from alphabet
+ testCoding(Base64.UrlSafe, alphabetBytes, urlSafeAlphabet)
+
+ // decode line separator
+ assertFailsWith { Base64.UrlSafe.decode("Zm9v\r\nYg==") }
+ assertFailsWith { Base64.UrlSafe.decode("Zm9v\nYg==") }
+ assertFailsWith { Base64.UrlSafe.decode("Zm9\rvYg==") }
+
+ // decode illegal char
+ assertFailsWith { Base64.UrlSafe.decode("Zm9vY(==") }
+ assertFailsWith { Base64.UrlSafe.decode("Zm[@]9vYg==") }
+ assertFailsWith { Base64.UrlSafe.decode("Zm9v+Yg==") }
+ assertFailsWith { Base64.UrlSafe.decode("Zm9vYg=(%^)=") }
+ assertFailsWith { Base64.UrlSafe.decode("Zm\u00FF9vYg==") }
+ assertFailsWith { Base64.UrlSafe.decode("\uFFFFZm9vYg==") }
+ assertFailsWith { Base64.UrlSafe.decode("Zm9vYg==\uD800\uDC00") }
+
+ // no line separator inserted
+ val expected = "Zm9vYmFy".repeat(76)
+ testEncode(Base64.UrlSafe, "foobar".repeat(76).encodeToByteArray(), expected)
+ }
+
+ @Test
+ fun mime() {
+ testCoding(Base64.Mime, bytes(0b1111_1011, 0b1111_0000), "+/A=")
+
+ // all symbols from alphabet
+ testCoding(Base64.Mime, alphabetBytes, basicAlphabet)
+
+ // dangling single symbol
+ assertFailsWith { Base64.Mime.decode("Zm9vY(==") }
+
+ // decode line separator
+ testDecode(Base64.Mime, "Zm9v\r\nYg==", "foob".encodeToByteArray())
+ testDecode(Base64.Mime, "Zm9v\nYg==", "foob".encodeToByteArray())
+ testDecode(Base64.Mime, "Zm9\rvYg==", "foob".encodeToByteArray())
+
+ // decode illegal char
+ testDecode(Base64.Mime, "Zm9vYg(==", "foob".encodeToByteArray())
+ testDecode(Base64.Mime, "Zm[@]9vYg==", "foob".encodeToByteArray())
+ testDecode(Base64.Mime, "Zm9v-Yg==", "foob".encodeToByteArray())
+ testDecode(Base64.Mime, "Zm9vYg=(%^)=", "foob".encodeToByteArray())
+ testDecode(Base64.Mime, "Zm\u00FF9vYg==", "foob".encodeToByteArray())
+ testDecode(Base64.Mime, "\uFFFFZm9vYg==", "foob".encodeToByteArray())
+ testDecode(Base64.Mime, "Zm9vYg==\uD800\uDC00", "foob".encodeToByteArray())
+
+ // inserts line separator, but not to the end of the output
+ val expected = "Zm9vYmFy".repeat(76).chunked(76).joinToString(separator = "\r\n")
+ testEncode(Base64.Mime, "foobar".repeat(76).encodeToByteArray(), expected)
+ }
+}
\ No newline at end of file
diff --git a/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt b/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt
index 49881f5049d..1081fa4844f 100644
--- a/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt
+++ b/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt
@@ -3268,6 +3268,37 @@ public final class kotlin/io/TextStreamsKt {
public static final fun useLines (Ljava/io/Reader;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object;
}
+public class kotlin/io/encoding/Base64 {
+ public static final field Default Lkotlin/io/encoding/Base64$Default;
+ public synthetic fun (ZZLkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun decode (Ljava/lang/CharSequence;II)[B
+ public final fun decode ([BII)[B
+ public static synthetic fun decode$default (Lkotlin/io/encoding/Base64;Ljava/lang/CharSequence;IIILjava/lang/Object;)[B
+ public static synthetic fun decode$default (Lkotlin/io/encoding/Base64;[BIIILjava/lang/Object;)[B
+ public final fun decodeIntoByteArray (Ljava/lang/CharSequence;[BIII)I
+ public final fun decodeIntoByteArray ([B[BIII)I
+ public static synthetic fun decodeIntoByteArray$default (Lkotlin/io/encoding/Base64;Ljava/lang/CharSequence;[BIIIILjava/lang/Object;)I
+ public static synthetic fun decodeIntoByteArray$default (Lkotlin/io/encoding/Base64;[B[BIIIILjava/lang/Object;)I
+ public final fun encode ([BII)Ljava/lang/String;
+ public static synthetic fun encode$default (Lkotlin/io/encoding/Base64;[BIIILjava/lang/Object;)Ljava/lang/String;
+ public final fun encodeIntoByteArray ([B[BIII)I
+ public static synthetic fun encodeIntoByteArray$default (Lkotlin/io/encoding/Base64;[B[BIIIILjava/lang/Object;)I
+ public final fun encodeToAppendable ([BLjava/lang/Appendable;II)Ljava/lang/Appendable;
+ public static synthetic fun encodeToAppendable$default (Lkotlin/io/encoding/Base64;[BLjava/lang/Appendable;IIILjava/lang/Object;)Ljava/lang/Appendable;
+ public final fun encodeToByteArray ([BII)[B
+ public static synthetic fun encodeToByteArray$default (Lkotlin/io/encoding/Base64;[BIIILjava/lang/Object;)[B
+}
+
+public final class kotlin/io/encoding/Base64$Default : kotlin/io/encoding/Base64 {
+ public final fun getMime ()Lkotlin/io/encoding/Base64;
+ public final fun getUrlSafe ()Lkotlin/io/encoding/Base64;
+}
+
+public final class kotlin/io/encoding/StreamEncodingKt {
+ public static final fun decodingWith (Ljava/io/InputStream;Lkotlin/io/encoding/Base64;)Ljava/io/InputStream;
+ public static final fun encodingWith (Ljava/io/OutputStream;Lkotlin/io/encoding/Base64;)Ljava/io/OutputStream;
+}
+
public abstract interface class kotlin/io/path/CopyActionContext {
public abstract fun copyToIgnoringExistingDirectory (Ljava/nio/file/Path;Ljava/nio/file/Path;Z)Lkotlin/io/path/CopyActionResult;
}