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 android.system.ErrnoException;
20import android.system.StructStat;
21import java.io.File;
22import java.io.IOException;
23import java.net.MalformedURLException;
24import java.net.URL;
25import java.nio.ByteBuffer;
26import java.util.ArrayList;
27import java.util.Arrays;
28import java.util.Collections;
29import java.util.Enumeration;
30import java.util.List;
31import libcore.io.ClassPathURLStreamHandler;
32import libcore.io.IoUtils;
33import libcore.io.Libcore;
34
35import static android.system.OsConstants.S_ISDIR;
36
37/**
38 * A pair of lists of entries, associated with a {@code ClassLoader}.
39 * One of the lists is a dex/resource path — typically referred
40 * to as a "class path" — list, and the other names directories
41 * containing native code libraries. Class path entries may be any of:
42 * a {@code .jar} or {@code .zip} file containing an optional
43 * top-level {@code classes.dex} file as well as arbitrary resources,
44 * or a plain {@code .dex} file (with no possibility of associated
45 * resources).
46 *
47 * <p>This class also contains methods to use these lists to look up
48 * classes and resources.</p>
49 */
50/*package*/ final class DexPathList {
51    private static final String DEX_SUFFIX = ".dex";
52    private static final String zipSeparator = "!/";
53
54    /** class definition context */
55    private final ClassLoader definingContext;
56
57    /**
58     * List of dex/resource (class path) elements.
59     * Should be called pathElements, but the Facebook app uses reflection
60     * to modify 'dexElements' (http://b/7726934).
61     */
62    private Element[] dexElements;
63
64    /** List of native library path elements. */
65    private final NativeLibraryElement[] nativeLibraryPathElements;
66
67    /** List of application native library directories. */
68    private final List<File> nativeLibraryDirectories;
69
70    /** List of system native library directories. */
71    private final List<File> systemNativeLibraryDirectories;
72
73    /**
74     * Exceptions thrown during creation of the dexElements list.
75     */
76    private IOException[] dexElementsSuppressedExceptions;
77
78    /**
79     * Construct an instance.
80     *
81     * @param definingContext the context in which any as-yet unresolved
82     * classes should be defined
83     *
84     * @param dexFiles the bytebuffers containing the dex files that we should load classes from.
85     */
86    public DexPathList(ClassLoader definingContext, ByteBuffer[] dexFiles) {
87        if (definingContext == null) {
88            throw new NullPointerException("definingContext == null");
89        }
90        if (dexFiles == null) {
91            throw new NullPointerException("dexFiles == null");
92        }
93        if (Arrays.stream(dexFiles).anyMatch(v -> v == null)) {
94            throw new NullPointerException("dexFiles contains a null Buffer!");
95        }
96
97        this.definingContext = definingContext;
98        // TODO It might be useful to let in-memory dex-paths have native libraries.
99        this.nativeLibraryDirectories = Collections.emptyList();
100        this.systemNativeLibraryDirectories =
101                splitPaths(System.getProperty("java.library.path"), true);
102        this.nativeLibraryPathElements = makePathElements(this.systemNativeLibraryDirectories);
103
104        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
105        this.dexElements = makeInMemoryDexElements(dexFiles, suppressedExceptions);
106        if (suppressedExceptions.size() > 0) {
107            this.dexElementsSuppressedExceptions =
108                    suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
109        } else {
110            dexElementsSuppressedExceptions = null;
111        }
112    }
113
114    /**
115     * Constructs an instance.
116     *
117     * @param definingContext the context in which any as-yet unresolved
118     * classes should be defined
119     * @param dexPath list of dex/resource path elements, separated by
120     * {@code File.pathSeparator}
121     * @param librarySearchPath list of native library directory path elements,
122     * separated by {@code File.pathSeparator}
123     * @param optimizedDirectory directory where optimized {@code .dex} files
124     * should be found and written to, or {@code null} to use the default
125     * system directory for same
126     */
127    public DexPathList(ClassLoader definingContext, String dexPath,
128            String librarySearchPath, File optimizedDirectory) {
129
130        if (definingContext == null) {
131            throw new NullPointerException("definingContext == null");
132        }
133
134        if (dexPath == null) {
135            throw new NullPointerException("dexPath == null");
136        }
137
138        if (optimizedDirectory != null) {
139            if (!optimizedDirectory.exists())  {
140                throw new IllegalArgumentException(
141                        "optimizedDirectory doesn't exist: "
142                        + optimizedDirectory);
143            }
144
145            if (!(optimizedDirectory.canRead()
146                            && optimizedDirectory.canWrite())) {
147                throw new IllegalArgumentException(
148                        "optimizedDirectory not readable/writable: "
149                        + optimizedDirectory);
150            }
151        }
152
153        this.definingContext = definingContext;
154
155        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
156        // save dexPath for BaseDexClassLoader
157        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
158                                           suppressedExceptions, definingContext);
159
160        // Native libraries may exist in both the system and
161        // application library paths, and we use this search order:
162        //
163        //   1. This class loader's library path for application libraries (librarySearchPath):
164        //   1.1. Native library directories
165        //   1.2. Path to libraries in apk-files
166        //   2. The VM's library path from the system property for system libraries
167        //      also known as java.library.path
168        //
169        // This order was reversed prior to Gingerbread; see http://b/2933456.
170        this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
171        this.systemNativeLibraryDirectories =
172                splitPaths(System.getProperty("java.library.path"), true);
173        List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
174        allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);
175
176        this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories);
177
178        if (suppressedExceptions.size() > 0) {
179            this.dexElementsSuppressedExceptions =
180                suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
181        } else {
182            dexElementsSuppressedExceptions = null;
183        }
184    }
185
186    @Override public String toString() {
187        List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
188        allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);
189
190        File[] nativeLibraryDirectoriesArray =
191                allNativeLibraryDirectories.toArray(
192                    new File[allNativeLibraryDirectories.size()]);
193
194        return "DexPathList[" + Arrays.toString(dexElements) +
195            ",nativeLibraryDirectories=" + Arrays.toString(nativeLibraryDirectoriesArray) + "]";
196    }
197
198    /**
199     * For BaseDexClassLoader.getLdLibraryPath.
200     */
201    public List<File> getNativeLibraryDirectories() {
202        return nativeLibraryDirectories;
203    }
204
205    /**
206     * Adds a new path to this instance
207     * @param dexPath list of dex/resource path element, separated by
208     * {@code File.pathSeparator}
209     * @param optimizedDirectory directory where optimized {@code .dex} files
210     * should be found and written to, or {@code null} to use the default
211     * system directory for same
212     */
213    public void addDexPath(String dexPath, File optimizedDirectory) {
214        final List<IOException> suppressedExceptionList = new ArrayList<IOException>();
215        final Element[] newElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
216                suppressedExceptionList, definingContext);
217
218        if (newElements != null && newElements.length > 0) {
219            final Element[] oldElements = dexElements;
220            dexElements = new Element[oldElements.length + newElements.length];
221            System.arraycopy(
222                    oldElements, 0, dexElements, 0, oldElements.length);
223            System.arraycopy(
224                    newElements, 0, dexElements, oldElements.length, newElements.length);
225        }
226
227        if (suppressedExceptionList.size() > 0) {
228            final IOException[] newSuppressedExceptions = suppressedExceptionList.toArray(
229                    new IOException[suppressedExceptionList.size()]);
230            if (dexElementsSuppressedExceptions != null) {
231                final IOException[] oldSuppressedExceptions = dexElementsSuppressedExceptions;
232                final int suppressedExceptionsLength = oldSuppressedExceptions.length +
233                        newSuppressedExceptions.length;
234                dexElementsSuppressedExceptions = new IOException[suppressedExceptionsLength];
235                System.arraycopy(oldSuppressedExceptions, 0, dexElementsSuppressedExceptions,
236                        0, oldSuppressedExceptions.length);
237                System.arraycopy(newSuppressedExceptions, 0, dexElementsSuppressedExceptions,
238                        oldSuppressedExceptions.length, newSuppressedExceptions.length);
239            } else {
240                dexElementsSuppressedExceptions = newSuppressedExceptions;
241            }
242        }
243    }
244
245    /**
246     * Splits the given dex path string into elements using the path
247     * separator, pruning out any elements that do not refer to existing
248     * and readable files.
249     */
250    private static List<File> splitDexPath(String path) {
251        return splitPaths(path, false);
252    }
253
254    /**
255     * Splits the given path strings into file elements using the path
256     * separator, combining the results and filtering out elements
257     * that don't exist, aren't readable, or aren't either a regular
258     * file or a directory (as specified). Either string may be empty
259     * or {@code null}, in which case it is ignored. If both strings
260     * are empty or {@code null}, or all elements get pruned out, then
261     * this returns a zero-element list.
262     */
263    private static List<File> splitPaths(String searchPath, boolean directoriesOnly) {
264        List<File> result = new ArrayList<>();
265
266        if (searchPath != null) {
267            for (String path : searchPath.split(File.pathSeparator)) {
268                if (directoriesOnly) {
269                    try {
270                        StructStat sb = Libcore.os.stat(path);
271                        if (!S_ISDIR(sb.st_mode)) {
272                            continue;
273                        }
274                    } catch (ErrnoException ignored) {
275                        continue;
276                    }
277                }
278                result.add(new File(path));
279            }
280        }
281
282        return result;
283    }
284
285    private static Element[] makeInMemoryDexElements(ByteBuffer[] dexFiles,
286            List<IOException> suppressedExceptions) {
287        Element[] elements = new Element[dexFiles.length];
288        int elementPos = 0;
289        for (ByteBuffer buf : dexFiles) {
290            try {
291                DexFile dex = new DexFile(buf);
292                elements[elementPos++] = new Element(dex);
293            } catch (IOException suppressed) {
294                System.logE("Unable to load dex file: " + buf, suppressed);
295                suppressedExceptions.add(suppressed);
296            }
297        }
298        if (elementPos != elements.length) {
299            elements = Arrays.copyOf(elements, elementPos);
300        }
301        return elements;
302    }
303
304    /**
305     * Makes an array of dex/resource path elements, one per element of
306     * the given array.
307     */
308    private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
309            List<IOException> suppressedExceptions, ClassLoader loader) {
310      Element[] elements = new Element[files.size()];
311      int elementsPos = 0;
312      /*
313       * Open all files and load the (direct or contained) dex files up front.
314       */
315      for (File file : files) {
316          if (file.isDirectory()) {
317              // We support directories for looking up resources. Looking up resources in
318              // directories is useful for running libcore tests.
319              elements[elementsPos++] = new Element(file);
320          } else if (file.isFile()) {
321              String name = file.getName();
322
323              if (name.endsWith(DEX_SUFFIX)) {
324                  // Raw dex file (not inside a zip/jar).
325                  try {
326                      DexFile dex = loadDexFile(file, optimizedDirectory, loader, elements);
327                      if (dex != null) {
328                          elements[elementsPos++] = new Element(dex, null);
329                      }
330                  } catch (IOException suppressed) {
331                      System.logE("Unable to load dex file: " + file, suppressed);
332                      suppressedExceptions.add(suppressed);
333                  }
334              } else {
335                  DexFile dex = null;
336                  try {
337                      dex = loadDexFile(file, optimizedDirectory, loader, elements);
338                  } catch (IOException suppressed) {
339                      /*
340                       * IOException might get thrown "legitimately" by the DexFile constructor if
341                       * the zip file turns out to be resource-only (that is, no classes.dex file
342                       * in it).
343                       * Let dex == null and hang on to the exception to add to the tea-leaves for
344                       * when findClass returns null.
345                       */
346                      suppressedExceptions.add(suppressed);
347                  }
348
349                  if (dex == null) {
350                      elements[elementsPos++] = new Element(file);
351                  } else {
352                      elements[elementsPos++] = new Element(dex, file);
353                  }
354              }
355          } else {
356              System.logW("ClassLoader referenced unknown path: " + file);
357          }
358      }
359      if (elementsPos != elements.length) {
360          elements = Arrays.copyOf(elements, elementsPos);
361      }
362      return elements;
363    }
364
365    /**
366     * Constructs a {@code DexFile} instance, as appropriate depending on whether
367     * {@code optimizedDirectory} is {@code null}. An application image file may be associated with
368     * the {@code loader} if it is not null.
369     */
370    private static DexFile loadDexFile(File file, File optimizedDirectory, ClassLoader loader,
371                                       Element[] elements)
372            throws IOException {
373        if (optimizedDirectory == null) {
374            return new DexFile(file, loader, elements);
375        } else {
376            String optimizedPath = optimizedPathFor(file, optimizedDirectory);
377            return DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements);
378        }
379    }
380
381    /**
382     * Converts a dex/jar file path and an output directory to an
383     * output file path for an associated optimized dex file.
384     */
385    private static String optimizedPathFor(File path,
386            File optimizedDirectory) {
387        /*
388         * Get the filename component of the path, and replace the
389         * suffix with ".dex" if that's not already the suffix.
390         *
391         * We don't want to use ".odex", because the build system uses
392         * that for files that are paired with resource-only jar
393         * files. If the VM can assume that there's no classes.dex in
394         * the matching jar, it doesn't need to open the jar to check
395         * for updated dependencies, providing a slight performance
396         * boost at startup. The use of ".dex" here matches the use on
397         * files in /data/dalvik-cache.
398         */
399        String fileName = path.getName();
400        if (!fileName.endsWith(DEX_SUFFIX)) {
401            int lastDot = fileName.lastIndexOf(".");
402            if (lastDot < 0) {
403                fileName += DEX_SUFFIX;
404            } else {
405                StringBuilder sb = new StringBuilder(lastDot + 4);
406                sb.append(fileName, 0, lastDot);
407                sb.append(DEX_SUFFIX);
408                fileName = sb.toString();
409            }
410        }
411
412        File result = new File(optimizedDirectory, fileName);
413        return result.getPath();
414    }
415
416    /*
417     * TODO (dimitry): Revert after apps stops relying on the existence of this
418     * method (see http://b/21957414 and http://b/26317852 for details)
419     */
420    @SuppressWarnings("unused")
421    private static Element[] makePathElements(List<File> files, File optimizedDirectory,
422            List<IOException> suppressedExceptions) {
423        return makeDexElements(files, optimizedDirectory, suppressedExceptions, null);
424    }
425
426    /**
427     * Makes an array of directory/zip path elements for the native library search path, one per
428     * element of the given array.
429     */
430    private static NativeLibraryElement[] makePathElements(List<File> files) {
431        NativeLibraryElement[] elements = new NativeLibraryElement[files.size()];
432        int elementsPos = 0;
433        for (File file : files) {
434            String path = file.getPath();
435
436            if (path.contains(zipSeparator)) {
437                String split[] = path.split(zipSeparator, 2);
438                File zip = new File(split[0]);
439                String dir = split[1];
440                elements[elementsPos++] = new NativeLibraryElement(zip, dir);
441            } else if (file.isDirectory()) {
442                // We support directories for looking up native libraries.
443                elements[elementsPos++] = new NativeLibraryElement(file);
444            }
445        }
446        if (elementsPos != elements.length) {
447            elements = Arrays.copyOf(elements, elementsPos);
448        }
449        return elements;
450    }
451
452    /**
453     * Finds the named class in one of the dex files pointed at by
454     * this instance. This will find the one in the earliest listed
455     * path element. If the class is found but has not yet been
456     * defined, then this method will define it in the defining
457     * context that this instance was constructed with.
458     *
459     * @param name of class to find
460     * @param suppressed exceptions encountered whilst finding the class
461     * @return the named class or {@code null} if the class is not
462     * found in any of the dex files
463     */
464    public Class<?> findClass(String name, List<Throwable> suppressed) {
465        for (Element element : dexElements) {
466            Class<?> clazz = element.findClass(name, definingContext, suppressed);
467            if (clazz != null) {
468                return clazz;
469            }
470        }
471
472        if (dexElementsSuppressedExceptions != null) {
473            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
474        }
475        return null;
476    }
477
478    /**
479     * Finds the named resource in one of the zip/jar files pointed at
480     * by this instance. This will find the one in the earliest listed
481     * path element.
482     *
483     * @return a URL to the named resource or {@code null} if the
484     * resource is not found in any of the zip/jar files
485     */
486    public URL findResource(String name) {
487        for (Element element : dexElements) {
488            URL url = element.findResource(name);
489            if (url != null) {
490                return url;
491            }
492        }
493
494        return null;
495    }
496
497    /**
498     * Finds all the resources with the given name, returning an
499     * enumeration of them. If there are no resources with the given
500     * name, then this method returns an empty enumeration.
501     */
502    public Enumeration<URL> findResources(String name) {
503        ArrayList<URL> result = new ArrayList<URL>();
504
505        for (Element element : dexElements) {
506            URL url = element.findResource(name);
507            if (url != null) {
508                result.add(url);
509            }
510        }
511
512        return Collections.enumeration(result);
513    }
514
515    /**
516     * Finds the named native code library on any of the library
517     * directories pointed at by this instance. This will find the
518     * one in the earliest listed directory, ignoring any that are not
519     * readable regular files.
520     *
521     * @return the complete path to the library or {@code null} if no
522     * library was found
523     */
524    public String findLibrary(String libraryName) {
525        String fileName = System.mapLibraryName(libraryName);
526
527        for (NativeLibraryElement element : nativeLibraryPathElements) {
528            String path = element.findNativeLibrary(fileName);
529
530            if (path != null) {
531                return path;
532            }
533        }
534
535        return null;
536    }
537
538    /**
539     * Returns the list of all individual dex files paths from the current list.
540     * The list will contain only file paths (i.e. no directories).
541     */
542    /*package*/ List<String> getDexPaths() {
543        List<String> dexPaths = new ArrayList<String>();
544        for (Element e : dexElements) {
545            String dexPath = e.getDexPath();
546            if (dexPath != null) {
547                // Add the element to the list only if it is a file. A null dex path signals the
548                // element is a resource directory or an in-memory dex file.
549                dexPaths.add(dexPath);
550            }
551        }
552        return dexPaths;
553    }
554
555    /**
556     * Element of the dex/resource path. Note: should be called DexElement, but apps reflect on
557     * this.
558     */
559    /*package*/ static class Element {
560        /**
561         * A file denoting a zip file (in case of a resource jar or a dex jar), or a directory
562         * (only when dexFile is null).
563         */
564        private final File path;
565
566        private final DexFile dexFile;
567
568        private ClassPathURLStreamHandler urlHandler;
569        private boolean initialized;
570
571        /**
572         * Element encapsulates a dex file. This may be a plain dex file (in which case dexZipPath
573         * should be null), or a jar (in which case dexZipPath should denote the zip file).
574         */
575        public Element(DexFile dexFile, File dexZipPath) {
576            this.dexFile = dexFile;
577            this.path = dexZipPath;
578        }
579
580        public Element(DexFile dexFile) {
581            this.dexFile = dexFile;
582            this.path = null;
583        }
584
585        public Element(File path) {
586          this.path = path;
587          this.dexFile = null;
588        }
589
590        /**
591         * Constructor for a bit of backwards compatibility. Some apps use reflection into
592         * internal APIs. Warn, and emulate old behavior if we can. See b/33399341.
593         *
594         * @deprecated The Element class has been split. Use new Element constructors for
595         *             classes and resources, and NativeLibraryElement for the library
596         *             search path.
597         */
598        @Deprecated
599        public Element(File dir, boolean isDirectory, File zip, DexFile dexFile) {
600            System.err.println("Warning: Using deprecated Element constructor. Do not use internal"
601                    + " APIs, this constructor will be removed in the future.");
602            if (dir != null && (zip != null || dexFile != null)) {
603                throw new IllegalArgumentException("Using dir and zip|dexFile no longer"
604                        + " supported.");
605            }
606            if (isDirectory && (zip != null || dexFile != null)) {
607                throw new IllegalArgumentException("Unsupported argument combination.");
608            }
609            if (dir != null) {
610                this.path = dir;
611                this.dexFile = null;
612            } else {
613                this.path = zip;
614                this.dexFile = dexFile;
615            }
616        }
617
618        /*
619         * Returns the dex path of this element or null if the element refers to a directory.
620         */
621        private String getDexPath() {
622            if (path != null) {
623                return path.isDirectory() ? null : path.getAbsolutePath();
624            } else if (dexFile != null) {
625                // DexFile.getName() returns the path of the dex file.
626                return dexFile.getName();
627            }
628            return null;
629        }
630
631        @Override
632        public String toString() {
633            if (dexFile == null) {
634              return (path.isDirectory() ? "directory \"" : "zip file \"") + path + "\"";
635            } else {
636              if (path == null) {
637                return "dex file \"" + dexFile + "\"";
638              } else {
639                return "zip file \"" + path + "\"";
640              }
641            }
642        }
643
644        public synchronized void maybeInit() {
645            if (initialized) {
646                return;
647            }
648
649            if (path == null || path.isDirectory()) {
650                initialized = true;
651                return;
652            }
653
654            try {
655                urlHandler = new ClassPathURLStreamHandler(path.getPath());
656            } catch (IOException ioe) {
657                /*
658                 * Note: ZipException (a subclass of IOException)
659                 * might get thrown by the ZipFile constructor
660                 * (e.g. if the file isn't actually a zip/jar
661                 * file).
662                 */
663                System.logE("Unable to open zip file: " + path, ioe);
664                urlHandler = null;
665            }
666
667            // Mark this element as initialized only after we've successfully created
668            // the associated ClassPathURLStreamHandler. That way, we won't leave this
669            // element in an inconsistent state if an exception is thrown during initialization.
670            //
671            // See b/35633614.
672            initialized = true;
673        }
674
675        public Class<?> findClass(String name, ClassLoader definingContext,
676                List<Throwable> suppressed) {
677            return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
678                    : null;
679        }
680
681        public URL findResource(String name) {
682            maybeInit();
683
684            if (urlHandler != null) {
685              return urlHandler.getEntryUrlOrNull(name);
686            }
687
688            // We support directories so we can run tests and/or legacy code
689            // that uses Class.getResource.
690            if (path != null && path.isDirectory()) {
691                File resourceFile = new File(path, name);
692                if (resourceFile.exists()) {
693                    try {
694                        return resourceFile.toURI().toURL();
695                    } catch (MalformedURLException ex) {
696                        throw new RuntimeException(ex);
697                    }
698                }
699            }
700
701            return null;
702        }
703    }
704
705    /**
706     * Element of the native library path
707     */
708    /*package*/ static class NativeLibraryElement {
709        /**
710         * A file denoting a directory or zip file.
711         */
712        private final File path;
713
714        /**
715         * If path denotes a zip file, this denotes a base path inside the zip.
716         */
717        private final String zipDir;
718
719        private ClassPathURLStreamHandler urlHandler;
720        private boolean initialized;
721
722        public NativeLibraryElement(File dir) {
723            this.path = dir;
724            this.zipDir = null;
725
726            // We should check whether path is a directory, but that is non-eliminatable overhead.
727        }
728
729        public NativeLibraryElement(File zip, String zipDir) {
730            this.path = zip;
731            this.zipDir = zipDir;
732
733            // Simple check that should be able to be eliminated by inlining. We should also
734            // check whether path is a file, but that is non-eliminatable overhead.
735            if (zipDir == null) {
736              throw new IllegalArgumentException();
737            }
738        }
739
740        @Override
741        public String toString() {
742            if (zipDir == null) {
743                return "directory \"" + path + "\"";
744            } else {
745                return "zip file \"" + path + "\"" +
746                  (!zipDir.isEmpty() ? ", dir \"" + zipDir + "\"" : "");
747            }
748        }
749
750        public synchronized void maybeInit() {
751            if (initialized) {
752                return;
753            }
754
755            if (zipDir == null) {
756                initialized = true;
757                return;
758            }
759
760            try {
761                urlHandler = new ClassPathURLStreamHandler(path.getPath());
762            } catch (IOException ioe) {
763                /*
764                 * Note: ZipException (a subclass of IOException)
765                 * might get thrown by the ZipFile constructor
766                 * (e.g. if the file isn't actually a zip/jar
767                 * file).
768                 */
769                System.logE("Unable to open zip file: " + path, ioe);
770                urlHandler = null;
771            }
772
773            // Mark this element as initialized only after we've successfully created
774            // the associated ClassPathURLStreamHandler. That way, we won't leave this
775            // element in an inconsistent state if an exception is thrown during initialization.
776            //
777            // See b/35633614.
778            initialized = true;
779        }
780
781        public String findNativeLibrary(String name) {
782            maybeInit();
783
784            if (zipDir == null) {
785                String entryPath = new File(path, name).getPath();
786                if (IoUtils.canOpenReadOnly(entryPath)) {
787                    return entryPath;
788                }
789            } else if (urlHandler != null) {
790                // Having a urlHandler means the element has a zip file.
791                // In this case Android supports loading the library iff
792                // it is stored in the zip uncompressed.
793                String entryName = zipDir + '/' + name;
794                if (urlHandler.isEntryStored(entryName)) {
795                  return path.getPath() + zipSeparator + entryName;
796                }
797            }
798
799            return null;
800        }
801    }
802}
803