/* * Version: MPL 1.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (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.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is Rhino code, released * May 6, 1999. * * The Initial Developer of the Original Code is * Netscape Communications Corporation. * Portions created by the Initial Developer are Copyright (C) 1997-1999 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Alex Russell * Richard Backhouse */ package org.dojotoolkit.shrinksafe; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Stack; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.mozilla.javascript.CompilerEnvirons; import org.mozilla.javascript.Decompiler; import org.mozilla.javascript.FunctionNode; import org.mozilla.javascript.Interpreter; import org.mozilla.javascript.Kit; import org.mozilla.javascript.Parser; import org.mozilla.javascript.ScriptOrFnNode; import org.mozilla.javascript.ScriptRuntime; import org.mozilla.javascript.Token; import org.mozilla.javascript.UintMap; /** * @author rbackhouse * */ public class Compressor { private static final int FUNCTION_END = Token.LAST_TOKEN + 1; /** * Compress the script *

* * @param encodedSource encoded source string * @param flags Flags specifying format of decompilation output * @param properties Decompilation properties * @param parseTree Mapping for each function node and corresponding parameters & variables names * @return compressed script */ private static String compress(String encodedSource, int flags, UintMap properties, ScriptOrFnNode parseTree, boolean escapeUnicode, String stripConsole, TokenMapper tm, Map replacedTokensLookup){ int indent = properties.getInt(Decompiler.INITIAL_INDENT_PROP, 0); if (indent < 0) throw new IllegalArgumentException(); int indentGap = properties.getInt(Decompiler.INDENT_GAP_PROP, 4); if (indentGap < 0) throw new IllegalArgumentException(); int caseGap = properties.getInt(Decompiler.CASE_GAP_PROP, 2); if (caseGap < 0) throw new IllegalArgumentException(); String stripConsoleRegex = "assert|count|debug|dir|dirxml|group|groupEnd|info|profile|profileEnd|time|timeEnd|trace|log"; if (stripConsole == null) { // may be null if unspecified on Main cmd line stripConsoleRegex = null; } else if (stripConsole.equals("normal")) { // leave default } else if (stripConsole.equals("warn")) { stripConsoleRegex += "|warn"; } else if (stripConsole.equals("all")) { stripConsoleRegex += "|warn|error"; } else { throw new IllegalArgumentException("unrecognised value for stripConsole: " + stripConsole + "!"); } Pattern stripConsolePattern = null; if (stripConsoleRegex != null) { stripConsolePattern = Pattern.compile(stripConsoleRegex); } StringBuffer result = new StringBuffer(); boolean justFunctionBody = (0 != (flags & Decompiler.ONLY_BODY_FLAG)); boolean toSource = (0 != (flags & Decompiler.TO_SOURCE_FLAG)); int braceNesting = 0; boolean afterFirstEOL = false; int i = 0; int prevToken = 0; boolean primeFunctionNesting = false; boolean inArgsList = false; boolean primeInArgsList = false; boolean discardingConsole = false; // control skipping "console.stuff()" int consoleParenCount = 0; // counter for parenthesis counting StringBuffer discardMe = new StringBuffer(); // throwaway buffer ReplacedTokens dummyTokens = new ReplacedTokens(new HashMap(), new int[]{}, replacedTokensLookup, null); int lastMeaningfulToken = Token.SEMI; int lastMeaningfulTokenBeforeConsole = Token.SEMI; int topFunctionType; if (encodedSource.charAt(i) == Token.SCRIPT) { ++i; topFunctionType = -1; } else { topFunctionType = encodedSource.charAt(i + 1); } if (!toSource) { // add an initial newline to exactly match js. // result.append('\n'); for (int j = 0; j < indent; j++){ // result.append(' '); result.append(""); } } else { if (topFunctionType == FunctionNode.FUNCTION_EXPRESSION) { result.append('('); } } Stack positionStack = new Stack(); Stack functionPositionStack = new Stack(); int length = encodedSource.length(); int lineCount = 1; while (i < length) { if(i>0){ prevToken = encodedSource.charAt(i-1); } if (discardingConsole) { // while we are skipping a console command, discard tokens int thisToken = encodedSource.charAt(i); /* Logic for controlling state of discardingConsole */ switch (thisToken) { case Token.LP: consoleParenCount++; break; case Token.RP: consoleParenCount--; if (consoleParenCount == 0) { // paren count fell to zero, must be end of console call discardingConsole = false; if (i < (length - 1)) { int nextToken = getNext(encodedSource, length, i); if ((lastMeaningfulTokenBeforeConsole != Token.SEMI && lastMeaningfulTokenBeforeConsole != Token.LC && lastMeaningfulTokenBeforeConsole != Token.RC) || nextToken != Token.SEMI) { // Either the previous or the following token // may use our return value, insert undefined // e.g. true ? console.log("bizarre") : (bar = true); result.append("undefined"); } else { if (Token.SEMI == nextToken) { // munch following semicolon i++; } } } if ((i < (length - 1)) && (Token.EOL == getNext(encodedSource, length, i))) { // as a nicety, munch following linefeed i++; } } break; } /* * advance i - borrow code from later switch statements (could * mingle this whole discardingConsole block in with rest of * function but it would be _ugly_) Use discardMe in place of * result, so we don't use the processed source Specific case * blocks for all source elements > 1 char long */ switch (thisToken) { case Token.NAME: case Token.REGEXP: int jumpPos = getSourceStringEnd(encodedSource, i + 1, escapeUnicode); if (Token.OBJECTLIT == encodedSource.charAt(jumpPos)) { i = printSourceString(encodedSource, i + 1, false, discardMe, escapeUnicode); } else { i = tm.sourceCompress(encodedSource, i + 1, false, discardMe, prevToken, inArgsList, braceNesting, dummyTokens); } break; case Token.STRING: i = printSourceString(encodedSource, i + 1, true, discardMe, escapeUnicode); break; case Token.NUMBER: i = printSourceNumber(encodedSource, i + 1, discardMe); break; default: // all plain tokens (no data to skip) i++; } // while discarding console, avoid the normal processing continue; } // System.out.println(Token.name(getNext(source, length, i))); int thisToken = encodedSource.charAt(i); switch(thisToken) { case Token.NAME: case Token.REGEXP: // re-wrapped in '/'s in parser... int jumpPos = getSourceStringEnd(encodedSource, i+1, escapeUnicode); if (stripConsolePattern != null && thisToken == Token.NAME) { // Check to see if this is a console.something() call that we // care about, if so switch on discardingConsole int nextTokenAt = tm.sourceCompress(encodedSource, i + 1, false, discardMe, prevToken, inArgsList, braceNesting, dummyTokens); if (encodedSource.substring(i+2, i+2+encodedSource.charAt(i+1)).equals("console") && (encodedSource.charAt(nextTokenAt) == Token.DOT)) { // Find the name of the console method and check it int afterFnName = printSourceString(encodedSource, nextTokenAt+2, false, discardMe, escapeUnicode); Matcher m = stripConsolePattern.matcher(encodedSource.substring(nextTokenAt + 3, afterFnName)); if (m.matches()) { // Must be an open parenthesis e.g. "console.log(" if (encodedSource.charAt(afterFnName) == Token.LP) { discardingConsole = true; consoleParenCount = 0; lastMeaningfulTokenBeforeConsole = lastMeaningfulToken; continue; } } } } if(Token.OBJECTLIT == encodedSource.charAt(jumpPos)){ i = printSourceString(encodedSource, i + 1, false, result, escapeUnicode); }else{ ReplacedTokens replacedTokens = null; if (positionStack.size() > 0) { Integer pos = (Integer)positionStack.peek(); replacedTokens = (ReplacedTokens)replacedTokensLookup.get(pos); } else { replacedTokens = new ReplacedTokens(new HashMap(), new int[]{}, replacedTokensLookup, null); } i = tm.sourceCompress( encodedSource, i + 1, false, result, prevToken, inArgsList, braceNesting, replacedTokens); } continue; case Token.STRING: // NOTE: this is the disabled "string munging" code provided in bugs.dojotoolkit.org/ticket/8828 // simply uncomment this block, and run the build.sh script located in the root shrinksafe folder. // there is a far-egde-case this is deemed unsafe in, so is entirely disabled for sanity of devs. // // StringBuffer buf = new StringBuffer(); // i--; // do { // i++; // i = printSourceString(encodedSource, i + 1, false, buf, escapeUnicode); // } while(Token.ADD == encodedSource.charAt(i) && // Token.STRING == getNext(encodedSource, length, i)); // result.append('"'); // result.append(escapeString(buf.toString(), escapeUnicode)); // result.append('"'); // // now comment out this line to complete the patch: i = printSourceString(encodedSource, i + 1, true, result, escapeUnicode); continue; case Token.NUMBER: i = printSourceNumber(encodedSource, i + 1, result); continue; case Token.TRUE: result.append("true"); break; case Token.FALSE: result.append("false"); break; case Token.NULL: result.append("null"); break; case Token.THIS: result.append("this"); break; case Token.FUNCTION: { ++i; // skip function type tm.incrementFunctionNumber(); primeInArgsList = true; primeFunctionNesting = true; result.append("function"); if (Token.LP != getNext(encodedSource, length, i)) { result.append(' '); } Integer functionPos = new Integer(i-1); functionPositionStack.push(functionPos); DebugData debugData = tm.getDebugData(functionPos); debugData.compressedStart = lineCount; break; } case FUNCTION_END: { Integer functionPos = (Integer)functionPositionStack.pop(); DebugData debugData = tm.getDebugData(functionPos); debugData.compressedEnd = lineCount; break; } case Token.COMMA: result.append(","); break; case Token.LC: ++braceNesting; if (Token.EOL == getNext(encodedSource, length, i)){ indent += indentGap; } result.append('{'); // // result.append('\n'); break; case Token.RC: { if (tm.leaveNestingLevel(braceNesting)) { positionStack.pop(); } --braceNesting; /* don't print the closing RC if it closes the * toplevel function and we're called from * decompileFunctionBody. */ if(justFunctionBody && braceNesting == 0){ break; } // // result.append('\n'); result.append('}'); // // result.append(' '); switch (getNext(encodedSource, length, i)) { case Token.EOL: case FUNCTION_END: if ( (getNext(encodedSource, length, i+1) != Token.SEMI) && (getNext(encodedSource, length, i+1) != Token.LP) && (getNext(encodedSource, length, i+1) != Token.RP) && (getNext(encodedSource, length, i+1) != Token.RB) && (getNext(encodedSource, length, i+1) != Token.RC) && (getNext(encodedSource, length, i+1) != Token.COMMA) && (getNext(encodedSource, length, i+1) != Token.COLON) && (getNext(encodedSource, length, i+1) != Token.DOT) && (getNext(encodedSource, length, i) == FUNCTION_END ) ){ result.append(';'); } indent -= indentGap; break; case Token.WHILE: case Token.ELSE: indent -= indentGap; // result.append(' '); result.append(""); break; } break; } case Token.LP: if(primeInArgsList){ inArgsList = true; primeInArgsList = false; } if(primeFunctionNesting){ positionStack.push(new Integer(i)); tm.enterNestingLevel(braceNesting); primeFunctionNesting = false; } result.append('('); break; case Token.RP: if(inArgsList){ inArgsList = false; } result.append(')'); /* if (Token.LC == getNext(source, length, i)){ result.append(' '); } */ break; case Token.LB: result.append('['); break; case Token.RB: result.append(']'); break; case Token.EOL: { if (toSource) break; boolean newLine = true; if (!afterFirstEOL) { afterFirstEOL = true; if (justFunctionBody) { /* throw away just added 'function name(...) {' * and restore the original indent */ result.setLength(0); indent -= indentGap; newLine = false; } } if (newLine) { result.append('\n'); lineCount++; } /* add indent if any tokens remain, * less setback if next token is * a label, case or default. */ if (i + 1 < length) { int less = 0; int nextToken = encodedSource.charAt(i + 1); if (nextToken == Token.CASE || nextToken == Token.DEFAULT) { less = indentGap - caseGap; } else if (nextToken == Token.RC) { less = indentGap; } /* elaborate check against label... skip past a * following inlined NAME and look for a COLON. */ else if (nextToken == Token.NAME) { int afterName = getSourceStringEnd(encodedSource, i + 2, escapeUnicode); if (encodedSource.charAt(afterName) == Token.COLON) less = indentGap; } for (; less < indent; less++){ // result.append(' '); result.append(""); } } break; } case Token.DOT: result.append('.'); break; case Token.NEW: result.append("new "); break; case Token.DELPROP: result.append("delete "); break; case Token.IF: result.append("if"); break; case Token.ELSE: result.append("else"); break; case Token.FOR: result.append("for"); break; case Token.IN: result.append(" in "); break; case Token.WITH: result.append("with"); break; case Token.WHILE: result.append("while"); break; case Token.DO: result.append("do"); break; case Token.TRY: result.append("try"); break; case Token.CATCH: result.append("catch"); break; case Token.FINALLY: result.append("finally"); break; case Token.THROW: result.append("throw "); break; case Token.SWITCH: result.append("switch"); break; case Token.BREAK: result.append("break"); if(Token.NAME == getNext(encodedSource, length, i)){ result.append(' '); } break; case Token.CONTINUE: result.append("continue"); if(Token.NAME == getNext(encodedSource, length, i)){ result.append(' '); } break; case Token.CASE: result.append("case "); break; case Token.DEFAULT: result.append("default"); break; case Token.RETURN: result.append("return"); if(Token.SEMI != getNext(encodedSource, length, i)){ result.append(' '); } break; case Token.VAR: result.append("var "); break; case Token.SEMI: result.append(';'); // result.append('\n'); /* if (Token.EOL != getNext(source, length, i)) { // separators in FOR result.append(' '); } */ break; case Token.ASSIGN: result.append("="); break; case Token.ASSIGN_ADD: result.append("+="); break; case Token.ASSIGN_SUB: result.append("-="); break; case Token.ASSIGN_MUL: result.append("*="); break; case Token.ASSIGN_DIV: result.append("/="); break; case Token.ASSIGN_MOD: result.append("%="); break; case Token.ASSIGN_BITOR: result.append("|="); break; case Token.ASSIGN_BITXOR: result.append("^="); break; case Token.ASSIGN_BITAND: result.append("&="); break; case Token.ASSIGN_LSH: result.append("<<="); break; case Token.ASSIGN_RSH: result.append(">>="); break; case Token.ASSIGN_URSH: result.append(">>>="); break; case Token.HOOK: result.append("?"); break; case Token.OBJECTLIT: // pun OBJECTLIT to mean colon in objlit property // initialization. // This needs to be distinct from COLON in the general case // to distinguish from the colon in a ternary... which needs // different spacing. result.append(':'); break; case Token.COLON: if (Token.EOL == getNext(encodedSource, length, i)) // it's the end of a label result.append(':'); else // it's the middle part of a ternary result.append(":"); break; case Token.OR: result.append("||"); break; case Token.AND: result.append("&&"); break; case Token.BITOR: result.append("|"); break; case Token.BITXOR: result.append("^"); break; case Token.BITAND: result.append("&"); break; case Token.SHEQ: result.append("==="); break; case Token.SHNE: result.append("!=="); break; case Token.EQ: result.append("=="); break; case Token.NE: result.append("!="); break; case Token.LE: result.append("<="); break; case Token.LT: result.append("<"); break; case Token.GE: result.append(">="); break; case Token.GT: result.append(">"); break; case Token.INSTANCEOF: // FIXME: does this really need leading space? result.append(" instanceof "); break; case Token.LSH: result.append("<<"); break; case Token.RSH: result.append(">>"); break; case Token.URSH: result.append(">>>"); break; case Token.TYPEOF: result.append("typeof "); break; case Token.VOID: result.append("void "); break; case Token.NOT: result.append('!'); break; case Token.BITNOT: result.append('~'); break; case Token.POS: result.append('+'); break; case Token.NEG: result.append('-'); break; case Token.INC: if(Token.ADD == prevToken){ result.append(' '); } result.append("++"); if(Token.ADD == getNext(encodedSource, length, i)){ result.append(' '); } break; case Token.DEC: if(Token.SUB == prevToken){ result.append(' '); } result.append("--"); if(Token.SUB == getNext(encodedSource, length, i)){ result.append(' '); } break; case Token.ADD: result.append("+"); int nextToken = encodedSource.charAt(i + 1); if (nextToken == Token.POS) { result.append(' '); } break; case Token.SUB: result.append("-"); nextToken = encodedSource.charAt(i + 1); if (nextToken == Token.NEG) { result.append(' '); } break; case Token.MUL: result.append("*"); break; case Token.DIV: result.append("/"); break; case Token.MOD: result.append("%"); break; case Token.COLONCOLON: result.append("::"); break; case Token.DOTDOT: result.append(".."); break; case Token.XMLATTR: result.append('@'); break; case Token.DEBUGGER: System.out.println("WARNING: Found a `debugger;` statement in code being compressed"); result.append("debugger"); break; default: // If we don't know how to decompile it, raise an exception. throw new RuntimeException(); } if (thisToken != Token.EOL) { lastMeaningfulToken = thisToken; } ++i; } if (!toSource) { // add that trailing newline if it's an outermost function. // if (!justFunctionBody){ // result.append('\n'); // } } else { if (topFunctionType == FunctionNode.FUNCTION_EXPRESSION) { result.append(')'); } } return result.toString(); } /** * Collect the replaced tokens and store them in a lookup table for the next * source pass. * * @param encodedSource encoded source string * @param escapeUnicode escape chars with unicode. * @param tm token mapper object. * @return Map containing replaced tokens lookup information */ private static Map collectReplacedTokens(String encodedSource, boolean escapeUnicode, TokenMapper tm) { int length = encodedSource.length(); int i = 0; int prevToken = 0; int braceNesting = 0; boolean inArgsList = false; boolean primeFunctionNesting = false; boolean primeInArgsList = false; if (encodedSource.charAt(i) == Token.SCRIPT) { ++i; } Stack positionStack = new Stack(); Stack functionPositionStack = new Stack(); Map tokenLookup = new HashMap(); while (i < length) { if (i > 0) { prevToken = encodedSource.charAt(i - 1); } switch (encodedSource.charAt(i)) { case Token.NAME: case Token.REGEXP: { int jumpPos = getSourceStringEnd(encodedSource, i + 1, escapeUnicode); if (Token.OBJECTLIT == encodedSource.charAt(jumpPos)) { i = printSourceString(encodedSource, i + 1, false, null, escapeUnicode); } else { i = tm.sourceCompress(encodedSource, i + 1, false, null, prevToken, inArgsList, braceNesting, null); } continue; } case Token.STRING: { i = printSourceString(encodedSource, i + 1, true, null, escapeUnicode); continue; } case Token.NUMBER: { i = printSourceNumber(encodedSource, i + 1, null); continue; } case Token.FUNCTION: { ++i; // skip function type tm.incrementFunctionNumber(); primeInArgsList = true; primeFunctionNesting = true; functionPositionStack.push(new Integer(i-1)); break; } case Token.LC: { ++braceNesting; break; } case Token.RC: { Map m = tm.getCurrentTokens(); if (tm.leaveNestingLevel(braceNesting)) { Integer pos = (Integer)positionStack.pop(); Integer functionPos = (Integer)functionPositionStack.pop(); int[] parents = new int[positionStack.size()]; int idx = 0; for (Iterator itr = positionStack.iterator(); itr.hasNext();) { parents[idx++] = ((Integer)itr.next()).intValue(); } DebugData debugData = tm.getDebugData(functionPos); ReplacedTokens replacedTokens = new ReplacedTokens(m, parents, tokenLookup, debugData); tokenLookup.put(pos, replacedTokens); } --braceNesting; break; } case Token.LP: { if (primeInArgsList) { inArgsList = true; primeInArgsList = false; } if (primeFunctionNesting) { positionStack.push(new Integer(i)); tm.enterNestingLevel(braceNesting); primeFunctionNesting = false; } break; } case Token.RP: { if (inArgsList) { inArgsList = false; } break; } } ++i; } return tokenLookup; } private static int getNext(String source, int length, int i) { return (i + 1 < length) ? source.charAt(i + 1) : Token.EOF; } private static int getSourceStringEnd(String source, int offset, boolean escapeUnicode) { return printSourceString(source, offset, false, null, escapeUnicode); } private static int printSourceString(String source, int offset, boolean asQuotedString, StringBuffer sb, boolean escapeUnicode) { int length = source.charAt(offset); ++offset; if ((0x8000 & length) != 0) { length = ((0x7FFF & length) << 16) | source.charAt(offset); ++offset; } if (sb != null) { String str = source.substring(offset, offset + length); if (!asQuotedString) { sb.append(str); } else { sb.append('"'); sb.append(escapeString(str, escapeUnicode)); sb.append('"'); } } return offset + length; } private static int printSourceNumber(String source, int offset, StringBuffer sb) { double number = 0.0; char type = source.charAt(offset); ++offset; if (type == 'S') { if (sb != null) { int ival = source.charAt(offset); number = ival; } ++offset; } else if (type == 'J' || type == 'D') { if (sb != null) { long lbits; lbits = (long) source.charAt(offset) << 48; lbits |= (long) source.charAt(offset + 1) << 32; lbits |= (long) source.charAt(offset + 2) << 16; lbits |= source.charAt(offset + 3); if (type == 'J') { number = lbits; } else { number = Double.longBitsToDouble(lbits); } } offset += 4; } else { // Bad source throw new RuntimeException(); } if (sb != null) { sb.append(ScriptRuntime.numberToString(number, 10)); } return offset; } private static String escapeString(String s, boolean escapeUnicode) { return escapeString(s, '"', escapeUnicode); } private static String escapeString(String s, char escapeQuote, boolean escapeUnicode) { if (!(escapeQuote == '"' || escapeQuote == '\'')) Kit.codeBug(); StringBuffer sb = null; for(int i = 0, L = s.length(); i != L; ++i) { int c = s.charAt(i); if (' ' <= c && c <= '~' && c != escapeQuote && c != '\\') { // an ordinary print character (like C isprint()) and not " // or \ . if (sb != null) { sb.append((char)c); } continue; } if (sb == null) { sb = new StringBuffer(L + 3); sb.append(s); sb.setLength(i); } int escape = -1; switch (c) { case '\b': escape = 'b'; break; case '\f': escape = 'f'; break; case '\n': escape = 'n'; break; case '\r': escape = 'r'; break; case '\t': escape = 't'; break; case 0xb: escape = 'v'; break; // Java lacks \v. case ' ': escape = ' '; break; case '\\': escape = '\\'; break; } if (escape >= 0) { // an \escaped sort of character sb.append('\\'); sb.append((char)escape); } else if (c == escapeQuote) { sb.append('\\'); sb.append(escapeQuote); } else { if (escapeUnicode || c == 0) { // always escape the null character (#5027) int hexSize; if (c < 256) { // 2-digit hex sb.append("\\x"); hexSize = 2; } else { // Unicode. sb.append("\\u"); hexSize = 4; } // append hexadecimal form of c left-padded with 0 for (int shift = (hexSize - 1) * 4; shift >= 0; shift -= 4) { int digit = 0xf & (c >> shift); int hc = (digit < 10) ? '0' + digit : 'a' - 10 + digit; sb.append((char)hc); } } else { sb.append((char)c); } } } return (sb == null) ? s : sb.toString(); } public static final String compressScript(String source, int indent, int lineno, String stripConsole) { return compressScript(source, indent, lineno, false, stripConsole); } public static final String compressScript(String source, int indent, int lineno, boolean escapeUnicode, String stripConsole) { return compressScript(source, indent, lineno, escapeUnicode, stripConsole, null); } public static final String compressScript(String source, int indent, int lineno, boolean escapeUnicode, String stripConsole, StringBuffer debugData) { CompilerEnvirons compilerEnv = new CompilerEnvirons(); Parser parser = new Parser(compilerEnv, compilerEnv.getErrorReporter()); ScriptOrFnNode tree = parser.parse(source, null, lineno); String encodedSource = parser.getEncodedSource(); if (encodedSource.length() == 0) { return ""; } Interpreter compiler = new Interpreter(); compiler.compile(compilerEnv, tree, encodedSource, false); UintMap properties = new UintMap(1); properties.put(Decompiler.INITIAL_INDENT_PROP, indent); TokenMapper tm = new TokenMapper(tree); Map replacedTokensLookup = collectReplacedTokens(encodedSource, escapeUnicode, tm); tm.reset(); String compressedSource = compress(encodedSource, 0, properties, tree, escapeUnicode, stripConsole, tm, replacedTokensLookup); if (debugData != null) { debugData.append("[\n"); int count = 1; for (Iterator itr = replacedTokensLookup.keySet().iterator(); itr.hasNext();) { Integer pos = (Integer)itr.next(); ReplacedTokens replacedTokens = (ReplacedTokens)replacedTokensLookup.get(pos); debugData.append(replacedTokens.toJson()); if (count++ < replacedTokensLookup.size()) { debugData.append(','); } debugData.append("\n"); } debugData.append("]"); } return compressedSource; } }