• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2012, 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 
32 package org.jf.dexlib2;
33 
34 import com.google.common.base.Joiner;
35 import com.google.common.collect.ImmutableList;
36 import com.google.common.collect.Lists;
37 import com.google.common.io.ByteStreams;
38 import com.google.common.io.Files;
39 import org.jf.dexlib2.dexbacked.DexBackedDexFile;
40 import org.jf.dexlib2.dexbacked.DexBackedDexFile.NotADexFile;
41 import org.jf.dexlib2.dexbacked.DexBackedOdexFile;
42 import org.jf.dexlib2.dexbacked.OatFile;
43 import org.jf.dexlib2.dexbacked.OatFile.NotAnOatFileException;
44 import org.jf.dexlib2.dexbacked.OatFile.OatDexFile;
45 import org.jf.dexlib2.dexbacked.OatFile.VdexProvider;
46 import org.jf.dexlib2.dexbacked.ZipDexContainer;
47 import org.jf.dexlib2.dexbacked.ZipDexContainer.NotAZipFileException;
48 import org.jf.dexlib2.iface.DexFile;
49 import org.jf.dexlib2.iface.MultiDexContainer;
50 import org.jf.dexlib2.writer.pool.DexPool;
51 import org.jf.util.ExceptionWithContext;
52 
53 import javax.annotation.Nonnull;
54 import javax.annotation.Nullable;
55 import java.io.*;
56 import java.util.List;
57 
58 public final class DexFileFactory {
59 
60     @Nonnull
loadDexFile(@onnull String path, @Nullable Opcodes opcodes)61     public static DexBackedDexFile loadDexFile(@Nonnull String path, @Nullable Opcodes opcodes) throws IOException {
62         return loadDexFile(new File(path), opcodes);
63     }
64 
65     /**
66      * Loads a dex/apk/odex/oat file.
67      *
68      * For oat files with multiple dex files, the first will be opened. For zip/apk files, the "classes.dex" entry
69      * will be opened.
70      *
71      * @param file The file to open
72      * @param opcodes The set of opcodes to use
73      * @return A DexBackedDexFile for the given file
74      *
75      * @throws UnsupportedOatVersionException If file refers to an unsupported oat file
76      * @throws DexFileNotFoundException If file does not exist, if file is a zip file but does not have a "classes.dex"
77      * entry, or if file is an oat file that has no dex entries.
78      * @throws UnsupportedFileTypeException If file is not a valid dex/zip/odex/oat file, or if the "classes.dex" entry
79      * in a zip file is not a valid dex file
80      */
81     @Nonnull
loadDexFile(@onnull File file, @Nullable Opcodes opcodes)82     public static DexBackedDexFile loadDexFile(@Nonnull File file, @Nullable Opcodes opcodes) throws IOException {
83         if (!file.exists()) {
84             throw new DexFileNotFoundException("%s does not exist", file.getName());
85         }
86 
87         try {
88             ZipDexContainer container = new ZipDexContainer(file, opcodes);
89             return new DexEntryFinder(file.getPath(), container).findEntry("classes.dex", true);
90         } catch (NotAZipFileException ex) {
91             // eat it and continue
92         }
93 
94         InputStream inputStream = new BufferedInputStream(new FileInputStream(file));
95         try {
96             try {
97                 return DexBackedDexFile.fromInputStream(opcodes, inputStream);
98             } catch (DexBackedDexFile.NotADexFile ex) {
99                 // just eat it
100             }
101 
102             try {
103                 return DexBackedOdexFile.fromInputStream(opcodes, inputStream);
104             } catch (DexBackedOdexFile.NotAnOdexFile ex) {
105                 // just eat it
106             }
107 
108             // Note: DexBackedDexFile.fromInputStream and DexBackedOdexFile.fromInputStream will reset inputStream
109             // back to the same position, if they fails
110 
111             OatFile oatFile = null;
112             try {
113                 oatFile = OatFile.fromInputStream(inputStream, new FilenameVdexProvider(file));
114             } catch (NotAnOatFileException ex) {
115                 // just eat it
116             }
117 
118             if (oatFile != null) {
119                 if (oatFile.isSupportedVersion() == OatFile.UNSUPPORTED) {
120                     throw new UnsupportedOatVersionException(oatFile);
121                 }
122 
123                 List<OatDexFile> oatDexFiles = oatFile.getDexFiles();
124 
125                 if (oatDexFiles.size() == 0) {
126                     throw new DexFileNotFoundException("Oat file %s contains no dex files", file.getName());
127                 }
128 
129                 return oatDexFiles.get(0);
130             }
131         } finally {
132             inputStream.close();
133         }
134 
135         throw new UnsupportedFileTypeException("%s is not an apk, dex, odex or oat file.", file.getPath());
136     }
137 
138     /**
139      * Loads a dex entry from a container format (zip/oat)
140      *
141      * This has two modes of operation, depending on the exactMatch parameter. When exactMatch is true, it will only
142      * load an entry whose name exactly matches that provided by the dexEntry parameter.
143      *
144      * When exactMatch is false, then it will search for any entry that dexEntry is a path suffix of. "path suffix"
145      * meaning all the path components in dexEntry must fully match the corresponding path components in the entry name,
146      * but some path components at the beginning of entry name can be missing.
147      *
148      * For example, if an oat file contains a "/system/framework/framework.jar:classes2.dex" entry, then the following
149      * will match (not an exhaustive list):
150      *
151      * "/system/framework/framework.jar:classes2.dex"
152      * "system/framework/framework.jar:classes2.dex"
153      * "framework/framework.jar:classes2.dex"
154      * "framework.jar:classes2.dex"
155      * "classes2.dex"
156      *
157      * Note that partial path components specifically don't match. So something like "work/framework.jar:classes2.dex"
158      * would not match.
159      *
160      * If dexEntry contains an initial slash, it will be ignored for purposes of this suffix match -- but not when
161      * performing an exact match.
162      *
163      * If multiple entries match the given dexEntry, a MultipleMatchingDexEntriesException will be thrown
164      *
165      * @param file The container file. This must be either a zip (apk) file or an oat file.
166      * @param dexEntry The name of the entry to load. This can either be the exact entry name, if exactMatch is true,
167      *                 or it can be a path suffix.
168      * @param exactMatch If true, dexE
169      * @param opcodes The set of opcodes to use
170      * @return A DexBackedDexFile for the given entry
171      *
172      * @throws UnsupportedOatVersionException If file refers to an unsupported oat file
173      * @throws DexFileNotFoundException If the file does not exist, or if no matching entry could be found
174      * @throws UnsupportedFileTypeException If file is not a valid zip/oat file, or if the matching entry is not a
175      * valid dex file
176      * @throws MultipleMatchingDexEntriesException If multiple entries match the given dexEntry
177      */
loadDexEntry(@onnull File file, @Nonnull String dexEntry, boolean exactMatch, @Nullable Opcodes opcodes)178     public static DexBackedDexFile loadDexEntry(@Nonnull File file, @Nonnull String dexEntry,
179                                                 boolean exactMatch, @Nullable Opcodes opcodes) throws IOException {
180         if (!file.exists()) {
181             throw new DexFileNotFoundException("Container file %s does not exist", file.getName());
182         }
183 
184         try {
185             ZipDexContainer container = new ZipDexContainer(file, opcodes);
186             return new DexEntryFinder(file.getPath(), container).findEntry(dexEntry, exactMatch);
187         } catch (NotAZipFileException ex) {
188             // eat it and continue
189         }
190 
191         InputStream inputStream = new BufferedInputStream(new FileInputStream(file));
192         try {
193             OatFile oatFile = null;
194             try {
195                 oatFile = OatFile.fromInputStream(inputStream, new FilenameVdexProvider(file));
196             } catch (NotAnOatFileException ex) {
197                 // just eat it
198             }
199 
200             if (oatFile != null) {
201                 if (oatFile.isSupportedVersion() == OatFile.UNSUPPORTED) {
202                     throw new UnsupportedOatVersionException(oatFile);
203                 }
204 
205                 List<OatDexFile> oatDexFiles = oatFile.getDexFiles();
206 
207                 if (oatDexFiles.size() == 0) {
208                     throw new DexFileNotFoundException("Oat file %s contains no dex files", file.getName());
209                 }
210 
211                 return new DexEntryFinder(file.getPath(), oatFile).findEntry(dexEntry, exactMatch);
212             }
213         } finally {
214             inputStream.close();
215         }
216 
217         throw new UnsupportedFileTypeException("%s is not an apk or oat file.", file.getPath());
218     }
219 
220     /**
221      * Loads a file containing 1 or more dex files
222      *
223      * If the given file is a dex or odex file, it will return a MultiDexContainer containing that single entry.
224      * Otherwise, for an oat or zip file, it will return an OatFile or ZipDexContainer respectively.
225      *
226      * @param file The file to open
227      * @param opcodes The set of opcodes to use
228      * @return A MultiDexContainer
229      * @throws DexFileNotFoundException If the given file does not exist
230      * @throws UnsupportedFileTypeException If the given file is not a valid dex/zip/odex/oat file
231      */
loadDexContainer( @onnull File file, @Nullable final Opcodes opcodes)232     public static MultiDexContainer<? extends DexBackedDexFile> loadDexContainer(
233             @Nonnull File file, @Nullable final Opcodes opcodes) throws IOException {
234         if (!file.exists()) {
235             throw new DexFileNotFoundException("%s does not exist", file.getName());
236         }
237 
238         ZipDexContainer zipDexContainer = new ZipDexContainer(file, opcodes);
239         if (zipDexContainer.isZipFile()) {
240             return zipDexContainer;
241         }
242 
243         InputStream inputStream = new BufferedInputStream(new FileInputStream(file));
244         try {
245             try {
246                 DexBackedDexFile dexFile = DexBackedDexFile.fromInputStream(opcodes, inputStream);
247                 return new SingletonMultiDexContainer(file.getPath(), dexFile);
248             } catch (DexBackedDexFile.NotADexFile ex) {
249                 // just eat it
250             }
251 
252             try {
253                 DexBackedOdexFile odexFile = DexBackedOdexFile.fromInputStream(opcodes, inputStream);
254                 return new SingletonMultiDexContainer(file.getPath(), odexFile);
255             } catch (DexBackedOdexFile.NotAnOdexFile ex) {
256                 // just eat it
257             }
258 
259             // Note: DexBackedDexFile.fromInputStream and DexBackedOdexFile.fromInputStream will reset inputStream
260             // back to the same position, if they fails
261 
262             OatFile oatFile = null;
263             try {
264                 oatFile = OatFile.fromInputStream(inputStream, new FilenameVdexProvider(file));
265             } catch (NotAnOatFileException ex) {
266                 // just eat it
267             }
268 
269             if (oatFile != null) {
270                 // TODO: we should support loading earlier oat files, just not deodexing them
271                 if (oatFile.isSupportedVersion() == OatFile.UNSUPPORTED) {
272                     throw new UnsupportedOatVersionException(oatFile);
273                 }
274                 return oatFile;
275             }
276         } finally {
277             inputStream.close();
278         }
279 
280         throw new UnsupportedFileTypeException("%s is not an apk, dex, odex or oat file.", file.getPath());
281     }
282 
283     /**
284      * Writes a DexFile out to disk
285      *
286      * @param path The path to write the dex file to
287      * @param dexFile a DexFile to write
288      */
writeDexFile(@onnull String path, @Nonnull DexFile dexFile)289     public static void writeDexFile(@Nonnull String path, @Nonnull DexFile dexFile) throws IOException {
290         DexPool.writeTo(path, dexFile);
291     }
292 
DexFileFactory()293     private DexFileFactory() {}
294 
295     public static class DexFileNotFoundException extends ExceptionWithContext {
DexFileNotFoundException(@ullable String message, Object... formatArgs)296         public DexFileNotFoundException(@Nullable String message, Object... formatArgs) {
297             super(message, formatArgs);
298         }
299     }
300 
301     public static class UnsupportedOatVersionException extends ExceptionWithContext {
302         @Nonnull public final OatFile oatFile;
303 
UnsupportedOatVersionException(@onnull OatFile oatFile)304         public UnsupportedOatVersionException(@Nonnull OatFile oatFile) {
305             super("Unsupported oat version: %d", oatFile.getOatVersion());
306             this.oatFile = oatFile;
307         }
308     }
309 
310     public static class MultipleMatchingDexEntriesException extends ExceptionWithContext {
MultipleMatchingDexEntriesException(@onnull String message, Object... formatArgs)311         public MultipleMatchingDexEntriesException(@Nonnull String message, Object... formatArgs) {
312             super(String.format(message, formatArgs));
313         }
314     }
315 
316     public static class UnsupportedFileTypeException extends ExceptionWithContext {
UnsupportedFileTypeException(@onnull String message, Object... formatArgs)317         public UnsupportedFileTypeException(@Nonnull String message, Object... formatArgs) {
318             super(String.format(message, formatArgs));
319         }
320     }
321 
322     /**
323      * Matches two entries fully, ignoring any initial slash, if any
324      */
fullEntryMatch(@onnull String entry, @Nonnull String targetEntry)325     private static boolean fullEntryMatch(@Nonnull String entry, @Nonnull String targetEntry) {
326         if (entry.equals(targetEntry)) {
327             return true;
328         }
329 
330         if (entry.charAt(0) == '/') {
331             entry = entry.substring(1);
332         }
333 
334         if (targetEntry.charAt(0) == '/') {
335             targetEntry = targetEntry.substring(1);
336         }
337 
338         return entry.equals(targetEntry);
339     }
340 
341     /**
342      * Performs a partial match against entry and targetEntry.
343      *
344      * This is considered a partial match if targetEntry is a suffix of entry, and if the suffix starts
345      * on a path "part" (ignoring the initial separator, if any). Both '/' and ':' are considered separators for this.
346      *
347      * So entry="/blah/blah/something.dex" and targetEntry="lah/something.dex" shouldn't match, but
348      * both targetEntry="blah/something.dex" and "/blah/something.dex" should match.
349      */
partialEntryMatch(String entry, String targetEntry)350     private static boolean partialEntryMatch(String entry, String targetEntry) {
351         if (entry.equals(targetEntry)) {
352             return true;
353         }
354 
355         if (!entry.endsWith(targetEntry)) {
356             return false;
357         }
358 
359         // Make sure the first matching part is a full entry. We don't want to match "/blah/blah/something.dex" with
360         // "lah/something.dex", but both "/blah/something.dex" and "blah/something.dex" should match
361         char precedingChar = entry.charAt(entry.length() - targetEntry.length() - 1);
362         char firstTargetChar = targetEntry.charAt(0);
363         // This is a device path, so we should always use the linux separator '/', rather than the current platform's
364         // separator
365         return firstTargetChar == ':' || firstTargetChar == '/' || precedingChar == ':' || precedingChar == '/';
366     }
367 
368     protected static class DexEntryFinder {
369         private final String filename;
370         private final MultiDexContainer<? extends DexBackedDexFile> dexContainer;
371 
DexEntryFinder(@onnull String filename, @Nonnull MultiDexContainer<? extends DexBackedDexFile> dexContainer)372         public DexEntryFinder(@Nonnull String filename,
373                               @Nonnull MultiDexContainer<? extends DexBackedDexFile> dexContainer) {
374             this.filename = filename;
375             this.dexContainer = dexContainer;
376         }
377 
378         @Nonnull
findEntry(@onnull String targetEntry, boolean exactMatch)379         public DexBackedDexFile findEntry(@Nonnull String targetEntry, boolean exactMatch) throws IOException {
380             if (exactMatch) {
381                 try {
382                     DexBackedDexFile dexFile = dexContainer.getEntry(targetEntry);
383                     if (dexFile == null) {
384                         throw new DexFileNotFoundException("Could not find entry %s in %s.", targetEntry, filename);
385                     }
386                     return dexFile;
387                 } catch (NotADexFile ex) {
388                     throw new UnsupportedFileTypeException("Entry %s in %s is not a dex file", targetEntry, filename);
389                 }
390             }
391 
392             // find all full and partial matches
393             List<String> fullMatches = Lists.newArrayList();
394             List<DexBackedDexFile> fullEntries = Lists.newArrayList();
395             List<String> partialMatches = Lists.newArrayList();
396             List<DexBackedDexFile> partialEntries = Lists.newArrayList();
397             for (String entry: dexContainer.getDexEntryNames()) {
398                 if (fullEntryMatch(entry, targetEntry)) {
399                     // We want to grab all full matches, regardless of whether they're actually a dex file.
400                     fullMatches.add(entry);
401                     fullEntries.add(dexContainer.getEntry(entry));
402                 } else if (partialEntryMatch(entry, targetEntry)) {
403                     partialMatches.add(entry);
404                     partialEntries.add(dexContainer.getEntry(entry));
405                 }
406             }
407 
408             // full matches always take priority
409             if (fullEntries.size() == 1) {
410                 try {
411                     DexBackedDexFile dexFile = fullEntries.get(0);
412                     assert dexFile != null;
413                     return dexFile;
414                 } catch (NotADexFile ex) {
415                     throw new UnsupportedFileTypeException("Entry %s in %s is not a dex file",
416                             fullMatches.get(0), filename);
417                 }
418             }
419             if (fullEntries.size() > 1) {
420                 // This should be quite rare. This would only happen if an oat file has two entries that differ
421                 // only by an initial path separator. e.g. "/blah/blah.dex" and "blah/blah.dex"
422                 throw new MultipleMatchingDexEntriesException(String.format(
423                         "Multiple entries in %s match %s: %s", filename, targetEntry,
424                         Joiner.on(", ").join(fullMatches)));
425             }
426 
427             if (partialEntries.size() == 0) {
428                 throw new DexFileNotFoundException("Could not find a dex entry in %s matching %s",
429                         filename, targetEntry);
430             }
431             if (partialEntries.size() > 1) {
432                 throw new MultipleMatchingDexEntriesException(String.format(
433                         "Multiple dex entries in %s match %s: %s", filename, targetEntry,
434                         Joiner.on(", ").join(partialMatches)));
435             }
436             return partialEntries.get(0);
437         }
438     }
439 
440     private static class SingletonMultiDexContainer implements MultiDexContainer<DexBackedDexFile> {
441         private final String entryName;
442         private final DexBackedDexFile dexFile;
443 
SingletonMultiDexContainer(@onnull String entryName, @Nonnull DexBackedDexFile dexFile)444         public SingletonMultiDexContainer(@Nonnull String entryName, @Nonnull DexBackedDexFile dexFile) {
445             this.entryName = entryName;
446             this.dexFile = dexFile;
447         }
448 
getDexEntryNames()449         @Nonnull @Override public List<String> getDexEntryNames() throws IOException {
450             return ImmutableList.of(entryName);
451         }
452 
getEntry(@onnull String entryName)453         @Nullable @Override public DexBackedDexFile getEntry(@Nonnull String entryName) throws IOException {
454             if (entryName.equals(this.entryName)) {
455                 return dexFile;
456             }
457             return null;
458         }
459     }
460 
461     public static class FilenameVdexProvider implements VdexProvider {
462         private final File vdexFile;
463 
464         @Nullable
465         private byte[] buf = null;
466         private boolean loadedVdex = false;
467 
FilenameVdexProvider(File oatFile)468         public FilenameVdexProvider(File oatFile) {
469             File oatParent = oatFile.getAbsoluteFile().getParentFile();
470             String baseName = Files.getNameWithoutExtension(oatFile.getAbsolutePath());
471             vdexFile = new File(oatParent, baseName + ".vdex");
472         }
473 
getVdex()474         @Nullable @Override public byte[] getVdex() {
475             if (!loadedVdex) {
476                 if (vdexFile.exists()) {
477                     try {
478                         buf = ByteStreams.toByteArray(new FileInputStream(vdexFile));
479                     } catch (FileNotFoundException e) {
480                         buf = null;
481                     } catch (IOException ex) {
482                         throw new RuntimeException(ex);
483                     }
484                 }
485                 loadedVdex = true;
486             }
487 
488             return buf;
489         }
490     }
491 }
492