• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package kotlinx.serialization.json.internal
2 
3 import kotlinx.serialization.*
4 import kotlinx.serialization.descriptors.*
5 import kotlinx.serialization.internal.*
6 
7 /**
8  * Internal representation of the current JSON path.
9  * It is stored as the array of serial descriptors (for regular classes)
10  * and `Any?` in case of Map keys.
11  *
12  * Example of the state when decoding the list
13  * ```
14  * class Foo(val a: Int, val l: List<String>)
15  *
16  * // {"l": ["a", "b", "c"] }
17  *
18  * Current path when decoding array elements:
19  * Foo.descriptor, List(String).descriptor
20  * 1 (index of the 'l'), 2 (index of currently being decoded "c")
21  * ```
22  */
23 internal class JsonPath {
24 
25     // Tombstone indicates that we are within a map, but the map key is currently being decoded.
26     // It is also used to overwrite a previous map key to avoid memory leaks and misattribution.
27     private object Tombstone
28 
29     /*
30      * Serial descriptor, map key or the tombstone for map key
31      */
32     private var currentObjectPath = arrayOfNulls<Any?>(8)
33     /*
34      * Index is a small state-machine used to determine the state of the path:
35      * >=0 -> index of the element being decoded with the outer class currentObjectPath[currentDepth]
36      * -1 -> nested elements are not yet decoded
37      * -2 -> the map is being decoded and both its descriptor AND the last key were added to the path.
38      *
39      * -2 is effectively required to specify that two slots has been claimed and both should be
40      * cleaned up when the decoding is done.
41      * The cleanup is essential in order to avoid memory leaks for huge strings and structured keys.
42      */
<lambda>null43     private var indicies = IntArray(8) { -1 }
44     private var currentDepth = -1
45 
46     // Invoked when class is started being decoded
pushDescriptornull47     fun pushDescriptor(sd: SerialDescriptor) {
48         val depth = ++currentDepth
49         if (depth == currentObjectPath.size) {
50             resize()
51         }
52         currentObjectPath[depth] = sd
53     }
54 
55     // Invoked when index-th element of the current descriptor is being decoded
updateDescriptorIndexnull56     fun updateDescriptorIndex(index: Int) {
57         indicies[currentDepth] = index
58     }
59 
60     /*
61      * For maps we cannot use indicies and should use the key as an element of the path instead.
62      * The key can be even an object (e.g. in a case of 'allowStructuredMapKeys') where
63      * 'toString' is way too heavy or have side-effects.
64      * For that we are storing the key instead.
65      */
updateCurrentMapKeynull66     fun updateCurrentMapKey(key: Any?) {
67         // idx != -2 -> this is the very first key being added
68         if (indicies[currentDepth] != -2 && ++currentDepth == currentObjectPath.size) {
69             resize()
70         }
71         currentObjectPath[currentDepth] = key
72         indicies[currentDepth] = -2
73     }
74 
75     /** Used to indicate that we are in the process of decoding the key itself and can't specify it in path */
resetCurrentMapKeynull76     fun resetCurrentMapKey() {
77         if (indicies[currentDepth] == -2) {
78             currentObjectPath[currentDepth] = Tombstone
79         }
80     }
81 
popDescriptornull82     fun popDescriptor() {
83         // When we are ending map, we pop the last key and the outer field as well
84         val depth = currentDepth
85         if (indicies[depth] == -2) {
86             indicies[depth] = -1
87             currentDepth--
88         }
89         // Guard against top-level maps
90         if (currentDepth != -1) {
91             // No need to clean idx up as it was already cleaned by updateDescriptorIndex(DECODE_DONE)
92             currentDepth--
93         }
94     }
95 
96     @OptIn(ExperimentalSerializationApi::class)
getPathnull97     fun getPath(): String {
98         return buildString {
99             append("$")
100             repeat(currentDepth + 1) {
101                 val element = currentObjectPath[it]
102                 if (element is SerialDescriptor) {
103                     if (element.kind == StructureKind.LIST) {
104                         if (indicies[it] != -1) {
105                             append("[")
106                             append(indicies[it])
107                             append("]")
108                         }
109                     } else {
110                         val idx = indicies[it]
111                         // If an actual element is being decoded
112                         if (idx >= 0) {
113                             append(".")
114                             append(element.getElementName(idx))
115                         }
116                     }
117                 } else if (element !== Tombstone) {
118                     append("[")
119                     // All non-indicies should be properly quoted by JsonPath convention
120                     append("'")
121                     // Else -- map key
122                     append(element)
123                     append("'")
124                     append("]")
125                 }
126             }
127         }
128     }
129 
130 
131     @OptIn(ExperimentalSerializationApi::class)
prettyStringnull132     private fun prettyString(it: Any?) = (it as? SerialDescriptor)?.serialName ?: it.toString()
133 
134     private fun resize() {
135         val newSize = currentDepth * 2
136         currentObjectPath = currentObjectPath.copyOf(newSize)
137         indicies = indicies.copyOf(newSize)
138     }
139 
toStringnull140     override fun toString(): String = getPath()
141 }
142