diff --git a/README.md b/README.md index 0ca16eec3..ee954b2d2 100644 --- a/README.md +++ b/README.md @@ -123,11 +123,17 @@ options: -q, --quiet - turn off output (set --log-level to QUIET) --version - print jadx version -h, --help - print this help + +Plugin options (-P=): + 1) dex-input (Load .dex and .apk files) + -Pdex-input.verify-checksum - Verify dex file checksum before load, values: [yes, no], default: yes + Examples: jadx -d out classes.dex jadx --rename-flags "none" classes.dex jadx --rename-flags "valid, printable" classes.dex jadx --log-level ERROR app.apk + jadx -Pdex-input.verify-checksum=no app.apk ``` These options also worked on jadx-gui running from command line and override options from preferences dialog diff --git a/jadx-cli/src/main/java/jadx/cli/JCommanderWrapper.java b/jadx-cli/src/main/java/jadx/cli/JCommanderWrapper.java index b69b65e68..3548cb62a 100644 --- a/jadx-cli/src/main/java/jadx/cli/JCommanderWrapper.java +++ b/jadx-cli/src/main/java/jadx/cli/JCommanderWrapper.java @@ -17,6 +17,11 @@ import com.beust.jcommander.ParameterException; import com.beust.jcommander.Parameterized; import jadx.api.JadxDecompiler; +import jadx.api.plugins.JadxPlugin; +import jadx.api.plugins.JadxPluginInfo; +import jadx.api.plugins.JadxPluginManager; +import jadx.api.plugins.options.JadxPluginOptions; +import jadx.api.plugins.options.OptionDescription; public class JCommanderWrapper { private final JCommander jc; @@ -70,24 +75,25 @@ public class JCommanderWrapper { maxNamesLen = len; } } + maxNamesLen += 3; JadxCLIArgs args = (JadxCLIArgs) jc.getObjects().get(0); for (Field f : getFields(args.getClass())) { String name = f.getName(); ParameterDescription p = paramsMap.get(name); - if (p == null) { + if (p == null || p.getParameter().hidden()) { continue; } StringBuilder opt = new StringBuilder(); opt.append(" ").append(p.getNames()); String description = p.getDescription(); - addSpaces(opt, maxNamesLen - opt.length() + 3); + addSpaces(opt, maxNamesLen - opt.length()); if (description.contains("\n")) { String[] lines = description.split("\n"); opt.append("- ").append(lines[0]); for (int i = 1; i < lines.length; i++) { opt.append('\n'); - addSpaces(opt, maxNamesLen + 5); + addSpaces(opt, maxNamesLen + 2); opt.append(lines[i]); } } else { @@ -99,11 +105,14 @@ public class JCommanderWrapper { } out.println(opt); } + out.println(appendPluginOptions(maxNamesLen)); + out.println(); out.println("Examples:"); out.println(" jadx -d out classes.dex"); out.println(" jadx --rename-flags \"none\" classes.dex"); out.println(" jadx --rename-flags \"valid, printable\" classes.dex"); out.println(" jadx --log-level ERROR app.apk"); + out.println(" jadx -Pdex-input.verify-checksum=no app.apk"); } /** @@ -145,4 +154,46 @@ public class JCommanderWrapper { str.append(' '); } } + + private String appendPluginOptions(int maxNamesLen) { + StringBuilder sb = new StringBuilder(); + JadxPluginManager pluginManager = new JadxPluginManager(); + pluginManager.load(); + int k = 1; + for (JadxPlugin plugin : pluginManager.getAllPlugins()) { + if (plugin instanceof JadxPluginOptions) { + if (appendPlugin(((JadxPluginOptions) plugin), sb, maxNamesLen, k)) { + k++; + } + } + } + if (sb.length() == 0) { + return ""; + } + return "\nPlugin options (-P=):" + sb; + } + + private boolean appendPlugin(JadxPluginOptions plugin, StringBuilder out, int maxNamesLen, int k) { + List descs = plugin.getOptionsDescriptions(); + if (descs.isEmpty()) { + return false; + } + JadxPluginInfo pluginInfo = plugin.getPluginInfo(); + out.append("\n ").append(k).append(") "); + out.append(pluginInfo.getPluginId()).append(" (").append(pluginInfo.getDescription()).append(") "); + for (OptionDescription desc : descs) { + StringBuilder opt = new StringBuilder(); + opt.append(" -P").append(desc.name()); + addSpaces(opt, maxNamesLen - opt.length()); + opt.append("- ").append(desc.description()); + if (!desc.values().isEmpty()) { + opt.append(", values: ").append(desc.values()); + } + if (desc.defaultValue() != null) { + opt.append(", default: ").append(desc.defaultValue()); + } + out.append("\n").append(opt); + } + return true; + } } diff --git a/jadx-cli/src/main/java/jadx/cli/JadxCLI.java b/jadx-cli/src/main/java/jadx/cli/JadxCLI.java index 4b121a96a..114b3863a 100644 --- a/jadx-cli/src/main/java/jadx/cli/JadxCLI.java +++ b/jadx-cli/src/main/java/jadx/cli/JadxCLI.java @@ -7,6 +7,7 @@ import jadx.api.JadxArgs; import jadx.api.JadxDecompiler; import jadx.api.impl.NoOpCodeCache; import jadx.api.impl.SimpleCodeWriter; +import jadx.cli.LogHelper.LogLevelEnum; import jadx.core.utils.exceptions.JadxArgsValidateException; import jadx.core.utils.files.FileUtils; @@ -21,7 +22,7 @@ public class JadxCLI { LOG.error("Incorrect arguments: {}", e.getMessage()); result = 1; } catch (Exception e) { - LOG.error("jadx error: {}", e.getMessage(), e); + LOG.error("Process error:", e); result = 1; } finally { FileUtils.deleteTempRootDir(); @@ -38,11 +39,16 @@ public class JadxCLI { } private static int processAndSave(JadxCLIArgs cliArgs) { + setLogLevelsForLoadingStage(cliArgs); JadxArgs jadxArgs = cliArgs.toJadxArgs(); jadxArgs.setCodeCache(new NoOpCodeCache()); jadxArgs.setCodeWriterProvider(SimpleCodeWriter::new); try (JadxDecompiler jadx = new JadxDecompiler(jadxArgs)) { jadx.load(); + if (checkForErrors(jadx)) { + return 1; + } + LogHelper.setLogLevelFromArgs(cliArgs); if (!SingleClassMode.process(jadx, cliArgs)) { save(jadx); } @@ -57,8 +63,34 @@ public class JadxCLI { return 0; } + private static void setLogLevelsForLoadingStage(JadxCLIArgs cliArgs) { + switch (cliArgs.getLogLevel()) { + case QUIET: + LogHelper.setLogLevelFromArgs(cliArgs); + break; + + case PROGRESS: + // show load errors + LogHelper.applyLogLevel(LogLevelEnum.ERROR); + break; + } + } + + private static boolean checkForErrors(JadxDecompiler jadx) { + if (jadx.getRoot().getClasses().isEmpty()) { + LOG.error("Load failed! No classes for decompile!"); + return true; + } + if (jadx.getErrorsCount() > 0) { + LOG.error("Load with errors! Check log for details"); + // continue processing + return false; + } + return false; + } + private static void save(JadxDecompiler jadx) { - if (LogHelper.getLogLevel() == LogHelper.LogLevelEnum.QUIET) { + if (LogHelper.getLogLevel() == LogLevelEnum.QUIET) { jadx.save(); } else { jadx.save(500, (done, total) -> { diff --git a/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java b/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java index a53e4ccba..499d42013 100644 --- a/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java +++ b/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java @@ -2,12 +2,15 @@ package jadx.cli; import java.util.ArrayList; import java.util.EnumSet; +import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; +import com.beust.jcommander.DynamicParameter; import com.beust.jcommander.IStringConverter; import com.beust.jcommander.Parameter; @@ -177,6 +180,9 @@ public class JadxCLIArgs { @Parameter(names = { "-h", "--help" }, description = "print this help", help = true) protected boolean printHelp = false; + @DynamicParameter(names = "-P", description = "Plugin options", hidden = true) + protected Map pluginOptions = new HashMap<>(); + public boolean processArgs(String[] args) { JCommanderWrapper jcw = new JCommanderWrapper<>(this); return jcw.parse(args) && process(jcw); @@ -212,7 +218,6 @@ public class JadxCLIArgs { if (threadsCount <= 0) { throw new JadxException("Threads count must be positive, got: " + threadsCount); } - LogHelper.setLogLevelFromArgs(this); } catch (JadxException e) { System.err.println("ERROR: " + e.getMessage()); jcw.printUsage(); @@ -260,6 +265,7 @@ public class JadxCLIArgs { args.setFsCaseSensitive(fsCaseSensitive); args.setCommentsLevel(commentsLevel); args.setUseDxInput(useDx); + args.setPluginOptions(pluginOptions); return args; } @@ -411,6 +417,14 @@ public class JadxCLIArgs { return commentsLevel; } + public LogHelper.LogLevelEnum getLogLevel() { + return logLevel; + } + + public Map getPluginOptions() { + return pluginOptions; + } + static class RenameConverter implements IStringConverter> { private final String paramName; diff --git a/jadx-core/src/main/java/jadx/api/JadxArgs.java b/jadx-core/src/main/java/jadx/api/JadxArgs.java index c2f7ad9ef..54adce1a7 100644 --- a/jadx-core/src/main/java/jadx/api/JadxArgs.java +++ b/jadx-core/src/main/java/jadx/api/JadxArgs.java @@ -4,7 +4,9 @@ import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; @@ -100,6 +102,8 @@ public class JadxArgs { */ private boolean skipFilesSave = false; + private Map pluginOptions = new HashMap<>(); + public JadxArgs() { // use default options } @@ -474,6 +478,14 @@ public class JadxArgs { this.skipFilesSave = skipFilesSave; } + public Map getPluginOptions() { + return pluginOptions; + } + + public void setPluginOptions(Map pluginOptions) { + this.pluginOptions = pluginOptions; + } + @Override public String toString() { return "JadxArgs{" + "inputFiles=" + inputFiles @@ -507,6 +519,7 @@ public class JadxArgs { + ", codeCache=" + codeCache + ", codeWriter=" + codeWriterProvider.apply(this).getClass().getSimpleName() + ", useDxInput=" + useDxInput + + ", pluginOptions=" + pluginOptions + '}'; } } diff --git a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java index 70f8e9fb7..10d455181 100644 --- a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java +++ b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java @@ -30,6 +30,7 @@ import jadx.api.plugins.JadxPlugin; import jadx.api.plugins.JadxPluginManager; import jadx.api.plugins.input.JadxInputPlugin; import jadx.api.plugins.input.data.ILoadResult; +import jadx.api.plugins.options.JadxPluginOptions; import jadx.core.Jadx; import jadx.core.dex.attributes.AFlag; import jadx.core.dex.attributes.nodes.LineAttrNode; @@ -168,6 +169,18 @@ public final class JadxDecompiler implements Closeable { LOG.debug("Resolved plugins: {}", Utils.collectionMap(pluginManager.getResolvedPlugins(), p -> p.getPluginInfo().getPluginId())); } + Map pluginOptions = args.getPluginOptions(); + if (!pluginOptions.isEmpty()) { + LOG.debug("Applying plugin options: {}", pluginOptions); + for (JadxPluginOptions plugin : pluginManager.getPluginsWithOptions()) { + try { + plugin.setOptions(pluginOptions); + } catch (Exception e) { + String pluginId = plugin.getPluginInfo().getPluginId(); + throw new JadxRuntimeException("Failed to apply options for plugin: " + pluginId, e); + } + } + } } public void registerPlugin(JadxPlugin plugin) { diff --git a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java index 3ee27b84d..1d741e53a 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java @@ -582,6 +582,10 @@ public class JadxSettings extends JadxCLIArgs { this.lineNumbersMode = lineNumbersMode; } + public void setPluginOptions(Map pluginOptions) { + this.pluginOptions = pluginOptions; + } + private void upgradeSettings(int fromVersion) { LOG.debug("upgrade settings from version: {} to {}", fromVersion, CURRENT_SETTINGS_VERSION); if (fromVersion == 0) { diff --git a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java index c9ae3faff..a4a863c4e 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java @@ -62,6 +62,11 @@ import jadx.api.CommentsLevel; import jadx.api.JadxArgs; import jadx.api.JadxArgs.UseKotlinMethodsForVarNames; import jadx.api.args.DeobfuscationMapFileMode; +import jadx.api.plugins.JadxPlugin; +import jadx.api.plugins.JadxPluginInfo; +import jadx.api.plugins.JadxPluginManager; +import jadx.api.plugins.options.JadxPluginOptions; +import jadx.api.plugins.options.OptionDescription; import jadx.gui.ui.MainWindow; import jadx.gui.ui.codearea.EditorTheme; import jadx.gui.utils.FontUtils; @@ -69,6 +74,7 @@ import jadx.gui.utils.LafManager; import jadx.gui.utils.LangLocale; import jadx.gui.utils.NLS; import jadx.gui.utils.UiUtils; +import jadx.gui.utils.ui.DocumentUpdateListener; public class JadxSettingsWindow extends JDialog { private static final long serialVersionUID = -1804570470377354148L; @@ -117,8 +123,11 @@ public class JadxSettingsWindow extends JDialog { leftPanel.add(makeAppearanceGroup()); leftPanel.add(makeOtherGroup()); leftPanel.add(makeSearchResGroup()); + leftPanel.add(makePluginOptionsGroup()); + leftPanel.add(Box.createVerticalGlue()); rightPanel.add(makeDecompilationGroup()); + rightPanel.add(Box.createVerticalGlue()); JButton saveBtn = new JButton(NLS.str("preferences.save")); saveBtn.addActionListener(event -> { @@ -550,6 +559,39 @@ public class JadxSettingsWindow extends JDialog { return other; } + private SettingsGroup makePluginOptionsGroup() { + SettingsGroup pluginsGroup = new SettingsGroup(NLS.str("preferences.plugins")); + JadxPluginManager pluginManager = mainWindow.getWrapper().getDecompiler().getPluginManager(); + for (JadxPlugin plugin : pluginManager.getAllPlugins()) { + if (!(plugin instanceof JadxPluginOptions)) { + continue; + } + JadxPluginInfo pluginInfo = plugin.getPluginInfo(); + JadxPluginOptions optPlugin = (JadxPluginOptions) plugin; + for (OptionDescription opt : optPlugin.getOptionsDescriptions()) { + String title = "[" + pluginInfo.getPluginId() + "] " + opt.description(); + if (opt.values().isEmpty()) { + JTextField textField = new JTextField(); + textField.getDocument().addDocumentListener(new DocumentUpdateListener(event -> { + settings.getPluginOptions().put(opt.name(), textField.getText()); + needReload(); + })); + pluginsGroup.addRow(title, textField); + } else { + String curValue = settings.getPluginOptions().get(opt.name()); + JComboBox combo = new JComboBox<>(opt.values().toArray(new String[0])); + combo.setSelectedItem(curValue != null ? curValue : opt.defaultValue()); + combo.addActionListener(e -> { + settings.getPluginOptions().put(opt.name(), ((String) combo.getSelectedItem())); + needReload(); + }); + pluginsGroup.addRow(title, combo); + } + } + } + return pluginsGroup; + } + private SettingsGroup makeOtherGroup() { JComboBox languageCbx = new JComboBox<>(NLS.getLangLocales()); for (LangLocale locale : NLS.getLangLocales()) { diff --git a/jadx-gui/src/main/java/jadx/gui/utils/ui/DocumentUpdateListener.java b/jadx-gui/src/main/java/jadx/gui/utils/ui/DocumentUpdateListener.java new file mode 100644 index 000000000..aefb41fef --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/utils/ui/DocumentUpdateListener.java @@ -0,0 +1,30 @@ +package jadx.gui.utils.ui; + +import java.util.function.Consumer; + +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; + +public class DocumentUpdateListener implements DocumentListener { + + private final Consumer listener; + + public DocumentUpdateListener(Consumer listener) { + this.listener = listener; + } + + @Override + public void insertUpdate(DocumentEvent event) { + this.listener.accept(event); + } + + @Override + public void removeUpdate(DocumentEvent event) { + this.listener.accept(event); + } + + @Override + public void changedUpdate(DocumentEvent event) { + this.listener.accept(event); + } +} diff --git a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties index e28bbf8c2..6325b555a 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties @@ -119,6 +119,7 @@ preferences.title=Einstellungen preferences.deobfuscation=Deobfuskierung preferences.appearance=Aussehen preferences.decompile=Dekompilierung +#preferences.plugins=Plugins preferences.project=Projekt preferences.other=Andere preferences.language=Sprache diff --git a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties index d565a4fca..c3ad9b081 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties @@ -119,6 +119,7 @@ preferences.title=Preferences preferences.deobfuscation=Deobfuscation preferences.appearance=Appearance preferences.decompile=Decompilation +preferences.plugins=Plugins preferences.project=Project preferences.other=Other preferences.language=Language diff --git a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties index 39ab76900..bb48d31ab 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties @@ -119,6 +119,7 @@ preferences.title=Preferencias preferences.deobfuscation=Desofuscación #preferences.appearance=Appearance preferences.decompile=Descompilación +#preferences.plugins=Plugins #preferences.project= preferences.other=Otros preferences.language=Idioma diff --git a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties index ad807c7b8..57037f48c 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties @@ -119,6 +119,7 @@ preferences.title=설정 preferences.deobfuscation=난독화 해제 preferences.appearance=외관 preferences.decompile=디컴파일 +#preferences.plugins=Plugins preferences.project=프로젝트 preferences.other=기타 preferences.language=언어 diff --git a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties index 0e5b7da31..6ef3d6fce 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties @@ -119,6 +119,7 @@ preferences.title=首选项 preferences.deobfuscation=反混淆 preferences.appearance=界面 preferences.decompile=反编译 +#preferences.plugins=Plugins preferences.project=项目 preferences.other=其他 preferences.language=语言 diff --git a/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties b/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties index 4f095731d..ef0f522b6 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties @@ -119,6 +119,7 @@ preferences.title=選項 preferences.deobfuscation=去模糊化 preferences.appearance=外觀 preferences.decompile=反編譯 +#preferences.plugins=Plugins preferences.project=專案 preferences.other=其他 preferences.language=語言 diff --git a/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexFileLoader.java b/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexFileLoader.java index 40370f7c2..b2d76e868 100644 --- a/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexFileLoader.java +++ b/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexFileLoader.java @@ -23,19 +23,27 @@ import jadx.plugins.input.dex.utils.DexCheckSum; public class DexFileLoader { private static final Logger LOG = LoggerFactory.getLogger(DexFileLoader.class); + // sharing between all instances (can be used in other plugins) // TODO: private static int dexUniqId = 1; - public static List collectDexFiles(List pathsList) { + private final DexInputOptions options; + + public DexFileLoader(DexInputOptions options) { + this.options = options; + resetDexUniqId(); + } + + public List collectDexFiles(List pathsList) { return pathsList.stream() .map(Path::toFile) - .map(DexFileLoader::loadDexFromFile) + .map(this::loadDexFromFile) .filter(list -> !list.isEmpty()) .flatMap(Collection::stream) .peek(dr -> LOG.debug("Loading dex: {}", dr)) .collect(Collectors.toList()); } - private static List loadDexFromFile(File file) { + private List loadDexFromFile(File file) { try (InputStream inputStream = new FileInputStream(file)) { return checkFileMagic(file, inputStream, file.getAbsolutePath()); } catch (Exception e) { @@ -44,7 +52,7 @@ public class DexFileLoader { } } - private static List checkFileMagic(File file, InputStream inputStream, String inputFileName) throws IOException { + private List checkFileMagic(File file, InputStream inputStream, String inputFileName) throws IOException { try (InputStream in = inputStream.markSupported() ? inputStream : new BufferedInputStream(inputStream)) { byte[] magic = new byte[DexConsts.MAX_MAGIC_SIZE]; in.mark(magic.length); @@ -54,7 +62,9 @@ public class DexFileLoader { if (isStartWithBytes(magic, DexConsts.DEX_FILE_MAGIC)) { in.reset(); byte[] content = readAllBytes(in); - DexCheckSum.verify(content); + if (options.isVerifyChecksum()) { + DexCheckSum.verify(content); + } DexReader dexReader = new DexReader(getNextUniqId(), inputFileName, content); return Collections.singletonList(dexReader); } @@ -65,7 +75,7 @@ public class DexFileLoader { } } - private static List collectDexFromZip(File file) { + private List collectDexFromZip(File file) { List result = new ArrayList<>(); try { ZipSecurity.readZipEntries(file, (entry, in) -> { @@ -107,15 +117,15 @@ public class DexFileLoader { return buf.toByteArray(); } - private static int getNextUniqId() { + private static synchronized int getNextUniqId() { dexUniqId++; if (dexUniqId >= 0xFFFF) { - resetDexUniqId(); + dexUniqId = 1; } return dexUniqId; } - public static void resetDexUniqId() { + private static synchronized void resetDexUniqId() { dexUniqId = 1; } } diff --git a/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexInputOptions.java b/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexInputOptions.java new file mode 100644 index 000000000..36f9b4451 --- /dev/null +++ b/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexInputOptions.java @@ -0,0 +1,51 @@ +package jadx.plugins.input.dex; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import jadx.api.plugins.options.OptionDescription; +import jadx.api.plugins.options.impl.JadxOptionDescription; + +public class DexInputOptions { + + private static final String VERIFY_CHECKSUM_OPT = DexInputPlugin.PLUGIN_ID + ".verify-checksum"; + + private boolean verifyChecksum = true; + + public void apply(Map options) { + verifyChecksum = getBooleanOption(options, VERIFY_CHECKSUM_OPT, true); + } + + public List buildOptionsDescriptions() { + List list = new ArrayList<>(1); + list.add(new JadxOptionDescription( + VERIFY_CHECKSUM_OPT, + "Verify dex file checksum before load", + "yes", + Arrays.asList("yes", "no"))); + return list; + } + + private boolean getBooleanOption(Map options, String key, boolean defValue) { + String val = options.get(key); + if (val == null) { + return defValue; + } + String valLower = val.toLowerCase(Locale.ROOT); + if (valLower.equals("yes") || valLower.equals("true")) { + return true; + } + if (valLower.equals("no") || valLower.equals("false")) { + return false; + } + throw new IllegalArgumentException("Unknown value '" + val + "' for option '" + key + "'" + + ", expect: 'yes' or 'no'"); + } + + public boolean isVerifyChecksum() { + return verifyChecksum; + } +} diff --git a/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexInputPlugin.java b/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexInputPlugin.java index 5a6e855c8..bf944a2bd 100644 --- a/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexInputPlugin.java +++ b/jadx-plugins/jadx-dex-input/src/main/java/jadx/plugins/input/dex/DexInputPlugin.java @@ -3,33 +3,48 @@ package jadx.plugins.input.dex; import java.io.Closeable; import java.nio.file.Path; import java.util.List; +import java.util.Map; + +import org.jetbrains.annotations.Nullable; 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.api.plugins.options.JadxPluginOptions; +import jadx.api.plugins.options.OptionDescription; -public class DexInputPlugin implements JadxInputPlugin { +public class DexInputPlugin implements JadxInputPlugin, JadxPluginOptions { + public static final String PLUGIN_ID = "dex-input"; - public DexInputPlugin() { - DexFileLoader.resetDexUniqId(); - } + private final DexInputOptions options = new DexInputOptions(); + private final DexFileLoader loader = new DexFileLoader(options); @Override public JadxPluginInfo getPluginInfo() { - return new JadxPluginInfo("dex-input", "DexInput", "Load .dex and .apk files"); + return new JadxPluginInfo(PLUGIN_ID, "DexInput", "Load .dex and .apk files"); } @Override public ILoadResult loadFiles(List input) { - return loadDexFiles(input, null); + return loadFiles(input, null); } - public static ILoadResult loadDexFiles(List inputFiles, Closeable closeable) { - List dexReaders = DexFileLoader.collectDexFiles(inputFiles); + public ILoadResult loadFiles(List inputFiles, @Nullable Closeable closeable) { + List dexReaders = loader.collectDexFiles(inputFiles); if (dexReaders.isEmpty()) { return EmptyLoadResult.INSTANCE; } return new DexLoadResult(dexReaders, closeable); } + + @Override + public void setOptions(Map options) { + this.options.apply(options); + } + + @Override + public List getOptionsDescriptions() { + return this.options.buildOptionsDescriptions(); + } } diff --git a/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertPlugin.java b/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertPlugin.java index 71ab330bf..8c1c9d576 100644 --- a/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertPlugin.java +++ b/jadx-plugins/jadx-java-convert/src/main/java/jadx/plugins/input/javaconvert/JavaConvertPlugin.java @@ -11,6 +11,8 @@ import jadx.plugins.input.dex.DexInputPlugin; public class JavaConvertPlugin implements JadxInputPlugin { + private final DexInputPlugin dexInput = new DexInputPlugin(); + @Override public JadxPluginInfo getPluginInfo() { return new JadxPluginInfo( @@ -27,6 +29,6 @@ public class JavaConvertPlugin implements JadxInputPlugin { result.close(); return EmptyLoadResult.INSTANCE; } - return DexInputPlugin.loadDexFiles(result.getConverted(), result); + return dexInput.loadFiles(result.getConverted(), result); } } diff --git a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/JadxPluginManager.java b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/JadxPluginManager.java index 488f5cc69..785ab4409 100644 --- a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/JadxPluginManager.java +++ b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/JadxPluginManager.java @@ -16,6 +16,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import jadx.api.plugins.input.JadxInputPlugin; +import jadx.api.plugins.options.JadxPluginOptions; +import jadx.api.plugins.options.OptionDescription; public class JadxPluginManager { private static final Logger LOG = LoggerFactory.getLogger(JadxPluginManager.class); @@ -56,9 +58,34 @@ public class JadxPluginManager { if (!allPlugins.add(pluginData)) { throw new IllegalArgumentException("Duplicate plugin id: " + pluginData + ", class " + plugin.getClass()); } + if (plugin instanceof JadxPluginOptions) { + verifyOptions(((JadxPluginOptions) plugin), pluginData.getPluginId()); + } return pluginData; } + private void verifyOptions(JadxPluginOptions plugin, String pluginId) { + List descriptions = plugin.getOptionsDescriptions(); + if (descriptions == null) { + throw new IllegalArgumentException("Null option descriptions in plugin id: " + pluginId); + } + String prefix = pluginId + '.'; + descriptions.forEach(descObj -> { + String optName = descObj.name(); + if (optName == null || !optName.startsWith(prefix)) { + throw new IllegalArgumentException("Plugin option name should start with plugin id: '" + prefix + "', option: " + optName); + } + String desc = descObj.description(); + if (desc == null || desc.isEmpty()) { + throw new IllegalArgumentException("Plugin option description not set, plugin: " + pluginId); + } + List values = descObj.values(); + if (values == null) { + throw new IllegalArgumentException("Plugin option values is null, option: " + optName + ", plugin: " + pluginId); + } + }); + } + public boolean unload(String pluginId) { boolean result = allPlugins.removeIf(pd -> { String id = pd.getPluginId(); @@ -87,6 +114,13 @@ public class JadxPluginManager { .collect(Collectors.toList()); } + public List getPluginsWithOptions() { + return resolvedPlugins.stream() + .filter(JadxPluginOptions.class::isInstance) + .map(JadxPluginOptions.class::cast) + .collect(Collectors.toList()); + } + private synchronized void resolve() { Map> provides = allPlugins.stream() .collect(Collectors.groupingBy(p -> p.getInfo().getProvides())); diff --git a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/options/JadxPluginOptions.java b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/options/JadxPluginOptions.java new file mode 100644 index 000000000..55ace7ebf --- /dev/null +++ b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/options/JadxPluginOptions.java @@ -0,0 +1,13 @@ +package jadx.api.plugins.options; + +import java.util.List; +import java.util.Map; + +import jadx.api.plugins.JadxPlugin; + +public interface JadxPluginOptions extends JadxPlugin { + + void setOptions(Map options); + + List getOptionsDescriptions(); +} diff --git a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/options/OptionDescription.java b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/options/OptionDescription.java new file mode 100644 index 000000000..04eb83a06 --- /dev/null +++ b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/options/OptionDescription.java @@ -0,0 +1,25 @@ +package jadx.api.plugins.options; + +import java.util.List; + +import org.jetbrains.annotations.Nullable; + +public interface OptionDescription { + + String name(); + + String description(); + + /** + * Possible values. + * Empty if not a limited set + */ + List values(); + + /** + * Default value. + * Null if required + */ + @Nullable + String defaultValue(); +} diff --git a/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/options/impl/JadxOptionDescription.java b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/options/impl/JadxOptionDescription.java new file mode 100644 index 000000000..ff291872c --- /dev/null +++ b/jadx-plugins/jadx-plugins-api/src/main/java/jadx/api/plugins/options/impl/JadxOptionDescription.java @@ -0,0 +1,47 @@ +package jadx.api.plugins.options.impl; + +import java.util.List; + +import org.jetbrains.annotations.Nullable; + +import jadx.api.plugins.options.OptionDescription; + +public class JadxOptionDescription implements OptionDescription { + + private final String name; + private final String desc; + private final String defaultValue; + private final List values; + + public JadxOptionDescription(String name, String desc, @Nullable String defaultValue, List values) { + this.name = name; + this.desc = desc; + this.defaultValue = defaultValue; + this.values = values; + } + + @Override + public String name() { + return name; + } + + @Override + public String description() { + return desc; + } + + @Override + public @Nullable String defaultValue() { + return defaultValue; + } + + @Override + public List values() { + return values; + } + + @Override + public String toString() { + return "OptionDescription{" + desc + ", values=" + values + '}'; + } +} diff --git a/jadx-plugins/jadx-smali-input/src/main/java/jadx/plugins/input/smali/SmaliInputPlugin.java b/jadx-plugins/jadx-smali-input/src/main/java/jadx/plugins/input/smali/SmaliInputPlugin.java index bcd4332d0..502dddfec 100644 --- a/jadx-plugins/jadx-smali-input/src/main/java/jadx/plugins/input/smali/SmaliInputPlugin.java +++ b/jadx-plugins/jadx-smali-input/src/main/java/jadx/plugins/input/smali/SmaliInputPlugin.java @@ -11,6 +11,8 @@ import jadx.plugins.input.dex.DexInputPlugin; public class SmaliInputPlugin implements JadxInputPlugin { + private final DexInputPlugin dexInput = new DexInputPlugin(); + @Override public JadxPluginInfo getPluginInfo() { return new JadxPluginInfo("smali-input", "SmaliInput", "Load .smali files"); @@ -22,6 +24,6 @@ public class SmaliInputPlugin implements JadxInputPlugin { if (!convert.execute(input)) { return EmptyLoadResult.INSTANCE; } - return DexInputPlugin.loadDexFiles(convert.getDexFiles(), convert); + return dexInput.loadFiles(convert.getDexFiles(), convert); } }