fix(plugins): add a better way to init plugins options

This commit is contained in:
Skylot
2023-09-11 21:50:48 +01:00
parent e358476c71
commit 24657f6b3c
9 changed files with 373 additions and 105 deletions
@@ -6,6 +6,10 @@ import java.util.function.Function;
import jadx.api.plugins.options.JadxPluginOptions;
/**
* Prefer {@link BasePluginOptionsBuilder} as a better way to init and parse options
*/
@Deprecated
public abstract class BaseOptionsParser implements JadxPluginOptions {
protected Map<String, String> options;
@@ -0,0 +1,242 @@
package jadx.api.plugins.options.impl;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.jetbrains.annotations.Nullable;
import jadx.api.plugins.options.JadxPluginOptions;
import jadx.api.plugins.options.OptionDescription;
import jadx.api.plugins.options.OptionFlag;
import jadx.api.plugins.options.OptionType;
/**
* Base class for {@link JadxPluginOptions} implementation
* <p>
* Override {@link BasePluginOptionsBuilder#registerOptions()} method
* and use *option methods to add option info.
*/
@SuppressWarnings("unused")
public abstract class BasePluginOptionsBuilder implements JadxPluginOptions {
private final List<OptionData<?>> options = new ArrayList<>();
public abstract void registerOptions();
public BasePluginOptionsBuilder() {
registerOptions();
for (OptionData<?> option : options) {
option.validate();
}
}
public <T> OptionBuilder<T> option(String name) {
return addOption(new OptionData<>(name));
}
public <T> OptionBuilder<T> option(String name, Class<T> optionType) {
return addOption(new OptionData<>(name));
}
public OptionBuilder<Boolean> boolOption(String name) {
return addOption(
new OptionData<Boolean>(name)
.type(OptionType.BOOLEAN)
.values(Arrays.asList(Boolean.TRUE, Boolean.FALSE))
.formatter(b -> b ? "yes" : "no")
.parser(val -> parseBoolOption(name, val)));
}
public OptionBuilder<String> strOption(String name) {
return addOption(
new OptionData<String>(name)
.type(OptionType.STRING)
.formatter(v -> v)
.parser(v -> v));
}
public <E extends Enum<?>> OptionBuilder<E> enumOption(String name, E[] values, Function<String, E> valueOf) {
return addOption(
new OptionData<E>(name)
.type(OptionType.STRING)
.values(Arrays.asList(values))
.formatter(v -> v.name().toLowerCase(Locale.ROOT))
.parser(v -> valueOf.apply(v.toUpperCase(Locale.ROOT))));
}
@Override
public void setOptions(Map<String, String> map) {
for (OptionData<?> option : options) {
parseOption(option, map.get(option.name));
}
}
@Override
public List<OptionDescription> getOptionsDescriptions() {
return Collections.unmodifiableList(options);
}
private static <T> void parseOption(OptionData<T> option, @Nullable String value) {
T parsedValue;
if (value == null) {
parsedValue = option.defaultValue;
} else {
try {
parsedValue = option.getParser().apply(value);
} catch (Exception e) {
throw new RuntimeException("Parse failed for option: " + option.name + ", value: " + value, e);
}
}
try {
option.getSetter().accept(parsedValue);
} catch (Exception e) {
throw new RuntimeException("Setter invoke failed for option: " + option.name + ", value: " + parsedValue, e);
}
}
private static boolean parseBoolOption(String name, String val) {
String valLower = val.trim().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 '" + name + "', expect: 'yes' or 'no'");
}
private <T> OptionBuilder<T> addOption(OptionBuilder<T> optionData) {
this.options.add((OptionData<?>) optionData);
return optionData;
}
protected static class OptionData<T> implements OptionDescription, OptionBuilder<T> {
private final String name;
private String desc;
private List<T> values = Collections.emptyList();
private OptionType type = OptionType.STRING;
private Set<OptionFlag> flags = EnumSet.noneOf(OptionFlag.class);
private Function<String, T> parser;
private Function<T, String> formatter;
private Consumer<T> setter;
private T defaultValue;
public OptionData(String name) {
this.name = name;
}
@Override
public String name() {
return name;
}
@Override
public String description() {
return desc;
}
@Override
public List<String> values() {
return values.stream().map(formatter).collect(Collectors.toList());
}
@Override
public @Nullable String defaultValue() {
return formatter.apply(defaultValue);
}
@Override
public OptionType getType() {
return type;
}
@Override
public Set<OptionFlag> getFlags() {
return flags;
}
@Override
public OptionBuilder<T> description(String desc) {
this.desc = desc;
return this;
}
@Override
public OptionBuilder<T> defaultValue(@Nullable T defValue) {
this.defaultValue = defValue;
return this;
}
@Override
public OptionBuilder<T> parser(Function<String, T> parser) {
this.parser = parser;
return this;
}
@Override
public OptionBuilder<T> formatter(Function<T, String> formatter) {
this.formatter = formatter;
return this;
}
@Override
public OptionBuilder<T> setter(Consumer<T> setter) {
this.setter = setter;
return this;
}
@Override
public OptionBuilder<T> type(OptionType optionType) {
this.type = optionType;
return this;
}
@Override
public OptionBuilder<T> flags(OptionFlag... flags) {
this.flags = EnumSet.copyOf(Arrays.asList(flags));
return this;
}
@Override
public OptionBuilder<T> values(List<T> values) {
this.values = values;
return this;
}
public Function<String, T> getParser() {
return parser;
}
public Function<T, String> getFormatter() {
return formatter;
}
public Consumer<T> getSetter() {
return setter;
}
public void validate() {
if (desc == null || desc.isEmpty()) {
throw new IllegalArgumentException("Description should be set for option: " + name);
}
if (parser == null) {
throw new IllegalArgumentException("Parser should be set for option: " + name);
}
if (formatter == null) {
throw new IllegalArgumentException("Formatter should be set for option: " + name);
}
if (setter == null) {
throw new IllegalArgumentException("Setter should be set for option: " + name);
}
}
}
}
@@ -0,0 +1,42 @@
package jadx.api.plugins.options.impl;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import jadx.api.plugins.options.OptionFlag;
import jadx.api.plugins.options.OptionType;
public interface OptionBuilder<T> {
/**
* Option description (required)
*/
OptionBuilder<T> description(String desc);
OptionBuilder<T> defaultValue(T defValue);
/**
* Function to parse input string into option value (required)
*/
OptionBuilder<T> parser(Function<String, T> parser);
/**
* Function to format option value into string for build help (required)
*/
OptionBuilder<T> formatter(Function<T, String> formatter);
/**
* Function to save/apply parsed option value (required)
*/
OptionBuilder<T> setter(Consumer<T> setter);
/**
* Possible option values
*/
OptionBuilder<T> values(List<T> values);
OptionBuilder<T> type(OptionType optionType);
OptionBuilder<T> flags(OptionFlag... flags);
}
@@ -1,6 +1,7 @@
package jadx.core.utils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
@@ -64,6 +65,13 @@ public class ListUtils {
return new ArrayList<>(new LinkedHashSet<>(list));
}
public static <T> List<T> concat(T first, T[] values) {
List<T> list = new ArrayList<>(1 + values.length);
list.add(first);
list.addAll(Arrays.asList(values));
return list;
}
/**
* Replace old element to new one.
* Support null and empty immutable list (created by Collections.emptyList())
@@ -175,4 +183,5 @@ public class ListUtils {
}
return false;
}
}
@@ -1,29 +1,17 @@
package jadx.plugins.input.dex;
import java.util.Collections;
import java.util.List;
import jadx.api.plugins.options.impl.BasePluginOptionsBuilder;
import jadx.api.plugins.options.OptionDescription;
import jadx.api.plugins.options.impl.BaseOptionsParser;
import jadx.api.plugins.options.impl.JadxOptionDescription;
public class DexInputOptions extends BasePluginOptionsBuilder {
public class DexInputOptions extends BaseOptionsParser {
private static final String VERIFY_CHECKSUM_OPT = DexInputPlugin.PLUGIN_ID + ".verify-checksum";
private boolean verifyChecksum = true;
private boolean verifyChecksum;
@Override
public void parseOptions() {
verifyChecksum = getBooleanOption(VERIFY_CHECKSUM_OPT, true);
}
public List<OptionDescription> getOptionsDescriptions() {
return Collections.singletonList(
JadxOptionDescription.booleanOption(
VERIFY_CHECKSUM_OPT,
"verify dex file checksum before load",
true));
public void registerOptions() {
boolOption(DexInputPlugin.PLUGIN_ID + ".verify-checksum")
.description("verify dex file checksum before load")
.defaultValue(true)
.setter(v -> verifyChecksum = v);
}
public boolean isVerifyChecksum() {
@@ -1,44 +1,29 @@
package jadx.plugins.input.javaconvert;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import jadx.api.plugins.options.impl.BasePluginOptionsBuilder;
import jadx.api.plugins.options.OptionDescription;
import jadx.api.plugins.options.impl.BaseOptionsParser;
import jadx.api.plugins.options.impl.JadxOptionDescription;
import jadx.core.utils.files.FileUtils;
import static jadx.plugins.input.javaconvert.JavaConvertPlugin.PLUGIN_ID;
public class JavaConvertOptions extends BaseOptionsParser {
private static final String MODE_OPT = JavaConvertPlugin.PLUGIN_ID + ".mode";
private static final String D8_DESUGAR_OPT = JavaConvertPlugin.PLUGIN_ID + ".d8-desugar";
public class JavaConvertOptions extends BasePluginOptionsBuilder {
public enum Mode {
DX, D8, BOTH
}
private Mode mode = Mode.BOTH;
private boolean d8Desugar = false;
private Mode mode;
private boolean d8Desugar;
@Override
public void parseOptions() {
mode = getOption(MODE_OPT, name -> Mode.valueOf(name.toUpperCase(Locale.ROOT)), Mode.BOTH);
d8Desugar = getBooleanOption(D8_DESUGAR_OPT, false);
}
public void registerOptions() {
enumOption(PLUGIN_ID + ".mode", Mode.values(), Mode::valueOf)
.description("convert mode")
.defaultValue(Mode.BOTH)
.setter(v -> mode = v);
@Override
public List<OptionDescription> getOptionsDescriptions() {
return Arrays.asList(
new JadxOptionDescription(
MODE_OPT,
"convert mode",
"both",
Arrays.asList("dx", "d8", "both")),
JadxOptionDescription.booleanOption(
D8_DESUGAR_OPT,
"use desugar in d8",
false));
boolOption(PLUGIN_ID + ".d8-desugar")
.description("use desugar in d8")
.defaultValue(false)
.setter(v -> d8Desugar = v);
}
public Mode getMode() {
@@ -48,8 +33,4 @@ public class JavaConvertOptions extends BaseOptionsParser {
public boolean isD8Desugar() {
return d8Desugar;
}
public String getOptionsHash() {
return FileUtils.md5Sum(mode + ":" + d8Desugar);
}
}
@@ -32,7 +32,6 @@ public class JavaConvertPlugin implements JadxPlugin, JadxCodeInput {
public void init(JadxPluginContext context) {
context.registerOptions(options);
context.addCodeInput(this);
context.registerInputsHashSupplier(options::getOptionsHash);
}
@Override
@@ -1,11 +1,9 @@
package jadx.plugins.kotlin.metadata
import jadx.api.plugins.options.OptionDescription
import jadx.api.plugins.options.impl.BaseOptionsParser
import jadx.api.plugins.options.impl.JadxOptionDescription.booleanOption
import jadx.api.plugins.options.impl.BasePluginOptionsBuilder
import jadx.plugins.kotlin.metadata.KotlinMetadataPlugin.Companion.PLUGIN_ID
class KotlinMetadataOptions : BaseOptionsParser() {
class KotlinMetadataOptions : BasePluginOptionsBuilder() {
var isClassAlias: Boolean = true
private set
var isMethodArgs: Boolean = true
@@ -21,26 +19,41 @@ class KotlinMetadataOptions : BaseOptionsParser() {
var isGetters: Boolean = true
private set
override fun parseOptions() {
isClassAlias = getBooleanOption(CLASS_ALIAS_OPT, true)
isMethodArgs = getBooleanOption(METHOD_ARGS_OPT, true)
isFields = getBooleanOption(FIELDS_OPT, true)
isCompanion = getBooleanOption(COMPANION_OPT, true)
isDataClass = getBooleanOption(DATA_CLASS_OPT, true)
isToString = getBooleanOption(TO_STRING_OPT, true)
isGetters = getBooleanOption(GETTERS_OPT, true)
}
override fun registerOptions() {
boolOption(CLASS_ALIAS_OPT)
.description("rename class alias")
.defaultValue(true)
.setter { isClassAlias = it }
override fun getOptionsDescriptions(): List<OptionDescription> {
return listOf(
booleanOption(CLASS_ALIAS_OPT, "rename class alias", true),
booleanOption(METHOD_ARGS_OPT, "rename function arguments", true),
booleanOption(FIELDS_OPT, "rename fields", true),
booleanOption(COMPANION_OPT, "rename companion object", true),
booleanOption(DATA_CLASS_OPT, "add data class modifier", true),
booleanOption(TO_STRING_OPT, "rename fields using toString", true),
booleanOption(GETTERS_OPT, "rename simple getters to field names", true),
)
boolOption(METHOD_ARGS_OPT)
.description("rename function arguments")
.defaultValue(true)
.setter { isMethodArgs = it }
boolOption(FIELDS_OPT)
.description("rename fields")
.defaultValue(true)
.setter { isFields = it }
boolOption(COMPANION_OPT)
.description("rename companion object")
.defaultValue(true)
.setter { isCompanion = it }
boolOption(DATA_CLASS_OPT)
.description("add data class modifier")
.defaultValue(true)
.setter { isDataClass = it }
boolOption(TO_STRING_OPT)
.description("rename fields using toString")
.defaultValue(true)
.setter { isToString = it }
boolOption(GETTERS_OPT)
.description("rename simple getters to field names")
.defaultValue(true)
.setter { isGetters = it }
}
fun isPreparePassNeeded(): Boolean {
@@ -1,22 +1,18 @@
package jadx.plugins.mappings;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import org.jetbrains.annotations.Nullable;
import net.fabricmc.mappingio.format.MappingFormat;
import jadx.api.plugins.options.OptionDescription;
import jadx.api.plugins.options.OptionFlag;
import jadx.api.plugins.options.impl.BaseOptionsParser;
import jadx.api.plugins.options.impl.JadxOptionDescription;
import jadx.api.plugins.options.impl.BasePluginOptionsBuilder;
import jadx.core.utils.ListUtils;
import static jadx.plugins.mappings.RenameMappingsPlugin.PLUGIN_ID;
public class RenameMappingsOptions extends BaseOptionsParser {
public class RenameMappingsOptions extends BasePluginOptionsBuilder {
public static final String INVERT_OPT = PLUGIN_ID + ".invert";
public static final String FORMAT_OPT = PLUGIN_ID + ".format";
@@ -29,18 +25,21 @@ public class RenameMappingsOptions extends BaseOptionsParser {
private @Nullable MappingFormat format = null;
@Override
public void parseOptions() {
format = getOption(FORMAT_OPT, RenameMappingsOptions::parseMappingFormat, null);
invert = getBooleanOption(INVERT_OPT, false);
}
public void registerOptions() {
option(FORMAT_OPT, MappingFormat.class)
.description("mapping format")
.parser(RenameMappingsOptions::parseMappingFormat)
.formatter(v -> v == null ? "AUTO" : v.name())
.values(ListUtils.concat(null, MappingFormat.values()))
.defaultValue(null)
.flags(OptionFlag.PER_PROJECT, OptionFlag.DISABLE_IN_GUI)
.setter(v -> format = v);
@Override
public List<OptionDescription> getOptionsDescriptions() {
return Arrays.asList(
new JadxOptionDescription(FORMAT_OPT, "mapping format", "auto", getMappingFormats())
.withFlags(OptionFlag.PER_PROJECT, OptionFlag.DISABLE_IN_GUI),
JadxOptionDescription.booleanOption(INVERT_OPT, "invert mapping", false)
.withFlag(OptionFlag.PER_PROJECT));
boolOption(INVERT_OPT)
.description("invert mapping on load")
.defaultValue(false)
.flags(OptionFlag.PER_PROJECT)
.setter(v -> invert = v);
}
private static MappingFormat parseMappingFormat(String name) {
@@ -51,16 +50,7 @@ public class RenameMappingsOptions extends BaseOptionsParser {
return MappingFormat.valueOf(upName);
}
private static List<String> getMappingFormats() {
List<String> list = new ArrayList<>();
list.add("auto");
for (MappingFormat value : MappingFormat.values()) {
list.add(value.name());
}
return list;
}
public MappingFormat getFormat() {
public @Nullable MappingFormat getFormat() {
return format;
}