Skip to content

Commit 528e58f

Browse files
authored
Merge pull request #3 from ENDERZOMBI102/refactor/binarypatcher
feat: Add option to change diff context size, change binary format to NeoForge's
2 parents ad8ec1e + b49c633 commit 528e58f

File tree

9 files changed

+144
-47
lines changed

9 files changed

+144
-47
lines changed

.editorconfig

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
root = true
2+
3+
[*]
4+
indent_size = 4
5+
indent_style = space

build.gradle.kts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,11 @@ dependencies {
4242

4343
implementation("xyz.wagyourtail.unimined:unimined:1.3.9")
4444
"installerImplementation"("io.github.java-diff-utils:java-diff-utils:4.12")
45-
"installerImplementation"("io.github.prcraftmc:class-diff:1.0-SNAPSHOT")
45+
"installerImplementation"("net.neoforged.installertools:binarypatcher:3.0.2")
46+
"installerImplementation"("org.ow2.asm:asm:9.7")
47+
"installerImplementation"("org.ow2.asm:asm-tree:9.7")
4648
"installerImplementation"("org.jetbrains:annotations:24.0.1")
49+
"installerImplementation"("com.nothome:javaxdelta:2.0.1")
4750
}
4851

4952
gradlePlugin {
@@ -116,4 +119,4 @@ publishing {
116119
}
117120
}
118121
}
119-
}
122+
}

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@ org.gradle.parallel=true
55

66
maven_group=xyz.wagyourtail.unimined
77
archives_base_name=patchbase
8-
version=1.0.2
8+
version=1.1.0

src/installer/java/xyz/wagyourtail/patchbase/installer/PatchbaseInstaller.java

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
package xyz.wagyourtail.patchbase.installer;
22

3-
import io.github.prcraftmc.classdiff.ClassPatcher;
4-
import io.github.prcraftmc.classdiff.format.DiffReader;
3+
import com.nothome.delta.GDiffPatcher;
4+
import net.neoforged.binarypatcher.Patch;
55
import org.objectweb.asm.ClassReader;
66
import org.objectweb.asm.ClassWriter;
7-
import org.objectweb.asm.tree.ClassNode;
87

98
import java.io.IOException;
109
import java.io.InputStream;
@@ -17,6 +16,8 @@
1716
import java.util.zip.ZipOutputStream;
1817

