提交 558a8673 编写于 作者: S Skylot

fix: bring back smali files support (#961)

上级 bfd60b73
......@@ -26,6 +26,7 @@ node_modules/
jadx-output/
*-tmp/
**/tmp/
*.jobf
*.class
*.dump
......
plugins {
id 'org.sonarqube' version '3.0'
id 'com.github.ben-manes.versions' version '0.28.0'
id 'com.github.ben-manes.versions' version '0.29.0'
id "com.diffplug.gradle.spotless" version "4.5.1"
}
......
......@@ -6,6 +6,7 @@ dependencies {
implementation(project(':jadx-core'))
runtimeOnly(project(':jadx-plugins:jadx-dex-input'))
runtimeOnly(project(':jadx-plugins:jadx-smali-input'))
runtimeOnly(project(':jadx-plugins:jadx-java-convert'))
implementation 'com.beust:jcommander:1.78'
......
......@@ -15,10 +15,7 @@ public class JadxCLI {
public static void main(String[] args) {
int result = 0;
try {
JadxCLIArgs jadxArgs = new JadxCLIArgs();
if (jadxArgs.processArgs(args)) {
result = processAndSave(jadxArgs.toJadxArgs());
}
result = execute(args);
} catch (JadxArgsValidateException e) {
LOG.error("Incorrect arguments: {}", e.getMessage());
result = 1;
......@@ -31,7 +28,15 @@ public class JadxCLI {
}
}
static int processAndSave(JadxArgs jadxArgs) {
public static int execute(String[] args) {
JadxCLIArgs jadxArgs = new JadxCLIArgs();
if (jadxArgs.processArgs(args)) {
return processAndSave(jadxArgs.toJadxArgs());
}
return 0;
}
private static int processAndSave(JadxArgs jadxArgs) {
jadxArgs.setCodeCache(new NoOpCodeCache());
try (JadxDecompiler jadx = new JadxDecompiler(jadxArgs)) {
jadx.load();
......
package jadx.cli;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.core.utils.files.FileUtils;
import static org.assertj.core.api.Assertions.assertThat;
public class TestInput {
private static final Logger LOG = LoggerFactory.getLogger(TestInput.class);
@Test
public void testDexInput() throws Exception {
decompile("dex", "samples/hello.dex");
}
@Test
public void testSmaliInput() throws Exception {
decompile("smali", "samples/HelloWorld.smali");
}
private void decompile(String tmpDirName, String inputSample) throws URISyntaxException, IOException {
StringBuilder args = new StringBuilder();
Path tempDir = FileUtils.createTempDir(tmpDirName);
args.append("-v ");
args.append("-d ").append(tempDir.toAbsolutePath()).append(' ');
URL resource = getClass().getClassLoader().getResource(inputSample);
assertThat(resource).isNotNull();
String sampleFile = resource.toURI().getRawPath();
args.append(sampleFile);
int result = JadxCLI.execute(args.toString().split(" "));
assertThat(result).isEqualTo(0);
List<Path> resultJavaFiles = collectJavaFilesInDir(tempDir);
assertThat(resultJavaFiles).isNotEmpty();
}
private static List<Path> collectJavaFilesInDir(Path dir) throws IOException {
PathMatcher matcher = dir.getFileSystem().getPathMatcher("glob:**.java");
try (Stream<Path> pathStream = Files.walk(dir)) {
return pathStream
.filter(p -> Files.isRegularFile(p, LinkOption.NOFOLLOW_LINKS))
.peek(f -> LOG.debug("File in result dir: {}", f))
.filter(matcher::matches)
.collect(Collectors.toList());
}
}
@AfterAll
public static void cleanup() {
FileUtils.clearTempRootDir();
}
}
.class LHelloWorld;
.super Ljava/lang/Object;
.source "HelloWorld.java"
.method constructor <init>()V
.registers 1
.line 1
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
.method public static main([Ljava/lang/String;)V
.registers 2
.line 3
sget-object p0, Ljava/lang/System;->out:Ljava/io/PrintStream;
const-string v0, "Hello, World"
invoke-virtual {p0, v0}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
.line 4
return-void
.end method
......@@ -19,6 +19,7 @@ dependencies {
testImplementation 'org.apache.commons:commons-lang3:3.10'
testRuntimeOnly(project(':jadx-plugins:jadx-dex-input'))
testRuntimeOnly(project(':jadx-plugins:jadx-smali-input'))
testRuntimeOnly(project(':jadx-plugins:jadx-java-convert'))
}
......
......@@ -27,14 +27,11 @@ public class JadxArgsValidator {
if (inputFiles.isEmpty()) {
throw new JadxArgsValidateException("Please specify input file");
}
if (inputFiles.size() > 1) {
for (File inputFile : inputFiles) {
String fileName = inputFile.getName();
if (fileName.startsWith("--")) {
throw new JadxArgsValidateException("Unknown argument: " + fileName);
}
for (File inputFile : inputFiles) {
String fileName = inputFile.getName();
if (fileName.startsWith("--")) {
throw new JadxArgsValidateException("Unknown argument: " + fileName);
}
throw new JadxArgsValidateException("Only one input file supported");
}
for (File file : inputFiles) {
checkFile(file);
......
......@@ -108,7 +108,10 @@ public final class JadxDecompiler implements Closeable {
loadedInputs.clear();
List<Path> inputPaths = Utils.collectionMap(args.getInputFiles(), File::toPath);
for (JadxInputPlugin inputPlugin : pluginManager.getInputPlugins()) {
loadedInputs.add(inputPlugin.loadFiles(inputPaths));
ILoadResult loadResult = inputPlugin.loadFiles(inputPaths);
if (loadResult != null && !loadResult.isEmpty()) {
loadedInputs.add(loadResult);
}
}
}
......
......@@ -21,6 +21,7 @@ import jadx.core.codegen.CodeWriter;
import jadx.core.utils.Utils;
import jadx.core.utils.android.Res9patchStreamDecoder;
import jadx.core.utils.exceptions.JadxException;
import jadx.core.utils.files.FileUtils;
import jadx.core.utils.files.ZipSecurity;
import jadx.core.xmlgen.ResContainer;
import jadx.core.xmlgen.ResTableParser;
......@@ -127,16 +128,19 @@ public final class ResourcesLoader {
if (file == null) {
return;
}
try (ZipFile zip = new ZipFile(file)) {
Enumeration<? extends ZipEntry> entries = zip.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
if (ZipSecurity.isValidZipEntry(entry)) {
addEntry(list, file, entry);
if (FileUtils.isZipFile(file)) {
try (ZipFile zip = new ZipFile(file)) {
Enumeration<? extends ZipEntry> entries = zip.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
if (ZipSecurity.isValidZipEntry(entry)) {
addEntry(list, file, entry);
}
}
} catch (Exception e) {
LOG.warn("Failed to open zip file: {}", file.getAbsolutePath());
}
} catch (Exception e) {
LOG.debug("Not a zip file: {}", file.getAbsolutePath());
} else {
addResourceFile(list, file);
}
}
......
......@@ -87,7 +87,7 @@ public class ClsSet {
if (LOG.isDebugEnabled()) {
long time = System.currentTimeMillis() - startTime;
int methodsCount = Stream.of(classes).mapToInt(clspClass -> clspClass.getMethodsMap().size()).sum();
LOG.debug("Load class set in {}ms, classes: {}, methods: {}", time, classes.length, methodsCount);
LOG.debug("Clst file loaded in {}ms, classes: {}, methods: {}", time, classes.length, methodsCount);
}
}
......
......@@ -90,6 +90,7 @@ public class RootNode {
// sort classes by name, expect top classes before inner
classes.sort(Comparator.comparing(ClassNode::getFullName));
initInnerClasses();
LOG.debug("Classes loaded: {}", classes.size());
}
private void addDummyClass(IClassData classData, Exception exc) {
......
......@@ -222,7 +222,7 @@ public class FileUtils {
return new String(hexChars);
}
private static boolean isZipFile(File file) {
public static boolean isZipFile(File file) {
try (InputStream is = new FileInputStream(file)) {
byte[] headers = new byte[4];
int read = is.read(headers, 0, 4);
......
......@@ -104,7 +104,7 @@ public abstract class IntegrationTest extends TestUtils {
DebugChecks.checksEnabled = true;
}
private JadxDecompiler jadxDecompiler;
protected JadxDecompiler jadxDecompiler;
@BeforeEach
public void init() {
......@@ -142,7 +142,7 @@ public abstract class IntegrationTest extends TestUtils {
public ClassNode getClassNode(Class<?> clazz) {
try {
File jar = getJarForClass(clazz);
return getClassNodeFromFile(jar, clazz.getName());
return getClassNodeFromFiles(Collections.singletonList(jar), clazz.getName());
} catch (Exception e) {
e.printStackTrace();
fail(e.getMessage());
......@@ -150,8 +150,8 @@ public abstract class IntegrationTest extends TestUtils {
return null;
}
public ClassNode getClassNodeFromFile(File file, String clsName) {
jadxDecompiler = loadFiles(Collections.singletonList(file));
public ClassNode getClassNodeFromFiles(List<File> files, String clsName) {
jadxDecompiler = loadFiles(files);
RootNode root = JadxInternalAccess.getRoot(jadxDecompiler);
ClassNode cls = root.resolveClass(clsName);
......@@ -173,9 +173,8 @@ public abstract class IntegrationTest extends TestUtils {
}
protected JadxDecompiler loadFiles(List<File> inputFiles) {
JadxDecompiler d;
args.setInputFiles(inputFiles);
d = new JadxDecompiler(args);
JadxDecompiler d = new JadxDecompiler(args);
try {
d.load();
} catch (Exception e) {
......
......@@ -7,10 +7,7 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.jetbrains.annotations.Nullable;
import org.jf.smali.Smali;
import org.jf.smali.SmaliOptions;
import jadx.api.JadxDecompiler;
import jadx.api.JadxInternalAccess;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.RootNode;
......@@ -26,9 +23,7 @@ public abstract class SmaliTest extends IntegrationTest {
protected ClassNode getClassNodeFromSmali(String file, String clsName) {
File smaliFile = getSmaliFile(file);
File outDex = createTempFile(".dex");
compileSmali(outDex, Collections.singletonList(smaliFile));
return getClassNodeFromFile(outDex, clsName);
return getClassNodeFromFiles(Collections.singletonList(smaliFile), clsName);
}
/**
......@@ -51,9 +46,7 @@ public abstract class SmaliTest extends IntegrationTest {
}
protected ClassNode getClassNodeFromSmaliFiles(String pkg, String testName, String clsName) {
File outDex = createTempFile(".dex");
compileSmali(outDex, collectSmaliFiles(pkg, testName));
return getClassNodeFromFile(outDex, pkg + '.' + clsName);
return getClassNodeFromFiles(collectSmaliFiles(pkg, testName), pkg + '.' + clsName);
}
protected ClassNode getClassNodeFromSmaliFiles(String clsName) {
......@@ -61,13 +54,10 @@ public abstract class SmaliTest extends IntegrationTest {
}
protected List<ClassNode> loadFromSmaliFiles() {
File outDex = createTempFile(".dex");
compileSmali(outDex, collectSmaliFiles(getTestPkg(), getTestName()));
JadxDecompiler d = loadFiles(Collections.singletonList(outDex));
RootNode root = JadxInternalAccess.getRoot(d);
jadxDecompiler = loadFiles(collectSmaliFiles(getTestPkg(), getTestName()));
RootNode root = JadxInternalAccess.getRoot(jadxDecompiler);
List<ClassNode> classes = root.getClasses(false);
decompileAndCheck(d, classes);
decompileAndCheck(jadxDecompiler, classes);
return classes;
}
......@@ -97,17 +87,4 @@ public abstract class SmaliTest extends IntegrationTest {
}
throw new AssertionError("Smali file not found: " + smaliFile.getPath());
}
private static boolean compileSmali(File output, List<File> inputFiles) {
try {
SmaliOptions options = new SmaliOptions();
options.outputDexFile = output.getAbsolutePath();
options.verboseErrors = true;
List<String> inputFileNames = inputFiles.stream().map(File::getAbsolutePath).collect(Collectors.toList());
Smali.assemble(options, inputFileNames);
} catch (Exception e) {
throw new AssertionError("Smali assemble error", e);
}
return true;
}
}
......@@ -26,7 +26,8 @@ public class DexFileLoader {
public static List<DexReader> collectDexFiles(List<Path> pathsList) {
return pathsList.stream()
.map((Path path) -> loadDexFromPath(path, 0))
.map(path -> loadDexFromPath(path, 0))
.filter(list -> !list.isEmpty())
.flatMap(Collection::stream)
.peek(dr -> LOG.debug("Loading dex: {}", dr))
.collect(Collectors.toList());
......
package jadx.plugins.input.dex;
import java.io.Closeable;
import java.nio.file.Path;
import java.util.List;
import jadx.api.plugins.JadxPluginInfo;
import jadx.api.plugins.input.JadxInputPlugin;
import jadx.api.plugins.input.data.ILoadResult;
import jadx.api.plugins.input.data.impl.EmptyLoadResult;
public class DexInputPlugin implements JadxInputPlugin {
......@@ -16,6 +18,14 @@ public class DexInputPlugin implements JadxInputPlugin {
@Override
public ILoadResult loadFiles(List<Path> input) {
return new DexLoadResult(DexFileLoader.collectDexFiles(input));
return loadDexFiles(input, null);
}
public static ILoadResult loadDexFiles(List<Path> inputFiles, Closeable closeable) {
List<DexReader> dexReaders = DexFileLoader.collectDexFiles(inputFiles);
if (dexReaders.isEmpty()) {
return EmptyLoadResult.INSTANCE;
}
return new DexLoadResult(dexReaders, closeable);
}
}
package jadx.plugins.input.dex;
import java.io.Closeable;
import java.io.IOException;
import java.util.List;
import java.util.function.Consumer;
import org.jetbrains.annotations.Nullable;
import jadx.api.plugins.input.data.IClassData;
import jadx.api.plugins.input.data.ILoadResult;
import jadx.api.plugins.input.data.IResourceData;
public class DexLoadResult implements ILoadResult {
private final List<DexReader> dexReaders;
@Nullable
private final Closeable closeable;
public DexLoadResult(List<DexReader> dexReaders) {
this(dexReaders, null);
}
public DexLoadResult(List<DexReader> dexReaders, Closeable closeable) {
this.dexReaders = dexReaders;
this.closeable = closeable;
}
@Override
......@@ -31,5 +41,13 @@ public class DexLoadResult implements ILoadResult {
for (DexReader dexReader : dexReaders) {
dexReader.close();
}
if (closeable != null) {
closeable.close();
}
}
@Override
public boolean isEmpty() {
return dexReaders.isEmpty();
}
}
package jadx.plugins.input.javaconvert;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
......@@ -12,7 +13,7 @@ import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ConvertResult {
public class ConvertResult implements Closeable {
private static final Logger LOG = LoggerFactory.getLogger(ConvertResult.class);
private final List<Path> converted = new ArrayList<>();
......@@ -34,7 +35,8 @@ public class ConvertResult {
return converted.isEmpty();
}
public void deleteTemp() {
@Override
public void close() {
for (Path tmpPath : tmpPaths) {
try {
delete(tmpPath);
......
package jadx.plugins.input.javaconvert;
import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
......@@ -8,9 +7,7 @@ import jadx.api.plugins.JadxPluginInfo;
import jadx.api.plugins.input.JadxInputPlugin;
import jadx.api.plugins.input.data.ILoadResult;
import jadx.api.plugins.input.data.impl.EmptyLoadResult;
import jadx.plugins.input.dex.DexFileLoader;
import jadx.plugins.input.dex.DexLoadResult;
import jadx.plugins.input.dex.DexReader;
import jadx.plugins.input.dex.DexInputPlugin;
public class JavaConvertPlugin implements JadxInputPlugin {
......@@ -23,16 +20,9 @@ public class JavaConvertPlugin implements JadxInputPlugin {
public ILoadResult loadFiles(List<Path> input) {
ConvertResult result = JavaConvertLoader.process(input);
if (result.isEmpty()) {
result.deleteTemp();
result.close();
return EmptyLoadResult.INSTANCE;
}
List<DexReader> dexReaders = DexFileLoader.collectDexFiles(result.getConverted());
return new DexLoadResult(dexReaders) {
@Override
public void close() throws IOException {
super.close();
result.deleteTemp();
}
};
return DexInputPlugin.loadDexFiles(result.getConverted(), result);
}
}
......@@ -7,4 +7,6 @@ public interface ILoadResult extends Closeable {
void visitClasses(Consumer<IClassData> consumer);
void visitResources(Consumer<IResourceData> consumer);
boolean isEmpty();
}
......@@ -11,6 +11,11 @@ public class EmptyLoadResult implements ILoadResult {
public static final EmptyLoadResult INSTANCE = new EmptyLoadResult();
@Override
public boolean isEmpty() {
return true;
}
@Override
public void visitClasses(Consumer<IClassData> consumer) {
}
......
plugins {
id 'java-library'
}
dependencies {
api(project(":jadx-plugins:jadx-plugins-api"))
implementation(project(":jadx-plugins:jadx-dex-input"))
implementation 'org.smali:smali:2.4.0'
implementation 'com.google.guava:guava:29.0-jre' // force latest version for smali
}
package jadx.plugins.input.smali;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.stream.Collectors;
import org.jetbrains.annotations.Nullable;
import org.jf.smali.Smali;
import org.jf.smali.SmaliOptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SmaliConvert implements Closeable {
private static final Logger LOG = LoggerFactory.getLogger(SmaliConvert.class);
@Nullable
private Path tmpDex;
public boolean execute(List<Path> input) {
List<Path> smaliFiles = filterSmaliFiles(input);
if (smaliFiles.isEmpty()) {
return false;
}
try {
this.tmpDex = Files.createTempFile("jadx-", ".dex");
boolean result = compileSmali(tmpDex, smaliFiles);
if (result) {
return true;
}
} catch (Exception e) {
LOG.error("Smali process error", e);
}
close();
return false;
}
private static boolean compileSmali(Path output, List<Path> inputFiles) throws IOException {
SmaliOptions options = new SmaliOptions();
options.outputDexFile = output.toAbsolutePath().toString();
options.verboseErrors = true;
List<String> inputFileNames = inputFiles.stream()
.map(p -> p.toAbsolutePath().toString())
.distinct()
.collect(Collectors.toList());
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
boolean result = collectSystemErrors(out, () -> Smali.assemble(options, inputFileNames));
if (!result) {
LOG.error("Smali compilation error:\n{}", out);
}
return result;
}
}
private static boolean collectSystemErrors(OutputStream out, Callable<Boolean> exec) {
PrintStream systemErr = System.err;
try (PrintStream err = new PrintStream(out)) {
System.setErr(err);
try {
return exec.call();
} catch (Exception e) {
e.printStackTrace(err);
return false;
}
} finally {
System.setErr(systemErr);
}
}
private List<Path> filterSmaliFiles(List<Path> input) {
PathMatcher matcher = FileSystems.getDefault().getPathMatcher("glob:**.smali");
return input.stream()
.filter(p -> Files.isRegularFile(p, LinkOption.NOFOLLOW_LINKS))
.filter(matcher::matches)
.collect(Collectors.toList());
}
public List<Path> getDexFiles() {
if (tmpDex == null) {
return Collections.emptyList();
}
return Collections.singletonList(tmpDex);
}
@Override
public void close() {
try {
if (tmpDex != null) {
Files.deleteIfExists(tmpDex);
}
} catch (Exception e) {
LOG.error("Failed to remove tmp dex file: {}", tmpDex, e);
}
}
}
package jadx.plugins.input.smali;
import java.nio.file.Path;
import java.util.List;
import jadx.api.plugins.JadxPluginInfo;
import jadx.api.plugins.input.JadxInputPlugin;
import jadx.api.plugins.input.data.ILoadResult;
import jadx.api.plugins.input.data.impl.EmptyLoadResult;
import jadx.plugins.input.dex.DexInputPlugin;
public class SmaliInputPlugin implements JadxInputPlugin {
@Override
public JadxPluginInfo getPluginInfo() {
return new JadxPluginInfo("smali-input", "SmaliInput", "Load .smali files");
}
@Override
public ILoadResult loadFiles(List<Path> input) {
SmaliConvert convert = new SmaliConvert();
if (!convert.execute(input)) {
return EmptyLoadResult.INSTANCE;
}
return DexInputPlugin.loadDexFiles(convert.getDexFiles(), convert);
}
}
......@@ -7,4 +7,5 @@ include 'jadx-samples'
include 'jadx-plugins'
include 'jadx-plugins:jadx-plugins-api'
include 'jadx-plugins:jadx-dex-input'
include 'jadx-plugins:jadx-smali-input'
include 'jadx-plugins:jadx-java-convert'
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册