/* * Copyright (c) 2013, the Dart project authors. * * Licensed under the Eclipse Public License v1.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.eclipse.org/legal/epl-v10.html * * 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 com.github.sdbg.debug.core.internal.webkit.model; import com.github.sdbg.debug.core.SDBGDebugCorePlugin; import com.github.sdbg.debug.core.internal.source.WorkspaceSourceContainer; import com.github.sdbg.debug.core.internal.sourcemaps.SourceMap; import com.github.sdbg.debug.core.internal.sourcemaps.SourceMapInfo; import com.github.sdbg.debug.core.internal.util.URLStorage; import com.github.sdbg.debug.core.model.IResourceResolver; import com.github.sdbg.debug.core.util.Trace; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.StringReader; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Properties; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IStorage; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.URIUtil; // TODO(devoncarew): use the symbol name information in the maps? // it's possible this will help us de-mangle the method names for frames /** * A class to help manage parsing and querying source maps. It automatically parses source maps and * keeps that info up to date. It also helps retrieve information about map sources and map targets. * A source map contains information mapping locations in source files to locations in target files. * For instance foo.dart.js ==> [foo.dart, bar.dart, baz.dart]. The reverse direction is from * targets ==> sources. * * @see SourceMap */ //&&&!!! public class SourceMapManager { public static class SourceLocation { private IStorage storage; // TODO: Besides this, an IPath member is needed, because the storage may not always get resolved private String path; private int line; private int column; private String name; public SourceLocation(IStorage storage, String path, int line, int column, String name) { this.storage = storage; this.path = path; this.line = line; this.column = column; this.name = name; } public int getColumn() { return column; } public int getLine() { return line; } public String getName() { return name; } public String getPath() { return path; } public IStorage getStorage() { return storage; } @Override public String toString() { return "[" + (storage != null ? storage : "") + "," + path + "," + line + "," + column + "," + name + "]"; } } private static class TargetPathCheckingVisitor implements Visitor { private String targetPath; private boolean stopOnInexactMatch; private IStorage matchingScriptStorage; private IStorage matchingSourceMapStorage; private SourceMap matchingSourceMap; private String matchingSourcePath; public TargetPathCheckingVisitor(String targetPath) { this(targetPath, false); } public TargetPathCheckingVisitor(String targetPath, boolean stopOnInexactMatch) { this.targetPath = targetPath; this.stopOnInexactMatch = stopOnInexactMatch; } public IStorage getMatchingScriptStorage() { return matchingScriptStorage; } public SourceMap getMatchingSourceMap() { return matchingSourceMap; } public IStorage getMatchingSourceMapStorage() { return matchingSourceMapStorage; } public String getMatchingSourcePath() { return matchingSourcePath; } @Override public String toString() { return "[Script: " + matchingScriptStorage + ", Source map: " + matchingSourceMapStorage + ", Source path: " + matchingSourcePath + "]"; } @Override public boolean visit(IStorage scriptStorage, IStorage sourceMapStorage, SourceMap sourceMap, String sourcePath) { String sourceRoot = sourceMap.getSourceRoot(); String relativePath = sourceRoot != null && sourceRoot.length() > 0 ? sourcePath.substring(sourceRoot.length()) : sourcePath; if (relativePath.endsWith(targetPath)) { if (matchingSourcePath == null || matchingSourcePath.length() > sourcePath.length()) { matchingSourcePath = sourcePath; matchingSourceMap = sourceMap; matchingSourceMapStorage = sourceMapStorage; matchingScriptStorage = scriptStorage; if (stopOnInexactMatch || relativePath.length() == targetPath.length()) { trace(); return true; } } } trace(); return false; } private void trace() { if (isTracing()) { if (matchingSourcePath != null) { SourceMapManager.trace("Match: " + toString()); } else { SourceMapManager.trace("No match"); } } } } private static interface Visitor { boolean visit(IStorage scriptStorage, IStorage mapStorage, SourceMap map, String path); } private IResourceResolver resourceResolver; private Map<IStorage, IStorage> sourceMapsStorages = new HashMap<IStorage, IStorage>(); private Map<IStorage, SourceMap> sourceMaps = new HashMap<IStorage, SourceMap>(); static boolean isTracing() { return Trace.isTracing(Trace.SOURCEMAPS); } static void trace(String message) { Trace.trace(Trace.SOURCEMAPS, message); } public SourceMapManager(IResourceResolver resourceResolver) { this.resourceResolver = resourceResolver; } public void dispose() { } /** * Given a source (foo.dart.js) file and a location, return the corresponding target location (in * foo.dart). * * @param storage * @param line * @param column * @return */ public SourceLocation getMappingFor(IStorage storage, int line, int column) { if (isTracing()) { trace("Get mappings for " + storage + ":" + line + ":" + column); } synchronized (sourceMaps) { IStorage mapStorage = sourceMapsStorages.get(storage); if (mapStorage != null) { SourceMap map = sourceMaps.get(mapStorage); if (map != null) { SourceMapInfo mapping = map.getMappingFor(line, column); if (mapping != null) { IStorage resolvedStorage = resolveStorage(mapStorage, mapping.getFile()); if (resolvedStorage != null) { String sourceRoot = map.getSourceRoot(); String relativePath = mapping.getFile(); if (sourceRoot != null && sourceRoot.length() > 0) { relativePath = relativePath.substring(sourceRoot.length()); } SourceLocation location = new SourceLocation( resolvedStorage, relativePath, mapping.getLine(), mapping.getColumn(), mapping.getName()); if (isTracing()) { trace("Found mapping: " + location); } return location; } } } } } return null; } /** * Given a target location (in foo.dart), return the corresponding source location (in * foo.dart.js). * * @param storage * @param line * @return */ public List<SourceLocation> getReverseMappingsFor(String targetPath, int line) { if (isTracing()) { trace("Get reverse mappings for " + targetPath + ":" + line); } List<SourceLocation> mappings = new ArrayList<SourceMapManager.SourceLocation>(); TargetPathCheckingVisitor visitor = new TargetPathCheckingVisitor(targetPath); synchronized (sourceMaps) { visit(visitor); if (visitor.getMatchingSourcePath() != null) { List<SourceMapInfo> reverseMappings = visitor.getMatchingSourceMap().getReverseMappingsFor( visitor.getMatchingSourcePath(), line); for (SourceMapInfo reverseMapping : reverseMappings) { if (reverseMapping != null) { IStorage mapSource = visitor.getMatchingScriptStorage(); //&&&!!! visitor.getMAtchingSourceMapStorage(); if (mapSource != null) { mappings.add(new SourceLocation( mapSource, mapSource.getFullPath().toPortableString(), reverseMapping.getLine(), reverseMapping.getColumn(), reverseMapping.getName())); } } } } } return mappings; } public IStorage getSource(String targetPath) { //&&&!!! There can be race conditions because of that method if (targetPath != null) { if (isTracing()) { trace("Get source storage: " + targetPath); } TargetPathCheckingVisitor visitor = new TargetPathCheckingVisitor(targetPath); synchronized (sourceMaps) { visit(visitor); if (visitor.getMatchingSourcePath() != null) { return resolveStorage( visitor.getMatchingSourceMapStorage(), visitor.getMatchingSourcePath()); } } } return null; } /** * Returns true if the the source map manager contains mapping information for the given file back * to original resources. * * @param resource * @return true if the the source map manager contains mapping information for the given file */ public boolean isMapSource(IStorage storage) { //&&&!!! There can be race conditions because of that method if (storage != null) { if (isTracing()) { trace("Check for map source: " + storage); } synchronized (sourceMaps) { boolean result = sourceMapsStorages.containsKey(storage); if (isTracing() && result) { trace("Confirmed - map source"); } return result; } } return false; } public boolean isMapTarget(IStorage scriptStorage, String targetPath) { //&&&!!! There can be race conditions because of that method if (targetPath != null) { if (isTracing()) { trace("Check for map target: " + targetPath); } TargetPathCheckingVisitor visitor = new TargetPathCheckingVisitor(targetPath, true/*stopOnInexactMatch*/); synchronized (sourceMaps) { visit(scriptStorage, visitor); return visitor.getMatchingSourcePath() != null; } } else { return false; } } public boolean isMapTarget(String targetPath) { return isMapTarget(null/*scriptStorage*/, targetPath); } void handleGlobalObjectCleared() { synchronized (sourceMaps) { sourceMapsStorages.clear(); sourceMaps.clear(); } } void handleScriptParsed(IStorage script, String scriptUrl, String sourceMapUrl) { synchronized (sourceMaps) { IStorage mapStorage = sourceMapsStorages.remove(script); if (mapStorage != null) { sourceMaps.remove(script); } trace("Checking script for sourcemaps: " + script); try { processScript(script, scriptUrl, sourceMapUrl); } catch (CoreException e) { // Processing a source map is always a best effort, because the sourcemap could be missing or broken SDBGDebugCorePlugin.logError(e); trace("Processing script " + script + " failed: " + e.getMessage()); } } } private boolean isDownloadable(URI uri) { if (uri == null || uri.getScheme() == null) { return false; } return uri.getScheme().equals("file") || (uri.getScheme().equals("http") || uri.getScheme().equals("https")) && uri.getHost() != null && uri.getHost().length() > 0; } private SourceMap parseSourceMap(IStorage mapStorage) throws IOException, CoreException { if (mapStorage != null) { return SourceMap.createFrom(mapStorage); } else { return null; } } private void processScript(IStorage script, String scriptUrl, String sourceMapUrl) throws CoreException { try { if (sourceMapUrl == null) { BufferedReader reader; if (script instanceof IFile) { reader = new BufferedReader(new InputStreamReader( script.getContents(), ((IFile) script).getCharset())); } else { reader = new BufferedReader(new InputStreamReader(script.getContents())); } try { String sourceMapUrlLine = null; for (sourceMapUrlLine = reader.readLine(); sourceMapUrlLine != null; sourceMapUrlLine = reader.readLine()) { sourceMapUrlLine = sourceMapUrlLine.trim(); if (sourceMapUrlLine.startsWith("//#") || sourceMapUrlLine.startsWith("//@")) { sourceMapUrlLine = sourceMapUrlLine.substring(2).trim(); if (sourceMapUrlLine.matches("sourceMappingURL\\s*\\=")) { break; } } } if (sourceMapUrlLine != null) { Properties properties = new Properties(); properties.load(new StringReader(sourceMapUrlLine)); sourceMapUrl = properties.getProperty("sourceMappingURL"); trace("Sourcemap detected in a // comment"); } } finally { reader.close(); } } IStorage mapStorage; if (sourceMapUrl != null && sourceMapUrl.length() > 0) { trace("Found sourcemap with URL: " + sourceMapUrl); mapStorage = resolveStorage(script, scriptUrl, sourceMapUrl); if (mapStorage == null) { trace("Sourcemap with URL " + sourceMapUrl + " was not resolved"); } } else { mapStorage = null; } if (mapStorage != null) { SourceMap map = parseSourceMap(mapStorage); if (map != null) { sourceMapsStorages.put(script, mapStorage); sourceMaps.put(mapStorage, map); trace("Parsing sourcemap succeeded: " + mapStorage); } } } catch (IOException e) { throw SDBGDebugCorePlugin.wrapError(e); } } private IStorage resolveStorage(IStorage relativeStorage, String path) { return resolveStorage(relativeStorage, null/*relativeUriStr*/, path); } private IStorage resolveStorage(IStorage relativeStorage, String relativeUriStr, String path) { if (path.startsWith("file:")) { try { // These incoming uris are not properly uri encoded. If we uri encoded them, we would then // not be able to handle properly uri encoded uris. Instead we handle certain illegal chars. //String encodedPath = URIUtilities.uriEncode(path); String encodedPath = path.replaceAll(" ", "%20"); URI uri = new URI(encodedPath); IResource resource = WorkspaceSourceContainer.locatePathAsResource(uri.getPath()); if (resource instanceof IFile) { return (IFile) resource; } } catch (URISyntaxException e) { SDBGDebugCorePlugin.logError(e); trace("Resolving storage " + relativeStorage + " with path " + path + " failed: " + e.getMessage()); } } else if (relativeStorage instanceof IFile) { IFile file = ((IFile) relativeStorage).getParent().getFile(new Path(path)); if (file.exists()) { return file; } else { return null; } } else { try { URI uri = URIUtil.fromString(path); if (!isDownloadable(uri)) { IResource resource = resourceResolver.resolveUrl(uri.toString()); if (resource instanceof IFile) { return (IFile) resource; } // The source path is not a downloadable URI, try to build a downloadable URI URI relativeUri = null; // First, try with the passed URI if (relativeUriStr != null && relativeUriStr.length() > 0) { relativeUri = new URI(relativeUriStr); } // Next, try with the storage URI if (!isDownloadable(relativeUri) && relativeStorage instanceof URLStorage) { relativeUri = ((URLStorage) relativeStorage).getURL().toURI(); } if (isDownloadable(relativeUri)) { IPath newPath = Path.fromPortableString(relativeUri.getPath()).removeLastSegments(1).append( uri.getPath()); uri = new URI( relativeUri.getScheme(), null, relativeUri.getHost(), relativeUri.getPort(), newPath.toPortableString(), null, null); } } if (isDownloadable(uri)) { return new URLStorage(uri.toURL()); } } catch (URISyntaxException e) { SDBGDebugCorePlugin.logError(e); trace("Resolving storage " + relativeStorage + " with path " + path + " failed: " + e.getMessage()); } catch (MalformedURLException e) { SDBGDebugCorePlugin.logError(e); trace("Resolving storage " + relativeStorage + " with path " + path + " failed: " + e.getMessage()); } } return null; } private void visit(IStorage forScriptStorage, Visitor visitor) { //&&&!!! There can be race conditions because of that method synchronized (sourceMaps) { for (IStorage scriptStorage : forScriptStorage != null ? Collections.singleton(forScriptStorage) : sourceMapsStorages.keySet()) { IStorage sourceMapStorage = sourceMapsStorages.get(scriptStorage); if (sourceMapStorage != null) { SourceMap sourceMap = sourceMaps.get(sourceMapStorage); for (String sourcePath : sourceMap.getSourceNames()) { if (visitor.visit(scriptStorage, sourceMapStorage, sourceMap, sourcePath)) { break; } } } } } } private void visit(Visitor visitor) { visit(null/*forScriptStorage*/, visitor); } }