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
@@ -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()
}
}
}