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; }