/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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 flash.tools.debugger.concrete; import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import flash.tools.debugger.NoResponseException; import flash.tools.debugger.Session; import flash.tools.debugger.SourceFile; import flash.tools.debugger.SourceLocator; import flash.tools.debugger.VersionException; import flash.util.FileUtils; /** * A module which is uniquly identified by an id, contains * a short and long name and also a script */ public class DModule implements SourceFile { private ScriptText m_script; // lazy-initialized by getScript() private boolean m_gotRealScript; private final String m_rawName; private final String m_shortName; private final String m_path; private final String m_basePath; private final int m_id; private final int m_bitmap; private final ArrayList<Integer> m_line2Offset; private final ArrayList<Object> m_line2Func; // each array is either null, String, or String[] private final HashMap<String, Integer> m_func2FirstLine; // maps function name (String) to first line of function (Integer) private final HashMap<String, Integer> m_func2LastLine; // maps function name (String) to last line of function (Integer) private String m_packageName; private boolean m_gotAllFncNames; private int m_anonymousFunctionCounter = 0; private SourceLocator m_sourceLocator; private int m_sourceLocatorChangeCount; private int m_isolateId; private final static String m_newline = System.getProperty("line.separator"); //$NON-NLS-1$ /** * @param name filename in "basepath;package;filename" format */ public DModule(SourceLocator sourceLocator, int id, int bitmap, String name, String script, int isolateId) { // If the caller gave us the script text, then we will create m_script // now. But if the caller gave us an empty string, then we won't bother // looking for a disk file until someone actually asks for it. if (script != null && script.length() > 0) { m_script = new ScriptText(script); m_gotRealScript = true; } NameParser nameParser = new NameParser(name); m_sourceLocator = sourceLocator; m_rawName = name; m_basePath = nameParser.getBasePath(); // may be null m_bitmap = bitmap; m_id = id; m_shortName = generateShortName(nameParser); m_path = generatePath(nameParser); m_line2Offset = new ArrayList<Integer>(); m_line2Func = new ArrayList<Object>(); m_func2FirstLine = new HashMap<String, Integer>(); m_func2LastLine = new HashMap<String, Integer>(); m_packageName = nameParser.getPackage(); m_gotAllFncNames = false; m_isolateId = isolateId; } public synchronized ScriptText getScript() { // If we have been using "dummy" source, and the user has changed the list of // directories that are searched for source, then we want to search again if (!m_gotRealScript && m_sourceLocator != null && m_sourceLocator.getChangeCount() != m_sourceLocatorChangeCount) { m_script = null; } // lazy-initialize m_script, so that we don't read a disk file until // someone actually needs to look at the file if (m_script == null) { String script = scriptFromDisk(getRawName()); if (script == null) { script = ""; // use dummy source for now //$NON-NLS-1$ } else { m_gotRealScript = true; // we got the real source } m_script = new ScriptText(script); } return m_script; } /* getters */ public String getBasePath() { return m_basePath; } public String getName() { return m_shortName; } public String getFullPath() { return m_path; } public String getPackageName() { return (m_packageName == null) ? "" : m_packageName; } //$NON-NLS-1$ public String getRawName() { return m_rawName; } public int getId() { return m_id; } public int getBitmap() { return m_bitmap; } public int getLineCount() { return getScript().getLineCount(); } public String getLine(int i) { return (i > getLineCount()) ? "// code goes here" : getScript().getLine(i); } //$NON-NLS-1$ void setPackageName(String name) { m_packageName = name; } /** * @return the offset within the swf for a given line * of source. 0 if unknown. */ public int getOffsetForLine(int line) { int offset = 0; if (line < m_line2Offset.size()) { Integer i = m_line2Offset.get(line); if (i != null) offset = i.intValue(); } return offset; } public int getLineForFunctionName(Session s, String name) { int value = -1; primeAllFncNames(s); Integer i = m_func2FirstLine.get(name); if (i != null) value = i.intValue(); return value; } /* * @see flash.tools.debugger.SourceFile#getFunctionNameForLine(flash.tools.debugger.Session, int) */ public String getFunctionNameForLine(Session s, int line) { primeFncName(s, line); String[] funcNames = getFunctionNamesForLine(s, line); if (funcNames != null && funcNames.length == 1) return funcNames[0]; else return null; } /** * Return the function names for a given line number, or an empty array * if there are none; never returns <code>null</code>. */ private String[] getFunctionNamesForLine(Session s, int line) { primeFncName(s, line); if (line < m_line2Func.size()) { Object obj = m_line2Func.get(line); if (obj instanceof String) return new String[] { (String) obj }; else if (obj instanceof String[]) return (String[]) obj; } return new String[0]; } public String[] getFunctionNames(Session s) { /* find out the size of the array */ primeAllFncNames(s); int count = m_func2FirstLine.size(); return m_func2FirstLine.keySet().toArray(new String[count]); } private static String generateShortName(NameParser nameParser) { String name = nameParser.getOriginalName(); String s = name; if (nameParser.isPathPackageAndFilename()) { s = nameParser.getFilename(); } else { /* do we have a file name? */ int dotAt = name.lastIndexOf('.'); if (dotAt > 1) { /* yes let's strip the directory off */ int lastSlashAt = name.lastIndexOf('\\', dotAt); lastSlashAt = Math.max(lastSlashAt, name.lastIndexOf('/', dotAt)); s = name.substring(lastSlashAt+1); } else { /* not a file name ... */ s = name; } } return s.trim(); } /** * Produce a name that contains a file specification including full path. * File names may come in as 'mx.bla : file:/bla.foo.as' or as * 'file://bla.foo.as' or as 'C:\'(?) or as 'basepath;package;filename' */ private static String generatePath(NameParser nameParser) { String name = nameParser.getOriginalName(); String s = name; /* strip off first colon of stuff if package exists */ int colonAt = name.indexOf(':'); if (colonAt > 1 && !name.startsWith("Actions for")) //$NON-NLS-1$ { if (name.charAt(colonAt+1) == ' ') s = name.substring(colonAt+2); } else if (name.indexOf('.') > -1 && name.charAt(0) != '<' ) { /* some other type of file name */ s = nameParser.recombine(); } else { // no path s = ""; //$NON-NLS-1$ } return s.trim(); } public void lineMapping(StringBuilder sb) { Map<String, String> args = new HashMap<String, String>(); args.put("fileName", getName() ); //$NON-NLS-1$ args.put("fileNumber", Integer.toString(getId()) ); //$NON-NLS-1$ sb.append(PlayerSessionManager.getLocalizationManager().getLocalizedTextString("functionsInFile", args)); //$NON-NLS-1$ sb.append(m_newline); String[] funcNames = m_func2FirstLine.keySet().toArray(new String[m_func2FirstLine.size()]); Arrays.sort(funcNames, new Comparator<String>() { public int compare(String o1, String o2) { int line1 = m_func2FirstLine.get(o1).intValue(); int line2 = m_func2FirstLine.get(o2).intValue(); return line1 - line2; } }); for (int i=0; i<funcNames.length; ++i) { String name = funcNames[i]; int firstLine = m_func2FirstLine.get(name).intValue(); int lastLine = m_func2LastLine.get(name).intValue(); sb.append(" "); //$NON-NLS-1$ sb.append(name); sb.append(" "); //$NON-NLS-1$ sb.append(firstLine); sb.append(" "); //$NON-NLS-1$ sb.append(lastLine); sb.append(m_newline); } } int compareTo(DModule other) { return getName().compareTo(other.getName()); } /** * Called in order to make sure that we have a function name available * at the given location. For AVM+ swfs we don't need a swd and therefore * don't have access to function names in the same fashion. * We need to ask the player for a particular function name. */ void primeFncName(Session s, int line) { // for now we do all, optimize later primeAllFncNames(s); } void primeAllFncNames(Session s) { // send out the request for all functions that the player knows // about for this module // we block on this call waiting for an answer and after we get it // the DManager thread should have populated our mapping tables // under the covers. If its fails then no biggie we just won't // see anything in the tables. PlayerSession ps = (PlayerSession)s; if (!m_gotAllFncNames && ps.playerVersion() >= 9) { try { ps.requestFunctionNames(m_id, -1, m_isolateId); } catch (VersionException e) { ; } catch (NoResponseException e) { ; } } m_gotAllFncNames = true; } void addLineFunctionInfo(int offset, int line, String funcName) { addLineFunctionInfo(offset, line, line, funcName); } /** * Called by DSwfInfo in order to add function name / line / offset mapping * information to the module. */ void addLineFunctionInfo(int offset, int firstLine, int lastLine, String funcName) { int line; // strip down the function name if (funcName == null || funcName.length() == 0) { funcName = "<anonymous$" + (++m_anonymousFunctionCounter) + ">"; //$NON-NLS-1$ //$NON-NLS-2$ } else { // colons or slashes then this is an AS3 name, strip off the core:: int colon = funcName.lastIndexOf(':'); int slash = funcName.lastIndexOf('/'); if (colon > -1 || slash > -1) { int greater = Math.max(colon, slash); funcName = funcName.substring(greater+1); } else { int dot = funcName.lastIndexOf('.'); if (dot > -1) { // extract function and package String pkg = funcName.substring(0, dot); funcName = funcName.substring(dot+1); // attempt to set the package name while we're in here setPackageName(pkg); // System.out.println(m_id+"-func="+funcName+",pkg="+pkg); } } } // System.out.println(m_id+"@"+offset+"="+getPath()+".adding func="+funcName); // make sure m_line2Offset is big enough for the lines we're about to set m_line2Offset.ensureCapacity(firstLine+1); while (firstLine >= m_line2Offset.size()) m_line2Offset.add(null); // add the offset mapping m_line2Offset.set(firstLine, new Integer(offset)); // make sure m_line2Func is big enough for the lines we're about to se m_line2Func.ensureCapacity(lastLine+1); while (lastLine >= m_line2Func.size()) m_line2Func.add(null); // offset and byteCode ignored currently, only add the name for the first hit for (line = firstLine; line <= lastLine; ++line) { Object funcs = m_line2Func.get(line); // A line can correspond to more than one function. The most common case // of that is an MXML tag with two event handlers on the same line, e.g. // <mx:Button mouseOver="overHandler()" mouseOut="outHandler()" />; // another case is the line that declares an inner anonymous function: // var f:Function = function() { trace('hi') } // In any such case, we store a list of function names separated by commas, // e.g. "func1, func2" if (funcs == null) { m_line2Func.set(line, funcName); } else if (funcs instanceof String) { String oldFunc = (String) funcs; m_line2Func.set(line, new String[] { oldFunc, funcName }); } else if (funcs instanceof String[]) { String[] oldFuncs = (String[]) funcs; String[] newFuncs = new String[oldFuncs.length + 1]; System.arraycopy(oldFuncs, 0, newFuncs, 0, oldFuncs.length); newFuncs[newFuncs.length - 1] = funcName; m_line2Func.set(line, newFuncs); } } // add to our function name list if (m_func2FirstLine.get(funcName) == null) { m_func2FirstLine.put(funcName, new Integer(firstLine)); m_func2LastLine.put(funcName, new Integer(lastLine)); } } /** * Scan the disk looking for the location of where the source resides. May * also peel open a swd file looking for the source file. * @param name original full path name of the source file * @return string containing the contents of the file, or null if not found */ private String scriptFromDisk(String name) { // we expect the form of the filename to be in the form // "c:/src/project;debug;myFile.as" // where the semicolons demark the include directory searched by the // compiler followed by package directories then file name. // any slashes are to be forward slash only! // translate to neutral form name = name.replace('\\','/'); //@todo remove this when compiler is complete // pull the name apart final char SEP = ';'; String pkgPart = ""; //$NON-NLS-1$ String pathPart = ""; //$NON-NLS-1$ String namePart = ""; //$NON-NLS-1$ int at = name.indexOf(SEP); if (at > -1) { // have at least 2 parts to name int nextAt = name.indexOf(SEP, at+1); if (nextAt > -1) { // have 3 parts pathPart = name.substring(0, at); pkgPart = name.substring(at+1, nextAt); namePart = name.substring(nextAt+1); } else { // 2 parts means no package. pathPart = name.substring(0, at); namePart = name.substring(at+1); } } else { // should not be here.... // trim by last slash at = name.lastIndexOf('/'); if (at > -1) { // cheat by looking for dirname "mx" in path int mx = name.lastIndexOf("/mx/"); //$NON-NLS-1$ if (mx > -1) { pathPart = name.substring(0, mx); pkgPart = name.substring(mx+1, at); } else { pathPart = name.substring(0, at); } namePart = name.substring(at+1); } else { pathPart = "."; //$NON-NLS-1$ namePart = name; } } String script = null; try { // now try to locate the thing on disk or in a swd. Charset realEncoding = null; Charset bomEncoding = null; InputStream in = locateScriptFile(pathPart, pkgPart, namePart); if (in != null) { try { // Read the file using the appropriate encoding, based on // the BOM (if there is a BOM) or the default charset for // the system (if there isn't a BOM) BufferedInputStream bis = new BufferedInputStream( in ); bomEncoding = getEncodingFromBOM(bis); script = pullInSource(bis, bomEncoding); // If the file is an XML file with an <?xml> directive, // it may specify a different directive realEncoding = getEncodingFromXMLDirective(script); } finally { try { in.close(); } catch (IOException e) {} } } // If we found an <?xml> directive with a specified encoding, and // it doesn't match the encoding we used to read the file initially, // start over. if (realEncoding != null && !realEncoding.equals(bomEncoding)) { in = locateScriptFile(pathPart, pkgPart, namePart); if (in != null) { try { // Read the file using the real encoding, based on the // <?xml...> directive BufferedInputStream bis = new BufferedInputStream( in ); getEncodingFromBOM(bis); script = pullInSource(bis, realEncoding); } finally { try { in.close(); } catch (IOException e) {} } } } } catch(FileNotFoundException fnf) { fnf.printStackTrace(); // shouldn't really happen } return script; } /** * Logic to poke around on disk in order to find the given * filename. We look under the mattress and all other possible * places for the silly thing. We always try locating * the file directly first, if that fails then we hunt out * the swd. */ InputStream locateScriptFile(String path, String pkg, String name) throws FileNotFoundException { if (m_sourceLocator != null) { m_sourceLocatorChangeCount = m_sourceLocator.getChangeCount(); InputStream is = m_sourceLocator.locateSource(path, pkg, name); if (is != null) return is; } // convert slashes first path = path.replace('/', File.separatorChar); pkg = pkg.replace('/', File.separatorChar); File f; // use a package base directory if it exists if (path.length() > 0) { try { String pkgAndName = ""; //$NON-NLS-1$ if (pkg.length() > 0) // have to do this so we don't end up with just "/filename" pkgAndName += pkg + File.separatorChar; pkgAndName += name; f = new File(path, pkgAndName); if (f.exists()) return new FileInputStream(f); } catch(NullPointerException npe) { // skip it. } } // try the current directory plus package if (pkg.length() > 0) // new File("", foo) looks in root directory! { f = new File(pkg, name); if (f.exists()) return new FileInputStream(f); } // look in the current directory without the package f = new File(name); if (f.exists()) return new FileInputStream(f); // @todo try to pry open a swd file... return null; } /** * See if this document starts with a BOM and try to figure * out an encoding from it. * @param bis BufferedInputStream for document (so that we can reset the stream * if we establish that the first characters aren't a BOM) * @return CharSet from BOM (or system default / null) */ private Charset getEncodingFromBOM(BufferedInputStream bis) { Charset bomEncoding = null; bis.mark(3); String bomEncodingString; try { bomEncodingString = FileUtils.consumeBOM(bis, null); } catch (IOException e) { bomEncodingString = System.getProperty("file.encoding"); //$NON-NLS-1$ } bomEncoding = Charset.forName(bomEncodingString); return bomEncoding; } /** * Syntax for an <?xml ...> directive with an encoding (used by getEncodingFromXMLDirective) */ private static final Pattern sXMLDeclarationPattern = Pattern.compile("^<\\?xml[^>]*encoding\\s*=\\s*(\"([^\"]*)\"|'([^']*)')"); //$NON-NLS-1$ /** * See if this document starts with an <?xml ...> directive and * try to figure out an encoding from it. * @param entireSource source of document * @return specified Charset (or null) */ private Charset getEncodingFromXMLDirective(String entireSource) { String encoding = null; Matcher xmlDeclarationMatcher = sXMLDeclarationPattern.matcher(entireSource); if (xmlDeclarationMatcher.find()) { encoding = xmlDeclarationMatcher.group(2); if (encoding == null) encoding = xmlDeclarationMatcher.group(3); try { return Charset.forName(encoding); } catch (IllegalArgumentException e) {} } return null; } /** * Given an input stream containing source file contents, read in each line * @param in stream of source file contents (with BOM removed) * @param encoding encoding to use (based on BOM, system default, or <?xml...> directive * if this is null, the system default will be used) * @return source file contents (as String) */ String pullInSource(InputStream in, Charset encoding) { String script = ""; //$NON-NLS-1$ BufferedReader f = null; try { StringBuilder sb = new StringBuilder(); Reader reader = null; if (encoding == null) reader = new InputStreamReader(in); else reader = new InputStreamReader(in, encoding); f = new BufferedReader(reader); String line; while((line = f.readLine()) != null) { sb.append(line); sb.append('\n'); } script = sb.toString(); } catch (IOException e) { e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates. } return script; } /** for debugging */ @Override public String toString() { return getFullPath(); } /** * Given a filename of the form "basepath;package;filename", return an * array of 3 strings, one for each segment. * @param name a string which *may* be of the form "basepath;package;filename" * @return an array of 3 strings for the three pieces; or, if 'name' is * not of expected form, returns null */ private static class NameParser { private String fOriginalName; private String fBasePath; private String fPackage; private String fFilename; private String fRecombinedName; public NameParser(String name) { fOriginalName = name; /* is it of "basepath;package;filename" format? */ int semicolonCount = 0; int i = 0; int firstSemi = -1; int lastSemi = -1; while ( (i = name.indexOf(';', i)) >= 0 ) { ++semicolonCount; if (firstSemi == -1) firstSemi = i; lastSemi = i; ++i; } if (semicolonCount == 2) { fBasePath = name.substring(0, firstSemi); fPackage = name.substring(firstSemi+1, lastSemi); fFilename = name.substring(lastSemi+1); } } public boolean isPathPackageAndFilename() { return (fBasePath != null); } public String getOriginalName() { return fOriginalName; } public String getBasePath() { return fBasePath; } public String getFilename() { return fFilename; } public String getPackage() { return fPackage; } /** * Returns a "recombined" form of the original name. * * For filenames which came in in the form "basepath;package;filename", * the recombined name is the original name with the semicolons replaced * by platform-appropriate slash characters. For any other type of original * name, the recombined name is the same as the incoming name. */ public String recombine() { if (fRecombinedName == null) { if (isPathPackageAndFilename()) { char slashChar; if (fOriginalName.indexOf('\\') != -1) slashChar = '\\'; else slashChar = '/'; fRecombinedName = fOriginalName.replaceAll(";;", ";").replace(';', slashChar); //$NON-NLS-1$ //$NON-NLS-2$ } else { fRecombinedName = fOriginalName; } } return fRecombinedName; } } }