summaryrefslogtreecommitdiff
path: root/formats/json/jvmMain/src/kotlinx/serialization/json/internal/CharsetReader.kt
blob: 0f8a566c66c535c1a221a1437f402bff31348940 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
package kotlinx.serialization.json.internal

import java.io.*
import java.nio.*
import java.nio.charset.*

internal class CharsetReader(
    private val inputStream: InputStream,
    private val charset: Charset
) {
    private val decoder: CharsetDecoder
    private val byteBuffer: ByteBuffer

    // Surrogate-handling in cases when a single char is requested, but two were read
    private var hasLeftoverPotentiallySurrogateChar = false
    private var leftoverChar = 0.toChar()

    init {
        decoder = charset.newDecoder()
            .onMalformedInput(CodingErrorAction.REPLACE)
            .onUnmappableCharacter(CodingErrorAction.REPLACE)
        byteBuffer = ByteBuffer.wrap(ByteArrayPool8k.take())
        byteBuffer.flip() // Make empty
    }

    @Suppress("NAME_SHADOWING")
    fun read(array: CharArray, offset: Int, length: Int): Int {
        if (length == 0) return 0
        require(offset in 0 until array.size && length >= 0 && offset + length <= array.size) {
            "Unexpected arguments: $offset, $length, ${array.size}"
        }

        var offset = offset
        var length = length
        var bytesRead = 0
        if (hasLeftoverPotentiallySurrogateChar) {
            array[offset] = leftoverChar
            offset++
            length--
            hasLeftoverPotentiallySurrogateChar = false
            bytesRead = 1
            if (length == 0) return bytesRead
        }
        if (length == 1) {
            // Treat single-character array reads just like read()
            val c = oneShotReadSlowPath()
            if (c == -1) return if (bytesRead == 0) -1 else bytesRead
            array[offset] = c.toChar()
            return bytesRead + 1
        }
        return doRead(array, offset, length) + bytesRead
    }

    private fun doRead(array: CharArray, offset: Int, length: Int): Int {
        var charBuffer = CharBuffer.wrap(array, offset, length)
        if (charBuffer.position() != 0) {
            charBuffer = charBuffer.slice()
        }
        var isEof = false
        while (true) {
            val cr = decoder.decode(byteBuffer, charBuffer, isEof)
            if (cr.isUnderflow) {
                if (isEof) break
                if (!charBuffer.hasRemaining()) break
                val n = fillByteBuffer()
                if (n < 0) {
                    isEof = true
                    if (charBuffer.position() == 0 && !byteBuffer.hasRemaining()) break
                    decoder.reset()
                }
                continue
            }
            if (cr.isOverflow) {
                assert(charBuffer.position() > 0)
                break
            }
            cr.throwException()
        }
        if (isEof) decoder.reset()
        return if (charBuffer.position() == 0) -1
        else charBuffer.position()
    }

    private fun fillByteBuffer(): Int {
        byteBuffer.compact()
        try {
            // Read from the input stream, and then update the buffer
            val limit = byteBuffer.limit()
            val position = byteBuffer.position()
            val remaining = if (position <= limit) limit - position else 0
            val bytesRead = inputStream.read(byteBuffer.array(), byteBuffer.arrayOffset() + position, remaining)
            if (bytesRead < 0) return bytesRead
            // Method `position(I)LByteBuffer` does not exist in Java 8. For details, see comment for `flip` in `init` method
            (byteBuffer as Buffer).position(position + bytesRead)
        } finally {
            byteBuffer.flip()
        }
        return byteBuffer.remaining()
    }

    private fun oneShotReadSlowPath(): Int {
        // Return the leftover char, if there is one
        if (hasLeftoverPotentiallySurrogateChar) {
            hasLeftoverPotentiallySurrogateChar = false
            return leftoverChar.code
        }

        val array = CharArray(2)
        val bytesRead = read(array, 0, 2)
        return when (bytesRead) {
            -1 -> -1
            1 -> array[0].code
            2 -> {
                leftoverChar = array[1]
                hasLeftoverPotentiallySurrogateChar = true
                array[0].code
            }
            else -> error("Unreachable state: $bytesRead")
        }
    }

    public fun release() {
        ByteArrayPool8k.release(byteBuffer.array())
    }
}