Introduce basic, url-safe and mime Base64 variants #KT-9823
This commit is contained in:
committed by
Space Team
parent
a5c8e30bb1
commit
dc03a03762
@@ -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)
|
||||
}
|
||||
}
|
||||
+31
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user