1 /*
2  * Copyright (C) 2007 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.dx.cf.direct;
18 
19 import com.android.dex.util.FileUtils;
20 import java.io.ByteArrayOutputStream;
21 import java.io.File;
22 import java.io.IOException;
23 import java.io.InputStream;
24 import java.util.ArrayList;
25 import java.util.Arrays;
26 import java.util.Collections;
27 import java.util.Comparator;
28 import java.util.zip.ZipEntry;
29 import java.util.zip.ZipFile;
30 
31 /**
32  * Opens all the class files found in a class path element. Path elements
33  * can point to class files, {jar,zip,apk} files, or directories containing
34  * class files.
35  */
36 public class ClassPathOpener {
37 
38     /** {@code non-null;} pathname to start with */
39     private final String pathname;
40     /** {@code non-null;} callback interface */
41     private final Consumer consumer;
42     /**
43      * If true, sort such that classes appear before their inner
44      * classes and "package-info" occurs before all other classes in that
45      * package.
46      */
47     private final boolean sort;
48     private FileNameFilter filter;
49 
50     /**
51      * Callback interface for {@code ClassOpener}.
52      */
53     public interface Consumer {
54 
55         /**
56          * Provides the file name and byte array for a class path element.
57          *
58          * @param name {@code non-null;} filename of element. May not be a valid
59          * filesystem path.
60          *
61          * @param lastModified milliseconds since 1970-Jan-1 00:00:00 GMT
62          * @param bytes {@code non-null;} file data
63          * @return true on success. Result is or'd with all other results
64          * from {@code processFileBytes} and returned to the caller
65          * of {@code process()}.
66          */
processFileBytes(String name, long lastModified, byte[] bytes)67         boolean processFileBytes(String name, long lastModified, byte[] bytes);
68 
69         /**
70          * Informs consumer that an exception occurred while processing
71          * this path element. Processing will continue if possible.
72          *
73          * @param ex {@code non-null;} exception
74          */
onException(Exception ex)75         void onException(Exception ex);
76 
77         /**
78          * Informs consumer that processing of an archive file has begun.
79          *
80          * @param file {@code non-null;} archive file being processed
81          */
onProcessArchiveStart(File file)82         void onProcessArchiveStart(File file);
83     }
84 
85     /**
86      * Filter interface for {@code ClassOpener}.
87      */
88     public interface FileNameFilter {
89 
accept(String path)90         boolean accept(String path);
91     }
92 
93     /**
94      * An accept all filter.
95      */
96     public static final FileNameFilter acceptAll = new FileNameFilter() {
97 
98         @Override
99         public boolean accept(String path) {
100             return true;
101         }
102     };
103 
104     /**
105      * Constructs an instance.
106      *
107      * @param pathname {@code non-null;} path element to process
108      * @param sort if true, sort such that classes appear before their inner
109      * classes and "package-info" occurs before all other classes in that
110      * package.
111      * @param consumer {@code non-null;} callback interface
112      */
ClassPathOpener(String pathname, boolean sort, Consumer consumer)113     public ClassPathOpener(String pathname, boolean sort, Consumer consumer) {
114         this(pathname, sort, acceptAll, consumer);
115     }
116 
117     /**
118      * Constructs an instance.
119      *
120      * @param pathname {@code non-null;} path element to process
121      * @param sort if true, sort such that classes appear before their inner
122      * classes and "package-info" occurs before all other classes in that
123      * package.
124      * @param consumer {@code non-null;} callback interface
125      */
ClassPathOpener(String pathname, boolean sort, FileNameFilter filter, Consumer consumer)126     public ClassPathOpener(String pathname, boolean sort, FileNameFilter filter,
127             Consumer consumer) {
128         this.pathname = pathname;
129         this.sort = sort;
130         this.consumer = consumer;
131         this.filter = filter;
132     }
133 
134     /**
135      * Processes a path element.
136      *
137      * @return the OR of all return values
138      * from {@code Consumer.processFileBytes()}.
139      */
process()140     public boolean process() {
141         File file = new File(pathname);
142 
143         return processOne(file, true);
144     }
145 
146     /**
147      * Processes one file.
148      *
149      * @param file {@code non-null;} the file to process
150      * @param topLevel whether this is a top-level file (that is,
151      * specified directly on the commandline)
152      * @return whether any processing actually happened
153      */
processOne(File file, boolean topLevel)154     private boolean processOne(File file, boolean topLevel) {
155         try {
156             if (file.isDirectory()) {
157                 return processDirectory(file, topLevel);
158             }
159 
160             String path = file.getPath();
161 
162             if (path.endsWith(".zip") ||
163                     path.endsWith(".jar") ||
164                     path.endsWith(".apk")) {
165                 return processArchive(file);
166             }
167             if (filter.accept(path)) {
168                 byte[] bytes = FileUtils.readFile(file);
169                 return consumer.processFileBytes(path, file.lastModified(), bytes);
170             } else {
171                 return false;
172             }
173         } catch (Exception ex) {
174             consumer.onException(ex);
175             return false;
176         }
177     }
178 
179     /**
180      * Sorts java class names such that outer classes preceed their inner
181      * classes and "package-info" preceeds all other classes in its package.
182      *
183      * @param a {@code non-null;} first class name
184      * @param b {@code non-null;} second class name
185      * @return {@code compareTo()}-style result
186      */
compareClassNames(String a, String b)187     private static int compareClassNames(String a, String b) {
188         // Ensure inner classes sort second
189         a = a.replace('$','0');
190         b = b.replace('$','0');
191 
192         /*
193          * Assuming "package-info" only occurs at the end, ensures package-info
194          * sorts first.
195          */
196         a = a.replace("package-info", "");
197         b = b.replace("package-info", "");
198 
199         return a.compareTo(b);
200     }
201 
202     /**
203      * Processes a directory recursively.
204      *
205      * @param dir {@code non-null;} file representing the directory
206      * @param topLevel whether this is a top-level directory (that is,
207      * specified directly on the commandline)
208      * @return whether any processing actually happened
209      */
processDirectory(File dir, boolean topLevel)210     private boolean processDirectory(File dir, boolean topLevel) {
211         if (topLevel) {
212             dir = new File(dir, ".");
213         }
214 
215         File[] files = dir.listFiles();
216         int len = files.length;
217         boolean any = false;
218 
219         if (sort) {
220             Arrays.sort(files, new Comparator<File>() {
221                 @Override
222                 public int compare(File a, File b) {
223                     return compareClassNames(a.getName(), b.getName());
224                 }
225             });
226         }
227 
228         for (int i = 0; i < len; i++) {
229             any |= processOne(files[i], false);
230         }
231 
232         return any;
233     }
234 
235     /**
236      * Processes the contents of an archive ({@code .zip},
237      * {@code .jar}, or {@code .apk}).
238      *
239      * @param file {@code non-null;} archive file to process
240      * @return whether any processing actually happened
241      * @throws IOException on i/o problem
242      */
processArchive(File file)243     private boolean processArchive(File file) throws IOException {
244         ZipFile zip = new ZipFile(file);
245 
246         ArrayList<? extends java.util.zip.ZipEntry> entriesList
247                 = Collections.list(zip.entries());
248 
249         if (sort) {
250             Collections.sort(entriesList, new Comparator<ZipEntry>() {
251                @Override
252                public int compare (ZipEntry a, ZipEntry b) {
253                    return compareClassNames(a.getName(), b.getName());
254                }
255             });
256         }
257 
258         consumer.onProcessArchiveStart(file);
259 
260         ByteArrayOutputStream baos = new ByteArrayOutputStream(40000);
261         byte[] buf = new byte[20000];
262         boolean any = false;
263 
264         for (ZipEntry one : entriesList) {
265             final boolean isDirectory = one.isDirectory();
266 
267             String path = one.getName();
268             if (filter.accept(path)) {
269                 final byte[] bytes;
270                 if (!isDirectory) {
271                     InputStream in = zip.getInputStream(one);
272 
273                     baos.reset();
274                     int read;
275                     while ((read = in.read(buf)) != -1) {
276                         baos.write(buf, 0, read);
277                     }
278 
279                     in.close();
280                     bytes = baos.toByteArray();
281                 } else {
282                     bytes = new byte[0];
283                 }
284 
285                 any |= consumer.processFileBytes(path, one.getTime(), bytes);
286             }
287         }
288 
289         zip.close();
290         return any;
291     }
292 }
293