/* $Id: ProGuard.java,v 1.72 2004/08/21 21:37:28 eric Exp $
*
* ProGuard -- shrinking, optimization, and obfuscation of Java class files.
*
* Copyright (c) 2002-2004 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;
import proguard.classfile.*;
import proguard.classfile.editor.*;
import proguard.classfile.instruction.*;
import proguard.classfile.util.*;
import proguard.classfile.visitor.*;
import proguard.io.*;
import proguard.obfuscate.*;
import proguard.optimize.*;
import proguard.optimize.evaluation.*;
import proguard.optimize.peephole.*;
import proguard.shrink.*;
import java.io.*;
/**
* Tool for shrinking, optimizing, and obfuscating Java class files.
*
* @author Eric Lafortune
*/
public class ProGuard
{
public static final String VERSION = "ProGuard, version 3.0";
private Configuration configuration;
private ClassPool programClassPool = new ClassPool();
private ClassPool libraryClassPool = new ClassPool();
/**
* Creates a new ProGuard object to process jars as specified by the given
* configuration.
*/
public ProGuard(Configuration configuration)
{
this.configuration = configuration;
}
/**
* Performs all subsequent ProGuard operations.
*/
public void execute() throws IOException
{
System.out.println(VERSION);
readInput();
if (configuration.shrink ||
configuration.optimize ||
configuration.obfuscate)
{
initialize();
}
if (configuration.printSeeds != null)
{
printSeeds();
}
if (configuration.shrink)
{
shrink();
}
if (configuration.optimize)
{
optimize();
// Shrink again, if we may.
if (configuration.shrink)
{
shrink();
}
}
if (configuration.obfuscate)
{
obfuscate();
}
if (configuration.shrink ||
configuration.optimize ||
configuration.obfuscate)
{
sortConstantPools();
}
if (configuration.programJars.hasOutput())
{
writeOutput();
}
if (configuration.dump != null)
{
dump();
}
}
/**
* Reads the input jars (or directories).
*/
private void readInput() throws IOException
{
if (configuration.verbose)
{
System.out.println("Reading jars...");
}
// Check if we have at least some program jars.
if (configuration.programJars == null)
{
throw new IOException("The input is empty. You have to specify one or more '-injars' options.");
}
// Read the input program jars.
readInput("Reading program ",
configuration.programJars,
createDataEntryClassPoolFiller(false));
// Check if we have at least some input class files.
if (programClassPool.size() == 0)
{
throw new IOException("The input doesn't contain any class files. Did you specify the proper '-injars' options?");
}
// Read all library jars.
if (configuration.libraryJars != null)
{
readInput("Reading library ",
configuration.libraryJars,
createDataEntryClassPoolFiller(true));
}
// The defaultPackage option implies the allowAccessModification option.
if (configuration.defaultPackage != null)
{
configuration.allowAccessModification = true;
}
}
/**
* Creates a DataEntryReader that will decode class files and put them in
* the proper class pool.
*/
private DataEntryReader createDataEntryClassPoolFiller(boolean isLibrary)
{
// Get the proper class pool.
ClassPool classPool = isLibrary ?
libraryClassPool :
programClassPool;
// Prepare a data entry reader to filter all class files,
// which are then decoded to class files by a class file reader,
// which are then put in the class pool by a class pool filler.
return
new ClassFileFilter(
new ClassFileReader(isLibrary,
configuration.skipNonPublicLibraryClasses,
new ClassPoolFiller(classPool, configuration.note)));
}
/**
* Reads all input entries from the given class path.
*/
private void readInput(String messagePrefix,
ClassPath classPath,
DataEntryReader reader) throws IOException
{
readInput(messagePrefix,
classPath,
0,
classPath.size(),
reader);
}
/**
* Reads all input entries from the given section of the given class path.
*/
private void readInput(String messagePrefix,
ClassPath classPath,
int fromIndex,
int toIndex,
DataEntryReader reader) throws IOException
{
for (int index = fromIndex; index < toIndex; index++)
{
ClassPathEntry entry = classPath.get(index);
if (!entry.isOutput())
{
readInput(messagePrefix, entry, reader);
}
}
}
/**
* Reads the given input class path entry.
*/
private void readInput(String messagePrefix,
ClassPathEntry classPathEntry,
DataEntryReader dataEntryReader) throws IOException
{
try
{
// Create a reader that can unwrap jars, wars, ears, and zips.
DataEntryReader reader =
DataEntryReaderFactory.createDataEntryReader(messagePrefix,
classPathEntry,
dataEntryReader);
// Create the data entry pump.
DirectoryPump directoryPump =
new DirectoryPump(new File(classPathEntry.getName()));
// Pump the data entries into the reader.
directoryPump.pumpDataEntries(reader);
}
catch (IOException ex)
{
throw new IOException("Can't read [" + classPathEntry + "] (" + ex.getMessage() + ")");
}
}
/**
* Initializes the cross-references between all class files.
*/
private void initialize() throws IOException
{
// Initialize the class hierarchy for program class files.
ClassFileHierarchyInitializer classFileHierarchyInitializer =
new ClassFileHierarchyInitializer(programClassPool,
libraryClassPool,
configuration.warn);
programClassPool.classFilesAccept(classFileHierarchyInitializer);
// Initialize the rest of the class hierarchy.
ClassFileHierarchyInitializer classFileHierarchyInitializer2 =
new ClassFileHierarchyInitializer(programClassPool,
libraryClassPool,
false);
libraryClassPool.classFilesAccept(classFileHierarchyInitializer2);
// Initialize the other class references.
ClassFileReferenceInitializer classFileReferenceInitializer =
new ClassFileReferenceInitializer(programClassPool,
libraryClassPool,
configuration.warn,
configuration.note);
programClassPool.classFilesAccept(classFileReferenceInitializer);
int noteCount = classFileReferenceInitializer.getNoteCount();
if (noteCount > 0)
{
System.err.println("Note: there were " + noteCount +
" class casts of dynamically created class instances.");
System.err.println(" You might consider explicitly keeping the mentioned classes and/or");
System.err.println(" their implementations (using '-keep').");
}
int hierarchyWarningCount = classFileHierarchyInitializer.getWarningCount();
if (hierarchyWarningCount > 0)
{
System.err.println("Warning: there were " + hierarchyWarningCount +
" unresolved references to superclasses or interfaces.");
System.err.println(" You may need to specify additional library jars (using '-libraryjars'),");
System.err.println(" or perhaps the '-dontskipnonpubliclibraryclasses' option.");
}
int referenceWarningCount = classFileReferenceInitializer.getWarningCount();
if (referenceWarningCount > 0)
{
System.err.println("Warning: there were " + referenceWarningCount +
" unresolved references to program class members.");
System.err.println(" Your input class files appear to be inconsistent.");
System.err.println(" You may need to recompile them and try again.");
}
if ((hierarchyWarningCount > 0 ||
referenceWarningCount > 0) &&
!configuration.ignoreWarnings)
{
System.err.println(" If you are sure the mentioned classes are not used anyway,");
System.err.println(" you could try your luck using the '-ignorewarnings' option.");
throw new IOException("Please correct the above warnings first.");
}
// Discard unused library classes.
if (configuration.verbose)
{
System.out.println("Removing unused library classes...");
System.out.println(" Original number of library classes: "+libraryClassPool.size());
}
// Reinitialize the library class pool with only those library classes
// whose hierarchies are referenced by the program classes.
ClassPool newLibraryClassPool = new ClassPool();
programClassPool.classFilesAccept(
new AllCpInfoVisitor(
new ReferencedClassFileVisitor(
new LibraryClassFileFilter(
new ClassFileHierarchyTraveler(true, true, true, false,
new LibraryClassFileFilter(
new ClassPoolFiller(newLibraryClassPool, false)))))));
libraryClassPool = newLibraryClassPool;
if (configuration.verbose)
{
System.out.println(" Final number of library classes: "+libraryClassPool.size());
}
}
/**
* Prints out classes and class members that are used as seeds in the
* shrinking and obfuscation steps.
*/
private void printSeeds() throws IOException
{
if (configuration.verbose)
{
System.out.println("Printing kept classes, fields, and methods...");
}
// Check if we have at least some keep commands.
if (configuration.keep == null)
{
throw new IOException("You have to specify '-keep' options for the shrinking step.");
}
PrintStream ps = configuration.printSeeds.length() > 0 ?
new PrintStream(new BufferedOutputStream(new FileOutputStream(configuration.printSeeds))) :
System.out;
// Create a visitor for printing out the seeds. Note that we're only
// printing out the program elements that are preserved against shrinking.
SimpleClassFilePrinter printer = new SimpleClassFilePrinter(false, ps);
ClassPoolVisitor classPoolvisitor =
ClassSpecificationVisitorFactory.createClassPoolVisitor(configuration.keep,
new ProgramClassFileFilter(printer),
new ProgramMemberInfoFilter(printer));
// Print out the seeds.
programClassPool.accept(classPoolvisitor);
libraryClassPool.accept(classPoolvisitor);
if (ps != System.out)
{
ps.close();
}
}
/**
* Performs the shrinking step.
*/
private void shrink() throws IOException
{
if (configuration.verbose)
{
System.out.println("Shrinking...");
}
// Check if we have at least some keep commands.
if (configuration.keep == null)
{
throw new IOException("You have to specify '-keep' options for the shrinking step.");
}
// Clean up any old visitor info.
ClassFileCleaner classFileCleaner = new ClassFileCleaner();
programClassPool.classFilesAccept(classFileCleaner);
libraryClassPool.classFilesAccept(classFileCleaner);
// Create a visitor for marking the seeds.
UsageMarker usageMarker = new UsageMarker();
ClassPoolVisitor classPoolvisitor =
ClassSpecificationVisitorFactory.createClassPoolVisitor(configuration.keep,
usageMarker,
usageMarker);
// Mark the seeds.
programClassPool.accept(classPoolvisitor);
libraryClassPool.accept(classPoolvisitor);
// Mark interfaces that have to be kept.
programClassPool.classFilesAccept(new InterfaceUsageMarker());
// Mark the inner class information that has to be kept.
programClassPool.classFilesAccept(new InnerUsageMarker());
if (configuration.printUsage != null)
{
if (configuration.verbose)
{
System.out.println("Printing usage...");
}
PrintStream ps = configuration.printUsage.length() > 0 ?
new PrintStream(new BufferedOutputStream(new FileOutputStream(configuration.printUsage))) :
System.out;
// Print out items that will be removed.
programClassPool.classFilesAcceptAlphabetically(new UsagePrinter(true, ps));
if (ps != System.out)
{
ps.close();
}
}
// Discard unused program classes.
if (configuration.verbose)
{
System.out.println("Removing unused program classes and class elements...");
System.out.println(" Original number of program classes: "+programClassPool.size());
}
ClassPool newProgramClassPool = new ClassPool();
programClassPool.classFilesAccept(
new UsedClassFileFilter(
new MultiClassFileVisitor(
new ClassFileVisitor[] {
new ClassFileShrinker(1024),
new ClassPoolFiller(newProgramClassPool, false)
})));
programClassPool = newProgramClassPool;
if (configuration.verbose)
{
System.out.println(" Final number of program classes: "+programClassPool.size());
}
// Check if we have at least some output class files.
if (programClassPool.size() == 0)
{
throw new IOException("The output jar is empty. Did you specify the proper '-keep' options?");
}
}
/**
* Performs the optimization step.
*/
private void optimize() throws IOException
{
if (configuration.verbose)
{
System.out.println("Optimizing...");
}
// Clean up any old visitor info.
ClassFileCleaner classFileCleaner = new ClassFileCleaner();
programClassPool.classFilesAccept(classFileCleaner);
libraryClassPool.classFilesAccept(classFileCleaner);
// Check if we have at least some keep commands.
if (configuration.keep == null)
{
throw new IOException("You have to specify '-keep' options for the optimization step.");
}
// Create a visitor for marking the seeds.
KeepMarker keepMarker = new KeepMarker();
ClassPoolVisitor classPoolvisitor =
ClassSpecificationVisitorFactory.createClassPoolVisitor(configuration.keep,
keepMarker,
keepMarker);
// Mark the seeds.
programClassPool.accept(classPoolvisitor);
libraryClassPool.accept(classPoolvisitor);
// Make class files and methods final, as far as possible.
programClassPool.classFilesAccept(new ClassFileFinalizer());
// Mark all fields that are write-only.
programClassPool.classFilesAccept(
new AllMethodVisitor(
new AllAttrInfoVisitor(
new AllInstructionVisitor(
new WriteOnlyFieldMarker()))));
if (configuration.assumeNoSideEffects != null)
{
// Create a visitor for marking methods that don't have any side effects.
NoSideEffectMethodMarker noSideEffectMethodMarker = new NoSideEffectMethodMarker();
ClassPoolVisitor noClassPoolvisitor =
ClassSpecificationVisitorFactory.createClassPoolVisitor(configuration.assumeNoSideEffects,
null,
noSideEffectMethodMarker);
// Mark the seeds.
programClassPool.accept(noClassPoolvisitor);
libraryClassPool.accept(noClassPoolvisitor);
}
// Mark all methods that have side effects.
programClassPool.accept(new SideEffectMethodMarker());
// Perform partial evaluation.
programClassPool.classFilesAccept(new AllMethodVisitor(
new PartialEvaluator()));
// Create a branch target marker and a code attribute editor that can
// be reused for all code attributes.
BranchTargetFinder branchTargetFinder = new BranchTargetFinder(1024);
CodeAttrInfoEditor codeAttrInfoEditor = new CodeAttrInfoEditor(1024);
// Visit all code attributes.
// First let the branch marker mark all branch targets.
// Then perform peephole optimisations on the instructions:
// - Remove push/pop instruction pairs.
// - Remove load/store instruction pairs.
// - Replace store/load instruction pairs by dup/store instructions.
// - Replace branches to return instructions by return instructions.
// - Remove nop instructions.
// - Inline simple getters and setters.
// Finally apply all changes to the code.
programClassPool.classFilesAccept(
new AllMethodVisitor(
new AllAttrInfoVisitor(
new MultiAttrInfoVisitor(
new AttrInfoVisitor[]
{
branchTargetFinder,
new CodeAttrInfoEditorResetter(codeAttrInfoEditor),
new AllInstructionVisitor(
new MultiInstructionVisitor(
new InstructionVisitor[]
{
new PushPopRemover(branchTargetFinder, codeAttrInfoEditor),
new LoadStoreRemover(branchTargetFinder, codeAttrInfoEditor),
new StoreLoadReplacer(branchTargetFinder, codeAttrInfoEditor),
new GotoReturnReplacer(codeAttrInfoEditor),
new NopRemover(codeAttrInfoEditor),
new GetterSetterInliner(codeAttrInfoEditor, configuration.allowAccessModification),
})),
codeAttrInfoEditor
}))));
}
/**
* Performs the obfuscation step.
*/
private void obfuscate() throws IOException
{
if (configuration.verbose)
{
System.out.println("Obfuscating...");
}
// Check if we have at least some keep commands.
if (configuration.keep == null &&
configuration.keepNames == null)
{
throw new IOException("You have to specify '-keep' options for the obfuscation step.");
}
// Clean up any old visitor info.
ClassFileCleaner classFileCleaner = new ClassFileCleaner();
programClassPool.classFilesAccept(classFileCleaner);
libraryClassPool.classFilesAccept(classFileCleaner);
// Link all class members that should get the same names.
programClassPool.classFilesAccept(new BottomClassFileFilter(
new MemberInfoLinker()));
// Create a visitor for marking the seeds.
NameMarker nameMarker = new NameMarker();
ClassPoolVisitor classPoolvisitor =
new MultiClassPoolVisitor(new ClassPoolVisitor[]
{
ClassSpecificationVisitorFactory.createClassPoolVisitor(configuration.keep,
nameMarker,
nameMarker),
ClassSpecificationVisitorFactory.createClassPoolVisitor(configuration.keepNames,
nameMarker,
nameMarker)
});
// Mark the seeds.
programClassPool.accept(classPoolvisitor);
libraryClassPool.accept(classPoolvisitor);
// Apply the mapping, if one has been specified.
if (configuration.applyMapping != null)
{
if (configuration.verbose)
{
System.out.println("Applying mapping...");
}
MappingReader reader = new MappingReader(configuration.applyMapping);
MappingProcessor keeper =
new MultiMappingProcessor(new MappingProcessor[]
{
new MappingKeeper(programClassPool),
new MappingKeeper(libraryClassPool),
});
reader.pump(keeper);
}
// Mark attributes that have to be kept.
AttributeUsageMarker attributeUsageMarker = new AttributeUsageMarker();
if (configuration.keepAttributes != null)
{
if (configuration.keepAttributes.size() != 0)
{
attributeUsageMarker.setKeepAttributes(configuration.keepAttributes);
}
else
{
attributeUsageMarker.setKeepAllAttributes();
}
}
programClassPool.classFilesAccept(attributeUsageMarker);
if (configuration.verbose)
{
System.out.println("Renaming program classes and class elements...");
}
// Come up with new names for all class files.
programClassPool.classFilesAccept(new ClassFileObfuscator(programClassPool,
configuration.defaultPackage,
configuration.useMixedCaseClassNames));
// Come up with new names for all class members.
programClassPool.classFilesAccept(new BottomClassFileFilter(
new MemberInfoObfuscator(configuration.overloadAggressively)));
// Print out the mapping, if requested.
if (configuration.printMapping != null)
{
if (configuration.verbose)
{
System.out.println("Printing mapping...");
}
PrintStream ps = configuration.printMapping.length() > 0 ?
new PrintStream(new BufferedOutputStream(new FileOutputStream(configuration.printMapping))) :
System.out;
// Print out items that will be removed.
programClassPool.classFilesAcceptAlphabetically(new MappingPrinter(ps));
if (ps != System.out)
{
ps.close();
}
}
// Actually apply these new names.
programClassPool.classFilesAccept(new ClassFileRenamer(configuration.defaultPackage != null,
configuration.newSourceFileAttribute));
// Remove the attributes that can be discarded.
programClassPool.classFilesAccept(new AttributeShrinker());
// Mark NameAndType constant pool entries that have to be kept and remove the other ones.
programClassPool.classFilesAccept(new NameAndTypeUsageMarker());
programClassPool.classFilesAccept(new NameAndTypeShrinker());
// Mark Utf8 constant pool entries that have to be kept and remove the other ones.
programClassPool.classFilesAccept(new Utf8UsageMarker());
programClassPool.classFilesAccept(new Utf8Shrinker(1024));
}
/**
* Sorts the constant pools of all program class files.
*/
private void sortConstantPools()
{
programClassPool.classFilesAccept(new ConstantPoolSorter(1024));
}
/**
* Writes the output jars.
*/
private void writeOutput() throws IOException
{
if (configuration.verbose)
{
System.out.println("Writing jars...");
}
ClassPath programJars = configuration.programJars;
// Perform a check on the first jar.
ClassPathEntry firstEntry = programJars.get(0);
if (firstEntry.isOutput())
{
throw new IOException("The output jar [" + firstEntry.getName() +
"] must be specified after an input jar, or it will be empty.");
}
// Perform some checks on the output jars.
for (int index = 0; index < programJars.size() - 1; index++)
{
ClassPathEntry entry = programJars.get(index);
if (entry.isOutput())
{
// Check if all but the last output jars have filters.
if (entry.getFilter() == null &&
entry.getJarFilter() == null &&
entry.getWarFilter() == null &&
entry.getEarFilter() == null &&
entry.getZipFilter() == null &&
programJars.get(index + 1).isOutput())
{
throw new IOException("The output jar [" + entry.getName() +
"] must have a filter, or all subsequent jars will be empty.");
}
// Check if the output jar name is different from the input jar names.
for (int inIndex = 0; inIndex < programJars.size(); inIndex++)
{
ClassPathEntry otherEntry = programJars.get(inIndex);
if (!otherEntry.isOutput() &&
entry.getName().equals(otherEntry.getName()))
{
throw new IOException("The output jar [" + entry.getName() +
"] must be different from all input jars.");
}
}
}
}
int firstInputIndex = 0;
int lastInputIndex = 0;
// Go over all program class path entries.
for (int index = 0; index < programJars.size(); index++)
{
// Is it an input entry?
ClassPathEntry entry = programJars.get(index);
if (!entry.isOutput())
{
// Remember the index of the last input entry.
lastInputIndex = index;
}
else
{
// Check if this the last output entry in a series.
int nextIndex = index + 1;
if (nextIndex == programJars.size() ||
!programJars.get(nextIndex).isOutput())
{
// Write the processed input entries to the output entries.
writeOutput(programJars,
firstInputIndex,
lastInputIndex + 1,
nextIndex);
// Start with the next series of input entries.
firstInputIndex = nextIndex;
}
}
}
}
/**
* Transfers the specified input jars to the specified output jars.
*/
private void writeOutput(ClassPath classPath,
int fromInputIndex,
int fromOutputIndex,
int toOutputIndex)
throws IOException
{
try
{
// Construct the writer that can write jars, wars, ears, zips, and
// directories, cascading over the specified output entries.
DataEntryWriter writer =
DataEntryWriterFactory.createDataEntryWriter(classPath,
fromOutputIndex,
toOutputIndex);
// Create the reader that can write class files and copy resource
// files to the above writer.
DataEntryReader reader =
new ClassFileFilter(new ClassFileRewriter(programClassPool,
writer),
new DataEntryCopier(writer));
// Read and handle the specified input entries.
readInput("Copying resources from program ",
classPath,
fromInputIndex,
fromOutputIndex,
reader);
// Close all output entries.
writer.close();
}
catch (IOException ex)
{
throw new IOException("Can't write [" + classPath.get(fromOutputIndex).getName() + "] (" + ex.getMessage() + ")");
}
}
/**
* Prints out the contents of the program class files.
*/
private void dump() throws IOException
{
if (configuration.verbose)
{
System.out.println("Printing classes...");
}
PrintStream ps = configuration.dump.length() > 0 ?
new PrintStream(new BufferedOutputStream(new FileOutputStream(configuration.dump))) :
System.out;
programClassPool.classFilesAccept(new ClassFilePrinter(ps));
if (configuration.dump.length() > 0)
{
ps.close();
}
}
/**
* The main method for ProGuard.
*/
public static void main(String[] args)
{
if (args.length == 0)
{
System.out.println("Usage: java proguard.ProGuard [options ...]");
System.exit(1);
}
// Create the default options.
Configuration configuration = new Configuration();
try
{
// Parse the options specified in the command line arguments.
ConfigurationParser parser = new ConfigurationParser(args);
parser.parse(configuration);
// Execute ProGuard with these options.
ProGuard proGuard = new ProGuard(configuration);
proGuard.execute();
}
catch (Exception ex)
{
if (configuration.verbose)
{
// Print a verbose stack trace.
ex.printStackTrace();
}
else
{
// Print just the stack trace message.
System.err.println("Error: "+ex.getMessage());
}
System.exit(1);
}
System.exit(0);
}
}