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