Skip to content

Commit

Permalink
Add removal to patch model
Browse files Browse the repository at this point in the history
  • Loading branch information
Col-E committed Aug 22, 2024
1 parent 2dee74f commit 8b69118
Show file tree
Hide file tree
Showing 6 changed files with 275 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,21 @@
import me.darknet.assembler.error.Error;
import org.slf4j.Logger;
import software.coley.recaf.analytics.logging.Logging;
import software.coley.recaf.info.Info;
import software.coley.recaf.info.JvmClassInfo;
import software.coley.recaf.info.TextFileInfo;
import software.coley.recaf.path.ClassPathNode;
import software.coley.recaf.path.FilePathNode;
import software.coley.recaf.path.PathNode;
import software.coley.recaf.services.Service;
import software.coley.recaf.services.assembler.AssemblerPipelineManager;
import software.coley.recaf.services.assembler.JvmAssemblerPipeline;
import software.coley.recaf.services.workspace.patch.model.JvmAssemblerPatch;
import software.coley.recaf.services.workspace.patch.model.RemovePath;
import software.coley.recaf.services.workspace.patch.model.TextFilePatch;
import software.coley.recaf.services.workspace.patch.model.WorkspacePatch;
import software.coley.recaf.util.StringDiff;
import software.coley.recaf.workspace.model.bundle.Bundle;
import software.coley.recaf.workspace.model.bundle.FileBundle;
import software.coley.recaf.workspace.model.bundle.JvmClassBundle;