1918
public class PatchbaseInstaller {
19+
private static final GDiffPatcher PATCHER = new GDiffPatcher();
20+
private static final byte[] EMPTY_DATA = new byte[0];
2021

2122
/*
2223
* create your installer around this method.
@@ -26,18 +27,21 @@ public void patch(Path patchJar, Path baseJar, Path outputJar) throws IOExceptio
2627
Files.copy(baseJar, outputJar, StandardCopyOption.REPLACE_EXISTING);
2728
try (FileSystem fs = openZipFileSystem(outputJar)) {
2829
forEachInZip(patchJar, (entry, is) -> {
29-
if (entry.endsWith(".cdiff")) {
30+
if (entry.endsWith(".binpatch")) {
3031
try {
31-
readZipInputStreamFor(baseJar, entry.substring(0, entry.length() - 6), true, original -> {
32+
readZipInputStreamFor(baseJar, entry.substring(0, entry.length() - 6), true, originalStream -> {
3233
try {
33-
ClassWriter cw = new ClassWriter(0);
34-
ClassNode node = new ClassNode();
35-
new ClassReader(original).accept(node, ClassReader.SKIP_DEBUG);
36-
ClassPatcher.patch(node, new DiffReader(is.readAllBytes()));
37-
node.accept(cw);
38-
Path p = fs.getPath(entry.substring(0, entry.length() - 6));
39-
if (p.getParent() != null) Files.createDirectories(p.getParent());
40-
Files.write(p, cw.toByteArray(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
34+
byte[] original = new ClassWriter(new ClassReader(originalStream), ClassReader.SKIP_DEBUG).toByteArray();
35+
byte[] result = patch(original, Patch.from(is));
36+
37+
// if we removed the file, don't write on disk
38+
if ( result != EMPTY_DATA ) {
39+
Path p = fs.getPath(entry.substring(0, entry.length() - 6));
40+
if (p.getParent() != null) {
41+
Files.createDirectories(p.getParent());
42+
}
43+
Files.write(p, result, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
44+
}
4145
} catch (IOException e) {
4246
throw new RuntimeException(e);
4347
}
@@ -106,4 +110,24 @@ public static FileSystem openZipFileSystem(Path path, Map<String, Object> args)
106110
return FileSystems.newFileSystem(URI.create("jar:" + path.toUri()), args, null);
107111
}
108112

113+
private static byte[] patch( byte[] data, Patch patch ) throws IOException {
114+
if (patch.exists && data.length == 0) {
115+
throw new IOException( "Patch expected " + patch.getName() + " to exist, but received empty data" );
116+
}
117+
if (!patch.exists && data.length > 0) {
118+
throw new IOException( "Patch expected " + patch.getName() + " to not exist, but received " + data.length + " bytes" );
119+
}
120+
121+
int checksum = patch.checksum(data);
122+
if (checksum != patch.checksum) {
123+
throw new IOException( "Patch expected " + patch.getName() + " to have the checksum " + Integer.toHexString( patch.checksum ) + " but it was " + Integer.toHexString( checksum ) );
124+
}
125+
126+
if (patch.data.length == 0) { // File removed
127+
return EMPTY_DATA;
128+
} else {
129+
return PATCHER.patch( data, patch.data );
130+
}
131+
}
132+
109133
}

src/main/kotlin/xyz/wagyourtail/patchbase/gradle/PatchBaseMinecraftTransformer.kt

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,21 @@ fun MinecraftConfig.patchBase(action: PatchBaseMinecraftTransformer.() -> Unit =
1414
customPatcher(PatchBaseMinecraftTransformer(this.project, this as MinecraftProvider), action)
1515
}
1616

17-
class PatchBaseMinecraftTransformer(project: Project, provider: MinecraftProvider) : JarModAgentMinecraftTransformer(project, provider) {
17+
class PatchBaseMinecraftTransformer(project: Project, provider: MinecraftProvider) :
18+
JarModAgentMinecraftTransformer(project, provider) {
1819
val patchBase = project.configurations.maybeCreate("patchBase".withSourceSet(provider.sourceSet))
1920

2021
override fun transform(minecraft: MinecraftJar): MinecraftJar {
2122
val patchDep = patchBase.dependencies.last()
22-
val patchJar = patchBase
23-
.incoming
24-
.artifactView { view -> view.componentFilter { it is ModuleComponentIdentifier && it.group == patchDep.group && it.version == patchDep.version && it.module == patchDep.name } }
25-
.files
26-
.first { it.extension == "jar" || it.extension == "zip" }
23+
val patchJar = patchBase
24+
.incoming
25+
.artifactView { view -> view.componentFilter { it is ModuleComponentIdentifier && it.group == patchDep.group && it.version == patchDep.version && it.module == patchDep.name } }
26+
.files
27+
.first { it.extension == "jar" || it.extension == "zip" }
2728
val outputFolder = minecraft.path
28-
.parent
29-
.resolve(patchDep.name)
30-
.resolve(patchDep.version!!)
29+
.parent
30+
.resolve(patchDep.name)
31+
.resolve(patchDep.version!!)
3132

3233
val patchedMC = MinecraftJar(minecraft, outputFolder, patches = minecraft.patches + "patchbase")
3334

src/main/kotlin/xyz/wagyourtail/patchbase/gradle/PatchExtension.kt

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import org.gradle.api.Project
44
import org.gradle.api.tasks.SourceSet
55
import org.gradle.configurationcache.extensions.capitalized
66
import org.gradle.jvm.tasks.Jar
7+
import org.jetbrains.annotations.ApiStatus
78
import xyz.wagyourtail.patchbase.gradle.tasks.ApplySourcePatchTask
89
import xyz.wagyourtail.patchbase.gradle.tasks.CreateClassPatchTask
910
import xyz.wagyourtail.patchbase.gradle.tasks.CreateSourcePatchTask
@@ -12,13 +13,31 @@ import xyz.wagyourtail.unimined.api.minecraft.MinecraftConfig
1213
import xyz.wagyourtail.unimined.api.unimined
1314
import xyz.wagyourtail.unimined.internal.minecraft.MinecraftProvider
1415
import xyz.wagyourtail.unimined.internal.minecraft.patch.jarmod.JarModAgentMinecraftTransformer
16+
import xyz.wagyourtail.unimined.util.FinalizeOnRead
1517
import xyz.wagyourtail.unimined.util.withSourceSet
1618
import kotlin.io.path.nameWithoutExtension
1719

1820
@Suppress("UnstableApiUsage")
1921
abstract class PatchExtension(val project: Project) {
22+
/**
23+
* The default value for [[CreateSourcePatchTask.diffContextSize]
24+
*/
25+
@set:ApiStatus.Experimental
26+
var diffContextSize: Int by FinalizeOnRead(3)
2027

21-
fun patchBaseCreator(sourceSet: SourceSet) {
28+
/**
29+
* The default value for [CreateSourcePatchTask.trimWhitespace]
30+
*/
31+
@set:ApiStatus.Experimental
32+
var trimWhitespace: Boolean by FinalizeOnRead(true)
33+
34+
/**
35+
* The default value for [CreateClassPatchTask.minimizePatch]
36+
*/
37+
@set:ApiStatus.Experimental
38+
var minimizePatch: Boolean by FinalizeOnRead(true)
39+
40+
fun patchBaseCreator(sourceSet: SourceSet, devJar: Boolean = false) {
2241
val mc = project.unimined.minecrafts[sourceSet]!!
2342
if (mc.side == EnvType.COMBINED) {
2443
project.logger.warn("[PatchBase/Creator ${this.project.path} ${sourceSet}] Merged may make applying patches more difficult, proceed with caution")
@@ -30,13 +49,16 @@ abstract class PatchExtension(val project: Project) {
3049
project.logger.warn("[PatchBase/Creator ${this.project.path} ${sourceSet}] mcPatcher is not a JarModAgentMinecraftTransformer, this may cause issues with dev runs")
3150
}
3251

33-
val mcp = mc as MinecraftProvider // needed for access to `getMcDevFile()`
52+
val mcp = mc as MinecraftProvider // needed for access to `getMcDevFile()`
3453

3554
project.tasks.register("createSourcePatch".withSourceSet(sourceSet), CreateSourcePatchTask::class.java) {
3655
it.group = "patchbase"
3756
it.sourceDir.set(project.file("src/${sourceSet.name}/java"))
3857
it.outputDir.set(project.file("patches/${sourceSet.name}"))
39-
val sourceFile = mc.minecraftFileDev.resolveSibling(mcp.getMcDevFile().nameWithoutExtension + "-sources.jar")
58+
it.trimWhitespace.set(trimWhitespace)
59+
it.diffContextSize.set(diffContextSize)
60+
val sourceFile =
61+
mc.minecraftFileDev.resolveSibling(mcp.getMcDevFile().nameWithoutExtension + "-sources.jar")
4062
it.sources.set(project.files(sourceFile))
4163
if (!sourceFile.exists()) {
4264
it.dependsOn("genSources")
@@ -47,7 +69,10 @@ abstract class PatchExtension(val project: Project) {
4769
it.group = "patchbase"
4870
it.patchDir.set(project.file("patches/${sourceSet.name}"))
4971
it.outputDir.set(project.file("src/${sourceSet.name}/java"))
50-
val sourceFile = mc.minecraftFileDev.resolveSibling(mcp.getMcDevFile().nameWithoutExtension + "-sources.jar")
72+
it.trimWhitespace.set(trimWhitespace)
73+
it.diffContextSize.set(diffContextSize)
74+
val sourceFile =
75+
mc.minecraftFileDev.resolveSibling(mcp.getMcDevFile().nameWithoutExtension + "-sources.jar")
5176
it.sources.set(project.files(sourceFile))
5277
if (!sourceFile.exists()) {
5378
it.dependsOn("genSources".withSourceSet(sourceSet))
@@ -56,7 +81,12 @@ abstract class PatchExtension(val project: Project) {
5681

5782
project.tasks.register("createClassPatch".withSourceSet(sourceSet), CreateClassPatchTask::class.java) {
5883
it.group = "patchbase"
59-
it.inputFile.set((project.tasks.findByName("remap" + "jar".withSourceSet(sourceSet).capitalized()) as Jar).outputs.files.singleFile)
84+
it.inputFile.set(
85+
(project.tasks.findByName(
86+
"remap" + "jar".withSourceSet(sourceSet).capitalized()
87+
) as Jar).outputs.files.singleFile
88+
)
89+
it.minimizePatch.set(minimizePatch)
6090

6191
when (mc.side) {
6292
EnvType.CLIENT -> it.classpath.set(project.files(mc.minecraftData.minecraftClientFile))
@@ -68,6 +98,21 @@ abstract class PatchExtension(val project: Project) {
6898
it.archiveClassifier.set("patch")
6999
it.dependsOn("remap" + "jar".withSourceSet(sourceSet).capitalized())
70100
}
101+
102+
if (devJar) {
103+
project.tasks.register("createDevClassPatch".withSourceSet(sourceSet), CreateClassPatchTask::class.java) {
104+
it.group = "patchbase"
105+
it.inputFile.set(
106+
(project.tasks.findByName(
107+
"jar".withSourceSet(sourceSet).capitalized()
108+
) as Jar).outputs.files.singleFile
109+
)
110+
111+
it.classpath.set(project.files(mc.getMcDevFile()))
112+
it.archiveClassifier.set("patch-dev")
113+
it.dependsOn("jar".withSourceSet(sourceSet).capitalized())
114+
}
115+
}
71116
}
72117

73118
fun patchBase(minecraftConfig: MinecraftConfig) {

src/main/kotlin/xyz/wagyourtail/patchbase/gradle/tasks/AbstractSourceTask.kt

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package xyz.wagyourtail.patchbase.gradle.tasks
22

33
import com.github.difflib.DiffUtils
44
import com.github.difflib.UnifiedDiffUtils
5-
import com.github.difflib.patch.Patch
65
import org.gradle.api.file.FileCollection
76
import org.gradle.api.internal.ConventionTask
87
import org.gradle.api.provider.Property
@@ -15,6 +14,17 @@ import kotlin.io.path.inputStream
1514
import kotlin.io.path.isDirectory
1615

1716
abstract class AbstractSourceTask : ConventionTask() {
17+
/**
18+
* Controls how much context (surrounding lines) are provided in the patch files.
19+
*/
20+
@get:Input
21+
abstract val diffContextSize: Property<Int>
22+
23+
/**
24+
* Trims leading whitespace in the patch files.
25+
*/
26+
@get:Input
27+
abstract val trimWhitespace: Property<Boolean>
1828

1929
@get:Input
2030
abstract val sources: Property<FileCollection>
@@ -42,7 +52,7 @@ abstract class AbstractSourceTask : ConventionTask() {
4252
}
4353

4454
fun diff(aName: String?, a: String, bName: String?, b: String): String {
45-
val aLines = a.lines().toMutableList()
55+
var aLines = a.lines().toMutableList()
4656
// trim end to posix
4757
for (i in aLines.indices.reversed()) {
4858
if (aLines[i].isNotBlank()) {
@@ -52,7 +62,7 @@ abstract class AbstractSourceTask : ConventionTask() {
5262
}
5363
}
5464
aLines.add("")
55-
val bLines = b.lines().toMutableList()
65+
var bLines = b.lines().toMutableList()
5666
// trim end to posix
5767
for (i in bLines.indices.reversed()) {
5868
if (bLines[i].isNotBlank()) {
@@ -62,12 +72,17 @@ abstract class AbstractSourceTask : ConventionTask() {
6272
}
6373
}
6474
bLines.add("")
65-
val patch = DiffUtils.diff(aLines.map { it.trim() }, bLines.map { it.trim() })
75+
if (trimWhitespace.get()) {
76+
aLines = aLines.map(String::trim).toMutableList()
77+
bLines = bLines.map(String::trim).toMutableList()
78+
}
79+
80+
val patch = DiffUtils.diff(aLines, bLines)
6681
patch.deltas.forEach {
6782
it.target.position
6883
it.target.lines = bLines.subList(it.target.position, it.target.position + it.target.size())
6984
}
70-
val unifiedDiff = UnifiedDiffUtils.generateUnifiedDiff(aName, bName, aLines, patch, 3)
85+
val unifiedDiff = UnifiedDiffUtils.generateUnifiedDiff(aName, bName, aLines, patch, diffContextSize.get())
7186
val sb = StringBuilder()
7287
for (s in unifiedDiff) {
7388
sb.append(s).append("\n")

src/main/kotlin/xyz/wagyourtail/patchbase/gradle/tasks/ApplySourcePatchTask.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ abstract class ApplySourcePatchTask : AbstractSourceTask() {
3232
} else {
3333
findSource(relative.resolveSibling(relative.nameWithoutExtension)) { original ->
3434
if (original != null) {
35-
targetParent.resolve(relative.nameWithoutExtension).writeText(applyDiff(original.readBytes().decodeToString(), path.readText()))
35+
targetParent.resolve(relative.nameWithoutExtension)
36+
.writeText(applyDiff(original.readBytes().decodeToString(), path.readText()))
3637
} else {
3738
throw IllegalStateException("Cannot apply patch to non-existent file: $relative")
3839
}

src/main/kotlin/xyz/wagyourtail/patchbase/gradle/tasks/CreateClassPatchTask.kt

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package xyz.wagyourtail.patchbase.gradle.tasks
22

3-
import io.github.prcraftmc.classdiff.ClassDiffer
4-
import io.github.prcraftmc.classdiff.format.DiffWriter
3+
import net.neoforged.binarypatcher.Patch
54
import org.gradle.api.file.FileCollection
65
import org.gradle.api.file.RegularFileProperty
76
import org.gradle.api.provider.Property
@@ -10,13 +9,18 @@ import org.gradle.api.tasks.InputFile
109
import org.gradle.api.tasks.TaskAction
1110
import org.gradle.jvm.tasks.Jar
1211
import org.objectweb.asm.ClassReader
13-
import org.objectweb.asm.tree.ClassNode
12+
import org.objectweb.asm.ClassWriter
1413
import xyz.wagyourtail.unimined.util.forEachInZip
1514
import xyz.wagyourtail.unimined.util.readZipInputStreamFor
1615
import java.io.InputStream
1716
import kotlin.io.path.*
1817

1918
abstract class CreateClassPatchTask : Jar() {
19+
/**
20+
* Shrinks the created `.class` patches by remapping constant pool indices.
21+
*/
22+
@get:Input
23+
abstract val minimizePatch: Property<Boolean>
2024

2125
@get:InputFile
2226
abstract val inputFile: RegularFileProperty
@@ -35,9 +39,9 @@ abstract class CreateClassPatchTask : Jar() {
3539
// find in classpath
3640
findClass(name) { original ->
3741
if (original != null) {
38-
val target = tempDir.resolve("$name.cdiff")
42+
val target = tempDir.resolve("$name.binpatch")
3943
target.parent.createDirectories()
40-
target.writeBytes(diff(original, stream))
44+
target.writeBytes(makePatch(original, stream, name))
4145
} else {
4246
val target = tempDir.resolve(name)
4347
target.parent.createDirectories()
@@ -54,12 +58,11 @@ abstract class CreateClassPatchTask : Jar() {
5458
copy()
5559
}
5660

57-
fun diff(a: InputStream, b: InputStream): ByteArray {
58-
val ra = ClassReader(a).let { ClassNode().apply { it.accept(this, ClassReader.SKIP_DEBUG) } }
59-
val rb = ClassReader(b).let { ClassNode().apply { it.accept(this, ClassReader.SKIP_DEBUG) } }
60-
val w = DiffWriter()
61-
ClassDiffer.diff(ra, rb, w)
62-
return w.toByteArray()
61+
fun makePatch(a: InputStream, b: InputStream, name: String): ByteArray {
62+
val ra = ClassWriter(ClassReader(a), ClassReader.SKIP_DEBUG).toByteArray()
63+
val rb = ClassWriter(ClassReader(b), ClassReader.SKIP_DEBUG).toByteArray()
64+
val x = Patch.from(name, "", ra, rb, minimizePatch.get())
65+
return x.toBytes()
6366
}
6467

6568
fun findClass(name: String, action: (InputStream?) -> Unit) {

0 commit comments

Comments
 (0)