/*
* ProGuard -- shrinking, optimization, obfuscation, and preverification
* of Java bytecode.
*
* Copyright (c) 2002-2014 Eric Lafortune (eric@graphics.cornell.edu)
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* This program 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 General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package proguard.obfuscate;
import proguard.classfile.*;
import proguard.classfile.attribute.*;
import proguard.classfile.attribute.visitor.*;
import proguard.classfile.constant.ClassConstant;
import proguard.classfile.constant.visitor.ConstantVisitor;
import proguard.classfile.util.*;
import proguard.classfile.visitor.ClassVisitor;
import proguard.util.*;
import java.util.*;
/**
* This ClassVisitor
comes up with obfuscated names for the
* classes it visits, and for their class members. The actual renaming is
* done afterward.
*
* @see ClassRenamer
*
* @author Eric Lafortune
*/
public class ClassObfuscator
extends SimplifiedVisitor
implements ClassVisitor,
AttributeVisitor,
InnerClassesInfoVisitor,
ConstantVisitor
{
private final DictionaryNameFactory classNameFactory;
private final DictionaryNameFactory packageNameFactory;
private final boolean useMixedCaseClassNames;
private final StringMatcher keepPackageNamesMatcher;
private final String flattenPackageHierarchy;
private final String repackageClasses;
private final boolean allowAccessModification;
private final Set classNamesToAvoid = new HashSet();
// Map: [package prefix - new package prefix]
private final Map packagePrefixMap = new HashMap();
// Map: [package prefix - package name factory]
private final Map packagePrefixPackageNameFactoryMap = new HashMap();
// Map: [package prefix - numeric class name factory]
private final Map packagePrefixClassNameFactoryMap = new HashMap();
// Map: [package prefix - numeric class name factory]
private final Map packagePrefixNumericClassNameFactoryMap = new HashMap();
// Field acting as temporary variables and as return values for names
// of outer classes and types of inner classes.
private String newClassName;
private boolean numericClassName;
/**
* Creates a new ClassObfuscator.
* @param programClassPool the class pool in which class names
* have to be unique.
* @param classNameFactory the optional class obfuscation dictionary.
* @param packageNameFactory the optional package obfuscation
* dictionary.
* @param useMixedCaseClassNames specifies whether obfuscated packages and
* classes can get mixed-case names.
* @param keepPackageNames the optional filter for which matching
* package names are kept.
* @param flattenPackageHierarchy the base package if the obfuscated package
* hierarchy is to be flattened.
* @param repackageClasses the base package if the obfuscated classes
* are to be repackaged.
* @param allowAccessModification specifies whether obfuscated classes can
* be freely moved between packages.
*/
public ClassObfuscator(ClassPool programClassPool,
DictionaryNameFactory classNameFactory,
DictionaryNameFactory packageNameFactory,
boolean useMixedCaseClassNames,
List keepPackageNames,
String flattenPackageHierarchy,
String repackageClasses,
boolean allowAccessModification)
{
this.classNameFactory = classNameFactory;
this.packageNameFactory = packageNameFactory;
// First append the package separator if necessary.
if (flattenPackageHierarchy != null &&
flattenPackageHierarchy.length() > 0)
{
flattenPackageHierarchy += ClassConstants.PACKAGE_SEPARATOR;
}
// First append the package separator if necessary.
if (repackageClasses != null &&
repackageClasses.length() > 0)
{
repackageClasses += ClassConstants.PACKAGE_SEPARATOR;
}
this.useMixedCaseClassNames = useMixedCaseClassNames;
this.keepPackageNamesMatcher = keepPackageNames == null ? null :
new ListParser(new FileNameParser()).parse(keepPackageNames);
this.flattenPackageHierarchy = flattenPackageHierarchy;
this.repackageClasses = repackageClasses;
this.allowAccessModification = allowAccessModification;
// Map the root package onto the root package.
packagePrefixMap.put("", "");
// Collect all names that have been taken already.
programClassPool.classesAccept(new MyKeepCollector());
}
// Implementations for ClassVisitor.
public void visitProgramClass(ProgramClass programClass)
{
// Does this class still need a new name?
newClassName = newClassName(programClass);
if (newClassName == null)
{
// Make sure the outer class has a name, if it exists. The name will
// be stored as the new class name, as a side effect, so we'll be
// able to use it as a prefix.
programClass.attributesAccept(this);
// Figure out a package prefix. The package prefix may actually be
// the an outer class prefix, if any, or it may be the fixed base
// package, if classes are to be repackaged.
String newPackagePrefix = newClassName != null ?
newClassName + ClassConstants.INNER_CLASS_SEPARATOR :
newPackagePrefix(ClassUtil.internalPackagePrefix(programClass.getName()));
// Come up with a new class name, numeric or ordinary.
newClassName = newClassName != null && numericClassName ?
generateUniqueNumericClassName(newPackagePrefix) :
generateUniqueClassName(newPackagePrefix);
setNewClassName(programClass, newClassName);
}
}
public void visitLibraryClass(LibraryClass libraryClass)
{
// This can happen for dubious input, if the outer class of a program
// class is a library class, and its name is requested.
newClassName = libraryClass.getName();
}
// Implementations for AttributeVisitor.
public void visitAnyAttribute(Clazz clazz, Attribute attribute) {}
public void visitInnerClassesAttribute(Clazz clazz, InnerClassesAttribute innerClassesAttribute)
{
// Make sure the outer classes have a name, if they exist.
innerClassesAttribute.innerClassEntriesAccept(clazz, this);
}
public void visitEnclosingMethodAttribute(Clazz clazz, EnclosingMethodAttribute enclosingMethodAttribute)
{
// Make sure the enclosing class has a name.
enclosingMethodAttribute.referencedClassAccept(this);
String innerClassName = clazz.getName();
String outerClassName = clazz.getClassName(enclosingMethodAttribute.u2classIndex);
numericClassName = isNumericClassName(innerClassName,
outerClassName);
}
// Implementations for InnerClassesInfoVisitor.
public void visitInnerClassesInfo(Clazz clazz, InnerClassesInfo innerClassesInfo)
{
// Make sure the outer class has a name, if it exists.
int innerClassIndex = innerClassesInfo.u2innerClassIndex;
int outerClassIndex = innerClassesInfo.u2outerClassIndex;
if (innerClassIndex != 0 &&
outerClassIndex != 0)
{
String innerClassName = clazz.getClassName(innerClassIndex);
if (innerClassName.equals(clazz.getName()))
{
clazz.constantPoolEntryAccept(outerClassIndex, this);
String outerClassName = clazz.getClassName(outerClassIndex);
numericClassName = isNumericClassName(innerClassName,
outerClassName);
}
}
}
/**
* Returns whether the given inner class name is a numeric name.
*/
private boolean isNumericClassName(String innerClassName,
String outerClassName)
{
int innerClassNameStart = outerClassName.length() + 1;
int innerClassNameLength = innerClassName.length();
if (innerClassNameStart >= innerClassNameLength)
{
return false;
}
for (int index = innerClassNameStart; index < innerClassNameLength; index++)
{
if (!Character.isDigit(innerClassName.charAt(index)))
{
return false;
}
}
return true;
}
// Implementations for ConstantVisitor.
public void visitClassConstant(Clazz clazz, ClassConstant classConstant)
{
// Make sure the outer class has a name.
classConstant.referencedClassAccept(this);
}
/**
* This ClassVisitor collects package names and class names that have to
* be kept.
*/
private class MyKeepCollector implements ClassVisitor
{
public void visitProgramClass(ProgramClass programClass)
{
// Does the class already have a new name?
String newClassName = newClassName(programClass);
if (newClassName != null)
{
// Remember not to use this name.
classNamesToAvoid.add(mixedCaseClassName(newClassName));
// Are we not aggressively repackaging all obfuscated classes?
if (repackageClasses == null ||
!allowAccessModification)
{
String className = programClass.getName();
// Keep the package name for all other classes in the same
// package. Do this recursively if we're not doing any
// repackaging.
mapPackageName(className,
newClassName,
repackageClasses == null &&
flattenPackageHierarchy == null);
}
}
}
public void visitLibraryClass(LibraryClass libraryClass)
{
}
/**
* Makes sure the package name of the given class will always be mapped
* consistently with its new name.
*/
private void mapPackageName(String className,
String newClassName,
boolean recursively)
{
String packagePrefix = ClassUtil.internalPackagePrefix(className);
String newPackagePrefix = ClassUtil.internalPackagePrefix(newClassName);
// Put the mapping of this package prefix, and possibly of its
// entire hierarchy, into the package prefix map.
do
{
packagePrefixMap.put(packagePrefix, newPackagePrefix);
if (!recursively)
{
break;
}
packagePrefix = ClassUtil.internalPackagePrefix(packagePrefix);
newPackagePrefix = ClassUtil.internalPackagePrefix(newPackagePrefix);
}
while (packagePrefix.length() > 0 &&
newPackagePrefix.length() > 0);
}
}
// Small utility methods.
/**
* Finds or creates the new package prefix for the given package.
*/
private String newPackagePrefix(String packagePrefix)
{
// Doesn't the package prefix have a new package prefix yet?
String newPackagePrefix = (String)packagePrefixMap.get(packagePrefix);
if (newPackagePrefix == null)
{
// Are we keeping the package name?
if (keepPackageNamesMatcher != null &&
keepPackageNamesMatcher.matches(packagePrefix.length() > 0 ?
packagePrefix.substring(0, packagePrefix.length()-1) :
packagePrefix))
{
return packagePrefix;
}
// Are we forcing a new package prefix?
if (repackageClasses != null)
{
return repackageClasses;
}
// Are we forcing a new superpackage prefix?
// Otherwise figure out the new superpackage prefix, recursively.
String newSuperPackagePrefix = flattenPackageHierarchy != null ?
flattenPackageHierarchy :
newPackagePrefix(ClassUtil.internalPackagePrefix(packagePrefix));
// Come up with a new package prefix.
newPackagePrefix = generateUniquePackagePrefix(newSuperPackagePrefix);
// Remember to use this mapping in the future.
packagePrefixMap.put(packagePrefix, newPackagePrefix);
}
return newPackagePrefix;
}
/**
* Creates a new package prefix in the given new superpackage.
*/
private String generateUniquePackagePrefix(String newSuperPackagePrefix)
{
// Find the right name factory for this package.
NameFactory packageNameFactory =
(NameFactory)packagePrefixPackageNameFactoryMap.get(newSuperPackagePrefix);
if (packageNameFactory == null)
{
// We haven't seen packages in this superpackage before. Create
// a new name factory for them.
packageNameFactory = new SimpleNameFactory(useMixedCaseClassNames);
if (this.packageNameFactory != null)
{
packageNameFactory =
new DictionaryNameFactory(this.packageNameFactory,
packageNameFactory);
}
packagePrefixPackageNameFactoryMap.put(newSuperPackagePrefix,
packageNameFactory);
}
return generateUniquePackagePrefix(newSuperPackagePrefix, packageNameFactory);
}
/**
* Creates a new package prefix in the given new superpackage, with the
* given package name factory.
*/
private String generateUniquePackagePrefix(String newSuperPackagePrefix,
NameFactory packageNameFactory)
{
// Come up with package names until we get an original one.
String newPackagePrefix;
do
{
// Let the factory produce a package name.
newPackagePrefix = newSuperPackagePrefix +
packageNameFactory.nextName() +
ClassConstants.PACKAGE_SEPARATOR;
}
while (packagePrefixMap.containsValue(newPackagePrefix));
return newPackagePrefix;
}
/**
* Creates a new class name in the given new package.
*/
private String generateUniqueClassName(String newPackagePrefix)
{
// Find the right name factory for this package.
NameFactory classNameFactory =
(NameFactory)packagePrefixClassNameFactoryMap.get(newPackagePrefix);
if (classNameFactory == null)
{
// We haven't seen classes in this package before.
// Create a new name factory for them.
classNameFactory = new SimpleNameFactory(useMixedCaseClassNames);
if (this.classNameFactory != null)
{
classNameFactory =
new DictionaryNameFactory(this.classNameFactory,
classNameFactory);
}
packagePrefixClassNameFactoryMap.put(newPackagePrefix,
classNameFactory);
}
return generateUniqueClassName(newPackagePrefix, classNameFactory);
}
/**
* Creates a new class name in the given new package.
*/
private String generateUniqueNumericClassName(String newPackagePrefix)
{
// Find the right name factory for this package.
NameFactory classNameFactory =
(NameFactory)packagePrefixNumericClassNameFactoryMap.get(newPackagePrefix);
if (classNameFactory == null)
{
// We haven't seen classes in this package before.
// Create a new name factory for them.
classNameFactory = new NumericNameFactory();
packagePrefixNumericClassNameFactoryMap.put(newPackagePrefix,
classNameFactory);
}
return generateUniqueClassName(newPackagePrefix, classNameFactory);
}
/**
* Creates a new class name in the given new package, with the given
* class name factory.
*/
private String generateUniqueClassName(String newPackagePrefix,
NameFactory classNameFactory)
{
// Come up with class names until we get an original one.
String newClassName;
String newMixedCaseClassName;
do
{
// Let the factory produce a class name.
newClassName = newPackagePrefix +
classNameFactory.nextName();
newMixedCaseClassName = mixedCaseClassName(newClassName);
}
while (classNamesToAvoid.contains(newMixedCaseClassName));
// Explicitly make sure the name isn't used again if we have a
// user-specified dictionary and we're not allowed to have mixed case
// class names -- just to protect against problematic dictionaries.
if (this.classNameFactory != null &&
!useMixedCaseClassNames)
{
classNamesToAvoid.add(newMixedCaseClassName);
}
return newClassName;
}
/**
* Returns the given class name, unchanged if mixed-case class names are
* allowed, or the lower-case version otherwise.
*/
private String mixedCaseClassName(String className)
{
return useMixedCaseClassNames ?
className :
className.toLowerCase();
}
/**
* Assigns a new name to the given class.
* @param clazz the given class.
* @param name the new name.
*/
static void setNewClassName(Clazz clazz, String name)
{
clazz.setVisitorInfo(name);
}
/**
* Retrieves the new name of the given class.
* @param clazz the given class.
* @return the class's new name, or null
if it doesn't
* have one yet.
*/
static String newClassName(Clazz clazz)
{
Object visitorInfo = clazz.getVisitorInfo();
return visitorInfo instanceof String ?
(String)visitorInfo :
null;
}
}