Expand Down Expand Up @@ -58,13 +62,24 @@ public void apply(@Nonnull WorkspacePatch patch, @Nullable Consumer<List<Error>>
List<Runnable> tasks = new ArrayList<>();
ErrorDelegate errorConsumerDelegate = new ErrorDelegate(errorConsumer);

for (RemovePath removal : patch.removals()) {
PathNode<?> path = removal.path();
Info toRemove = path.getValueOfType(Info.class);
Bundle<?> containingBundle = path.getValueOfType(Bundle.class);
if (toRemove != null && containingBundle != null) {
String entryName = toRemove.getName();
if (containingBundle.remove(entryName) == null)
logger.warn("Could not apply removal for path '{}' - not found in the workspace", entryName);
}
}

JvmAssemblerPipeline jvmAssemblerPipeline = assemblerPipelineManager.getJvmAssemblerPipeline();
for (JvmAssemblerPatch jvmAssemblerPatch : patch.jvmAssemblerPatches()) {
// Skip if any errors have been seen.
if (errorConsumerDelegate.hasSeenErrors())
return;

ClassPathNode path = jvmAssemblerPatch.path();
ClassPathNode path = jvmAssemblerPatch.path().withCurrentWorkspaceContent();
JvmClassInfo jvmClass = path.getValue().asJvmClass();
JvmClassBundle jvmBundle = path.getValueOfType(JvmClassBundle.class);
if (jvmBundle == null)
Expand Down Expand Up @@ -104,7 +119,7 @@ public void apply(@Nonnull WorkspacePatch patch, @Nullable Consumer<List<Error>>
if (errorConsumerDelegate.hasSeenErrors())
return;

FilePathNode path = filePatch.path();
FilePathNode path = filePatch.path().withCurrentWorkspaceContent();
TextFileInfo textFile = path.getValue().asTextFile();
FileBundle fileBundle = path.getValueOfType(FileBundle.class);
if (fileBundle == null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,18 @@
import me.darknet.assembler.error.Result;
import org.slf4j.Logger;
import software.coley.recaf.analytics.logging.Logging;
import software.coley.recaf.info.ClassInfo;
import software.coley.recaf.info.FileInfo;
import software.coley.recaf.info.Info;
import software.coley.recaf.info.JvmClassInfo;
import software.coley.recaf.info.*;
import software.coley.recaf.path.*;
import software.coley.recaf.services.Service;
import software.coley.recaf.services.assembler.AssemblerPipelineManager;
import software.coley.recaf.services.workspace.patch.model.JvmAssemblerPatch;
import software.coley.recaf.services.workspace.patch.model.RemovePath;
import software.coley.recaf.services.workspace.patch.model.TextFilePatch;
import software.coley.recaf.services.workspace.patch.model.WorkspacePatch;
import software.coley.recaf.util.StringDiff;
import software.coley.recaf.workspace.model.Workspace;
import software.coley.recaf.workspace.model.bundle.Bundle;
import software.coley.recaf.workspace.model.bundle.ClassBundle;
import software.coley.recaf.workspace.model.resource.WorkspaceResource;

import java.io.IOException;
Expand Down Expand Up @@ -109,6 +108,7 @@ public WorkspacePatch deserializePatch(@Nonnull Workspace workspace, @Nonnull St
*/
@Nonnull
public WorkspacePatch createPatch(@Nonnull Workspace workspace) throws PatchGenerationException {
List<RemovePath> removals = new ArrayList<>();
List<JvmAssemblerPatch> jvmAssemblerPatches = new ArrayList<>();
List<TextFilePatch> textFilePatches = new ArrayList<>();
PatchConsumer<ClassPathNode, JvmClassInfo> classConsumer = (classPath, initial, current) -> {
Expand All @@ -128,18 +128,40 @@ public WorkspacePatch createPatch(@Nonnull Workspace workspace) throws PatchGene
String initialDisassemble = initialDisassembleRes.get();
String currentDisassemble = currentDisassembleRes.get();
List<StringDiff.Diff> assemblerDiffs = StringDiff.diff(initialDisassemble, currentDisassemble);
jvmAssemblerPatches.add(new JvmAssemblerPatch(initialPath, assemblerDiffs));
if (!assemblerDiffs.isEmpty())
jvmAssemblerPatches.add(new JvmAssemblerPatch(initialPath, assemblerDiffs));
};
PatchConsumer<FilePathNode, FileInfo> fileConsumer = (filePath, initial, current) -> {
if (!initial.isTextFile() || !current.isTextFile()) {
if (initial.isTextFile() && current.isTextFile()) {
String initialText = initial.asTextFile().getText();
String currentText = current.asTextFile().getText();
List<StringDiff.Diff> textDiffs = StringDiff.diff(initialText, currentText);
if (!textDiffs.isEmpty())
textFilePatches.add(new TextFilePatch(filePath, textDiffs));
} else {
// TODO: Support binary patches of non-text files
logger.debug("Skipping file diff for '{}' as it is not a text file", initial.getName());
return;
}
};

try {
WorkspaceResource resource = workspace.getPrimaryResource();
ResourcePathNode resourcePath = PathNodes.resourcePath(workspace, resource);
resource.bundleStream().forEach(b -> {
BundlePathNode bundlePath = resourcePath.child(b);
Set<String> removedKeys = b.getRemovedKeys();
for (String key : removedKeys) {
if (b instanceof ClassBundle<?>) {
ClassInfo stub = new StubClassInfo(key);
ClassPathNode stubPath = bundlePath.child(stub.getPackageName()).child(stub);
removals.add(new RemovePath(stubPath));
} else {
FileInfo stub = new StubFileInfo(key);
FilePathNode stubPath = bundlePath.child(stub.getDirectoryName()).child(stub);
removals.add(new RemovePath(stubPath));
}
}
});
visitDirtyItems(workspace, resource, resource.getJvmClassBundle(), classConsumer);
for (var entry : resource.getVersionedJvmClassBundles().entrySet()) {
visitDirtyItems(workspace, resource, entry.getValue(), classConsumer);
Expand All @@ -150,6 +172,7 @@ public WorkspacePatch createPatch(@Nonnull Workspace workspace) throws PatchGene
}

return new WorkspacePatch(workspace,
Collections.unmodifiableList(removals),
Collections.unmodifiableList(jvmAssemblerPatches),
Collections.unmodifiableList(textFilePatches));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import jakarta.annotation.Nonnull;
import software.coley.recaf.info.Info;
import software.coley.recaf.path.ClassPathNode;
import software.coley.recaf.path.FilePathNode;
import software.coley.recaf.services.workspace.patch.model.JvmAssemblerPatch;
import software.coley.recaf.services.workspace.patch.model.RemovePath;
import software.coley.recaf.services.workspace.patch.model.TextFilePatch;
import software.coley.recaf.services.workspace.patch.model.WorkspacePatch;
import software.coley.recaf.util.StringDiff;
Expand All @@ -27,6 +29,7 @@
*/
public class PatchSerialization {
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private static final String KEY_REMOVALS = "removals";
private static final String KEY_CLASS_JVM_ASM_DIFFS = "class-jvm-asm-diffs";
private static final String KEY_FILE_TEXT_DIFFS = "file-text-diffs";
private static final String KEY_NAME = "name";
Expand All @@ -38,6 +41,8 @@ public class PatchSerialization {
private static final String KEY_START_B = "start-b";
private static final String KEY_END_B = "end-b";
private static final String KEY_TEXT_B = "text-b";
private static final String TYPE_CLASS = "class";
private static final String TYPE_FILE = "file";

private PatchSerialization() {}

Expand All @@ -54,43 +59,75 @@ public static String serialize(@Nonnull WorkspacePatch patch) {
StringWriter out = new StringWriter();
try {
JsonWriter jw = GSON.newJsonWriter(out);
List<RemovePath> removals = patch.removals();
List<JvmAssemblerPatch> jvmAssemblerPatches = patch.jvmAssemblerPatches();
List<TextFilePatch> textFilePatches = patch.textFilePatches();

jw.beginObject();
if (!jvmAssemblerPatches.isEmpty()) {
jw.name(KEY_CLASS_JVM_ASM_DIFFS).beginArray();
for (JvmAssemblerPatch classPatch : jvmAssemblerPatches) {
String className = classPatch.path().getValue().getName();
jw.beginObject();
jw.name(KEY_NAME).value(className);
jw.name(KEY_DIFFS).beginArray();
for (StringDiff.Diff assemblerDiff : classPatch.assemblerDiffs())
serializeStringDiff(jw, assemblerDiff);
jw.endArray().endObject();
serializeRemovals(jw, removals);
serializeJvmAsmPatches(jvmAssemblerPatches, jw);
serializeTextPatches(textFilePatches, jw);
} catch (Exception ex) {
throw new IllegalStateException("Failed to create json writer for patch", ex);
}

return out.toString();
}

private static void serializeRemovals(@Nonnull JsonWriter jw, @Nonnull List<RemovePath> removals) throws IOException {
jw.beginObject();
if (!removals.isEmpty()) {
jw.name(KEY_REMOVALS).beginArray();
for (RemovePath removal : removals) {
Info info = removal.path().getValueOfType(Info.class);
if (info == null)
continue;

String name = info.getName();
jw.beginObject();
if (info.isClass()) {
jw.name(KEY_TYPE).value(TYPE_CLASS);
jw.name(KEY_NAME).value(name);
} else if (info.isFile()) {
jw.name(KEY_TYPE).value(TYPE_FILE);
jw.name(KEY_NAME).value(name);
}
jw.endArray();
jw.endObject();
}
jw.endArray();
}
}

if (!textFilePatches.isEmpty()) {
jw.name(KEY_FILE_TEXT_DIFFS).beginArray();
for (TextFilePatch textPatch : textFilePatches) {
String fileName = textPatch.path().getValue().getName();
jw.beginObject();
jw.name(KEY_NAME).value(fileName);
jw.name(KEY_DIFFS).beginArray();
for (StringDiff.Diff assemblerDiff : textPatch.textDiffs())
serializeStringDiff(jw, assemblerDiff);
jw.endArray().endObject();
}
jw.endArray();
private static void serializeJvmAsmPatches(@Nonnull List<JvmAssemblerPatch> jvmAssemblerPatches, @Nonnull JsonWriter jw) throws IOException {
if (!jvmAssemblerPatches.isEmpty()) {
jw.name(KEY_CLASS_JVM_ASM_DIFFS).beginArray();
for (JvmAssemblerPatch classPatch : jvmAssemblerPatches) {
String className = classPatch.path().getValue().getName();
jw.beginObject();
jw.name(KEY_NAME).value(className);
jw.name(KEY_DIFFS).beginArray();
for (StringDiff.Diff assemblerDiff : classPatch.assemblerDiffs())
serializeStringDiff(jw, assemblerDiff);
jw.endArray().endObject();
}
jw.endObject();
} catch (Exception ex) {
throw new IllegalStateException("Failed to create json writer for patch", ex);
jw.endArray();
}
}

return out.toString();
private static void serializeTextPatches(@Nonnull List<TextFilePatch> textFilePatches, @Nonnull JsonWriter jw) throws IOException {
if (!textFilePatches.isEmpty()) {
jw.name(KEY_FILE_TEXT_DIFFS).beginArray();
for (TextFilePatch textPatch : textFilePatches) {
String fileName = textPatch.path().getValue().getName();
jw.beginObject();
jw.name(KEY_NAME).value(fileName);
jw.name(KEY_DIFFS).beginArray();
for (StringDiff.Diff assemblerDiff : textPatch.textDiffs())
serializeStringDiff(jw, assemblerDiff);
jw.endArray().endObject();
}
jw.endArray();
}
jw.endObject();
}

private static void serializeStringDiff(@Nonnull JsonWriter jw, @Nonnull StringDiff.Diff diff) throws IOException {
Expand Down Expand Up @@ -120,10 +157,11 @@ private static void serializeStringDiff(@Nonnull JsonWriter jw, @Nonnull StringD
*/
@Nonnull
public static WorkspacePatch deserialize(@Nonnull Workspace workspace, @Nonnull String patchContents) throws PatchGenerationException {
List<RemovePath> removals = Collections.emptyList();
List<JvmAssemblerPatch> jvmAssemblerPatches = Collections.emptyList();
List<TextFilePatch> textFilePatches = Collections.emptyList();
if (patchContents.isBlank() || patchContents.charAt(0) != '{' || patchContents.charAt(patchContents.length() - 1) != '}')
return new WorkspacePatch(workspace, jvmAssemblerPatches, textFilePatches);
return new WorkspacePatch(workspace, removals, jvmAssemblerPatches, textFilePatches);
try {
JsonReader jr = GSON.newJsonReader(new StringReader(patchContents));
jr.beginObject();
Expand All @@ -133,15 +171,49 @@ public static WorkspacePatch deserialize(@Nonnull Workspace workspace, @Nonnull
jvmAssemblerPatches = deserializeClassJvmAsmDiffs(workspace, jr);
} else if (name.equals(KEY_FILE_TEXT_DIFFS)) {
textFilePatches = deserializeFileTextDiffs(workspace, jr);
} else if (name.equals(KEY_REMOVALS)) {
removals = deserializeRemovals(workspace, jr);
}
}
jr.endObject();
return new WorkspacePatch(workspace, jvmAssemblerPatches, textFilePatches);
return new WorkspacePatch(workspace, removals, jvmAssemblerPatches, textFilePatches);
} catch (Exception ex) {
throw new PatchGenerationException(ex, "Failed to parse patch contents");
}
}

@Nonnull
private static List<RemovePath> deserializeRemovals(@Nonnull Workspace workspace, @Nonnull JsonReader jr) throws IOException, PatchGenerationException {
List<RemovePath> removals = new ArrayList<>();
jr.beginArray();
while (jr.hasNext()) {
String name = null;
String type = null;
jr.beginObject();
while (jr.hasNext()) {
String key = jr.nextName();
if (key.equals(KEY_NAME))
name = jr.nextString();
else if (key.equals(KEY_TYPE))
type = jr.nextString();
}
jr.endObject();

// Construct the removal
if (name != null) {
if (TYPE_CLASS.equals(type)) {
FilePathNode path = workspace.findFile(name);
if (path != null) removals.add(new RemovePath(path));
} else if (TYPE_FILE.equals(type)) {
ClassPathNode path = workspace.findClass(name);
if (path != null) removals.add(new RemovePath(path));
}
}
}
jr.endArray();
return removals;
}

@Nonnull
private static List<JvmAssemblerPatch> deserializeClassJvmAsmDiffs(@Nonnull Workspace workspace, @Nonnull JsonReader jr) throws IOException, PatchGenerationException {
List<JvmAssemblerPatch> patches = new ArrayList<>();
Expand Down Expand Up @@ -182,7 +254,7 @@ private static List<TextFilePatch> deserializeFileTextDiffs(@Nonnull Workspace w
while (jr.hasNext()) {
String key = jr.nextName();
if (key.equals(KEY_NAME))
name = key;
name = jr.nextString();
else if (key.equals(KEY_DIFFS))
diffs = deserializeStringDiffs(jr);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package software.coley.recaf.services.workspace.patch.model;

import jakarta.annotation.Nonnull;
import software.coley.recaf.path.PathNode;

/**
* Outlines the removal of a given path.
*
* @param path
* Path to the class/file to remove.
*
* @author Matt Coley
*/
public record RemovePath(@Nonnull PathNode<?> path) {}
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,16 @@
* @author Matt Coley
*/
public record WorkspacePatch(@Nonnull Workspace workspace,
@Nonnull List<RemovePath> removals,
@Nonnull List<JvmAssemblerPatch> jvmAssemblerPatches,
@Nonnull List<TextFilePatch> textFilePatches) {

// TODO: Add more patch type lists
// - New classes / files
// - Special patch types for specific transformations like:
// - "replace this method with 'return 0'"
// - "change this method's modifers"
// - "change this method's modifiers"
// - Can be JVM/Android agnostic

@Override
public boolean equals(Object o) {
Expand Down
Loading

0 comments on commit 8b69118

Please sign in to comment.