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
17package com.android.dexdeps;
18
19import java.io.File;
20import java.io.FileNotFoundException;
21import java.io.IOException;
22import java.io.InputStream;
23import java.io.RandomAccessFile;
24import java.util.zip.ZipEntry;
25import java.util.zip.ZipException;
26import java.util.zip.ZipFile;
27import java.util.zip.ZipInputStream;
28
29public class Main {
30    private static final String CLASSES_DEX = "classes.dex";
31
32    private String[] mInputFileNames;
33    private String mOutputFormat = "xml";
34
35    /**
36     * whether to only emit info about classes used; when {@code false},
37     * info about fields and methods is also emitted
38     */
39    private boolean mJustClasses = false;
40
41    /**
42     * Entry point.
43     */
44    public static void main(String[] args) {
45        Main main = new Main();
46        main.run(args);
47    }
48
49    /**
50     * Start things up.
51     */
52    void run(String[] args) {
53        try {
54            parseArgs(args);
55            boolean first = true;
56
57            for (String fileName : mInputFileNames) {
58                RandomAccessFile raf = openInputFile(fileName);
59                DexData dexData = new DexData(raf);
60                dexData.load();
61
62                if (first) {
63                    first = false;
64                    Output.generateFirstHeader(fileName, mOutputFormat);
65                } else {
66                    Output.generateHeader(fileName, mOutputFormat);
67                }
68
69                Output.generate(dexData, mOutputFormat, mJustClasses);
70                Output.generateFooter(mOutputFormat);
71                raf.close();
72            }
73        } catch (UsageException ue) {
74            usage();
75            System.exit(2);
76        } catch (IOException ioe) {
77            if (ioe.getMessage() != null) {
78                System.err.println("Failed: " + ioe);
79            }
80            System.exit(1);
81        } catch (DexDataException dde) {
82            /* a message was already reported, just bail quietly */
83            System.exit(1);
84        }
85    }
86
87    /**
88     * Opens an input file, which could be a .dex or a .jar/.apk with a
89     * classes.dex inside.  If the latter, we extract the contents to a
90     * temporary file.
91     *
92     * @param fileName the name of the file to open
93     */
94    RandomAccessFile openInputFile(String fileName) throws IOException {
95        RandomAccessFile raf;
96
97        raf = openInputFileAsZip(fileName);
98        if (raf == null) {
99            File inputFile = new File(fileName);
100            raf = new RandomAccessFile(inputFile, "r");
101        }
102
103        return raf;
104    }
105
106    /**
107     * Tries to open an input file as a Zip archive (jar/apk) with a
108     * "classes.dex" inside.
109     *
110     * @param fileName the name of the file to open
111     * @return a RandomAccessFile for classes.dex, or null if the input file
112     *         is not a zip archive
113     * @throws IOException if the file isn't found, or it's a zip and
114     *         classes.dex isn't found inside
115     */
116    RandomAccessFile openInputFileAsZip(String fileName) throws IOException {
117        ZipFile zipFile;
118
119        /*
120         * Try it as a zip file.
121         */
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        /*
135         * We know it's a zip; see if there's anything useful inside.  A
136         * failure here results in some type of IOException (of which
137         * ZipException is a subclass).
138         */
139        ZipEntry entry = zipFile.getEntry(CLASSES_DEX);
140        if (entry == null) {
141            System.err.println("Unable to find '" + CLASSES_DEX +
142                "' in '" + fileName + "'");
143            zipFile.close();
144            throw new ZipException();
145        }
146
147        InputStream zis = zipFile.getInputStream(entry);
148
149        /*
150         * Create a temp file to hold the DEX data, open it, and delete it
151         * to ensure it doesn't hang around if we fail.
152         */
153        File tempFile = File.createTempFile("dexdeps", ".dex");
154        //System.out.println("+++ using temp " + tempFile);
155        RandomAccessFile raf = new RandomAccessFile(tempFile, "rw");
156        tempFile.delete();
157
158        /*
159         * Copy all data from input stream to output file.
160         */
161        byte copyBuf[] = new byte[32768];
162        int actual;
163
164        while (true) {
165            actual = zis.read(copyBuf);
166            if (actual == -1)
167                break;
168
169            raf.write(copyBuf, 0, actual);
170        }
171
172        zis.close();
173        raf.seek(0);
174
175        return raf;
176    }
177
178
179    /**
180     * Parses command-line arguments.
181     *
182     * @throws UsageException if arguments are missing or poorly formed
183     */
184    void parseArgs(String[] args) {
185        int idx;
186
187        for (idx = 0; idx < args.length; idx++) {
188            String arg = args[idx];
189
190            if (arg.equals("--") || !arg.startsWith("--")) {
191                break;
192            } else if (arg.startsWith("--format=")) {
193                mOutputFormat = arg.substring(arg.indexOf('=') + 1);
194                if (!mOutputFormat.equals("brief") &&
195                    !mOutputFormat.equals("xml"))
196                {
197                    System.err.println("Unknown format '" + mOutputFormat +"'");
198                    throw new UsageException();
199                }
200                //System.out.println("+++ using format " + mOutputFormat);
201            } else if (arg.equals("--just-classes")) {
202                mJustClasses = true;
203            } else {
204                System.err.println("Unknown option '" + arg + "'");
205                throw new UsageException();
206            }
207        }
208
209        // We expect at least one more argument (file name).
210        int fileCount = args.length - idx;
211        if (fileCount == 0) {
212            throw new UsageException();
213        }
214
215        mInputFileNames = new String[fileCount];
216        System.arraycopy(args, idx, mInputFileNames, 0, fileCount);
217    }
218
219    /**
220     * Prints command-line usage info.
221     */
222    void usage() {
223        System.err.print(
224                "DEX dependency scanner v1.2\n" +
225                "Copyright (C) 2009 The Android Open Source Project\n\n" +
226                "Usage: dexdeps [options] <file.{dex,apk,jar}> ...\n" +
227                "Options:\n" +
228                "  --format={xml,brief}\n" +
229                "  --just-classes\n");
230    }
231}
232