Introduce basic, url-safe and mime Base64 variants #KT-9823

This commit is contained in:
Abduqodiri Qurbonzoda
2022-11-22 15:34:19 +02:00
committed by Space Team
parent a5c8e30bb1
commit dc03a03762
13 changed files with 1898 additions and 1 deletions
@@ -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
@@ -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 <A : kotlin.text.Appendable> 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; }
}
}
@@ -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 <A : kotlin.text.Appendable> 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; }
}
}
@@ -44,6 +44,7 @@ val commonMainSources by task<Sync> {
"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<Sync> {
"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",
@@ -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)
}
@@ -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;
@@ -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.")
}
}
@@ -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()
// }
//}
@@ -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<IOException> {
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<IOException> {
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<IllegalArgumentException> {
it.read()
}
}
// closed
assertFailsWith<IOException> {
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<IOException> {
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<IllegalArgumentException> {
it.read()
}
}
// closed
assertFailsWith<IOException> {
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<IOException> {
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<IllegalArgumentException> {
it.read()
}
}
// closed
assertFailsWith<IOException> {
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<IOException> {
wrapper.read()
}
}
}
@@ -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)
}
@@ -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 <A : Appendable> 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
@@ -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<IndexOutOfBoundsException>(scheme) { base64.encode(bytes, startIndex = -1) }
assertFailsWith<IndexOutOfBoundsException>(scheme) { base64.encode(bytes, endIndex = bytes.size + 1) }
assertFailsWith<IllegalArgumentException>(scheme) { base64.encode(bytes, startIndex = bytes.size + 1) }
assertFailsWith<IllegalArgumentException>(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<IndexOutOfBoundsException>(scheme) { base64.encodeToByteArray(bytes, startIndex = -1) }
assertFailsWith<IndexOutOfBoundsException>(scheme) { base64.encodeToByteArray(bytes, endIndex = bytes.size + 1) }
assertFailsWith<IllegalArgumentException>(scheme) { base64.encodeToByteArray(bytes, startIndex = bytes.size + 1) }
assertFailsWith<IllegalArgumentException>(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<IndexOutOfBoundsException> { base64.encodeIntoByteArray(bytes, destination, destinationOffset = -1) }
assertFailsWith<IndexOutOfBoundsException> { base64.encodeIntoByteArray(bytes, destination, destinationOffset = destination.size + 1) }
assertFailsWith<IndexOutOfBoundsException> { 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<IndexOutOfBoundsException>(scheme) { base64.decode(symbols, startIndex = -1) }
assertFailsWith<IndexOutOfBoundsException>(scheme) { base64.decode(symbols, endIndex = symbols.length + 1) }
assertFailsWith<IllegalArgumentException>(scheme) { base64.decode(symbols, startIndex = symbols.length + 1) }
assertFailsWith<IllegalArgumentException>(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<IndexOutOfBoundsException> { base64.decodeIntoByteArray(symbols, destination, destinationOffset = -1) }
assertFailsWith<IndexOutOfBoundsException> { base64.decodeIntoByteArray(symbols, destination, destinationOffset = destination.size + 1) }
assertFailsWith<IndexOutOfBoundsException> { 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<IndexOutOfBoundsException>(scheme) { base64.decode(symbolBytes, startIndex = -1) }
assertFailsWith<IndexOutOfBoundsException>(scheme) { base64.decode(symbolBytes, endIndex = symbolBytes.size + 1) }
assertFailsWith<IllegalArgumentException>(scheme) { base64.decode(symbolBytes, startIndex = symbolBytes.size + 1) }
assertFailsWith<IllegalArgumentException>(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<IndexOutOfBoundsException> { base64.decodeIntoByteArray(symbolBytes, destination, destinationOffset = -1) }
assertFailsWith<IndexOutOfBoundsException> { base64.decodeIntoByteArray(symbolBytes, destination, destinationOffset = destination.size + 1) }
assertFailsWith<IndexOutOfBoundsException> { 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<IllegalArgumentException>("$scheme <$symbols>") { codec.decode(symbols) }
}
// incorrect padding
assertFailsWith<IllegalArgumentException>(scheme) { codec.decode("Zg=") }
assertFailsWith<IllegalArgumentException>(scheme) { codec.decode("Zm9vYmE==") }
// padding in the middle
assertFailsWith<IllegalArgumentException>(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<IllegalArgumentException> { Base64.decode("Zm9v\r\nYg==") }
assertFailsWith<IllegalArgumentException> { Base64.decode("Zm9v\nYg==") }
assertFailsWith<IllegalArgumentException> { Base64.decode("Zm9\rvYg==") }
// decode illegal char
assertFailsWith<IllegalArgumentException> { Base64.decode("Zm9vY(==") }
assertFailsWith<IllegalArgumentException> { Base64.decode("Zm[@]9vYg==") }
assertFailsWith<IllegalArgumentException> { Base64.decode("Zm9v-Yg==") }
assertFailsWith<IllegalArgumentException> { Base64.decode("Zm9vYg=(%^)=") }
assertFailsWith<IllegalArgumentException> { Base64.decode("Zm\u00FF9vYg==") }
assertFailsWith<IllegalArgumentException> { Base64.decode("\uFFFFZm9vYg==") }
assertFailsWith<IllegalArgumentException> { 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<IllegalArgumentException> { Base64.UrlSafe.decode("Zm9v\r\nYg==") }
assertFailsWith<IllegalArgumentException> { Base64.UrlSafe.decode("Zm9v\nYg==") }
assertFailsWith<IllegalArgumentException> { Base64.UrlSafe.decode("Zm9\rvYg==") }
// decode illegal char
assertFailsWith<IllegalArgumentException> { Base64.UrlSafe.decode("Zm9vY(==") }
assertFailsWith<IllegalArgumentException> { Base64.UrlSafe.decode("Zm[@]9vYg==") }
assertFailsWith<IllegalArgumentException> { Base64.UrlSafe.decode("Zm9v+Yg==") }
assertFailsWith<IllegalArgumentException> { Base64.UrlSafe.decode("Zm9vYg=(%^)=") }
assertFailsWith<IllegalArgumentException> { Base64.UrlSafe.decode("Zm\u00FF9vYg==") }
assertFailsWith<IllegalArgumentException> { Base64.UrlSafe.decode("\uFFFFZm9vYg==") }
assertFailsWith<IllegalArgumentException> { 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<IllegalArgumentException> { 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)
}
}
@@ -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 <init> (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;
}