ClassPath.java revision 6fc32629c25d351119395922a6eb6701f09dffa4
1/*
2 * Copyright 2013, Google Inc.
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are
7 * met:
8 *
9 *     * Redistributions of source code must retain the above copyright
10 * notice, this list of conditions and the following disclaimer.
11 *     * Redistributions in binary form must reproduce the above
12 * copyright notice, this list of conditions and the following disclaimer
13 * in the documentation and/or other materials provided with the
14 * distribution.
15 *     * Neither the name of Google Inc. nor the names of its
16 * contributors may be used to endorse or promote products derived from
17 * this software without specific prior written permission.
18 *
19 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 */
31
32package org.jf.dexlib2.analysis;
33
34import com.google.common.collect.ImmutableSet;
35import com.google.common.collect.Iterables;
36import com.google.common.collect.Lists;
37import com.google.common.collect.Maps;
38import org.jf.dexlib2.DexFileFactory;
39import org.jf.dexlib2.analysis.reflection.ReflectionClassDef;
40import org.jf.dexlib2.iface.ClassDef;
41import org.jf.dexlib2.iface.DexFile;
42import org.jf.dexlib2.immutable.ImmutableDexFile;
43import org.jf.util.ExceptionWithContext;
44
45import javax.annotation.Nonnull;
46import java.io.File;
47import java.io.IOException;
48import java.io.Serializable;
49import java.util.ArrayList;
50import java.util.HashMap;
51import java.util.regex.Matcher;
52import java.util.regex.Pattern;
53
54public class ClassPath {
55    @Nonnull private final TypeProto unknownClass;
56    @Nonnull private DexFile[] dexFiles;
57    @Nonnull private HashMap<String, TypeProto> loadedClasses = Maps.newHashMap();
58    @Nonnull private HashMap<String, ClassDef> availableClasses = Maps.newHashMap();
59
60    /**
61     * Creates a new ClassPath instance that can load classes from the given dex files
62     *
63     * @param classPath An array of DexFile objects. When loading a class, these dex files will be searched in order
64     */
65    public ClassPath(DexFile... classPath) throws IOException {
66        this(classPath, true);
67    }
68
69    /**
70     * Creates a new ClassPath instance that can load classes from the given dex files
71     *
72     * @param classPath An iterable of DexFile objects. When loading a class, these dex files will be searched in order
73     */
74    public ClassPath(Iterable<DexFile> classPath) {
75        this(Iterables.toArray(classPath, DexFile.class), false);
76    }
77
78    private ClassPath(@Nonnull DexFile[] classPath, boolean copyArray) {
79        if (copyArray) {
80            dexFiles = new DexFile[classPath.length+1];
81            System.arraycopy(classPath, 0, dexFiles, 0, classPath.length);
82            // add fallbacks for certain special classes that must be present
83            dexFiles[dexFiles.length - 1] = getBasicClasses();
84        } else {
85            dexFiles = classPath;
86        }
87
88        unknownClass = new UnknownClassProto(this);
89        loadedClasses.put(unknownClass.getType(), unknownClass);
90
91        loadPrimitiveType("Z");
92        loadPrimitiveType("B");
93        loadPrimitiveType("S");
94        loadPrimitiveType("C");
95        loadPrimitiveType("I");
96        loadPrimitiveType("J");
97        loadPrimitiveType("F");
98        loadPrimitiveType("D");
99        loadPrimitiveType("L");
100
101        for (DexFile dexFile: dexFiles) {
102            for (ClassDef classDef: dexFile.getClasses()) {
103                ClassDef prev = availableClasses.get(classDef.getType());
104                if (prev == null) {
105                    availableClasses.put(classDef.getType(), classDef);
106                }
107            }
108        }
109    }
110
111    private void loadPrimitiveType(String type) {
112        loadedClasses.put(type, new PrimitiveProto(this, type));
113    }
114
115    private static DexFile getBasicClasses() {
116        // fallbacks for some special classes that we assume are present
117        return new ImmutableDexFile(ImmutableSet.of(
118                new ReflectionClassDef(Class.class),
119                new ReflectionClassDef(Cloneable.class),
120                new ReflectionClassDef(Object.class),
121                new ReflectionClassDef(Serializable.class),
122                new ReflectionClassDef(String.class),
123                new ReflectionClassDef(Throwable.class)));
124    }
125
126    @Nonnull
127    public TypeProto getClass(CharSequence type) {
128        String typeString = type.toString();
129        TypeProto typeProto = loadedClasses.get(typeString);
130        if (typeProto != null) {
131            return typeProto;
132        }
133
134        if (type.charAt(0) == '[') {
135            typeProto = new ArrayProto(this, typeString);
136        } else {
137            typeProto = new ClassProto(this, typeString);
138        }
139        // All primitive types are preloaded into loadedClasses, so we don't need to check for that here
140
141        loadedClasses.put(typeString, typeProto);
142        return typeProto;
143    }
144
145    @Nonnull
146    public ClassDef getClassDef(String type) {
147        ClassDef ret = availableClasses.get(type);
148        if (ret == null) {
149            throw new UnresolvedClassException("Could not resolve class %s", type);
150        }
151        return ret;
152    }
153
154    @Nonnull
155    public TypeProto getUnknownClass() {
156        return unknownClass;
157    }
158
159    @Nonnull
160    public static ClassPath fromClassPath(Iterable<String> classPathDirs, Iterable<String> classPath, DexFile dexFile,
161                                          int api) {
162        ArrayList<DexFile> dexFiles = Lists.newArrayList();
163
164        for (String classPathEntry: classPath) {
165            dexFiles.add(loadClassPathEntry(classPathDirs, classPathEntry, api));
166        }
167        dexFiles.add(dexFile);
168        return new ClassPath(dexFiles);
169    }
170
171    private static final Pattern dalvikCacheOdexPattern = Pattern.compile("@([^@]+)@classes.dex$");
172
173    @Nonnull
174    private static DexFile loadClassPathEntry(@Nonnull Iterable<String> classPathDirs,
175                                              @Nonnull String bootClassPathEntry, int api) {
176        File rawEntry = new File(bootClassPathEntry);
177        // strip off the path - we only care about the filename
178        String entryName = rawEntry.getName();
179
180        // if it's a dalvik-cache entry, grab the name of the jar/apk
181        if (entryName.endsWith("@classes.dex")) {
182            Matcher m = dalvikCacheOdexPattern.matcher(entryName);
183
184            if (!m.find()) {
185                throw new ExceptionWithContext(String.format("Cannot parse dependency value %s", bootClassPathEntry));
186            }
187
188            entryName = m.group(1);
189        }
190
191        int extIndex = entryName.lastIndexOf(".");
192
193        String baseEntryName;
194        if (extIndex == -1) {
195            baseEntryName = entryName;
196        } else {
197            baseEntryName = entryName.substring(0, extIndex);
198        }
199
200        for (String classPathDir: classPathDirs) {
201            for (String ext: new String[]{"", ".odex", ".jar", ".apk", ".zip"}) {
202                File file = new File(classPathDir, baseEntryName + ext);
203
204                if (file.exists() && file.isFile()) {
205                    if (!file.canRead()) {
206                        System.err.println(String.format(
207                                "warning: cannot open %s for reading. Will continue looking.", file.getPath()));
208                    } else {
209                        try {
210                            return DexFileFactory.loadDexFile(file, api);
211                        } catch (DexFileFactory.NoClassesDexException ex) {
212                            // ignore and continue
213                        } catch (Exception ex) {
214                            throw ExceptionWithContext.withContext(ex,
215                                    "Error while reading boot class path entry \"%s\"", bootClassPathEntry);
216                        }
217                    }
218                }
219            }
220        }
221        throw new ExceptionWithContext("Cannot locate boot class path file %s", bootClassPathEntry);
222    }
223}
224