Skip to content

Commit

Permalink
Ensure Tiny V2 always writes in the same order
Browse files Browse the repository at this point in the history
  • Loading branch information
IotaBread committed Aug 11, 2023
1 parent dd62dab commit 8609c2d
Show file tree
Hide file tree
Showing 4 changed files with 246 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
Expand All @@ -37,9 +38,50 @@ public TinyV2Writer(String obfHeader, String deobfHeader) {
this.deobfHeader = deobfHeader;
}

private static int getEntryKind(Entry<?> e) {
// field < method < class
if (e instanceof FieldEntry) {
return 0;
} else if (e instanceof MethodEntry) {
return 1;
} else if (e instanceof ClassEntry) {
return 2;
}

return -1;
}

private static Comparator<EntryTreeNode<EntryMapping>> mappingComparator() {
return Comparator.<EntryTreeNode<EntryMapping>>comparingInt(n -> getEntryKind(n.getEntry()))
.thenComparing(EntryTreeNode::getEntry, (o1, o2) -> {
if (o1 instanceof FieldEntry f1 && o2 instanceof FieldEntry f2) {
return f1.compareTo(f2);
} else if (o1 instanceof MethodEntry m1 && o2 instanceof MethodEntry m2) {
return m1.compareTo(m2);
} else if (o1 instanceof ClassEntry c1 && o2 instanceof ClassEntry c2) {
return c1.compareTo(c2);
} else if (o1 instanceof LocalVariableEntry v1 && o2 instanceof LocalVariableEntry v2) {
return v1.compareTo(v2);
} else {
Entry<?> p1 = o1.getParent();
Entry<?> p2 = o2.getParent();
if (p1 instanceof ClassEntry c1 && p2 instanceof ClassEntry c2) {
return c1.compareTo(c2);
} else if (p1 instanceof MethodEntry m1 && p2 instanceof MethodEntry m2) {
return m1.compareTo(m2);
}

return -1;
}
});
}

