1 /* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.dexdeps; 18 19 import java.io.File; 20 import java.io.FileNotFoundException; 21 import java.io.IOException; 22 import java.io.InputStream; 23 import java.io.RandomAccessFile; 24 import java.util.zip.ZipEntry; 25 import java.util.zip.ZipException; 26 import java.util.zip.ZipFile; 27 import java.util.zip.ZipInputStream; 28 import java.util.ArrayList; 29 import java.util.Collections; 30 import java.util.List; 31 32 public class Main { 33 private String[] mInputFileNames; 34 private String mOutputFormat = "xml"; 35 36 /** 37 * whether to only emit info about classes used; when {@code false}, 38 * info about fields and methods is also emitted 39 */ 40 private boolean mJustClasses = false; 41 42 /** 43 * Entry point. 44 */ main(String[] args)45 public static void main(String[] args) { 46 Main main = new Main(); 47 main.run(args); 48 } 49 50 /** 51 * Start things up. 52 */ run(String[] args)53 void run(String[] args) { 54 try { 55 parseArgs(args); 56 boolean first = true; 57 58 for (String fileName : mInputFileNames) { 59 if (first) { 60 first = false; 61 Output.generateFirstHeader(fileName, mOutputFormat); 62 } else { 63 Output.generateHeader(fileName, mOutputFormat); 64 } 65 List<RandomAccessFile> rafs = openInputFiles(fileName); 66 for (RandomAccessFile raf : rafs) { 67 DexData dexData = new DexData(raf); 68 dexData.load(); 69 Output.generate(dexData, mOutputFormat, mJustClasses); 70 raf.close(); 71 } 72 Output.generateFooter(mOutputFormat); 73 } 74 } catch (UsageException ue) { 75 usage(); 76 System.exit(2); 77 } catch (IOException ioe) { 78 if (ioe.getMessage() != null) { 79 System.err.println("Failed: " + ioe); 80 } 81 System.exit(1); 82 } catch (DexDataException dde) { 83 /* a message was already reported, just bail quietly */ 84 System.exit(1); 85 } 86 } 87 88 /** 89 * Opens an input file, which could be a .dex or a .jar/.apk with a 90 * classes.dex inside. If the latter, we extract the contents to a 91 * temporary file. 92 * 93 * @param fileName the name of the file to open 94 */ openInputFiles(String fileName)95 List<RandomAccessFile> openInputFiles(String fileName) throws IOException { 96 List<RandomAccessFile> rafs = openInputFileAsZip(fileName); 97 98 if (rafs == null) { 99 File inputFile = new File(fileName); 100 RandomAccessFile raf = new RandomAccessFile(inputFile, "r"); 101 rafs = Collections.singletonList(raf); 102 } 103 104 return rafs; 105 } 106 107 /** 108 * Tries to open an input file as a Zip archive (jar/apk) with dex files inside. 109 * 110 * @param fileName the name of the file to open 111 * @return a list of RandomAccessFile for classes.dex, 112 * classes2.dex, etc., or null if the input file is not a 113 * zip archive 114 * @throws IOException if the file isn't found, or it's a zip and 115 * no classes.dex isn't found inside 116 */ openInputFileAsZip(String fileName)117 List<RandomAccessFile> openInputFileAsZip(String fileName) throws IOException { 118 /* 119 * Try it as a zip file. 120 */ 121 ZipFile zipFile; 122 try { 123 zipFile = new ZipFile(fileName); 124 } catch (FileNotFoundException fnfe) { 125 /* not found, no point in retrying as non-zip */ 126 System.err.println("Unable to open '" + fileName + "': " + 127 fnfe.getMessage()); 128 throw fnfe; 129 } catch (ZipException ze) { 130 /* not a zip */ 131 return null; 132 } 133 134 List<RandomAccessFile> result = new ArrayList<RandomAccessFile>(); 135 try { 136 int classesDexNumber = 1; 137 while (true) { 138 result.add(openClassesDexZipFileEntry(zipFile, classesDexNumber)); 139 classesDexNumber++; 140 } 141 } catch (IOException ioe) { 142 // We didn't find any of the expected dex files in the zip. 143 if (result.isEmpty()) { 144 throw ioe; 145 } 146 return result; 147 } 148 } 149 openClassesDexZipFileEntry(ZipFile zipFile, int classesDexNumber)150 RandomAccessFile openClassesDexZipFileEntry(ZipFile zipFile, int classesDexNumber) 151 throws IOException { 152 /* 153 * We know it's a zip; see if there's anything useful inside. A 154 * failure here results in some type of IOException (of which 155 * ZipException is a subclass). 156 */ 157 String zipEntryName = ("classes" + 158 (classesDexNumber == 1 ? "" : classesDexNumber) + 159 ".dex"); 160 ZipEntry entry = zipFile.getEntry(zipEntryName); 161 if (entry == null) { 162 zipFile.close(); 163 throw new ZipException("Unable to find '" + zipEntryName + 164 "' in '" + zipFile.getName() + "'"); 165 } 166 167 InputStream zis = zipFile.getInputStream(entry); 168 169 /* 170 * Create a temp file to hold the DEX data, open it, and delete it 171 * to ensure it doesn't hang around if we fail. 172 */ 173 File tempFile = File.createTempFile("dexdeps", ".dex"); 174 //System.out.println("+++ using temp " + tempFile); 175 RandomAccessFile raf = new RandomAccessFile(tempFile, "rw"); 176 tempFile.delete(); 177 178 /* 179 * Copy all data from input stream to output file. 180 */ 181 byte copyBuf[] = new byte[32768]; 182 int actual; 183 184 while (true) { 185 actual = zis.read(copyBuf); 186 if (actual == -1) 187 break; 188 189 raf.write(copyBuf, 0, actual); 190 } 191 192 zis.close(); 193 raf.seek(0); 194 195 return raf; 196 } 197 198 199 /** 200 * Parses command-line arguments. 201 * 202 * @throws UsageException if arguments are missing or poorly formed 203 */ parseArgs(String[] args)204 void parseArgs(String[] args) { 205 int idx; 206 207 for (idx = 0; idx < args.length; idx++) { 208 String arg = args[idx]; 209 210 if (arg.equals("--") || !arg.startsWith("--")) { 211 break; 212 } else if (arg.startsWith("--format=")) { 213 mOutputFormat = arg.substring(arg.indexOf('=') + 1); 214 if (!mOutputFormat.equals("brief") && 215 !mOutputFormat.equals("xml")) 216 { 217 System.err.println("Unknown format '" + mOutputFormat +"'"); 218 throw new UsageException(); 219 } 220 //System.out.println("+++ using format " + mOutputFormat); 221 } else if (arg.equals("--just-classes")) { 222 mJustClasses = true; 223 } else { 224 System.err.println("Unknown option '" + arg + "'"); 225 throw new UsageException(); 226 } 227 } 228 229 // We expect at least one more argument (file name). 230 int fileCount = args.length - idx; 231 if (fileCount == 0) { 232 throw new UsageException(); 233 } 234 235 mInputFileNames = new String[fileCount]; 236 System.arraycopy(args, idx, mInputFileNames, 0, fileCount); 237 } 238 239 /** 240 * Prints command-line usage info. 241 */ usage()242 void usage() { 243 System.err.print( 244 "DEX dependency scanner v1.2\n" + 245 "Copyright (C) 2009 The Android Open Source Project\n\n" + 246 "Usage: dexdeps [options] <file.{dex,apk,jar}> ...\n" + 247 "Options:\n" + 248 " --format={xml,brief}\n" + 249 " --just-classes\n"); 250 } 251 } 252