diff --git a/jadx-cli/src/main/java/jadx/cli/JadxCLI.java b/jadx-cli/src/main/java/jadx/cli/JadxCLI.java index 4a342d564247a8da925192a4c09e6e49571f8978..4b121a96aa40ab0ed067866d8228c09648e677c8 100644 --- a/jadx-cli/src/main/java/jadx/cli/JadxCLI.java +++ b/jadx-cli/src/main/java/jadx/cli/JadxCLI.java @@ -32,23 +32,19 @@ public class JadxCLI { public static int execute(String[] args) { JadxCLIArgs jadxArgs = new JadxCLIArgs(); if (jadxArgs.processArgs(args)) { - return processAndSave(jadxArgs.toJadxArgs()); + return processAndSave(jadxArgs); } return 0; } - private static int processAndSave(JadxArgs jadxArgs) { + private static int processAndSave(JadxCLIArgs cliArgs) { + JadxArgs jadxArgs = cliArgs.toJadxArgs(); jadxArgs.setCodeCache(new NoOpCodeCache()); jadxArgs.setCodeWriterProvider(SimpleCodeWriter::new); try (JadxDecompiler jadx = new JadxDecompiler(jadxArgs)) { jadx.load(); - if (LogHelper.getLogLevel() == LogHelper.LogLevelEnum.QUIET) { - jadx.save(); - } else { - jadx.save(500, (done, total) -> { - int progress = (int) (done * 100.0 / total); - System.out.printf("INFO - progress: %d of %d (%d%%)\r", done, total, progress); - }); + if (!SingleClassMode.process(jadx, cliArgs)) { + save(jadx); } int errorsCount = jadx.getErrorsCount(); if (errorsCount != 0) { @@ -60,4 +56,15 @@ public class JadxCLI { } return 0; } + + private static void save(JadxDecompiler jadx) { + if (LogHelper.getLogLevel() == LogHelper.LogLevelEnum.QUIET) { + jadx.save(); + } else { + jadx.save(500, (done, total) -> { + int progress = (int) (done * 100.0 / total); + System.out.printf("INFO - progress: %d of %d (%d%%)\r", done, total, progress); + }); + } + } } diff --git a/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java b/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java index 258ef086e3e69a23f726bf847e2ea157cce00136..9e062ff412a91d7fae1f633fee27ffe14dec276d 100644 --- a/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java +++ b/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java @@ -40,9 +40,12 @@ public class JadxCLIArgs { @Parameter(names = { "-s", "--no-src" }, description = "do not decompile source code") protected boolean skipSources = false; - @Parameter(names = { "--single-class" }, description = "decompile a single class") + @Parameter(names = { "--single-class" }, description = "decompile a single class, can raw name or an alias") protected String singleClass = null; + @Parameter(names = { "--single-class-output" }, description = "file or dir for write if decompile a single class") + protected String singleClassOutput = null; + @Parameter(names = { "--output-format" }, description = "can be 'java' or 'json'") protected String outputFormat = "java"; @@ -227,9 +230,6 @@ public class JadxCLIArgs { args.setOutputFormat(JadxArgs.OutputFormatEnum.valueOf(outputFormat.toUpperCase())); args.setThreadsCount(threadsCount); args.setSkipSources(skipSources); - if (singleClass != null) { - args.setClassFilter(className -> singleClass.equals(className)); - } args.setSkipResources(skipResources); args.setFallbackMode(fallbackMode); args.setShowInconsistentCode(showInconsistentCode); @@ -279,6 +279,14 @@ public class JadxCLIArgs { return outDirRes; } + public String getSingleClass() { + return singleClass; + } + + public String getSingleClassOutput() { + return singleClassOutput; + } + public boolean isSkipResources() { return skipResources; } diff --git a/jadx-cli/src/main/java/jadx/cli/LogHelper.java b/jadx-cli/src/main/java/jadx/cli/LogHelper.java index 2dbc273d5b41d5f2c3c2809f7407b92daffbd44a..d4976620a117bc7c52f849da36b1f3e685b7fcb6 100644 --- a/jadx-cli/src/main/java/jadx/cli/LogHelper.java +++ b/jadx-cli/src/main/java/jadx/cli/LogHelper.java @@ -58,6 +58,7 @@ public class LogHelper { // show progress for all levels except quiet setLevelForClass(JadxCLI.class, Level.INFO); setLevelForClass(JadxDecompiler.class, Level.INFO); + setLevelForClass(SingleClassMode.class, Level.INFO); } } diff --git a/jadx-cli/src/main/java/jadx/cli/SingleClassMode.java b/jadx-cli/src/main/java/jadx/cli/SingleClassMode.java new file mode 100644 index 0000000000000000000000000000000000000000..97e529da8cff237f464126d8328090a60b849a89 --- /dev/null +++ b/jadx-cli/src/main/java/jadx/cli/SingleClassMode.java @@ -0,0 +1,87 @@ +package jadx.cli; + +import java.io.File; +import java.util.List; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.api.ICodeInfo; +import jadx.api.JadxDecompiler; +import jadx.core.dex.attributes.AFlag; +import jadx.core.dex.nodes.ClassNode; +import jadx.core.dex.visitors.SaveCode; +import jadx.core.utils.exceptions.JadxRuntimeException; +import jadx.core.utils.files.FileUtils; + +public class SingleClassMode { + private static final Logger LOG = LoggerFactory.getLogger(SingleClassMode.class); + + public static boolean process(JadxDecompiler jadx, JadxCLIArgs cliArgs) { + String singleClass = cliArgs.getSingleClass(); + String singleClassOutput = cliArgs.getSingleClassOutput(); + if (singleClass == null && singleClassOutput == null) { + return false; + } + ClassNode clsForProcess; + if (singleClass != null) { + clsForProcess = jadx.getRoot().resolveClass(singleClass); + if (clsForProcess == null) { + clsForProcess = jadx.getRoot().getClasses().stream() + .filter(cls -> cls.getClassInfo().getAliasFullName().equals(singleClass)) + .findFirst().orElse(null); + } + if (clsForProcess == null) { + throw new JadxRuntimeException("Input class not found: " + singleClass); + } + if (clsForProcess.contains(AFlag.DONT_GENERATE)) { + throw new JadxRuntimeException("Input class can't be saved by currect jadx settings (marked as DONT_GENERATE)"); + } + if (clsForProcess.isInner()) { + clsForProcess = clsForProcess.getTopParentClass(); + LOG.warn("Input class is inner, parent class will be saved: {}", clsForProcess.getFullName()); + } + } else { + // singleClassOutput is set + // expect only one class to be loaded + List classes = jadx.getRoot().getClasses().stream() + .filter(c -> !c.isInner() && !c.contains(AFlag.DONT_GENERATE)) + .collect(Collectors.toList()); + int size = classes.size(); + if (size == 1) { + clsForProcess = classes.get(0); + } else { + throw new JadxRuntimeException("Found " + size + " classes, single class output can't be used"); + } + } + ICodeInfo codeInfo; + try { + codeInfo = clsForProcess.decompile(); + } catch (Exception e) { + throw new JadxRuntimeException("Class decompilation failed", e); + } + String fileExt = SaveCode.getFileExtension(jadx.getRoot()); + File out; + if (singleClassOutput == null) { + out = new File(jadx.getArgs().getOutDirSrc(), clsForProcess.getClassInfo().getAliasFullPath() + fileExt); + } else { + if (singleClassOutput.endsWith(fileExt)) { + // treat as file name + out = new File(singleClassOutput); + } else { + // treat as directory + out = new File(singleClassOutput, clsForProcess.getShortName() + fileExt); + } + } + File resultOut = FileUtils.prepareFile(out); + if (clsForProcess.getClassInfo().hasAlias()) { + LOG.info("Saving class '{}' (alias: '{}') to file '{}'", + clsForProcess.getClassInfo().getFullName(), clsForProcess.getFullName(), resultOut.getAbsolutePath()); + } else { + LOG.info("Saving class '{}' to file '{}'", clsForProcess.getFullName(), resultOut.getAbsolutePath()); + } + SaveCode.save(codeInfo.getCodeStr(), resultOut); + return true; + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/SaveCode.java b/jadx-core/src/main/java/jadx/core/dex/visitors/SaveCode.java index b94761cba8ed9b5afe712424b5f99caceff731e5..c9eb6e64a50783acf021292986fe066aaed30f5c 100644 --- a/jadx-core/src/main/java/jadx/core/dex/visitors/SaveCode.java +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/SaveCode.java @@ -11,6 +11,7 @@ import jadx.api.JadxArgs; import jadx.api.plugins.utils.ZipSecurity; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.nodes.ClassNode; +import jadx.core.dex.nodes.RootNode; import jadx.core.utils.exceptions.JadxRuntimeException; import jadx.core.utils.files.FileUtils; @@ -37,7 +38,7 @@ public class SaveCode { if (cls.root().getArgs().isSkipFilesSave()) { return; } - String fileName = cls.getClassInfo().getAliasFullPath() + getFileExtension(cls); + String fileName = cls.getClassInfo().getAliasFullPath() + getFileExtension(cls.root()); save(codeStr, dir, fileName); } @@ -61,8 +62,8 @@ public class SaveCode { } } - private static String getFileExtension(ClassNode cls) { - JadxArgs.OutputFormatEnum outputFormat = cls.root().getArgs().getOutputFormat(); + public static String getFileExtension(RootNode root) { + JadxArgs.OutputFormatEnum outputFormat = root.getArgs().getOutputFormat(); switch (outputFormat) { case JAVA: return ".java";