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