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