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