Skip to content

Commit

Permalink
Add class file down sampling
Browse files Browse the repository at this point in the history
  • Loading branch information
Col-E committed Aug 12, 2023
1 parent 34c0be6 commit b6218d1
Show file tree
Hide file tree
Showing 10 changed files with 141 additions and 14 deletions.
2 changes: 2 additions & 0 deletions dependencies.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ project.ext {
directories = 'dev.dirs:directories:26'
docking = 'com.github.Col-E:tiwulfx-dock:1.2.3'

downgrader = 'com.github.RaphiMC.JavaDowngrader:core:13e29798a8'

extra_collections = 'software.coley:extra-collections:1.2.3'
extra_observables = 'software.coley:extra-observables:1.2.2'

Expand Down
4 changes: 4 additions & 0 deletions recaf-api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ dependencies {
exclude group: 'com.android.tools'
}
api directories
api(downgrader) {
exclude group: 'org.ow2.asm'
exclude group: 'org.slf4j'
}
api extra_collections
api extra_observables
api gson // required by R8 (from dex-translator)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
package software.coley.recaf.services.compile;

import net.raphimc.javadowngrader.JavaDowngrader;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.tree.ClassNode;
import org.slf4j.Logger;
import software.coley.recaf.analytics.logging.Logging;

import java.util.HashSet;
import java.util.Map;
import java.util.TreeMap;

Expand All @@ -9,6 +17,8 @@
* @author Matt Coley
*/
public class CompileMap extends TreeMap<String, byte[]> {
private static final Logger logger = Logging.get(CompileMap.class);

/**
* @param map
* Map to copy.
Expand Down Expand Up @@ -39,4 +49,28 @@ public boolean hasInnerClasses() {
}
return false;
}

/**
* Down sample all classes in the map to the target version.
*
* @param version
* Target version to downsample to.
*/
public void downsample(int version) {
for (Map.Entry<String, byte[]> entry : new HashSet<>(entrySet())) {
String key = entry.getKey();
try {
ClassNode node = new ClassNode();
new ClassReader(entry.getValue()).accept(node, 0);
if (node.version > version) {
JavaDowngrader.downgrade(node, version);
ClassWriter writer = new ClassWriter(0);
node.accept(writer);
put(key, writer.toByteArray());
}
} catch (Throwable t) {
logger.error("Failed down sampling '{}' to version {}", key, version, t);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public class JavacArguments {
// Options
private final String classPath;
private final int versionTarget;
private final int downsampleTarget;
private final boolean debugVariables;
private final boolean debugLineNumbers;
private final boolean debugSourceName;
Expand All @@ -31,7 +32,9 @@ public class JavacArguments {
* @param classPath
* Classpath to use with compiler.
* @param versionTarget
* Java bytecode version to target.
* Java version to target.
* @param downsampleTarget
* Java version to target via down sampling. Negative to disable downs sampling.
* @param debugVariables
* Debug flag to include variable info.
* @param debugLineNumbers
Expand All @@ -40,12 +43,13 @@ public class JavacArguments {
* Debug flag to include source file name.
*/
public JavacArguments(@Nonnull String className, @Nonnull String classSource,
@Nullable String classPath, int versionTarget,
@Nullable String classPath, int versionTarget, int downsampleTarget,
boolean debugVariables, boolean debugLineNumbers, boolean debugSourceName) {
this.className = className;
this.classSource = classSource;
this.classPath = classPath;
this.versionTarget = versionTarget;
this.downsampleTarget = downsampleTarget;
this.debugVariables = debugVariables;
this.debugLineNumbers = debugLineNumbers;
this.debugSourceName = debugSourceName;
Expand Down Expand Up @@ -100,12 +104,19 @@ public String getClassPath() {
}

/**
* @return Java bytecode version to target.
* @return Java version to target.
*/
public int getVersionTarget() {
return versionTarget;
}

/**
* @return Java version to target via down sampling. Negative to disable downs sampling.
*/
public int getDownsampleTarget() {
return Math.min(downsampleTarget, JavacCompiler.MIN_DOWNSAMPLE_VER);
}

/**
* @return Debug flag to include variable info.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public final class JavacArgumentsBuilder {
private String classSource;
private String classPath = System.getProperty("java.class.path");
private int versionTarget = JavaVersion.get();
private int downsampleTarget = -1;
private boolean debugVariables = true;
private boolean debugLineNumbers = true;
private boolean debugSourceName = true;
Expand Down Expand Up @@ -54,9 +55,22 @@ public JavacArgumentsBuilder withClassPath(@Nullable String classPath) {
return this;
}

/**
* @param downsampleTarget
* Java version to target via down sampling. Negative to disable downs sampling.
* See: {@link JavacCompiler#MIN_DOWNSAMPLE_VER} for lowest supported target.
*
* @return Builder.
*/
@Nonnull
public JavacArgumentsBuilder withDownsampleTarget(int downsampleTarget) {
this.downsampleTarget = downsampleTarget;
return this;
}

/**
* @param versionTarget
* Java bytecode version to target.
* Java version to target.
*
* @return Builder.
*/
Expand Down Expand Up @@ -113,7 +127,7 @@ public JavacArguments build() {
throw new IllegalArgumentException("Class source must not be null");

return new JavacArguments(className, classSource,
classPath, versionTarget,
classPath, versionTarget, downsampleTarget,
debugVariables, debugLineNumbers, debugSourceName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
@ApplicationScoped
public class JavacCompiler implements Service {
public static final String SERVICE_ID = "java-compiler";
public static final int MIN_DOWNSAMPLE_VER = 8;
private static final DebuggingLogger logger = Logging.get(JavacCompiler.class);
private static final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
private final JavacCompilerConfig config;
Expand Down Expand Up @@ -125,7 +126,13 @@ public CompilerResult compile(@Nonnull JavacArguments arguments,
} else {
logger.debugging(l -> l.error("Compilation of '{}' failed", className));
}
return new CompilerResult(unitMap.getCompilations(), diagnostics);
CompileMap compilations = unitMap.getCompilations();
int downsampleTarget = arguments.getDownsampleTarget();
if (downsampleTarget >= MIN_DOWNSAMPLE_VER)
compilations.downsample(downsampleTarget);
else if (downsampleTarget >= 0)
logger.warn("Cannot downsample beyond Java {}", JavacCompiler.MIN_DOWNSAMPLE_VER);
return new CompilerResult(compilations, diagnostics);
} catch (RuntimeException ex) {
logger.debugging(l -> l.error("Compilation of '{}' crashed: {}", className, ex));
return new CompilerResult(ex);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@ public class JavacCompilerConfig extends BasicConfigContainer implements Service
private final ObservableBoolean generatePhantoms = new ObservableBoolean(true);
private final ObservableBoolean defaultEmitDebug = new ObservableBoolean(true);
private final ObservableInteger defaultTargetVersion = new ObservableInteger(-1);
private final ObservableInteger defaultDownsampleTargetVersion = new ObservableInteger(-1);

@Inject
public JavacCompilerConfig() {
super(ConfigGroups.SERVICE_COMPILE, JavacCompiler.SERVICE_ID + CONFIG_SUFFIX);
addValue(new BasicConfigValue<>("generate-phantoms", Boolean.class, generatePhantoms));
addValue(new BasicConfigValue<>("default-emit-debug", Boolean.class, defaultEmitDebug));
addValue(new BasicConfigValue<>("default-target-version", Integer.class, defaultTargetVersion));
addValue(new BasicConfigValue<>("default-compile-target-version", Integer.class, defaultTargetVersion));
addValue(new BasicConfigValue<>("default-downsample-target-version", Integer.class, defaultDownsampleTargetVersion));
}

/**
Expand Down Expand Up @@ -69,4 +71,16 @@ public ObservableBoolean getDefaultEmitDebug() {
public ObservableInteger getDefaultTargetVersion() {
return defaultTargetVersion;
}

/**
* Not enforced internally by {@link JavacCompiler}.
* Callers should check this value and ensure to call {@link JavacArgumentsBuilder#withDownsampleTarget(int)}.
*
* @return Negative to disable down sampling, otherwise target version to downsample compiled code to
* <i>(In class file version format)</i>
*/
@Nonnull
public ObservableInteger getDefaultDownsampleTargetVersion() {
return defaultDownsampleTargetVersion;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ public class JvmDecompilerPane extends AbstractDecompilePane {
private static final Logger logger = Logging.get(JvmDecompilerPane.class);
private static final ExecutorService compilePool = ThreadPoolFactory.newSingleThreadExecutor("recompile");
private final ObservableInteger javacTarget;
private final ObservableInteger javacDownsampleTarget;
private final ObservableBoolean javacDebug;
private final ModalPaneComponent overlayModal = new ModalPaneComponent();
private final PhantomGenerator phantomGenerator;
Expand All @@ -93,11 +94,12 @@ public JvmDecompilerPane(@Nonnull DecompilerPaneConfig config,
this.phantomGenerator = phantomGenerator;
this.javacDebug = new ObservableBoolean(javacConfig.getDefaultEmitDebug().getValue());
this.javacTarget = new ObservableInteger(javacConfig.getDefaultTargetVersion().getValue());
this.javacDownsampleTarget = new ObservableInteger(javacConfig.getDefaultDownsampleTargetVersion().getValue());
this.javacConfig = javacConfig;
this.javac = javac;

// Install tools container with configurator
new JvmDecompilerPaneConfigurator(toolsContainer, config, decompiler, javacTarget, javacDebug, decompilerManager);
new JvmDecompilerPaneConfigurator(toolsContainer, config, decompiler, javacTarget, javacDownsampleTarget, javacDebug, decompilerManager);
toolsContainer.install(editor);

// Setup keybindings
Expand Down Expand Up @@ -171,6 +173,7 @@ private void save() {
boolean debug = javacDebug.getValue();
JavacArgumentsBuilder builder = new JavacArgumentsBuilder()
.withVersionTarget(useConfiguredVersion(info))
.withDownsampleTarget(javacDownsampleTarget.getValue())
.withDebugVariables(debug)
.withDebugSourceName(debug)
.withDebugLineNumbers(debug)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import software.coley.observables.ObservableBoolean;
import software.coley.observables.ObservableInteger;
import software.coley.observables.ObservableObject;
import software.coley.recaf.services.compile.JavacCompiler;
import software.coley.recaf.services.decompile.DecompilerManager;
import software.coley.recaf.services.decompile.JvmDecompiler;
import software.coley.recaf.ui.control.BoundLabel;
Expand All @@ -26,6 +27,7 @@
*/
public class JvmDecompilerPaneConfigurator extends AbstractDecompilerPaneConfigurator {
private final ObservableInteger javacTarget;
private final ObservableInteger javacDownsampleTarget;
private final ObservableBoolean javacDebug;

/**
Expand All @@ -37,6 +39,8 @@ public class JvmDecompilerPaneConfigurator extends AbstractDecompilerPaneConfigu
* Local decompiler implementation.
* @param javacTarget
* Local target version for {@code javac}.
* @param javacDownsampleTarget
* Local target version to downsample to for {@code javac}.
* @param javacDebug
* Local debug flag for {@code javac}.
* @param decompilerManager
Expand All @@ -46,10 +50,12 @@ public JvmDecompilerPaneConfigurator(@Nonnull ToolsContainerComponent toolsConta
@Nonnull DecompilerPaneConfig config,
@Nonnull ObservableObject<JvmDecompiler> decompiler,
@Nonnull ObservableInteger javacTarget,
@Nonnull ObservableInteger javacDownsampleTarget,
@Nonnull ObservableBoolean javacDebug,
@Nonnull DecompilerManager decompilerManager) {
super(toolsContainer, config, decompiler, decompilerManager);
this.javacTarget = javacTarget;
this.javacDownsampleTarget = javacDownsampleTarget;
this.javacDebug = javacDebug;
}

Expand All @@ -62,12 +68,20 @@ protected GridPane createGrid() {
Label compileTitle = new BoundLabel(Lang.getBinding("service.compile"));
compileTitle.getStyleClass().addAll(Styles.TEXT_UNDERLINED, Styles.TITLE_4);
Label labelTargetVersion = new BoundLabel(Lang.getBinding("java.targetversion"));
Label labelTargetDownsampleVersion = new BoundLabel(Lang.getBinding("java.targetdownsampleversion"));
Label labelDebug = new BoundLabel(Lang.getBinding("java.targetdebug"));
content.add(compileTitle, 0, 3, 2, 1);
content.add(labelTargetVersion, 0, 4);
content.add(fix(new JavacVersionComboBox()), 1, 4);
content.add(labelDebug, 0, 5);
content.add(fix(new ObservableCheckBox(javacDebug, Lang.getBinding("misc.enabled"))), 1, 5);

int row = content.getRowCount() + 1;
content.add(compileTitle, 0, row++, 2, 1);

content.add(labelTargetVersion, 0, row);
content.add(fix(new JavacVersionComboBox()), 1, row++);

content.add(labelTargetDownsampleVersion, 0, row);
content.add(fix(new JavacDownsampleVersionComboBox()), 1, row++);

content.add(labelDebug, 0, row);
content.add(fix(new ObservableCheckBox(javacDebug, Lang.getBinding("misc.enabled"))), 1, row++);

return content;
}
Expand All @@ -92,4 +106,25 @@ private JavacVersionComboBox() {
valueProperty().addListener((ob, old, cur) -> javacTarget.setValue(cur));
}
}

private class JavacDownsampleVersionComboBox extends ComboBox<Integer> {
private JavacDownsampleVersionComboBox() {
int max = JavaVersion.get();
for (int i = JavacCompiler.MIN_DOWNSAMPLE_VER; i <= max; i++)
getItems().add(i);

// Edge case for 'disabled'
getItems().add(-1);
setValue(-1);
setConverter(ToStringConverter.from(version -> {
int v = version;
if (v < 0)
return Lang.get("java.targetdownsampleversion.disabled");
return String.valueOf(v);
}));

// Update property.
valueProperty().addListener((ob, old, cur) -> javacDownsampleTarget.setValue(cur));
}
}
}
5 changes: 4 additions & 1 deletion recaf-ui/src/main/resources/translations/en_US.lang
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,8 @@ java.savewitherrors.title=Regarding recompile errors
java.decompiler=Decompiler
java.targetversion=Compile target version
java.targetversion.auto=Match class file version
java.targetdownsampleversion=Downsample target version
java.targetdownsampleversion.disabled=Disabled
java.targetdebug=Compile with debug info

## Search bar
Expand Down Expand Up @@ -396,7 +398,8 @@ service.compile=Compilation
service.compile.java-compiler-config=Javac
service.compile.java-compiler-config.generate-phantoms=Generate missing classes
service.compile.java-compiler-config.default-emit-debug=Default to include debug
service.compile.java-compiler-config.default-target-version=Default to class version
service.compile.java-compiler-config.default-compile-target-version=Default class version target
service.compile.java-compiler-config.default-downsample-target-version=Default downsample class version target
service.debug=Attach/Debug
service.debug.attach-config=Attach config
service.debug.attach-config.attach-jmx-bean-agent=Attach JMX bean agent
Expand Down

0 comments on commit b6218d1

Please sign in to comment.