summaryrefslogtreecommitdiff
path: root/platform/script-debugger/backend/src/debugger/sourcemap/SourceMapDecoder.kt
diff options
context:
space:
mode:
Diffstat (limited to 'platform/script-debugger/backend/src/debugger/sourcemap/SourceMapDecoder.kt')
-rw-r--r--platform/script-debugger/backend/src/debugger/sourcemap/SourceMapDecoder.kt372
1 files changed, 372 insertions, 0 deletions
diff --git a/platform/script-debugger/backend/src/debugger/sourcemap/SourceMapDecoder.kt b/platform/script-debugger/backend/src/debugger/sourcemap/SourceMapDecoder.kt
new file mode 100644
index 00000000..3ecdd792
--- /dev/null
+++ b/platform/script-debugger/backend/src/debugger/sourcemap/SourceMapDecoder.kt
@@ -0,0 +1,372 @@
+/*
+ * Copyright 2000-2017 JetBrains s.r.o.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jetbrains.debugger.sourcemap
+
+import com.google.gson.stream.JsonToken
+import com.intellij.openapi.diagnostic.logger
+import com.intellij.openapi.util.registry.Registry
+import com.intellij.openapi.util.text.StringUtil
+import com.intellij.openapi.util.text.StringUtilRt
+import com.intellij.util.PathUtil
+import com.intellij.util.SmartList
+import com.intellij.util.UriUtil
+import com.intellij.util.containers.isNullOrEmpty
+import org.jetbrains.debugger.sourcemap.Base64VLQ.CharIterator
+import org.jetbrains.io.JsonReaderEx
+import java.io.IOException
+import java.util.*
+import kotlin.properties.Delegates.notNull
+
+private val MAPPING_COMPARATOR_BY_SOURCE_POSITION = Comparator<MappingEntry> { o1, o2 ->
+ if (o1.sourceLine == o2.sourceLine) {
+ o1.sourceColumn - o2.sourceColumn
+ }
+ else {
+ o1.sourceLine - o2.sourceLine
+ }
+}
+
+val MAPPING_COMPARATOR_BY_GENERATED_POSITION: Comparator<MappingEntry> = Comparator { o1, o2 ->
+ if (o1.generatedLine == o2.generatedLine) {
+ o1.generatedColumn - o2.generatedColumn
+ }
+ else {
+ o1.generatedLine - o2.generatedLine
+ }
+}
+
+internal const val UNMAPPED = -1
+
+// https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit?hl=en_US
+fun decodeSourceMap(`in`: CharSequence, sourceResolverFactory: (sourceUrls: List<String>, sourceContents: List<String?>?) -> SourceResolver): SourceMap? {
+ if (`in`.isEmpty()) {
+ throw IOException("source map contents cannot be empty")
+ }
+
+ val reader = JsonReaderEx(`in`)
+ reader.isLenient = true
+ return parseMap(reader, 0, 0, ArrayList(), sourceResolverFactory)
+}
+
+private fun parseMap(reader: JsonReaderEx,
+ line: Int,
+ column: Int,
+ mappings: MutableList<MappingEntry>,
+ sourceResolverFactory: (sourceUrls: List<String>, sourceContents: List<String?>?) -> SourceResolver): SourceMap? {
+ reader.beginObject()
+ var sourceRoot: String? = null
+ var sourcesReader: JsonReaderEx? = null
+ var names: List<String>? = null
+ var encodedMappings: String? = null
+ var file: String? = null
+ var version = -1
+ var sourcesContent: MutableList<String?>? = null
+ while (reader.hasNext()) {
+ when (reader.nextName()) {
+ "sections" -> throw IOException("sections is not supported yet")
+ "version" -> {
+ version = reader.nextInt()
+ }
+ "sourceRoot" -> {
+ sourceRoot = StringUtil.nullize(readSourcePath(reader))
+ if (sourceRoot != null && sourceRoot != "/") {
+ sourceRoot = UriUtil.trimTrailingSlashes(sourceRoot)
+ }
+ }
+ "sources" -> {
+ sourcesReader = reader.subReader()
+ reader.skipValue()
+ }
+ "names" -> {
+ reader.beginArray()
+ if (reader.hasNext()) {
+ names = ArrayList()
+ do {
+ if (reader.peek() == JsonToken.BEGIN_OBJECT) {
+ // polymer map
+ reader.skipValue()
+ names.add("POLYMER UNKNOWN NAME")
+ }
+ else {
+ names.add(reader.nextString(true))
+ }
+ }
+ while (reader.hasNext())
+ }
+ else {
+ names = emptyList()
+ }
+ reader.endArray()
+ }
+ "mappings" -> {
+ encodedMappings = reader.nextString()
+ }
+ "file" -> {
+ file = reader.nextNullableString()
+ }
+ "sourcesContent" -> {
+ reader.beginArray()
+ if (reader.peek() != JsonToken.END_ARRAY) {
+ sourcesContent = SmartList<String>()
+ do {
+ if (reader.peek() == JsonToken.STRING) {
+ sourcesContent.add(StringUtilRt.convertLineSeparators(reader.nextString()))
+ }
+ else if (reader.peek() == JsonToken.NULL) {
+ // null means source file should be resolved by url
+ sourcesContent.add(null)
+ reader.nextNull()
+ }
+ else {
+ logger<SourceMap>().warn("Unknown sourcesContent element: ${reader.peek().name}")
+ reader.skipValue()
+ }
+ }
+ while (reader.hasNext())
+ }
+ reader.endArray()
+ }
+ else -> {
+ // skip file or extensions
+ reader.skipValue()
+ }
+ }
+ }
+ reader.close()
+
+ // check it before other checks, probably it is not a sourcemap file
+ if (encodedMappings.isNullOrEmpty()) {
+ // empty map
+ return null
+ }
+
+ if (Registry.`is`("js.debugger.fix.jspm.source.maps", false) && encodedMappings!!.startsWith(";") && file != null && file.endsWith(".ts!transpiled")) {
+ encodedMappings = encodedMappings.substring(1)
+ }
+
+ if (version != 3) {
+ throw IOException("Unsupported sourcemap version: $version")
+ }
+
+ if (sourcesReader == null) {
+ throw IOException("sources is not specified")
+ }
+
+ val sources = readSources(sourcesReader, sourceRoot)
+ if (sources.isEmpty()) {
+ // empty map, meteor can report such ugly maps
+ return null
+ }
+
+ val reverseMappingsBySourceUrl = arrayOfNulls<MutableList<MappingEntry>?>(sources.size)
+ readMappings(encodedMappings!!, line, column, mappings, reverseMappingsBySourceUrl, names)
+
+ val sourceToEntries = Array<MappingList?>(reverseMappingsBySourceUrl.size) {
+ val entries = reverseMappingsBySourceUrl[it]
+ if (entries == null) {
+ null
+ }
+ else {
+ entries.sortWith(MAPPING_COMPARATOR_BY_SOURCE_POSITION)
+ SourceMappingList(entries)
+ }
+ }
+ return OneLevelSourceMap(file, GeneratedMappingList(mappings), sourceToEntries, sourceResolverFactory(sources, sourcesContent), !names.isNullOrEmpty())
+}
+
+private fun readSourcePath(reader: JsonReaderEx): String = PathUtil.toSystemIndependentName(reader.nextString().trim { it <= ' ' })
+
+private fun readMappings(value: String,
+ initialLine: Int,
+ initialColumn: Int,
+ mappings: MutableList<MappingEntry>,
+ reverseMappingsBySourceUrl: Array<MutableList<MappingEntry>?>,
+ names: List<String>?) {
+ if (value.isEmpty()) {
+ return
+ }
+
+ var line = initialLine
+ var column = initialColumn
+ val charIterator = CharSequenceIterator(value)
+ var sourceIndex = 0
+ var reverseMappings: MutableList<MappingEntry> = getMapping(reverseMappingsBySourceUrl, sourceIndex)
+ var sourceLine = 0
+ var sourceColumn = 0
+ var nameIndex = 0
+ var prevEntry: MutableEntry? = null
+
+ fun addEntry(entry: MutableEntry) {
+ if (prevEntry != null) {
+ prevEntry!!.nextGenerated = entry
+ }
+ prevEntry = entry
+ mappings.add(entry)
+ }
+
+ while (charIterator.hasNext()) {
+ if (charIterator.peek() == ',') {
+ charIterator.next()
+ }
+ else {
+ while (charIterator.peek() == ';') {
+ line++
+ column = 0
+ charIterator.next()
+ if (!charIterator.hasNext()) {
+ return
+ }
+ }
+ }
+
+ column += Base64VLQ.decode(charIterator)
+ if (isSeparator(charIterator)) {
+ addEntry(UnmappedEntry(line, column))
+ continue
+ }
+
+ val sourceIndexDelta = Base64VLQ.decode(charIterator)
+ if (sourceIndexDelta != 0) {
+ sourceIndex += sourceIndexDelta
+ reverseMappings = getMapping(reverseMappingsBySourceUrl, sourceIndex)
+ }
+ sourceLine += Base64VLQ.decode(charIterator)
+ sourceColumn += Base64VLQ.decode(charIterator)
+
+ val entry: MutableEntry
+ if (isSeparator(charIterator)) {
+ entry = UnnamedEntry(line, column, sourceIndex, sourceLine, sourceColumn)
+ }
+ else {
+ nameIndex += Base64VLQ.decode(charIterator)
+ assert(names != null)
+ entry = NamedEntry(names!![nameIndex], line, column, sourceIndex, sourceLine, sourceColumn)
+ }
+ reverseMappings.add(entry)
+ addEntry(entry)
+ }
+}
+
+private fun readSources(reader: JsonReaderEx, sourceRoot: String?): List<String> {
+ reader.beginArray()
+ val sources: List<String>
+ if (reader.peek() == JsonToken.END_ARRAY) {
+ sources = emptyList()
+ }
+ else {
+ sources = SmartList<String>()
+ do {
+ var sourceUrl: String = readSourcePath(reader)
+ if (!sourceRoot.isNullOrEmpty()) {
+ if (sourceRoot == "/") {
+ sourceUrl = "/$sourceUrl"
+ }
+ else {
+ sourceUrl = "$sourceRoot/$sourceUrl"
+ }
+ }
+ sources.add(sourceUrl)
+ }
+ while (reader.hasNext())
+ }
+ reader.endArray()
+ return sources
+}
+
+private fun getMapping(reverseMappingsBySourceUrl: Array<MutableList<MappingEntry>?>, sourceIndex: Int): MutableList<MappingEntry> {
+ var reverseMappings = reverseMappingsBySourceUrl.get(sourceIndex)
+ if (reverseMappings == null) {
+ reverseMappings = ArrayList()
+ reverseMappingsBySourceUrl.set(sourceIndex, reverseMappings)
+ }
+ return reverseMappings
+}
+
+private fun isSeparator(charIterator: CharSequenceIterator): Boolean {
+ if (!charIterator.hasNext()) {
+ return true
+ }
+
+ val current = charIterator.peek()
+ return current == ',' || current == ';'
+}
+
+interface MutableEntry : MappingEntry {
+ override var nextGenerated: MappingEntry
+}
+
+/**
+ * Not mapped to a section in the original source.
+ */
+private data class UnmappedEntry(override val generatedLine: Int, override val generatedColumn: Int) : MappingEntry, MutableEntry {
+ override val sourceLine = UNMAPPED
+
+ override val sourceColumn = UNMAPPED
+
+ override var nextGenerated: MappingEntry by notNull()
+}
+
+/**
+ * Mapped to a section in the original source.
+ */
+private data class UnnamedEntry(override val generatedLine: Int,
+ override val generatedColumn: Int,
+ override val source: Int,
+ override val sourceLine: Int,
+ override val sourceColumn: Int) : MappingEntry, MutableEntry {
+ override var nextGenerated: MappingEntry by notNull()
+}
+
+/**
+ * Mapped to a section in the original source, and is associated with a name.
+ */
+private data class NamedEntry(override val name: String,
+ override val generatedLine: Int,
+ override val generatedColumn: Int,
+ override val source: Int,
+ override val sourceLine: Int,
+ override val sourceColumn: Int) : MappingEntry, MutableEntry {
+ override var nextGenerated: MappingEntry by notNull()
+}
+
+// java CharacterIterator is ugly, next() impl, so, we reinvent
+private class CharSequenceIterator(private val content: CharSequence) : CharIterator {
+ private val length = content.length
+ private var current = 0
+
+ override fun next() = content.get(current++)
+
+ internal fun peek() = content.get(current)
+
+ override fun hasNext() = current < length
+}
+
+private class SourceMappingList(mappings: List<MappingEntry>) : MappingList(mappings) {
+ override fun getLine(mapping: MappingEntry) = mapping.sourceLine
+
+ override fun getColumn(mapping: MappingEntry) = mapping.sourceColumn
+
+ override val comparator = MAPPING_COMPARATOR_BY_SOURCE_POSITION
+}
+
+private class GeneratedMappingList(mappings: List<MappingEntry>) : MappingList(mappings) {
+ override fun getLine(mapping: MappingEntry) = mapping.generatedLine
+
+ override fun getColumn(mapping: MappingEntry) = mapping.generatedColumn
+
+ override val comparator = MAPPING_COMPARATOR_BY_GENERATED_POSITION
+}
+