/** * Packer version 3.0 (final) * Copyright 2004-2007, Dean Edwards * Web: {@link http://dean.edwards.name/} * * This software is licensed under the MIT license * Web: {@link http://www.opensource.org/licenses/mit-license} * * Ported to Java by Pablo Santiago based on C# version by Jesse Hansen, <twindagger2k @ msn.com> * Web: {@link http://jpacker.googlecode.com/} * Email: <pablo.santiago @ gmail.com> */ package com.jpacker; import java.util.ArrayList; import java.util.Formatter; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.jpacker.encoders.BasicEncoder; import com.jpacker.encoders.Encoder; import com.jpacker.strategies.DefaultReplacementStrategy; import com.jpacker.strategies.ReplacementStrategy; /** * Packer class. * * Main jPacker class that packs the script. * * @author Pablo Santiago <pablo.santiago @ gmail.com> */ public class JPackerExecuter { private JPackerEncoding encoding; private static final String UNPACK = "eval(function(p,a,c,k,e,r){e=%5$s;if(!''.replace(/^/,String)){while(c--)r[%6$s]=k[c]" + "||%6$s;k=[function(e){return r[e]}];e=function(){return'\\\\w+'};c=1};while(c--)if(k[c])p=p." + "replace(new RegExp('\\\\b'+e(c)+'\\\\b','g'),k[c]);return p}('%1$s',%2$s,%3$s,'%4$s'.split('|'),0,{}))"; /** * Constructor * * @param encoding * The encoding level for this instance */ public JPackerExecuter(JPackerEncoding encoding) { setEncoding(encoding); } /** * Packs the script * * @param script * The script to pack * @param minifyOnly * True if script should only be minified and not encoded and/or * its variables shrunk, false otherwise. * @param shrinkVariables * True if variables should be shrunk, false otherwise. If * minifyOnly is true, this option has no side effect. * @return The packed script */ public String pack(String script, boolean minifyOnly, boolean shrinkVariables) { script += "\n"; script = minify(script); if (!minifyOnly) { if (shrinkVariables) { script = shrinkVariables(script); } if (encoding != JPackerEncoding.NONE) { script = encode(script); } } return script; } // zero encoding - just removal of whitespace and comments private String minify(String script) { JPackerParser parser = new JPackerParser(); ReplacementStrategy defaultStrat = new DefaultReplacementStrategy(); // protect data parser = addDataRegEx(parser); script = parser.exec(script, defaultStrat); // remove white-space parser = addWhiteSpaceRegEx(parser); script = parser.exec(script, defaultStrat); // clean parser = addCleanUpRegEx(parser); script = parser.exec(script, defaultStrat); // done return script; } private JPackerParser addDataRegEx(JPackerParser parser) { final String COMMENT1 = "(\\/\\/|;;;)[^\\n]*"; final String COMMENT2 = "\\/\\*[^*]*\\*+([^\\/][^*]*\\*+)*\\/"; final String REGEX = "\\/(\\\\[\\/\\\\]|[^*\\/])(\\\\.|[^\\/\\n\\\\])*\\/[gim]*"; // Packer.CONTINUE parser.remove("\\\\\\r?\\n"); parser.ignore("'(\\\\.|[^'\\\\])*'"); parser.ignore("\"(\\\\.|[^\"\\\\])*\""); parser.ignore("\\/\\*@|@\\*\\/|\\/\\/@[^\\n]*\\n"); parser.replace("(" + COMMENT1 + ")\\n\\s*(" + REGEX + ")?", "\n$3"); parser.replace("(" + COMMENT2 + ")\\s*(" + REGEX + ")?", " $3"); parser.replace("([\\[\\(\\^=,{}:;&|!*?])\\s*(" + REGEX + ")", "$1$2"); return parser; } private JPackerParser addCleanUpRegEx(JPackerParser parser) { parser.replace("\\(\\s*;\\s*;\\s*\\)", "(;;)"); parser.ignore("throw[};]+[};]"); // safari 1.3 bug parser.replace(";+\\s*([};])", "$1"); parser.remove(";;[^\\n\\r]+[\\n\\r]"); return parser; } private JPackerParser addWhiteSpaceRegEx(JPackerParser parser) { parser.replace("(\\d)\\s+(\\.\\s*[a-z\\$_\\[\\(])", "$1 $2"); parser.replace("([+\\-])\\s+([+\\-])", "$1 $2"); parser.replace("(\\b|\\$)\\s+(\\b|\\$)", "$1 $2"); parser.replace("\\b\\s+\\$\\s+\\b", " $ "); parser.replace("\\$\\s+\\b", "$ "); parser.replace("\\b\\s+\\$", " $"); parser.replace("\\b\\s+\\b", " "); parser.remove("\\s+"); return parser; } private String shrinkVariables(String script) { final Pattern pattern = Pattern.compile("^[^'\"]\\/"); // identify blocks, particularly identify function blocks (which define // scope) Pattern blockPattern = Pattern.compile("(function\\s*[\\w$]*\\s*\\(\\s*([^\\)]*)\\s*\\)\\s*)?(\\{([^{}]*)\\})"); List<String> blocks = new ArrayList<String>(); // store program blocks // (anything between // braces {}) final List<String> data = new ArrayList<String>(); // encoded strings // and regular // expressions JPackerParser parser = new JPackerParser(); parser = addDataRegEx(parser); script = parser.exec(script, new ReplacementStrategy() { @Override public String replace(List<JPackerPattern> patterns, Matcher matcher) { String replacement = "#" + data.size(); String string = matcher.group(); if (pattern.matcher(string).find()) { replacement = string.charAt(0) + replacement; string = string.substring(1); } data.add(string); return replacement; } }); do { // put the blocks back Matcher blockMatcher = blockPattern.matcher(script); StringBuffer sb = new StringBuffer(); while (blockMatcher.find()) { blockMatcher.appendReplacement(sb, encodeBlock(blockMatcher, blocks)); } blockMatcher.appendTail(sb); script = sb.toString(); } while (blockPattern.matcher(script).find()); while (Pattern.compile("~(\\d+)~").matcher(script).find()) { script = decodeBlock(script, blocks); } // put strings and regular expressions back Matcher storeMatcher = Pattern.compile("#(\\d+)").matcher(script); StringBuffer sb2 = new StringBuffer(); while (storeMatcher.find()) { int num = Integer.parseInt(storeMatcher.group(1)); storeMatcher.appendReplacement(sb2, Matcher.quoteReplacement(data.get(num))); } storeMatcher.appendTail(sb2); return sb2.toString(); } private String encode(String script) { JPackerWords words = new JPackerWords(script, encoding); Pattern wordsPattern = Pattern.compile("\\w+"); Matcher wordsMatcher = wordsPattern.matcher(script); StringBuffer sb = new StringBuffer(); while (wordsMatcher.find()) { JPackerWord tempWord = new JPackerWord(wordsMatcher.group()); wordsMatcher.appendReplacement(sb, words.find(tempWord).getEncoded()); } wordsMatcher.appendTail(sb); int ascii = Math.min(Math.max(words.getWords().size(), 2), encoding.getEncodingBase()); String p = escape(sb.toString()); String a = String.valueOf(ascii); String c = String.valueOf(words.getWords().size()); String k = words.toString(); String e = getEncode(ascii); String r = ascii > 10 ? "e(c)" : "c"; return new Formatter().format(UNPACK, p, a, c, k, e, r).toString(); } // encoder for program blocks private String encodeBlock(Matcher matcher, List<String> blocks) { String block = matcher.group(); String func = matcher.group(1); String args = matcher.group(2); if (func != null && !func.isEmpty()) { // the block is a function block // decode the function block (THIS IS THE IMPORTANT BIT) // We are retrieving all sub-blocks and will re-parse them in light // of newly shrunk variables while (Pattern.compile("~(\\d+)~").matcher(block).find()) { block = decodeBlock(block, blocks); } // create the list of variable and argument names Pattern varNamePattern = Pattern.compile("var\\s+[\\w$]+"); Matcher varNameMatcher = varNamePattern.matcher(block); StringBuilder sb = new StringBuilder(); while (varNameMatcher.find()) { sb.append(varNameMatcher.group()).append(","); } String vars = ""; if (!sb.toString().isEmpty()) { vars = sb.deleteCharAt(sb.length() - 1).toString().replaceAll("var\\s+", ""); } String[] ids = concat(args.split("\\s*,\\s*"), vars.split("\\s*,\\s*")); Set<String> idList = new LinkedHashSet<String>(); for (String s : ids) { if (!s.isEmpty()) { idList.add(s); } } // process each identifier int count = 0; String shortId; for (String id : idList) { id = id.trim(); if (id.length() > 1) { // > 1 char id = Matcher.quoteReplacement(id); // find the next free short name (check everything in the // current scope) Encoder e = new BasicEncoder(); do { shortId = e.encode(count++); } while (Pattern.compile("[^\\w$.]" + shortId + "[^\\w$:]").matcher(block).find()); // replace the long name with the short name while (Pattern.compile("([^\\w$.])" + id + "([^\\w$:])").matcher(block).find()) { block = block.replaceAll("([^\\w$.])" + id + "([^\\w$:])", "$1" + shortId + "$2"); } block = block.replaceAll("([^{,\\w$.])" + id + ":", "$1" + shortId + ":"); } } } String replacement = "~" + blocks.size() + "~"; blocks.add(block); return replacement; } private String decodeBlock(String block, List<String> blocks) { Matcher encoded = Pattern.compile("~(\\d+)~").matcher(block); StringBuffer sbe = new StringBuffer(); while (encoded.find()) { int num = Integer.parseInt(encoded.group(1)); encoded.appendReplacement(sbe, Matcher.quoteReplacement(blocks.get(num))); } encoded.appendTail(sbe); return sbe.toString(); } private String[] concat(String[] a, String[] b) { String[] c = new String[a.length + b.length]; System.arraycopy(a, 0, c, 0, a.length); System.arraycopy(b, 0, c, a.length, b.length); return c; } private String getEncode(int ascii) { if (ascii > 96) { return JPackerEncoding.HIGH_ASCII.getEncode(); } else if (ascii > 36) { return JPackerEncoding.NORMAL.getEncode(); } else if (ascii > 10) { return JPackerEncoding.MID.getEncode(); } else { return JPackerEncoding.NUMERIC.getEncode(); } } private String escape(String input) { // single quotes wrap the final string so escape them // also escape new lines required by conditional comments return input.replaceAll("([\\\\'])", "\\\\$1").replaceAll("[\\r\\n]+", "\\n"); } /** * Encoding level. Options are: {@link JPackerEncoding#NONE}, * {@link JPackerEncoding#NUMERIC}, {@link JPackerEncoding#MID}, * {@link JPackerEncoding#NORMAL} and {@link JPackerEncoding#HIGH_ASCII}. * * @return The current encoding level */ public JPackerEncoding getEncoding() { return encoding; } /** * Set the encoding level to use. * * @param encoding */ public final void setEncoding(JPackerEncoding encoding) { this.encoding = encoding; } }