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;
28import java.util.ArrayList;
29import java.util.Collections;
30import java.util.List;
31
32public 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     */
45    public static void main(String[] args) {
46        Main main = new Main();
47        main.run(args);
48    }
49
50    /**
51     * Start things up.
52     */
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     */
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     */
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
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     */
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     */
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