summaryrefslogtreecommitdiff
path: root/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonPath.kt
blob: 4e055b234cac6ce5a67f75def0149288cac2898e (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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
package kotlinx.serialization.json.internal

import kotlinx.serialization.*
import kotlinx.serialization.descriptors.*
import kotlinx.serialization.internal.*

/**
 * Internal representation of the current JSON path.
 * It is stored as the array of serial descriptors (for regular classes)
 * and `Any?` in case of Map keys.
 *
 * Example of the state when decoding the list
 * ```
 * class Foo(val a: Int, val l: List<String>)
 *
 * // {"l": ["a", "b", "c"] }
 *
 * Current path when decoding array elements:
 * Foo.descriptor, List(String).descriptor
 * 1 (index of the 'l'), 2 (index of currently being decoded "c")
 * ```
 */
internal class JsonPath {

    // Tombstone indicates that we are within a map, but the map key is currently being decoded.
    // It is also used to overwrite a previous map key to avoid memory leaks and misattribution.
    object Tombstone

    /*
     * Serial descriptor, map key or the tombstone for map key
     */
    private var currentObjectPath = arrayOfNulls<Any?>(8)
    /*
     * Index is a small state-machine used to determine the state of the path:
     * >=0 -> index of the element being decoded with the outer class currentObjectPath[currentDepth]
     * -1 -> nested elements are not yet decoded
     * -2 -> the map is being decoded and both its descriptor AND the last key were added to the path.
     *
     * -2 is effectively required to specify that two slots has been claimed and both should be
     * cleaned up when the decoding is done.
     * The cleanup is essential in order to avoid memory leaks for huge strings and structured keys.
     */
    private var indicies = IntArray(8) { -1 }
    private var currentDepth = -1

    // Invoked when class is started being decoded
    fun pushDescriptor(sd: SerialDescriptor) {
        val depth = ++currentDepth
        if (depth == currentObjectPath.size) {
            resize()
        }
        currentObjectPath[depth] = sd
    }

    // Invoked when index-th element of the current descriptor is being decoded
    fun updateDescriptorIndex(index: Int) {
        indicies[currentDepth] = index
    }

    /*
     * For maps we cannot use indicies and should use the key as an element of the path instead.
     * The key can be even an object (e.g. in a case of 'allowStructuredMapKeys') where
     * 'toString' is way too heavy or have side-effects.
     * For that we are storing the key instead.
     */
    fun updateCurrentMapKey(key: Any?) {
        // idx != -2 -> this is the very first key being added
        if (indicies[currentDepth] != -2 && ++currentDepth == currentObjectPath.size) {
            resize()
        }
        currentObjectPath[currentDepth] = key
        indicies[currentDepth] = -2
    }

    /** Used to indicate that we are in the process of decoding the key itself and can't specify it in path */
    fun resetCurrentMapKey() {
        if (indicies[currentDepth] == -2) {
            currentObjectPath[currentDepth] = Tombstone
        }
    }

    fun popDescriptor() {
        // When we are ending map, we pop the last key and the outer field as well
        val depth = currentDepth
        if (indicies[depth] == -2) {
            indicies[depth] = -1
            currentDepth--
        }
        // Guard against top-level maps
        if (currentDepth != -1) {
            // No need to clean idx up as it was already cleaned by updateDescriptorIndex(DECODE_DONE)
            currentDepth--
        }
    }

    @OptIn(ExperimentalSerializationApi::class)
    fun getPath(): String {
        return buildString {
            append("$")
            repeat(currentDepth + 1) {
                val element = currentObjectPath[it]
                if (element is SerialDescriptor) {
                    if (element.kind == StructureKind.LIST) {
                        if (indicies[it] != -1) {
                            append("[")
                            append(indicies[it])
                            append("]")
                        }
                    } else {
                        val idx = indicies[it]
                        // If an actual element is being decoded
                        if (idx >= 0) {
                            append(".")
                            append(element.getElementName(idx))
                        }
                    }
                } else if (element !== Tombstone) {
                    append("[")
                    // All non-indicies should be properly quoted by JsonPath convention
                    append("'")
                    // Else -- map key
                    append(element)
                    append("'")
                    append("]")
                }
            }
        }
    }


    @OptIn(ExperimentalSerializationApi::class)
    private fun prettyString(it: Any?) = (it as? SerialDescriptor)?.serialName ?: it.toString()

    private fun resize() {
        val newSize = currentDepth * 2
        currentObjectPath = currentObjectPath.copyOf(newSize)
        indicies = indicies.copyOf(newSize)
    }

    override fun toString(): String = getPath()
}