/*
* Copyright (C) 2010-2016 JPEXS, All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3.0 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library.
*/
package com.jpexs.decompiler.flash;
import com.jpexs.decompiler.flash.abc.RenameType;
import com.jpexs.decompiler.flash.helpers.GraphTextWriter;
import com.jpexs.decompiler.flash.tags.DefineSpriteTag;
import com.jpexs.decompiler.flash.tags.Tag;
import com.jpexs.decompiler.flash.tags.base.PlaceObjectTypeTag;
import com.jpexs.decompiler.graph.DottedChain;
import com.jpexs.helpers.Cache;
import com.jpexs.helpers.Helper;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.regex.Pattern;
/**
*
* @author JPEXS
*/
public class IdentifiersDeobfuscation {
private static final Random rnd = new Random();
private final int DEFAULT_FOO_SIZE = 10;
public HashSet<String> allVariableNamesStr = new HashSet<>();
private final HashMap<String, Integer> typeCounts = new HashMap<>();
private static final Cache<String, String> as2NameCache = Cache.getInstance(false, true, "as2_ident");
private static final Cache<String, String> as3NameCache = Cache.getInstance(false, true, "as3_ident");
public static final String VALID_FIRST_CHARACTERS = "\\p{Lu}\\p{Ll}\\p{Lt}\\p{Lm}\\p{Lo}_$";
public static final String VALID_NEXT_CHARACTERS = VALID_FIRST_CHARACTERS + "\\p{Nl}\\p{Mn}\\p{Mc}\\p{Nd}\\p{Pc}";
private static final Pattern VALID_NAME_PATTERN = Pattern.compile("^[a-zA-Z_\\$][a-zA-Z0-9_\\$]*$");
private static final Pattern IDENTIFIER_PATTERN = Pattern.compile("^[" + VALID_FIRST_CHARACTERS + "][" + VALID_NEXT_CHARACTERS + "]*$");
private static final Pattern VALID_NAME_PATTERN_DOT = Pattern.compile("^[a-zA-Z_\\$][a-zA-Z0-9_.\\$]*$");
private static final Pattern IDENTIFIER_PATTERN_DOT = Pattern.compile("^[" + VALID_FIRST_CHARACTERS + "][" + VALID_NEXT_CHARACTERS + ".]*$");
public static final String FOO_CHARACTERS = "bcdfghjklmnpqrstvwz";
public static final String FOO_JOIN_CHARACTERS = "aeiouy";
// http://help.adobe.com/en_US/AS2LCR/Flash_10.0/help.html?content=00000477.html
public static final String[] reservedWordsAS2 = {
// is "add" really a keyword? documentation says yes, but I can create "add" variable in CS6...
// "add",
"and", "break", "case", "catch", "class", "continue", "default", "delete", "do", "dynamic", "else",
"eq", "extends", "false", "finally", "for", "function", "ge", "get", "gt", "if", "ifFrameLoaded", "implements",
"import", "in", "instanceof", "interface", "intrinsic", "le",
// is "it" really a keyword? documentation says yes, but I can create "it" variable in CS6...
// "it",
"ne", "new", "not", "null", "on", "onClipEvent",
"or", "private", "public", "return", "set", "static",
//allow as variable:
//"super",
"switch", "tellTarget",
//allow as variable:
//"this",
"throw", "try",
"typeof", "undefined", "var", "void", "while", "with"
};
// http://www.adobe.com/devnet/actionscript/learning/as3-fundamentals/syntax.html
public static final String[] reservedWordsAS3 = {
"as", "break", "case", "catch", "class", "const", "continue", "default", "delete", "do", "else",
"extends", "false", "finally", "for", "function", "if", "implements", "import", "in", "instanceof",
"interface", "internal", "is", "new", "null", "package", "private", "protected", "public",
"return", "super", "switch", "this", "throw",
// is "to" really a keyword? documentation says yes, but I can create "to" variable...
// "to",
"true", "try", "typeof", "use", "var",
"void", "while", "with"
};
// TODO, why do we have 2 different list? Moved from AVM2Deobfuscation
public static final String[] reservedWordsAS3_2 = {
"as", "break", "case", "catch", "class", "const", "continue", "default", "delete", "do", "each", "else",
"extends", "false", "finally", "for", "function", "get", "if", "implements", "import", "in", "instanceof",
"interface", "internal", "is", "native", "new", "null", "override", "package", "private", "protected", "public",
"return", "set", "super", "switch", "this", "throw", "true", "try", "typeof", "use", "var", /*"void",*/ "while",
"with", "dynamic", "default", "final", "in", "static"};
//syntactic keywords - can be used as identifiers, but that have special meaning in certain contexts
public static final String[] syntacticKeywordsAS3 = {"each", "get", "set", "namespace", "include", "dynamic", "final", "native", "override", "static"};
public static boolean isReservedWord(String s, boolean as3) {
if (s == null) {
return false;
}
String[] reservedWords = as3 ? reservedWordsAS3 : reservedWordsAS2;
s = s.trim();
for (String rw : reservedWords) {
if (rw.equals(s)) {
return true;
}
}
return false;
}
// TODO: Why do we need this method???
public static boolean isReservedWord2(String s) {
if (s == null) {
return false;
}
String[] reservedWords = reservedWordsAS3_2;
s = s.trim();
for (String rw : reservedWords) {
if (rw.equals(s)) {
return true;
}
}
return false;
}
public static String fooString(boolean firstUppercase, int rndSize) {
int len = 3 + rnd.nextInt(rndSize - 3);
StringBuilder sb = new StringBuilder(len);
for (int i = 0; i < len; i++) {
char c;
if ((i % 2) == 0) {
c = FOO_CHARACTERS.charAt(rnd.nextInt(FOO_CHARACTERS.length()));
} else {
c = FOO_JOIN_CHARACTERS.charAt(rnd.nextInt(FOO_JOIN_CHARACTERS.length()));
}
if (i == 0 && firstUppercase) {
c = Character.toUpperCase(c);
}
sb.append(c);
}
return sb.toString();
}
public void deobfuscateInstanceNames(boolean as3, HashMap<DottedChain, DottedChain> namesMap, RenameType renameType, Iterable<Tag> tags, Map<DottedChain, DottedChain> selected) {
for (Tag t : tags) {
if (t instanceof DefineSpriteTag) {
deobfuscateInstanceNames(as3, namesMap, renameType, ((DefineSpriteTag) t).getTags(), selected);
}
if (t instanceof PlaceObjectTypeTag) {
PlaceObjectTypeTag po = (PlaceObjectTypeTag) t;
String name = po.getInstanceName();
if (name != null) {
String changedName = deobfuscateName(as3, name, false, "instance", namesMap, renameType, selected);
if (changedName != null) {
po.setInstanceName(changedName);
((Tag) po).setModified(true);
}
}
String className = po.getClassName();
if (className != null) {
String changedClassName = deobfuscateNameWithPackage(as3, className, namesMap, renameType, selected);
if (changedClassName != null) {
po.setClassName(changedClassName);
((Tag) po).setModified(true);
}
}
}
}
}
public DottedChain deobfuscatePackage(boolean as3, DottedChain pkg, HashMap<DottedChain, DottedChain> namesMap, RenameType renameType, Map<DottedChain, DottedChain> selected) {
if (namesMap.containsKey(pkg)) {
return namesMap.get(pkg);
}
List<String> ret = new ArrayList<>(pkg.size());
boolean isChanged = false;
for (int p = 0; p < pkg.size(); p++) {
String part = pkg.get(p);
String partChanged = deobfuscateName(as3, part, false, "package", namesMap, renameType, selected);
if (partChanged != null) {
ret.add(partChanged);
isChanged = true;
} else {
ret.add(part);
}
}
if (isChanged) {
DottedChain chain = new DottedChain(ret);
namesMap.put(pkg, chain);
return chain;
}
return null;
}
public String deobfuscateNameWithPackage(boolean as3, String n, HashMap<DottedChain, DottedChain> namesMap, RenameType renameType, Map<DottedChain, DottedChain> selected) {
DottedChain nChain = DottedChain.parseWithSuffix(n);
DottedChain pkg = nChain.getWithoutLast();
String name = nChain.getLast();
boolean changed = false;
if ((pkg != null) && (!pkg.isEmpty()) && (!pkg.isTopLevel())) {
DottedChain changedPkg = deobfuscatePackage(as3, pkg, namesMap, renameType, selected);
if (changedPkg != null) {
changed = true;
pkg = changedPkg;
}
}
String changedName = deobfuscateName(as3, name, true, "class", namesMap, renameType, selected);
if (changedName != null) {
changed = true;
name = changedName;
}
if (changed) {
String newClassName;
if ((pkg == null) || (pkg.isEmpty()) || (pkg.isTopLevel())) {
newClassName = name;
} else {
newClassName = pkg + "." + name;
}
return newClassName;
}
return null;
}
public static boolean isValidNameWithDot(boolean as3, String s, String... exceptions) {
for (String e : exceptions) {
if (e.equals(s)) {
return true;
}
}
if (isReservedWord(s, as3)) {
return false;
}
// simple fast test
if (VALID_NAME_PATTERN_DOT.matcher(s).matches()) {
return true;
}
// unicode test
if (IDENTIFIER_PATTERN_DOT.matcher(s).matches()) {
return true;
}
return false;
}
public static boolean isValidName(boolean as3, String s, String... exceptions) {
for (String e : exceptions) {
if (e.equals(s)) {
return true;
}
}
if (isReservedWord(s, as3)) {
return false;
}
// simple fast test
if (VALID_NAME_PATTERN.matcher(s).matches()) {
return true;
}
// unicode test
if (IDENTIFIER_PATTERN.matcher(s).matches()) {
return true;
}
return false;
}
public String deobfuscateName(boolean as3, String s, boolean firstUppercase, String usageType, HashMap<DottedChain, DottedChain> namesMap, RenameType renameType, Map<DottedChain, DottedChain> selected) {
if (usageType == null) {
usageType = "name";
}
DottedChain sChain = DottedChain.parseWithSuffix(s);
if (selected != null) {
if (selected.containsKey(sChain)) {
return selected.get(sChain).toRawString();
}
}
boolean isValid = isValidName(as3, s);
if (!isValid) {
if (namesMap.containsKey(sChain)) {
return namesMap.get(sChain).toRawString();
} else {
String ret = null;
boolean found;
int rndSize = DEFAULT_FOO_SIZE;
do {
found = false;
if (renameType == RenameType.TYPENUMBER) {
ret = Helper.getNextId(usageType, typeCounts, true);
if (allVariableNamesStr.contains(ret)) {
found = true;
}
} else if (renameType == RenameType.RANDOMWORD) {
ret = fooString(firstUppercase, rndSize);
if (allVariableNamesStr.contains(ret)
|| isReservedWord(ret, as3)
|| namesMap.containsValue(DottedChain.parseWithSuffix(ret))) {
found = true;
rndSize++;
}
}
} while (found);
namesMap.put(DottedChain.parseWithSuffix(s), DottedChain.parseWithSuffix(ret));
return ret;
}
}
return null;
}
public static GraphTextWriter appendObfuscatedIdentifier(String s, GraphTextWriter writer) {
writer.append("\u00A7");
escapeOIdentifier(s, writer);
return writer.append("\u00A7");
}
/**
* Ensures identifier is valid and if not, uses paragraph syntax
*
* @param as3 Is ActionScript3
* @param s Identifier
* @param validExceptions Exceptions which are valid (e.g. some reserved
* words)
* @return
*/
public static String printIdentifier(boolean as3, String s, String... validExceptions) {
if (s == null || s.isEmpty()) {
return "";
}
if (s.startsWith("\u00A7") && s.endsWith("\u00A7")) { // Assuming already printed - TODO:detect better
return s;
}
for (String e : validExceptions) {
if (e.equals(s)) {
return s;
}
}
Cache<String, String> nameCache = as3 ? as3NameCache : as2NameCache;
if (nameCache.contains(s)) {
return nameCache.get(s);
}
if (isValidName(as3, s)) {
nameCache.put(s, s);
return s;
}
String ret = "\u00A7" + escapeOIdentifier(s) + "\u00A7";
nameCache.put(s, ret);
return ret;
}
public static GraphTextWriter escapeOIdentifier(String s, GraphTextWriter writer) {
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == '\n') {
writer.append("\\n");
} else if (c == '\r') {
writer.append("\\r");
} else if (c == '\t') {
writer.append("\\t");
} else if (c == '\b') {
writer.append("\\b");
} else if (c == '\t') {
writer.append("\\t");
} else if (c == '\f') {
writer.append("\\f");
} else if (c == '\\') {
writer.append("\\\\");
} else if (c == '\u00A7') {
writer.append("\\\u00A7");
} else if (c < 32) {
writer.append("\\x").append(Helper.byteToHex((byte) c));
} else {
writer.append(c);
}
}
return writer;
}
public static String escapeOIdentifier(String s) {
StringBuilder ret = new StringBuilder(s.length());
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == '\n') {
ret.append("\\n");
} else if (c == '\r') {
ret.append("\\r");
} else if (c == '\t') {
ret.append("\\t");
} else if (c == '\b') {
ret.append("\\b");
} else if (c == '\t') {
ret.append("\\t");
} else if (c == '\f') {
ret.append("\\f");
} else if (c == '\\') {
ret.append("\\\\");
} else if (c == '\u00A7') {
ret.append("\\\u00A7");
} else if (c < 32) {
ret.append("\\x").append(Helper.byteToHex((byte) c));
} else {
ret.append(c);
}
}
return ret.toString();
}
public static void clearCache() {
as2NameCache.clear();
as3NameCache.clear();
}
}