Skip to content

Commit 6617723

Browse files
authored
feat: support multi-dex containers (#4091)
1 parent 6b26f60 commit 6617723

File tree

3 files changed

+109
-82
lines changed

3 files changed

+109
-82
lines changed

brut.apktool/apktool-lib/src/main/java/brut/androlib/ApkDecoder.java

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -129,13 +129,9 @@ private void decodeSources(File outDir) throws AndrolibException {
129129

130130
if (noSrc) {
131131
copySourcesRaw(outDir, fileName);
132-
continue;
132+
} else {
133+
decodeSourcesSmali(outDir, fileName);
133134
}
134-
135-
String dirName = "smali" + (!fileName.equals("classes.dex")
136-
? "_" + fileName.substring(0, fileName.lastIndexOf('.')).replace(in.separatorChar, '@') : "");
137-
138-
decodeSourcesSmali(new File(outDir, dirName), fileName);
139135
}
140136
} catch (DirectoryException ex) {
141137
throw new AndrolibException(ex);

brut.apktool/apktool-lib/src/main/java/brut/androlib/smali/SmaliBuilder.java

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,29 @@ public void build(File smaliDir, File dexFile) throws AndrolibException {
5757
DexBuilder dexBuilder = new DexBuilder(mApiLevel > 0 ? Opcodes.forApi(mApiLevel) : Opcodes.getDefault());
5858

5959
for (String fileName : new FileDirectory(smaliDir).getFiles(true)) {
60+
File smaliFile = new File(smaliDir, fileName);
61+
6062
if (!fileName.endsWith(".smali")) {
61-
Log.w(TAG, "Unknown file type, ignoring: " + fileName);
63+
Log.w(TAG, "Unknown file type, ignoring: " + smaliFile);
6264
continue;
6365
}
6466

65-
buildFile(smaliDir, fileName, dexBuilder);
67+
boolean success;
68+
Exception cause;
69+
try {
70+
success = buildFile(smaliFile, dexBuilder);
71+
cause = null;
72+
} catch (Exception ex) {
73+
success = false;
74+
cause = ex;
75+
}
76+
if (!success) {
77+
AndrolibException ex = new AndrolibException("Could not smali file: " + smaliFile);
78+
if (cause != null) {
79+
ex.initCause(cause);
80+
}
81+
throw ex;
82+
}
6683
}
6784

6885
if (dexFile.exists()) {
@@ -80,26 +97,6 @@ public void build(File smaliDir, File dexFile) throws AndrolibException {
8097
}
8198
}
8299

83-
private void buildFile(File smaliDir, String dexName, DexBuilder dexBuilder) throws AndrolibException {
84-
boolean success;
85-
Exception cause;
86-
try {
87-
File smaliFile = new File(smaliDir, dexName);
88-
success = buildFile(smaliFile, dexBuilder);
89-
cause = null;
90-
} catch (Exception ex) {
91-
success = false;
92-
cause = ex;
93-
}
94-
if (!success) {
95-
AndrolibException ex = new AndrolibException("Could not smali file: " + dexName);
96-
if (cause != null) {
97-
ex.initCause(cause);
98-
}
99-
throw ex;
100-
}
101-
}
102-
103100
private boolean buildFile(File smaliFile, DexBuilder dexBuilder) throws IOException, RecognitionException {
104101
try (InputStreamReader reader = new InputStreamReader(
105102
Files.newInputStream(smaliFile.toPath()), StandardCharsets.UTF_8)) {

brut.apktool/apktool-lib/src/main/java/brut/androlib/smali/SmaliDecoder.java

Lines changed: 88 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -20,97 +20,131 @@
2020
import brut.util.OS;
2121
import com.android.tools.smali.baksmali.Baksmali;
2222
import com.android.tools.smali.baksmali.BaksmaliOptions;
23-
import com.android.tools.smali.dexlib2.DexFileFactory;
2423
import com.android.tools.smali.dexlib2.analysis.InlineMethodResolver;
2524
import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile;
2625
import com.android.tools.smali.dexlib2.dexbacked.DexBackedOdexFile;
27-
import com.android.tools.smali.dexlib2.iface.DexFile;
28-
import com.android.tools.smali.dexlib2.iface.MultiDexContainer;
26+
import com.android.tools.smali.dexlib2.dexbacked.ZipDexContainer;
2927

3028
import java.io.File;
3129
import java.io.IOException;
32-
import java.util.HashSet;
30+
import java.util.Map;
3331
import java.util.Set;
32+
import java.util.TreeMap;
33+
import java.util.concurrent.ConcurrentHashMap;
34+
import java.util.concurrent.atomic.AtomicInteger;
3435

3536
public class SmaliDecoder {
36-
private final File mApkFile;
37+
private final ZipDexContainer mDexContainer;
3738
private final boolean mDebugMode;
3839
private final Set<String> mDexFiles;
39-
private int mInferredApiLevel;
40+
private final AtomicInteger mInferredApiLevel;
4041

41-
public SmaliDecoder(File apkFile, boolean debugMode) {
42-
mApkFile = apkFile;
42+
public SmaliDecoder(File apkFile, boolean debugMode) throws AndrolibException {
43+
mDexContainer = new ZipDexContainer(apkFile, null);
44+
// ZipDexContainer is lazily initialized and not thread-safe. Eagerly initialize on the constructing thread.
45+
try {
46+
mDexContainer.getEntry("");
47+
} catch (IOException ex) {
48+
throw new AndrolibException("Could not open apk file: " + apkFile, ex);
49+
}
4350
mDebugMode = debugMode;
44-
mDexFiles = new HashSet<>();
51+
mDexFiles = ConcurrentHashMap.newKeySet();
52+
mInferredApiLevel = new AtomicInteger();
4553
}
4654

4755
public Set<String> getDexFiles() {
4856
return mDexFiles;
4957
}
5058

5159
public int getInferredApiLevel() {
52-
return mInferredApiLevel;
60+
return mInferredApiLevel.get();
5361
}
5462

55-
public void decode(String dexName, File smaliDir) throws AndrolibException {
63+
public void decode(String dexName, File outDir) throws AndrolibException {
5664
try {
57-
BaksmaliOptions options = new BaksmaliOptions();
58-
options.deodex = false;
59-
options.implicitReferences = false;
60-
options.parameterRegisters = true;
61-
options.localsDirective = true;
62-
options.sequentialLabels = true;
63-
options.debugInfo = mDebugMode;
64-
options.codeOffsets = false;
65-
options.accessorComments = false;
66-
options.registerInfo = 0;
67-
options.inlineResolver = null;
68-
69-
// Set jobs automatically.
70-
int jobs = Runtime.getRuntime().availableProcessors();
71-
if (jobs > 6) {
72-
jobs = 6;
65+
// Fetch the requested dex file from the dex container.
66+
ZipDexContainer.DexEntry<DexBackedDexFile> dexEntry = mDexContainer.getEntry(dexName);
67+
if (dexEntry == null) {
68+
throw new AndrolibException("Could not find file: " + dexName);
7369
}
7470

75-
// Create the container.
76-
MultiDexContainer<? extends DexBackedDexFile> container = DexFileFactory.loadDexContainer(mApkFile, null);
77-
78-
// If we have 1 item, ignore the passed file. Pull the DexFile we need.
79-
MultiDexContainer.DexEntry<? extends DexBackedDexFile> dexEntry =
80-
container.getDexEntryNames().size() == 1
81-
? container.getEntry(container.getDexEntryNames().get(0))
82-
: container.getEntry(dexName);
71+
// Add the requested dex file.
72+
Map<Integer, DexBackedDexFile> dexFiles = new TreeMap<>();
73+
dexFiles.put(1, dexEntry.getDexFile());
8374

84-
// Double-check the passed param exists.
85-
if (dexEntry == null) {
86-
dexEntry = container.getEntry(container.getDexEntryNames().get(0));
87-
assert dexEntry != null;
88-
}
75+
// Add additional dex files if it's a multi-dex container.
76+
for (String dexEntryName : mDexContainer.getDexEntryNames()) {
77+
if (dexEntryName.equals(dexName)) {
78+
continue;
79+
}
8980

90-
DexBackedDexFile dexFile = dexEntry.getDexFile();
81+
String prefix = dexName + "/";
82+
if (!dexEntryName.startsWith(prefix)) {
83+
continue;
84+
}
9185

92-
if (dexFile.supportsOptimizedOpcodes()) {
93-
throw new AndrolibException("Could not disassemble an odex file without deodexing it.");
86+
int dexNum;
87+
try {
88+
dexNum = Integer.parseInt(dexEntryName.substring(prefix.length()));
89+
} catch (NumberFormatException ignored) {
90+
continue;
91+
}
92+
if (dexNum > 1) {
93+
dexFiles.put(dexNum, mDexContainer.getEntry(dexEntryName).getDexFile());
94+
}
9495
}
9596

96-
if (dexFile instanceof DexBackedOdexFile) {
97-
options.inlineResolver = InlineMethodResolver.createInlineMethodResolver(
98-
((DexBackedOdexFile) dexFile).getOdexVersion());
99-
}
97+
// Decode the dex files into separate folders.
98+
for (Map.Entry<Integer, DexBackedDexFile> entry : dexFiles.entrySet()) {
99+
int dexNum = entry.getKey();
100+
DexBackedDexFile dexFile = entry.getValue();
100101

101-
OS.mkdir(smaliDir);
102-
Baksmali.disassembleDexFile(dexFile, smaliDir, jobs, options);
102+
if (dexFile.supportsOptimizedOpcodes()) {
103+
throw new AndrolibException("Cannot disassemble an odex file without deodexing it: " + dexName);
104+
}
103105

104-
synchronized (mDexFiles) {
105-
int apiLevel = dexFile.getOpcodes().api;
106-
if (mInferredApiLevel == 0 || mInferredApiLevel > apiLevel) {
107-
mInferredApiLevel = apiLevel;
106+
String dirName = "smali";
107+
if (dexNum > 1 || !dexName.equals("classes.dex")) {
108+
dirName += "_" + dexName.substring(0, dexName.lastIndexOf('.')).replace('/', '@');
109+
if (dexNum > 1) {
110+
dirName += dexNum;
111+
}
108112
}
109113

110-
mDexFiles.add(dexName);
114+
decodeFile(dexFile, new File(outDir, dirName));
111115
}
116+
117+
mDexFiles.add(dexName);
112118
} catch (IOException ex) {
113119
throw new AndrolibException("Could not baksmali file: " + dexName, ex);
114120
}
115121
}
122+
123+
private void decodeFile(DexBackedDexFile dexFile, File smaliDir) {
124+
int jobs = Math.min(Runtime.getRuntime().availableProcessors(), 6);
125+
126+
BaksmaliOptions options = new BaksmaliOptions();
127+
options.parameterRegisters = true;
128+
options.localsDirective = true;
129+
options.sequentialLabels = true;
130+
options.debugInfo = mDebugMode;
131+
options.codeOffsets = false;
132+
options.accessorComments = false;
133+
options.allowOdex = false;
134+
options.deodex = false;
135+
options.implicitReferences = false;
136+
options.normalizeVirtualMethods = false;
137+
options.registerInfo = 0;
138+
139+
if (dexFile instanceof DexBackedOdexFile) {
140+
options.inlineResolver = InlineMethodResolver.createInlineMethodResolver(
141+
((DexBackedOdexFile) dexFile).getOdexVersion());
142+
}
143+
144+
OS.mkdir(smaliDir);
145+
Baksmali.disassembleDexFile(dexFile, smaliDir, jobs, options);
146+
147+
int apiLevel = dexFile.getOpcodes().api;
148+
mInferredApiLevel.updateAndGet(cur -> (cur == 0 || cur > apiLevel) ? apiLevel : cur);
149+
}
116150
}

0 commit comments

Comments
 (0)