Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,37 +1,18 @@
/*
* © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com
* Contributors: DenBond7
* Contributors: denbond7
*/

package com.flowcrypt.email.api.email.gmail.api

import java.io.FilterInputStream
import java.io.InputStream

/**
* @author Denys Bondarenko
*/
class GMailRawAttachmentFilterInputStream(inputStream: InputStream) :
FilterInputStream(inputStream) {
init {
skip(9)
}

/**
* Via this method we trim the end of the stream
*/
override fun read(b: ByteArray, off: Int, len: Int): Int {
val tempBuffer = b.copyOf()
val i = super.read(tempBuffer, off, len)
//find index of the last '"' char
val indexEndStart = tempBuffer.indexOf(34)

return if (indexEndStart != -1) {
tempBuffer.copyInto(b, endIndex = indexEndStart)
indexEndStart
} else {
tempBuffer.copyInto(b)
i
}
GMailRawResponseFilterInputStream(inputStream, JSON_PREFIX_LENGTH) {
private companion object {
const val JSON_PREFIX_LENGTH = 9L
}
}
Original file line number Diff line number Diff line change
@@ -1,38 +1,18 @@
/*
* © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com
* Contributors: DenBond7
* Contributors: denbond7
*/

package com.flowcrypt.email.api.email.gmail.api

import java.io.FilterInputStream
import java.io.InputStream

/**
* @author Denys Bondarenko
*/
class GMailRawMIMEMessageFilterInputStream(inputStream: InputStream) :
FilterInputStream(inputStream) {
init {
// we should skip first 12 bytes to begin to read raw MIME message as a stream
skip(12)
}

/**
* Via this method we trim the end of the stream
*/
override fun read(b: ByteArray, off: Int, len: Int): Int {
val tempBuffer = b.copyOf()
val i = super.read(tempBuffer, off, len)
//find index of the last '"' char
val indexEndStart = tempBuffer.indexOf(34)

return if (indexEndStart != -1) {
tempBuffer.copyInto(b, endIndex = indexEndStart)
indexEndStart
} else {
tempBuffer.copyInto(b)
i
}
GMailRawResponseFilterInputStream(inputStream, JSON_PREFIX_LENGTH) {
private companion object {
const val JSON_PREFIX_LENGTH = 12L
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com
* Contributors: denbond7
*/

package com.flowcrypt.email.api.email.gmail.api

import java.io.EOFException
import java.io.FilterInputStream
import java.io.InputStream

/**
* Exposes the Base64URL value from a Gmail API JSON response as an input stream.
*
* The opening JSON bytes are removed by [prefixLength]. The first quote after the prefix terminates
* the value because the Base64URL alphabet cannot contain quotes.
*/
open class GMailRawResponseFilterInputStream(
inputStream: InputStream,
prefixLength: Long
) : FilterInputStream(inputStream) {
private var endReached = false

init {
skipFully(prefixLength)
}

override fun read(): Int {
if (endReached) return -1

return when (val value = super.read()) {
-1 -> -1
JSON_STRING_DELIMITER -> {
endReached = true
-1
}

else -> value
}
}

override fun read(b: ByteArray, off: Int, len: Int): Int {
checkBounds(b, off, len)
if (len == 0) return 0
if (endReached) return -1

val readCount = super.read(b, off, len)
if (readCount <= 0) return readCount

for (index in off until off + readCount) {
if (b[index].toInt() == JSON_STRING_DELIMITER) {
endReached = true
val valueLength = index - off
return if (valueLength == 0) -1 else valueLength
}
}

return readCount
}

private fun skipFully(byteCount: Long) {
var remaining = byteCount
while (remaining > 0) {
val skipped = super.skip(remaining)
if (skipped > 0) {
remaining -= skipped
} else if (super.read() == -1) {
throw EOFException("Unexpected end of Gmail API response while skipping JSON prefix")
} else {
remaining--
}
}
}

private fun checkBounds(buffer: ByteArray, offset: Int, length: Int) {
if (offset < 0 || length < 0 || length > buffer.size - offset) {
throw IndexOutOfBoundsException(
"offset=$offset, length=$length, bufferSize=${buffer.size}"
)
}
}

private companion object {
const val JSON_STRING_DELIMITER = '"'.code
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com
* Contributors: denbond7
*/

package com.flowcrypt.email.api.email.gmail.api

import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Assert.assertThrows
import org.junit.Test
import java.io.ByteArrayInputStream
import java.io.EOFException

class GMailRawResponseFilterInputStreamTest {
@Test
fun testAttachmentResponseReadInSmallChunks() {
val stream = GMailRawAttachmentFilterInputStream(
"""{"data":"YWJjZA=="}""".byteInputStream()
)

assertEquals("YWJjZA==", stream.readBytesWithChunkSize(2).decodeToString())
assertEquals(-1, stream.read())
}

@Test
fun testMimeResponseReadOneByteAtATime() {
val stream = GMailRawMIMEMessageFilterInputStream(
"{\n \"raw\": \"YWJjZA==\"\n}\n".byteInputStream()
)

val result = buildList {
while (true) {
val value = stream.read()
if (value == -1) break
add(value.toByte())
}
}.toByteArray()

assertEquals("YWJjZA==", result.decodeToString())
assertEquals(-1, stream.read())
}

@Test
fun testBulkReadHonorsOffsetAndIgnoresExistingBufferContent() {
val stream = GMailRawAttachmentFilterInputStream(
"""{"data":"YWJj"}""".byteInputStream()
)
val buffer = ByteArray(10) { '"'.code.toByte() }

val count = stream.read(buffer, 3, 4)

assertEquals(4, count)
assertArrayEquals("YWJj".toByteArray(), buffer.copyOfRange(3, 7))
assertEquals(-1, stream.read(buffer, 3, 4))
}

@Test
fun testPrefixIsFullyConsumedWhenDelegateSkipMakesPartialProgress() {
val response = """{"data":"YWJj"}""".toByteArray()
val delegate = object : ByteArrayInputStream(response) {
override fun skip(n: Long): Long = super.skip(n.coerceAtMost(1))
}

val stream = GMailRawAttachmentFilterInputStream(delegate)

assertEquals("YWJj", stream.readBytes().decodeToString())
}

@Test
fun testTruncatedPrefixThrows() {
assertThrows(EOFException::class.java) {
GMailRawAttachmentFilterInputStream("{}".byteInputStream())
}
}

@Test
fun testEmptyValueReturnsEndOfStream() {
val stream = GMailRawAttachmentFilterInputStream("""{"data":""}""".byteInputStream())

assertEquals(-1, stream.read(ByteArray(8), 0, 8))
assertEquals(-1, stream.read())
}

private fun GMailRawResponseFilterInputStream.readBytesWithChunkSize(
chunkSize: Int
): ByteArray {
val result = mutableListOf<Byte>()
val buffer = ByteArray(chunkSize)
while (true) {
val count = read(buffer, 0, buffer.size)
if (count == -1) break
result.addAll(buffer.copyOf(count).toList())
}
return result.toByteArray()
}
}
Loading