@Override
public void write(EntryTree<EntryMapping> mappings, MappingDelta<EntryMapping> delta, Path path, ProgressListener progress, MappingSaveParameters parameters) {
List<EntryTreeNode<EntryMapping>> classes = StreamSupport.stream(mappings.spliterator(), false).filter(node -> node.getEntry() instanceof ClassEntry).toList();
List<EntryTreeNode<EntryMapping>> classes = StreamSupport.stream(mappings.spliterator(), false)
.filter(node -> node.getEntry() instanceof ClassEntry)
.sorted(mappingComparator())
.toList();

try (PrintWriter writer = new LfPrintWriter(Files.newBufferedWriter(path))) {
writer.println("tiny\t2\t" + MINOR_VERSION + "\t" + this.obfHeader + "\t" + this.deobfHeader);
Expand Down Expand Up @@ -81,7 +123,7 @@ private void writeClass(PrintWriter writer, EntryTreeNode<EntryMapping> node, En

this.writeComment(writer, node.getValue(), 1);

for (EntryTreeNode<EntryMapping> child : node.getChildNodes()) {
for (EntryTreeNode<EntryMapping> child : node.getChildNodes().stream().sorted(mappingComparator()).toList()) {
Entry<?> entry = child.getEntry();
if (entry instanceof FieldEntry) {
this.writeField(writer, child);
Expand Down Expand Up @@ -112,7 +154,7 @@ private void writeMethod(PrintWriter writer, EntryTreeNode<EntryMapping> node) {

this.writeComment(writer, mapping, 2);

for (EntryTreeNode<EntryMapping> child : node.getChildNodes()) {
for (EntryTreeNode<EntryMapping> child : node.getChildNodes().stream().sorted(mappingComparator()).toList()) {
Entry<?> entry = child.getEntry();
if (entry instanceof LocalVariableEntry) {
this.writeParameter(writer, child);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,13 +241,17 @@ public static String getInnerName(String name) {

@Override
public int compareTo(ClassEntry entry) {
String name = this.getFullName();
String otherFullName = entry.getFullName();
String packageName = this.getPackageName();
String otherPackageName = entry.getPackageName();

if (name.length() != otherFullName.length()) {
return name.length() - otherFullName.length();
int p = packageName.compareTo(otherPackageName);
if (p != 0) {
return p;
}

String name = this.getFullName();
String otherFullName = entry.getFullName();

return name.compareTo(otherFullName);
}
}
31 changes: 31 additions & 0 deletions enigma/src/test/java/cuchaz/enigma/TestEntryOrdering.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package cuchaz.enigma;

import cuchaz.enigma.translation.representation.entry.ClassEntry;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class TestEntryOrdering {
@Test
public void testClasses() {
ClassEntry c1 = new ClassEntry("pkg/1665BFCF");
ClassEntry c2 = new ClassEntry("pkg/2BB46638");
ClassEntry c3 = new ClassEntry("pkg/2BB46638$AC5B2355");
ClassEntry c4 = new ClassEntry("pkg/6F88ECF");
ClassEntry c5 = new ClassEntry("pkg/6F88ECF$57C7176A");
ClassEntry c6 = new ClassEntry("pkg/FEE829A0");
ClassEntry c7 = new ClassEntry("pkg/FEE829A0$3A78ECBB");
ClassEntry c8 = new ClassEntry("pkg/294e9cee/77C137C5");
ClassEntry c9 = new ClassEntry("pkg/294e9cee/7daa64ce/158D6D20");
ClassEntry c10 = new ClassEntry("pkg/72e178af/225B1ACE");
ClassEntry c11 = new ClassEntry("pkg/aaf82493/80c8afa8/2760E349");

List<ClassEntry> classes = new ArrayList<>(List.of(c7, c6, c1, c10, c5, c9, c3, c11, c8, c4, c2));

Collections.sort(classes);
Assertions.assertEquals(List.of(c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11), classes);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package cuchaz.enigma.translation.mapping;

import cuchaz.enigma.ProgressListener;
import cuchaz.enigma.translation.mapping.serde.MappingFormat;
import cuchaz.enigma.translation.mapping.tree.EntryTree;
import cuchaz.enigma.translation.mapping.tree.HashEntryTree;
import cuchaz.enigma.translation.representation.ArgumentDescriptor;
import cuchaz.enigma.translation.representation.MethodDescriptor;
import cuchaz.enigma.translation.representation.ParameterAccessFlags;
import cuchaz.enigma.translation.representation.TypeDescriptor;
import cuchaz.enigma.translation.representation.entry.ClassEntry;
import cuchaz.enigma.translation.representation.entry.Entry;
import cuchaz.enigma.translation.representation.entry.FieldEntry;
import cuchaz.enigma.translation.representation.entry.MethodEntry;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

public class TestDeterministicWrite {
@Test
public void testTinyV2() throws Exception {
Path dir = Files.createTempDirectory("enigmaDeterministicTinyV2-");

EntryTree<EntryMapping> mappings = randomMappingTree(1L);

String prev = null;
for (int i = 0; i < 32; i++) {
Path file = dir.resolve(i + ".tiny");
MappingFormat.TINY_V2.write(mappings, file, ProgressListener.none(), null);

String content = Files.readString(file);
if (prev != null) {
Assertions.assertEquals(prev, content, "Iteration " + i + " has a different result from the previous one");
}
prev = content;
mappings = MappingFormat.TINY_V2.read(file, ProgressListener.none(), null);
}
}

public static EntryTree<EntryMapping> randomMappingTree(long seed) {
EntryTree<EntryMapping> mappings = new HashEntryTree<>();
Random random = new Random(seed);
int size = 256;

List<ClassEntry> classes = new ArrayList<>();
ClassEntry currentClass = new ClassEntry("pkg/" + hexHash("seed" + seed).toUpperCase());
classes.add(currentClass);
Entry<?> current = currentClass;
insertRandomMapping(mappings, current, random, true);
for (int i = 1; i < size; i++) {
int next = random.nextInt(9);

switch (next) {
case 0 -> {
// SubClass
int val = random.nextInt();
currentClass = new ClassEntry(currentClass, hexHash("subClass" + val).toUpperCase());
classes.add(currentClass);
current = currentClass;
insertRandomMapping(mappings, current, random, true);
}
case 1, 2, 3 -> {
// Field
int val = random.nextInt();
TypeDescriptor type = randomType(classes, random, false);
current = new FieldEntry(currentClass, hexHash("field" + val), type);
insertRandomMapping(mappings, current, random);
}
case 4, 5, 6 -> {
// Method
int val = random.nextInt();
MethodDescriptor descriptor = randomMethodDescriptor(classes, random);
current = new MethodEntry(currentClass, hexHash("method" + val), descriptor);
insertRandomMapping(mappings, current, random);
}
case 7, 8 -> {
// Class in package
int val = random.nextInt();
String pkg = randomPackage(currentClass, random);
currentClass = new ClassEntry(pkg + "/" + hexHash("class" + val).toUpperCase());
classes.add(currentClass);
current = currentClass;
insertRandomMapping(mappings, current, random, true);
}
}
}

return mappings;
}

public static TypeDescriptor randomType(List<ClassEntry> classes, Random random, boolean allowVoid) {
String desc = randomDescriptor(classes, random, allowVoid);

return new TypeDescriptor(desc);
}

private static String randomDescriptor(List<ClassEntry> classes, Random random, boolean allowVoid) {
int val = random.nextInt(allowVoid ? 10 : 9);

return switch (val) {
default -> // Class
"L" + classes.get(random.nextInt(classes.size())).getFullName() + ";";
case 1 -> "B";
case 2 -> "C";
case 3 -> "D";
case 4 -> "F";
case 5 -> "I";
case 6 -> "J";
case 7 -> "S";
case 8 -> "Z";
case 9 -> "V";
};
}

public static MethodDescriptor randomMethodDescriptor(List<ClassEntry> classes, Random random) {
int count = random.nextInt(4);
List<ArgumentDescriptor> args = new ArrayList<>();
for (int i = 0; i < count; i++) {
args.add(new ArgumentDescriptor(randomDescriptor(classes, random, false), ParameterAccessFlags.DEFAULT));
}

return new MethodDescriptor(args, randomType(classes, random, true));
}

public static String randomPackage(ClassEntry cls, Random random) {
String pkg = cls.getPackageName();
int i = random.nextInt(3);
if (i == 0) {
return pkg;
} else if (i == 1) {
return pkg.contains("/") ? pkg.substring(0, pkg.lastIndexOf('/')) : pkg;
} else {
return pkg + "/" + hexHash("pkg" + random.nextInt());
}
}

public static void insertRandomMapping(EntryTree<EntryMapping> mappings, Entry<?> entry, Random random) {
insertRandomMapping(mappings, entry, random, false);
}

public static void insertRandomMapping(EntryTree<EntryMapping> mappings, Entry<?> entry, Random random, boolean uppercase) {
String name = hexHash("name" + random.nextInt());
if (uppercase) name = name.toUpperCase();
if (entry instanceof ClassEntry cls) {
if (cls.getParent() == null) {
name = cls.getPackageName() + "/" + name;
}
}

EntryMapping mapping = new EntryMapping(name);
mappings.insert(entry, mapping);
}

public static String hexHash(String s) {
return Integer.toHexString(s.hashCode());
}
}

0 comments on commit 8609c2d

Please sign in to comment.