1 /* 2 * ProGuard -- shrinking, optimization, obfuscation, and preverification 3 * of Java bytecode. 4 * 5 * Copyright (c) 2002-2013 Eric Lafortune (eric (at) graphics.cornell.edu) 6 * 7 * This program is free software; you can redistribute it and/or modify it 8 * under the terms of the GNU General Public License as published by the Free 9 * Software Foundation; either version 2 of the License, or (at your option) 10 * any later version. 11 * 12 * This program is distributed in the hope that it will be useful, but WITHOUT 13 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 14 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 15 * more details. 16 * 17 * You should have received a copy of the GNU General Public License along 18 * with this program; if not, write to the Free Software Foundation, Inc., 19 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 20 */ 21 package proguard; 22 23 import proguard.classfile.ClassPool; 24 import proguard.classfile.util.ClassUtil; 25 import proguard.io.*; 26 27 import java.io.IOException; 28 import java.util.*; 29 30 /** 31 * This class writes the output class files. 32 * 33 * @author Eric Lafortune 34 */ 35 public class OutputWriter 36 { 37 private final Configuration configuration; 38 39 40 /** 41 * Creates a new OutputWriter to write output class files as specified by 42 * the given configuration. 43 */ 44 public OutputWriter(Configuration configuration) 45 { 46 this.configuration = configuration; 47 } 48 49 50 /** 51 * Writes the given class pool to class files, based on the current 52 * configuration. 53 */ 54 public void execute(ClassPool programClassPool) throws IOException 55 { 56 ClassPath programJars = configuration.programJars; 57 58 // Perform a check on the first jar. 59 ClassPathEntry firstEntry = programJars.get(0); 60 if (firstEntry.isOutput()) 61 { 62 throw new IOException("The output jar [" + firstEntry.getName() + 63 "] must be specified after an input jar, or it will be empty."); 64 } 65 66 // Check if the first of two subsequent the output jars has a filter. 67 for (int index = 0; index < programJars.size() - 1; index++) 68 { 69 ClassPathEntry entry = programJars.get(index); 70 if (entry.isOutput()) 71 { 72 if (entry.getFilter() == null && 73 entry.getJarFilter() == null && 74 entry.getWarFilter() == null && 75 entry.getEarFilter() == null && 76 entry.getZipFilter() == null && 77 programJars.get(index + 1).isOutput()) 78 { 79 throw new IOException("The output jar [" + entry.getName() + 80 "] must have a filter, or all subsequent output jars will be empty."); 81 } 82 } 83 } 84 85 // Check if the output jar names are different from the input jar names. 86 for (int outIndex = 0; outIndex < programJars.size() - 1; outIndex++) 87 { 88 ClassPathEntry entry = programJars.get(outIndex); 89 if (entry.isOutput()) 90 { 91 for (int inIndex = 0; inIndex < programJars.size(); inIndex++) 92 { 93 ClassPathEntry otherEntry = programJars.get(inIndex); 94 95 if (!otherEntry.isOutput() && 96 entry.getFile().equals(otherEntry.getFile())) 97 { 98 throw new IOException("The output jar [" + entry.getName() + 99 "] must be different from all input jars."); 100 } 101 } 102 } 103 } 104 105 // Check for potential problems with mixed-case class names on 106 // case-insensitive file systems. 107 if (configuration.obfuscate && 108 configuration.useMixedCaseClassNames && 109 configuration.classObfuscationDictionary == null && 110 (configuration.note == null || 111 !configuration.note.isEmpty())) 112 { 113 String os = System.getProperty("os.name").toLowerCase(); 114 if (os.startsWith("windows") || 115 os.startsWith("mac os")) 116 { 117 // Go over all program class path entries. 118 for (int index = 0; index < programJars.size(); index++) 119 { 120 // Is it an output directory? 121 ClassPathEntry entry = programJars.get(index); 122 if (entry.isOutput() && 123 !entry.isJar() && 124 !entry.isWar() && 125 !entry.isEar() && 126 !entry.isZip()) 127 { 128 System.out.println("Note: you're writing the processed class files to a directory [" + entry.getName() +"]."); 129 System.out.println(" This will likely cause problems with obfuscated mixed-case class names."); 130 System.out.println(" You should consider writing the output to a jar file, or otherwise"); 131 System.out.println(" specify '-dontusemixedcaseclassnames'."); 132 133 break; 134 } 135 } 136 } 137 } 138 139 int firstInputIndex = 0; 140 int lastInputIndex = 0; 141 142 // Go over all program class path entries. 143 for (int index = 0; index < programJars.size(); index++) 144 { 145 // Is it an input entry? 146 ClassPathEntry entry = programJars.get(index); 147 if (!entry.isOutput()) 148 { 149 // Remember the index of the last input entry. 150 lastInputIndex = index; 151 } 152 else 153 { 154 // Check if this the last output entry in a series. 155 int nextIndex = index + 1; 156 if (nextIndex == programJars.size() || 157 !programJars.get(nextIndex).isOutput()) 158 { 159 // Write the processed input entries to the output entries. 160 writeOutput(programClassPool, 161 programJars, 162 firstInputIndex, 163 lastInputIndex + 1, 164 nextIndex); 165 166 // Start with the next series of input entries. 167 firstInputIndex = nextIndex; 168 } 169 } 170 } 171 } 172 173 174 /** 175 * Transfers the specified input jars to the specified output jars. 176 */ 177 private void writeOutput(ClassPool programClassPool, 178 ClassPath classPath, 179 int fromInputIndex, 180 int fromOutputIndex, 181 int toOutputIndex) 182 throws IOException 183 { 184 try 185 { 186 // Construct the writer that can write jars, wars, ears, zips, and 187 // directories, cascading over the specified output entries. 188 DataEntryWriter writer = 189 DataEntryWriterFactory.createDataEntryWriter(classPath, 190 fromOutputIndex, 191 toOutputIndex); 192 193 // The writer will be used to write possibly obfuscated class files. 194 DataEntryReader classRewriter = 195 new ClassRewriter(programClassPool, writer); 196 197 // The writer will also be used to write resource files. 198 DataEntryReader resourceCopier = 199 new DataEntryCopier(writer); 200 201 DataEntryReader resourceRewriter = resourceCopier; 202 203 // Wrap the resource writer with a filter and a data entry rewriter, 204 // if required. 205 if (configuration.adaptResourceFileContents != null) 206 { 207 resourceRewriter = 208 new NameFilter(configuration.adaptResourceFileContents, 209 new NameFilter("META-INF/MANIFEST.MF,META-INF/*.SF", 210 new ManifestRewriter(programClassPool, writer), 211 new DataEntryRewriter(programClassPool, writer)), 212 resourceRewriter); 213 } 214 215 // Wrap the resource writer with a filter and a data entry renamer, 216 // if required. 217 if (configuration.adaptResourceFileNames != null) 218 { 219 Map packagePrefixMap = createPackagePrefixMap(programClassPool); 220 221 resourceRewriter = 222 new NameFilter(configuration.adaptResourceFileNames, 223 new DataEntryObfuscator(programClassPool, 224 packagePrefixMap, 225 resourceRewriter), 226 resourceRewriter); 227 } 228 229 DataEntryReader directoryRewriter = null; 230 231 // Wrap the directory writer with a filter and a data entry renamer, 232 // if required. 233 if (configuration.keepDirectories != null) 234 { 235 Map packagePrefixMap = createPackagePrefixMap(programClassPool); 236 237 directoryRewriter = 238 new NameFilter(configuration.keepDirectories, 239 new DataEntryRenamer(packagePrefixMap, 240 resourceCopier, 241 resourceCopier)); 242 } 243 244 // Create the reader that can write class files and copy directories 245 // and resource files to the main writer. 246 DataEntryReader reader = 247 new ClassFilter( classRewriter, 248 new DirectoryFilter(directoryRewriter, 249 resourceRewriter)); 250 251 // Go over the specified input entries and write their processed 252 // versions. 253 new InputReader(configuration).readInput(" Copying resources from program ", 254 classPath, 255 fromInputIndex, 256 fromOutputIndex, 257 reader); 258 259 // Close all output entries. 260 writer.close(); 261 } 262 catch (IOException ex) 263 { 264 throw (IOException)new IOException("Can't write [" + classPath.get(fromOutputIndex).getName() + "] (" + ex.getMessage() + ")").initCause(ex); 265 } 266 } 267 268 269 /** 270 * Creates a map of old package prefixes to new package prefixes, based on 271 * the given class pool. 272 */ 273 private static Map createPackagePrefixMap(ClassPool classPool) 274 { 275 Map packagePrefixMap = new HashMap(); 276 277 Iterator iterator = classPool.classNames(); 278 while (iterator.hasNext()) 279 { 280 String className = (String)iterator.next(); 281 String packagePrefix = ClassUtil.internalPackagePrefix(className); 282 283 String mappedNewPackagePrefix = (String)packagePrefixMap.get(packagePrefix); 284 if (mappedNewPackagePrefix == null || 285 !mappedNewPackagePrefix.equals(packagePrefix)) 286 { 287 String newClassName = classPool.getClass(className).getName(); 288 String newPackagePrefix = ClassUtil.internalPackagePrefix(newClassName); 289 290 packagePrefixMap.put(packagePrefix, newPackagePrefix); 291 } 292 } 293 294 return packagePrefixMap; 295 } 296 } 297