diff options
Diffstat (limited to 'platform/script-debugger/backend/src/debugger/sourcemap')
7 files changed, 1084 insertions, 0 deletions
diff --git a/platform/script-debugger/backend/src/debugger/sourcemap/Base64VLQ.java b/platform/script-debugger/backend/src/debugger/sourcemap/Base64VLQ.java new file mode 100644 index 00000000..7576f500 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/sourcemap/Base64VLQ.java @@ -0,0 +1,79 @@ +/* + * 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; + +final class Base64VLQ { + private Base64VLQ() { + } + + interface CharIterator { + boolean hasNext(); + char next(); + } + + // A Base64 VLQ digit can represent 5 bits, so it is base-32. + private static final int VLQ_BASE_SHIFT = 5; + private static final int VLQ_BASE = 1 << VLQ_BASE_SHIFT; + + // A mask of bits for a VLQ digit (11111), 31 decimal. + private static final int VLQ_BASE_MASK = VLQ_BASE - 1; + + // The continuation bit is the 6th bit. + private static final int VLQ_CONTINUATION_BIT = VLQ_BASE; + + /** + * Decodes the next VLQValue from the provided CharIterator. + */ + public static int decode(CharIterator in) { + int result = 0; + int shift = 0; + int digit; + do { + digit = Base64.BASE64_DECODE_MAP[in.next()]; + assert (digit != -1) : "invalid char"; + + result += (digit & VLQ_BASE_MASK) << shift; + shift += VLQ_BASE_SHIFT; + } + while ((digit & VLQ_CONTINUATION_BIT) != 0); + + boolean negate = (result & 1) == 1; + result >>= 1; + return negate ? -result : result; + } + + private static final class Base64 { + /** + * A map used to convert integer values in the range 0-63 to their base64 + * values. + */ + private static final String BASE64_MAP = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + "abcdefghijklmnopqrstuvwxyz" + + "0123456789+/"; + + /** + * A map used to convert base64 character into integer values. + */ + private static final int[] BASE64_DECODE_MAP = new int[256]; + + static { + for (int i = 0; i < BASE64_MAP.length(); i++) { + BASE64_DECODE_MAP[BASE64_MAP.charAt(i)] = i; + } + } + } +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/sourcemap/MappingEntry.kt b/platform/script-debugger/backend/src/debugger/sourcemap/MappingEntry.kt new file mode 100644 index 00000000..8f9aad86 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/sourcemap/MappingEntry.kt @@ -0,0 +1,37 @@ +/* + * 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 + +/** + * Mapping entry in the source map + */ +interface MappingEntry { + val generatedColumn: Int + + val generatedLine: Int + + val sourceLine: Int + + val sourceColumn: Int + + val source: Int + get() = -1 + + val name: String? + get() = null + + val nextGenerated: MappingEntry +} diff --git a/platform/script-debugger/backend/src/debugger/sourcemap/MappingList.kt b/platform/script-debugger/backend/src/debugger/sourcemap/MappingList.kt new file mode 100644 index 00000000..c87786c0 --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/sourcemap/MappingList.kt @@ -0,0 +1,210 @@ +/* + * 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.intellij.openapi.editor.Document +import java.util.* + +interface Mappings { + fun get(line: Int, column: Int): MappingEntry? + + fun getNextOnTheSameLine(index: Int, skipIfColumnEquals: Boolean = true): MappingEntry? + + fun getNext(mapping: MappingEntry): MappingEntry? + + fun getNextOnTheSameLine(mapping: MappingEntry): MappingEntry? { + val nextMapping = getNext(mapping) + return if (nextMapping != null && getLine(nextMapping) == getLine(mapping)) nextMapping else null + } + + fun indexOf(line: Int, column: Int): Int + + fun getByIndex(index: Int): MappingEntry + + fun getLine(mapping: MappingEntry): Int + + fun getColumn(mapping: MappingEntry): Int +} + +abstract class MappingList(private val mappings: List<MappingEntry>) : Mappings { + val size: Int + get() = mappings.size + + protected abstract val comparator: Comparator<MappingEntry> + + override fun indexOf(line: Int, column: Int): Int { + var low = 0 + var high = mappings.size - 1 + if (mappings.isEmpty() || getLine(mappings[low]) > line || getLine(mappings[high]) < line) { + return -1 + } + + while (low <= high) { + val middle = (low + high).ushr(1) + val mapping = mappings[middle] + val mappingLine = getLine(mapping) + if (line == mappingLine) { + if (column == getColumn(mapping)) { + // find first + var firstIndex = middle + while (firstIndex > 0) { + val prevMapping = mappings[firstIndex - 1] + if (getLine(prevMapping) == line && getColumn(prevMapping) == column) { + firstIndex-- + } + else { + break + } + } + return firstIndex + } + else if (column < getColumn(mapping)) { + if (column == 0 || column == -1) { + // find first + var firstIndex = middle + while (firstIndex > 0 && getLine(mappings[firstIndex - 1]) == line) { + firstIndex-- + } + return firstIndex + } + + if (middle == 0) { + return -1 + } + + val prevMapping = mappings[middle - 1] + when { + line != getLine(prevMapping) -> return -1 + column >= getColumn(prevMapping) -> return middle - 1 + else -> high = middle - 1 + } + } + else { + // https://code.google.com/p/google-web-toolkit/issues/detail?id=9103 + // We skipIfColumnEquals because GWT has two entries - source position equals, but generated no. We must use first entry (at least, in case of GWT it is correct) + val nextMapping = getNextOnTheSameLine(middle) + if (nextMapping == null) { + return middle + } + else { + low = middle + 1 + } + } + } + else if (line > mappingLine) { + low = middle + 1 + } + else { + high = middle - 1 + } + } + + return -1 + } + + // todo honor Google Chrome bug related to paused location + override fun get(line: Int, column: Int): MappingEntry? = mappings.getOrNull(indexOf(line, column)) + + private fun getNext(index: Int) = mappings.getOrNull(index + 1) + + override fun getNext(mapping: MappingEntry): MappingEntry? { + if (comparator == MAPPING_COMPARATOR_BY_GENERATED_POSITION) { + return mapping.nextGenerated + } + + var index = mappings.binarySearch(mapping, comparator) + if (index < 0) { + return null + } + index++ + + var result: MappingEntry? + do { + result = mappings.getOrNull(index++) + } + while (mapping === result) + return result + } + + override fun getNextOnTheSameLine(index: Int, skipIfColumnEquals: Boolean): MappingEntry? { + var nextMapping = getNext(index) ?: return null + + val mapping = getByIndex(index) + if (getLine(nextMapping) != getLine(mapping)) { + return null + } + + if (skipIfColumnEquals) { + var i = index + // several generated segments can point to one source segment, so, in mapping list ordered by source, could be several mappings equal in terms of source position + while (getColumn(nextMapping) == getColumn(mapping)) { + nextMapping = getNextOnTheSameLine(++i, false) ?: return null + } + } + + return nextMapping + } + + fun getEndOffset(mapping: MappingEntry, lineStartOffset: Int, document: Document): Int { + val nextMapping = getNextOnTheSameLine(Collections.binarySearch(mappings, mapping, comparator)) + return if (nextMapping == null) document.getLineEndOffset(getLine(mapping)) else lineStartOffset + getColumn(nextMapping) + } + + override fun getByIndex(index: Int): MappingEntry = mappings.get(index) + + // entries will be processed in this list order + fun processMappingsInLine(line: Int, entryProcessor: MappingsProcessorInLine): Boolean { + var low = 0 + var high = mappings.size - 1 + while (low <= high) { + val middle = (low + high).ushr(1) + val mapping = mappings.get(middle) + val mappingLine = getLine(mapping) + when { + line == mappingLine -> { + // find first + var firstIndex = middle + while (firstIndex > 0 && getLine(mappings.get(firstIndex - 1)) == line) { + firstIndex-- + } + + var entry: MappingEntry? = mappings.get(firstIndex) + do { + var nextEntry = mappings.getOrNull(++firstIndex) + if (nextEntry != null && getLine(nextEntry) != line) { + nextEntry = null + } + + if (!entryProcessor.process(entry!!, nextEntry)) { + return true + } + + entry = nextEntry + } + while (entry != null) + return true + } + line > mappingLine -> low = middle + 1 + else -> high = middle - 1 + } + } + return false + } +} + +interface MappingsProcessorInLine { + fun process(entry: MappingEntry, nextEntry: MappingEntry?): Boolean +} diff --git a/platform/script-debugger/backend/src/debugger/sourcemap/NestedSourceMap.kt b/platform/script-debugger/backend/src/debugger/sourcemap/NestedSourceMap.kt new file mode 100644 index 00000000..c4a5ebfc --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/sourcemap/NestedSourceMap.kt @@ -0,0 +1,119 @@ +/* + * 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.intellij.openapi.vfs.VirtualFile +import com.intellij.util.Url +import gnu.trove.THashMap + +class NestedSourceMap(private val childMap: SourceMap, private val parentMap: SourceMap) : SourceMap { + override val sourceResolver: SourceResolver + get() = parentMap.sourceResolver + + override val sources: Array<Url> + get() = parentMap.sources + + private val sourceIndexToSourceMappings = arrayOfNulls<Mappings>(parentMap.sources.size) + + private val childMappingToTransformed = THashMap<MappingEntry, MappingEntry>() + + override val outFile: String? + get() = childMap.outFile + + override val hasNameMappings: Boolean + get() = childMap.hasNameMappings || parentMap.hasNameMappings + + override val generatedMappings: Mappings by lazy { + NestedMappings(childMap.generatedMappings, parentMap.generatedMappings, false) + } + + override fun findSourceMappings(sourceIndex: Int): Mappings { + var result = sourceIndexToSourceMappings.get(sourceIndex) + if (result == null) { + result = NestedMappings(childMap.findSourceMappings(sourceIndex), parentMap.findSourceMappings(sourceIndex), true) + sourceIndexToSourceMappings.set(sourceIndex, result) + } + return result + } + + override fun findSourceIndex(sourceFile: VirtualFile, localFileUrlOnly: Boolean): Int = parentMap.findSourceIndex(sourceFile, localFileUrlOnly) + + override fun findSourceIndex(sourceUrls: List<Url>, + sourceFile: VirtualFile?, + resolver: Lazy<SourceFileResolver?>?, + localFileUrlOnly: Boolean): Int = parentMap.findSourceIndex(sourceUrls, sourceFile, resolver, localFileUrlOnly) + + override fun processSourceMappingsInLine(sourceIndex: Int, sourceLine: Int, mappingProcessor: MappingsProcessorInLine): Boolean { + val childSourceMappings = childMap.findSourceMappings(sourceIndex) + return (parentMap.findSourceMappings(sourceIndex) as MappingList).processMappingsInLine(sourceLine, object: MappingsProcessorInLine { + override fun process(entry: MappingEntry, nextEntry: MappingEntry?): Boolean { + val childIndex = childSourceMappings.indexOf(entry.generatedLine, entry.generatedColumn) + if (childIndex == -1) { + return true + } + + val childEntry = childSourceMappings.getByIndex(childIndex) + // todo not clear - should we resolve next child entry by current child index or by provided parent nextEntry? + val nextChildEntry = if (nextEntry == null) null else childSourceMappings.getNextOnTheSameLine(childIndex) + return mappingProcessor.process(childMappingToTransformed.getOrPut(childEntry) { NestedMappingEntry(childEntry, entry) }, + nextChildEntry?.let { childMappingToTransformed.getOrPut(it) { NestedMappingEntry(it, entry) } }) + } + }) + } +} + +private class NestedMappings(private val child: Mappings, private val parent: Mappings, private val isSourceMappings: Boolean) : Mappings { + override fun getNextOnTheSameLine(index: Int, skipIfColumnEquals: Boolean) = parent.getNextOnTheSameLine(index, skipIfColumnEquals) + + override fun getNext(mapping: MappingEntry) = parent.getNext(mapping) + + override fun indexOf(line: Int, column: Int) = parent.indexOf(line, column) + + override fun getByIndex(index: Int) = parent.getByIndex(index) + + override fun getLine(mapping: MappingEntry) = parent.getLine(mapping) + + override fun getColumn(mapping: MappingEntry) = parent.getColumn(mapping) + + override fun get(line: Int, column: Int): MappingEntry? { + return if (isSourceMappings) { + parent.get(line, column)?.let { child.get(it.generatedLine, it.generatedColumn) } + } + else { + child.get(line, column)?.let { parent.get(it.sourceLine, it.sourceColumn) } + } + } +} + +private data class NestedMappingEntry(private val child: MappingEntry, private val parent: MappingEntry) : MappingEntry { + override val generatedLine: Int + get() = child.generatedLine + + override val generatedColumn: Int + get() = child.generatedColumn + + override val sourceLine: Int + get() = parent.sourceLine + + override val sourceColumn: Int + get() = parent.sourceColumn + + override val name: String? + get() = parent.name + + override val nextGenerated: MappingEntry + get() = child.nextGenerated +}
\ No newline at end of file diff --git a/platform/script-debugger/backend/src/debugger/sourcemap/SourceMap.kt b/platform/script-debugger/backend/src/debugger/sourcemap/SourceMap.kt new file mode 100644 index 00000000..283a5b6c --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/sourcemap/SourceMap.kt @@ -0,0 +1,81 @@ +/* + * 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.intellij.openapi.vfs.VirtualFile +import com.intellij.util.Url + +// sources - is not originally specified, but canonicalized/normalized +// lines and columns are zero-based according to specification +interface SourceMap { + val outFile: String? + + /** + * note: Nested map returns only parent sources + */ + val sources: Array<Url> + + val generatedMappings: Mappings + val hasNameMappings: Boolean + val sourceResolver: SourceResolver + + fun findSourceMappings(sourceIndex: Int): Mappings + + fun findSourceIndex(sourceUrls: List<Url>, sourceFile: VirtualFile?, resolver: Lazy<SourceFileResolver?>?, localFileUrlOnly: Boolean): Int + + fun findSourceMappings(sourceUrls: List<Url>, sourceFile: VirtualFile?, resolver: Lazy<SourceFileResolver?>?, localFileUrlOnly: Boolean): Mappings? { + val sourceIndex = findSourceIndex(sourceUrls, sourceFile, resolver, localFileUrlOnly) + return if (sourceIndex >= 0) findSourceMappings(sourceIndex) else null + } + + fun getSourceLineByRawLocation(rawLine: Int, rawColumn: Int): Int = generatedMappings.get(rawLine, rawColumn)?.sourceLine ?: -1 + + fun findSourceIndex(sourceFile: VirtualFile, localFileUrlOnly: Boolean): Int + + fun processSourceMappingsInLine(sourceIndex: Int, sourceLine: Int, mappingProcessor: MappingsProcessorInLine): Boolean + + fun processSourceMappingsInLine(sourceUrls: List<Url>, sourceLine: Int, mappingProcessor: MappingsProcessorInLine, sourceFile: VirtualFile?, resolver: Lazy<SourceFileResolver?>?, localFileUrlOnly: Boolean): Boolean { + val sourceIndex = findSourceIndex(sourceUrls, sourceFile, resolver, localFileUrlOnly) + return sourceIndex >= 0 && processSourceMappingsInLine(sourceIndex, sourceLine, mappingProcessor) + } +} + + +class OneLevelSourceMap(override val outFile: String?, + override val generatedMappings: Mappings, + private val sourceIndexToMappings: Array<MappingList?>, + override val sourceResolver: SourceResolver, + override val hasNameMappings: Boolean) : SourceMap { + override val sources: Array<Url> + get() = sourceResolver.canonicalizedUrls + + override fun findSourceIndex(sourceUrls: List<Url>, sourceFile: VirtualFile?, resolver: Lazy<SourceFileResolver?>?, localFileUrlOnly: Boolean): Int { + val index = sourceResolver.findSourceIndex(sourceUrls, sourceFile, localFileUrlOnly) + if (index == -1 && resolver != null) { + return resolver.value?.let { sourceResolver.findSourceIndex(it) } ?: -1 + } + return index + } + + // returns SourceMappingList + override fun findSourceMappings(sourceIndex: Int): MappingList = sourceIndexToMappings.get(sourceIndex)!! + + override fun findSourceIndex(sourceFile: VirtualFile, localFileUrlOnly: Boolean): Int = sourceResolver.findSourceIndexByFile(sourceFile, localFileUrlOnly) + + override fun processSourceMappingsInLine(sourceIndex: Int, sourceLine: Int, mappingProcessor: MappingsProcessorInLine): Boolean { + return findSourceMappings(sourceIndex).processMappingsInLine(sourceLine, mappingProcessor) + } +}
\ No newline at end of file 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 +} + diff --git a/platform/script-debugger/backend/src/debugger/sourcemap/SourceResolver.kt b/platform/script-debugger/backend/src/debugger/sourcemap/SourceResolver.kt new file mode 100644 index 00000000..2a9c868f --- /dev/null +++ b/platform/script-debugger/backend/src/debugger/sourcemap/SourceResolver.kt @@ -0,0 +1,186 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package org.jetbrains.debugger.sourcemap + +import com.intellij.openapi.util.SystemInfo +import com.intellij.openapi.util.io.FileUtil +import com.intellij.openapi.vfs.StandardFileSystems +import com.intellij.openapi.vfs.VfsUtilCore +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.util.Url +import com.intellij.util.Urls +import com.intellij.util.containers.ObjectIntHashMap +import com.intellij.util.containers.isNullOrEmpty +import com.intellij.util.io.URLUtil +import java.io.File + +interface SourceFileResolver { + /** + * Return -1 if no match + */ + fun resolve(map: ObjectIntHashMap<Url>): Int = -1 + fun resolve(rawSources: List<String>): Int = -1 +} + +class SourceResolver(private val rawSources: List<String>, + trimFileScheme: Boolean, + baseUrl: Url?, + private val sourceContents: List<String?>?, + baseUrlIsFile: Boolean = true) { + companion object { + fun isAbsolute(path: String): Boolean = path.startsWith('/') || (SystemInfo.isWindows && (path.length > 2 && path[1] == ':')) + } + + val canonicalizedUrls: Array<Url> by lazy { + Array(rawSources.size) { canonicalizeUrl(rawSources[it], baseUrl, trimFileScheme, baseUrlIsFile) } + } + + private val canonicalizedUrlToSourceIndex: ObjectIntHashMap<Url> by lazy { + ( + if (SystemInfo.isFileSystemCaseSensitive) ObjectIntHashMap(rawSources.size) + else ObjectIntHashMap(rawSources.size, Urls.caseInsensitiveUrlHashingStrategy) + ).also { + for (i in rawSources.indices) { + it.put(canonicalizedUrls[i], i) + } + } + } + + fun getSource(entry: MappingEntry): Url? { + val index = entry.source + return if (index < 0) null else canonicalizedUrls[index] + } + + fun getSourceContent(entry: MappingEntry): String? { + if (sourceContents.isNullOrEmpty()) { + return null + } + + val index = entry.source + return if (index < 0 || index >= sourceContents!!.size) null else sourceContents[index] + } + + fun getSourceContent(sourceIndex: Int): String? { + if (sourceContents.isNullOrEmpty()) { + return null + } + return if (sourceIndex < 0 || sourceIndex >= sourceContents!!.size) null else sourceContents[sourceIndex] + } + + fun getSourceIndex(url: Url): Int = canonicalizedUrlToSourceIndex[url] + + fun getRawSource(entry: MappingEntry): String? { + val index = entry.source + return if (index < 0) null else rawSources[index] + } + + internal fun findSourceIndex(resolver: SourceFileResolver): Int { + val resolveByCanonicalizedUrls = resolver.resolve(canonicalizedUrlToSourceIndex) + return if (resolveByCanonicalizedUrls != -1) resolveByCanonicalizedUrls else resolver.resolve(rawSources) + } + + fun findSourceIndex(sourceUrls: List<Url>, sourceFile: VirtualFile?, localFileUrlOnly: Boolean): Int { + for (sourceUrl in sourceUrls) { + val index = canonicalizedUrlToSourceIndex.get(sourceUrl) + if (index != -1) { + return index + } + } + + if (sourceFile != null) { + return findSourceIndexByFile(sourceFile, localFileUrlOnly) + } + return -1 + } + + internal fun findSourceIndexByFile(sourceFile: VirtualFile, localFileUrlOnly: Boolean): Int { + if (!localFileUrlOnly) { + val index = canonicalizedUrlToSourceIndex.get(Urls.newFromVirtualFile(sourceFile).trimParameters()) + if (index != -1) { + return index + } + } + + if (!sourceFile.isInLocalFileSystem) { + return -1 + } + + // local file url - without "file" scheme, just path + val index = canonicalizedUrlToSourceIndex.get(Urls.newLocalFileUrl(sourceFile)) + if (index != -1) { + return index + } + + // ok, search by canonical path + val canonicalFile = sourceFile.canonicalFile + if (canonicalFile != null && canonicalFile != sourceFile) { + for (i in canonicalizedUrls.indices) { + val url = canonicalizedUrls.get(i) + if (Urls.equalsIgnoreParameters(url, canonicalFile)) { + return i + } + } + } + return -1 + } + + fun getUrlIfLocalFile(entry: MappingEntry): Url? = canonicalizedUrls.getOrNull(entry.source)?.let { if (it.isInLocalFileSystem) it else null } +} + +fun canonicalizePath(url: String, baseUrl: Url, baseUrlIsFile: Boolean): String { + var path = url + if (!FileUtil.isAbsolute(url) && !url.isEmpty() && url[0] != '/') { + val basePath = baseUrl.path + if (baseUrlIsFile) { + val lastSlashIndex = basePath.lastIndexOf('/') + val pathBuilder = StringBuilder() + if (lastSlashIndex == -1) { + pathBuilder.append('/') + } + else { + pathBuilder.append(basePath, 0, lastSlashIndex + 1) + } + path = pathBuilder.append(url).toString() + } + else { + path = "$basePath/$url" + } + } + return FileUtil.toCanonicalPath(path, '/') +} + +// see canonicalizeUri kotlin impl and https://trac.webkit.org/browser/trunk/Source/WebCore/inspector/front-end/ParsedURL.js completeURL +fun canonicalizeUrl(url: String, baseUrl: Url?, trimFileScheme: Boolean, baseUrlIsFile: Boolean = true): Url { + if (trimFileScheme && url.startsWith(StandardFileSystems.FILE_PROTOCOL_PREFIX)) { + return Urls.newLocalFileUrl(FileUtil.toCanonicalPath(VfsUtilCore.toIdeaUrl(url, true).substring(StandardFileSystems.FILE_PROTOCOL_PREFIX.length), '/')) + } + else if (baseUrl == null || url.contains(URLUtil.SCHEME_SEPARATOR) || url.startsWith("data:") || url.startsWith("blob:") || + url.startsWith("javascript:") || url.startsWith("webpack:")) { + // consider checking :/ instead of :// because scheme may be followed by path, not by authority + // https://tools.ietf.org/html/rfc3986#section-1.1.2 + // be careful with windows paths: C:/Users + return Urls.parseEncoded(url) ?: Urls.newUri(null, url) + } + else { + return doCanonicalize(url, baseUrl, baseUrlIsFile, true) + } +} + +fun doCanonicalize(url: String, baseUrl: Url, baseUrlIsFile: Boolean, asLocalFileIfAbsoluteAndExists: Boolean): Url { + val path = canonicalizePath(url, baseUrl, baseUrlIsFile) + if ((baseUrl.scheme == null && baseUrl.isInLocalFileSystem) || + asLocalFileIfAbsoluteAndExists && SourceResolver.isAbsolute(path) && File(path).exists()) { + // file:///home/user/foo.js.map, foo.ts -> /home/user/foo.ts (baseUrl is in local fs) + // http://localhost/home/user/foo.js.map, foo.ts -> /home/user/foo.ts (File(path) exists) + return Urls.newLocalFileUrl(path) + } + else if (!path.startsWith("/")) { + // http://localhost/source.js.map, C:/foo.ts webpack-dsj3c45 -> C:/foo.ts webpack-dsj3c45 + // (we can't append path suffixes unless they start with / + return Urls.parse(path, true) ?: Urls.newUnparsable(path) + } + else { + // new url from path and baseUrl's scheme and authority + val split = path.split('?', limit = 2) + return Urls.newUrl(baseUrl.scheme!!, baseUrl.authority!!, split[0], if (split.size > 1) '?' + split[1] else null) + } +}
\ No newline at end of file |