1/*
2 * Copyright (C) 2011 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 dalvik.system;
18
19import java.io.File;
20import java.io.IOException;
21import java.net.MalformedURLException;
22import java.net.URL;
23import java.util.ArrayList;
24import java.util.Collections;
25import java.util.Enumeration;
26import java.util.regex.Pattern;
27import java.util.zip.ZipFile;
28
29/**
30 * A pair of lists of entries, associated with a {@code ClassLoader}.
31 * One of the lists is a dex/resource path — typically referred
32 * to as a "class path" — list, and the other names directories
33 * containing native code libraries. Class path entries may be any of:
34 * a {@code .jar} or {@code .zip} file containing an optional
35 * top-level {@code classes.dex} file as well as arbitrary resources,
36 * or a plain {@code .dex} file (with no possibility of associated
37 * resources).
38 *
39 * <p>This class also contains methods to use these lists to look up
40 * classes and resources.</p>
41 */
42/*package*/ final class DexPathList {
43    private static final String DEX_SUFFIX = ".dex";
44    private static final String JAR_SUFFIX = ".jar";
45    private static final String ZIP_SUFFIX = ".zip";
46    private static final String APK_SUFFIX = ".apk";
47
48    /** class definition context */
49    private final ClassLoader definingContext;
50
51    /** list of dex/resource (class path) elements */
52    private final Element[] dexElements;
53
54    /** list of native library directory elements */
55    private final File[] nativeLibraryDirectories;
56
57    /**
58     * Constructs an instance.
59     *
60     * @param definingContext the context in which any as-yet unresolved
61     * classes should be defined
62     * @param dexPath list of dex/resource path elements, separated by
63     * {@code File.pathSeparator}
64     * @param libraryPath list of native library directory path elements,
65     * separated by {@code File.pathSeparator}
66     * @param optimizedDirectory directory where optimized {@code .dex} files
67     * should be found and written to, or {@code null} to use the default
68     * system directory for same
69     */
70    public DexPathList(ClassLoader definingContext, String dexPath,
71            String libraryPath, File optimizedDirectory) {
72        if (definingContext == null) {
73            throw new NullPointerException("definingContext == null");
74        }
75
76        if (dexPath == null) {
77            throw new NullPointerException("dexPath == null");
78        }
79
80        if (optimizedDirectory != null) {
81            if (!optimizedDirectory.exists())  {
82                throw new IllegalArgumentException(
83                        "optimizedDirectory doesn't exist: "
84                        + optimizedDirectory);
85            }
86
87            if (!(optimizedDirectory.canRead()
88                            && optimizedDirectory.canWrite())) {
89                throw new IllegalArgumentException(
90                        "optimizedDirectory not readable/writable: "
91                        + optimizedDirectory);
92            }
93        }
94
95        this.definingContext = definingContext;
96        this.dexElements =
97            makeDexElements(splitDexPath(dexPath), optimizedDirectory);
98        this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
99    }
100
101    /**
102     * Splits the given dex path string into elements using the path
103     * separator, pruning out any elements that do not refer to existing
104     * and readable files. (That is, directories are not included in the
105     * result.)
106     */
107    private static ArrayList<File> splitDexPath(String path) {
108        return splitPaths(path, null, false);
109    }
110
111    /**
112     * Splits the given library directory path string into elements
113     * using the path separator ({@code File.pathSeparator}, which
114     * defaults to {@code ":"} on Android, appending on the elements
115     * from the system library path, and pruning out any elements that
116     * do not refer to existing and readable directories.
117     */
118    private static File[] splitLibraryPath(String path) {
119        /*
120         * Native libraries may exist in both the system and
121         * application library paths, and we use this search order:
122         *
123         *   1. this class loader's library path for application
124         *      libraries
125         *   2. the VM's library path from the system
126         *      property for system libraries
127         *
128         * This order was reversed prior to Gingerbread; see http://b/2933456.
129         */
130        ArrayList<File> result = splitPaths(
131                path, System.getProperty("java.library.path", "."), true);
132        return result.toArray(new File[result.size()]);
133    }
134
135    /**
136     * Splits the given path strings into file elements using the path
137     * separator, combining the results and filtering out elements
138     * that don't exist, aren't readable, or aren't either a regular
139     * file or a directory (as specified). Either string may be empty
140     * or {@code null}, in which case it is ignored. If both strings
141     * are empty or {@code null}, or all elements get pruned out, then
142     * this returns a zero-element list.
143     */
144    private static ArrayList<File> splitPaths(String path1, String path2,
145            boolean wantDirectories) {
146        ArrayList<File> result = new ArrayList<File>();
147
148        splitAndAdd(path1, wantDirectories, result);
149        splitAndAdd(path2, wantDirectories, result);
150        return result;
151    }
152
153    /**
154     * Helper for {@link #splitPaths}, which does the actual splitting
155     * and filtering and adding to a result.
156     */
157    private static void splitAndAdd(String path, boolean wantDirectories,
158            ArrayList<File> resultList) {
159        if (path == null) {
160            return;
161        }
162
163        String[] strings = path.split(Pattern.quote(File.pathSeparator));
164
165        for (String s : strings) {
166            File file = new File(s);
167
168            if (! (file.exists() && file.canRead())) {
169                continue;
170            }
171
172            /*
173             * Note: There are other entities in filesystems than
174             * regular files and directories.
175             */
176            if (wantDirectories) {
177                if (!file.isDirectory()) {
178                    continue;
179                }
180            } else {
181                if (!file.isFile()) {
182                    continue;
183                }
184            }
185
186            resultList.add(file);
187        }
188    }
189
190    /**
191     * Makes an array of dex/resource path elements, one per element of
192     * the given array.
193     */
194    private static Element[] makeDexElements(ArrayList<File> files,
195            File optimizedDirectory) {
196        ArrayList<Element> elements = new ArrayList<Element>();
197
198        /*
199         * Open all files and load the (direct or contained) dex files
200         * up front.
201         */
202        for (File file : files) {
203            File zip = null;
204            DexFile dex = null;
205            String name = file.getName();
206
207            if (name.endsWith(DEX_SUFFIX)) {
208                // Raw dex file (not inside a zip/jar).
209                try {
210                    dex = loadDexFile(file, optimizedDirectory);
211                } catch (IOException ex) {
212                    System.logE("Unable to load dex file: " + file, ex);
213                }
214            } else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
215                    || name.endsWith(ZIP_SUFFIX)) {
216                zip = file;
217
218                try {
219                    dex = loadDexFile(file, optimizedDirectory);
220                } catch (IOException ignored) {
221                    /*
222                     * IOException might get thrown "legitimately" by
223                     * the DexFile constructor if the zip file turns
224                     * out to be resource-only (that is, no
225                     * classes.dex file in it). Safe to just ignore
226                     * the exception here, and let dex == null.
227                     */
228                }
229            } else {
230                System.logW("Unknown file type for: " + file);
231            }
232
233            if ((zip != null) || (dex != null)) {
234                elements.add(new Element(file, zip, dex));
235            }
236        }
237
238        return elements.toArray(new Element[elements.size()]);
239    }
240
241    /**
242     * Constructs a {@code DexFile} instance, as appropriate depending
243     * on whether {@code optimizedDirectory} is {@code null}.
244     */
245    private static DexFile loadDexFile(File file, File optimizedDirectory)
246            throws IOException {
247        if (optimizedDirectory == null) {
248            return new DexFile(file);
249        } else {
250            String optimizedPath = optimizedPathFor(file, optimizedDirectory);
251            return DexFile.loadDex(file.getPath(), optimizedPath, 0);
252        }
253    }
254
255    /**
256     * Converts a dex/jar file path and an output directory to an
257     * output file path for an associated optimized dex file.
258     */
259    private static String optimizedPathFor(File path,
260            File optimizedDirectory) {
261        /*
262         * Get the filename component of the path, and replace the
263         * suffix with ".dex" if that's not already the suffix.
264         *
265         * We don't want to use ".odex", because the build system uses
266         * that for files that are paired with resource-only jar
267         * files. If the VM can assume that there's no classes.dex in
268         * the matching jar, it doesn't need to open the jar to check
269         * for updated dependencies, providing a slight performance
270         * boost at startup. The use of ".dex" here matches the use on
271         * files in /data/dalvik-cache.
272         */
273        String fileName = path.getName();
274        if (!fileName.endsWith(DEX_SUFFIX)) {
275            int lastDot = fileName.lastIndexOf(".");
276            if (lastDot < 0) {
277                fileName += DEX_SUFFIX;
278            } else {
279                StringBuilder sb = new StringBuilder(lastDot + 4);
280                sb.append(fileName, 0, lastDot);
281                sb.append(DEX_SUFFIX);
282                fileName = sb.toString();
283            }
284        }
285
286        File result = new File(optimizedDirectory, fileName);
287        return result.getPath();
288    }
289
290    /**
291     * Finds the named class in one of the dex files pointed at by
292     * this instance. This will find the one in the earliest listed
293     * path element. If the class is found but has not yet been
294     * defined, then this method will define it in the defining
295     * context that this instance was constructed with.
296     *
297     * @return the named class or {@code null} if the class is not
298     * found in any of the dex files
299     */
300    public Class findClass(String name) {
301        for (Element element : dexElements) {
302            DexFile dex = element.dexFile;
303
304            if (dex != null) {
305                Class clazz = dex.loadClassBinaryName(name, definingContext);
306                if (clazz != null) {
307                    return clazz;
308                }
309            }
310        }
311
312        return null;
313    }
314
315    /**
316     * Finds the named resource in one of the zip/jar files pointed at
317     * by this instance. This will find the one in the earliest listed
318     * path element.
319     *
320     * @return a URL to the named resource or {@code null} if the
321     * resource is not found in any of the zip/jar files
322     */
323    public URL findResource(String name) {
324        for (Element element : dexElements) {
325            URL url = element.findResource(name);
326            if (url != null) {
327                return url;
328            }
329        }
330
331        return null;
332    }
333
334    /**
335     * Finds all the resources with the given name, returning an
336     * enumeration of them. If there are no resources with the given
337     * name, then this method returns an empty enumeration.
338     */
339    public Enumeration<URL> findResources(String name) {
340        ArrayList<URL> result = new ArrayList<URL>();
341
342        for (Element element : dexElements) {
343            URL url = element.findResource(name);
344            if (url != null) {
345                result.add(url);
346            }
347        }
348
349        return Collections.enumeration(result);
350    }
351
352    /**
353     * Finds the named native code library on any of the library
354     * directories pointed at by this instance. This will find the
355     * one in the earliest listed directory, ignoring any that are not
356     * readable regular files.
357     *
358     * @return the complete path to the library or {@code null} if no
359     * library was found
360     */
361    public String findLibrary(String libraryName) {
362        String fileName = System.mapLibraryName(libraryName);
363
364        for (File directory : nativeLibraryDirectories) {
365            File file = new File(directory, fileName);
366            if (file.exists() && file.isFile() && file.canRead()) {
367                return file.getPath();
368            }
369        }
370
371        return null;
372    }
373
374    /**
375     * Element of the dex/resource file path
376     */
377    /*package*/ static class Element {
378        private final File file;
379        private final File zip;
380        private final DexFile dexFile;
381
382        private ZipFile zipFile;
383        private boolean init;
384
385        public Element(File file, File zip, DexFile dexFile) {
386            this.file = file;
387            this.zip = zip;
388            this.dexFile = dexFile;
389        }
390
391        public synchronized void maybeInit() {
392            if (init) {
393                return;
394            }
395
396            init = true;
397
398            if (zip == null) {
399                /*
400                 * Either this element has no zip/jar file (first
401                 * clause), or the zip/jar file doesn't have an entry
402                 * for the given name (second clause).
403                 */
404                return;
405            }
406
407            try {
408                zipFile = new ZipFile(zip);
409            } catch (IOException ioe) {
410                /*
411                 * Note: ZipException (a subclass of IOException)
412                 * might get thrown by the ZipFile constructor
413                 * (e.g. if the file isn't actually a zip/jar
414                 * file).
415                 */
416                System.logE("Unable to open zip file: " + file, ioe);
417                zipFile = null;
418            }
419        }
420
421        public URL findResource(String name) {
422            maybeInit();
423
424            if (zipFile == null || zipFile.getEntry(name) == null) {
425                return null;
426            }
427
428            try {
429                /*
430                 * File.toURL() is compliant with RFC 1738 in
431                 * always creating absolute path names. If we
432                 * construct the URL by concatenating strings, we
433                 * might end up with illegal URLs for relative
434                 * names.
435                 */
436                return new URL("jar:" + file.toURL() + "!/" + name);
437            } catch (MalformedURLException ex) {
438                throw new RuntimeException(ex);
439            }
440        }
441    }
442}
443