Skip to content

Commit

Permalink
Add validateRecords commands, validates that the record class and pri…
Browse files Browse the repository at this point in the history
…nts out some infoto go along with it.
  • Loading branch information
modmuss50 committed Mar 18, 2021
1 parent 5b21a4b commit 4a764f5
Show file tree
Hide file tree
Showing 3 changed files with 241 additions and 0 deletions.
1 change: 1 addition & 0 deletions src/main/java/net/fabricmc/stitch/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public static void addCommand(Command command) {
addCommand(new CommandReorderTinyV2());
addCommand(new CommandMergeTinyV2());
addCommand(new CommandProposeV2FieldNames());
addCommand(new CommandValidateRecords());
}

public static void main(String[] args) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package net.fabricmc.stitch.commands;

import net.fabricmc.stitch.Command;
import net.fabricmc.stitch.util.RecordValidator;

import java.io.File;
import java.io.FileNotFoundException;

public class CommandValidateRecords extends Command {
public CommandValidateRecords() {
super("validateRecords");
}

@Override
public String getHelpString() {
return "<jar>";
}

@Override
public boolean isArgumentCountValid(int count) {
return count == 1;
}

@Override
public void run(String[] args) throws Exception {
File file = new File(args[0]);

if (!file.exists() || !file.isFile()) {
throw new FileNotFoundException("JAR could not be found!");
}

try (RecordValidator validator = new RecordValidator(file, true)) {
try {
validator.validate();
} catch (RecordValidator.RecordValidationException e) {
for (String error : e.errors) {
System.err.println(error);
}
throw e;
}
}

System.out.println("Record validation successful!");
}
}
195 changes: 195 additions & 0 deletions src/main/java/net/fabricmc/stitch/util/RecordValidator.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
package net.fabricmc.stitch.util;


import org.objectweb.asm.ClassReader;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.FieldNode;
import org.objectweb.asm.tree.InvokeDynamicInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.RecordComponentNode;

import java.io.File;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.LinkedList;
import java.util.List;

public class RecordValidator implements AutoCloseable {
private static final String[] REQUIRED_METHOD_SIGNATURES = new String[]{
"toString()Ljava/lang/String;",
"hashCode()I",
"equals(Ljava/lang/Object;)Z"
};

private final StitchUtil.FileSystemDelegate inputFs;
private final Path inputJar;
private final boolean printInfo;

private final List<String> errors = new LinkedList<>();

public RecordValidator(File jarFile, boolean printInfo) throws IOException {
this.inputJar = (inputFs = StitchUtil.getJarFileSystem(jarFile, false)).get().getPath("/");
this.printInfo = printInfo;
}

public void validate() throws IOException, RecordValidationException {
Files.walkFileTree(inputJar, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if (attrs.isDirectory()) {
return FileVisitResult.CONTINUE;
}

if (file.getFileName().toString().endsWith(".class")) {
byte[] classBytes = Files.readAllBytes(file);
validateClass(classBytes);
}

return FileVisitResult.CONTINUE;
}
});

if (!errors.isEmpty()) {
throw new RecordValidationException(errors);
}
}

// Returns true when a record
private boolean validateClass(byte[] classBytes) {
ClassNode classNode = new ClassNode(StitchUtil.ASM_VERSION);
ClassReader classReader = new ClassReader(classBytes);
classReader.accept(classNode, 0);

if ((classNode.access & Opcodes.ACC_RECORD) == 0) {
// Not a record
return false;
}

for (RecordComponentNode component : classNode.recordComponents) {
// Ensure that a matching method is present
boolean foundMethod = false;
for (MethodNode method : classNode.methods) {
if (method.name.equals(component.name) && method.desc.equals("()" +component.descriptor)) {
foundMethod = true;
break;
}
}

// Ensure that a matching field is present
boolean foundField = false;
for (FieldNode field : classNode.fields) {
if (field.name.equals(component.name) && field.desc.equals(component.descriptor)) {
foundField = true;
break;
}
}

if (!foundMethod) {
errors.add(String.format("Could not find matching getter method for %s()%s in %s", component.name, component.descriptor, classNode.name));
}

if (!foundField) {
errors.add(String.format("Could not find matching field for %s;%s in %s", component.name, component.descriptor, classNode.name));
}
}

// Ensure that all of the expected methods are present
for (String requiredMethodSignature : REQUIRED_METHOD_SIGNATURES) {
boolean foundMethod = false;
for (MethodNode method : classNode.methods) {
if ((method.name + method.desc).equals(requiredMethodSignature)) {
foundMethod = true;
break;
}
}

if (!foundMethod) {
errors.add(String.format("Could not find required method %s in %s", requiredMethodSignature, classNode.name));
}
}

if (printInfo) {
printInfo(classNode);
}

// This is a record
return true;
}

// Just print some info out about the record.
private void printInfo(ClassNode classNode) {
StringBuilder sb = new StringBuilder();

sb.append("Found record ").append(classNode.name).append(" with components:\n");

for (RecordComponentNode componentNode : classNode.recordComponents) {
sb.append('\t').append(componentNode.name).append("\t").append(componentNode.descriptor).append('\n');
}

String toString = extractToString(classNode);

if (toString != null) {
sb.append("toString: ").append(toString).append('\n');
}

System.out.print(sb.append('\n').toString());
}

// Pulls out the string used in the toString call, can hopefully be used to auto populate these names.
private String extractToString(ClassNode classNode) {
MethodNode methodNode = null;

for (MethodNode method : classNode.methods) {
if ((method.name + method.desc).equals("toString()Ljava/lang/String;")) {
methodNode = method;
break;
}
}

if (methodNode == null) {
return null;
}

for (AbstractInsnNode insnNode : methodNode.instructions) {
if (insnNode instanceof InvokeDynamicInsnNode) {
InvokeDynamicInsnNode invokeDynamic = (InvokeDynamicInsnNode) insnNode;
if (
!invokeDynamic.name.equals("toString") ||
!invokeDynamic.desc.equals(String.format("(L%s;)Ljava/lang/String;", classNode.name)) ||
!invokeDynamic.bsm.getName().equals("bootstrap") ||
!invokeDynamic.bsm.getOwner().equals("java/lang/runtime/ObjectMethods")
) {
// Not what we are looking for
continue;
}

for (Object bsmArg : invokeDynamic.bsmArgs) {
if (bsmArg instanceof String) {
return (String) bsmArg;
}
}
}
}

return null;
}

@Override
public void close() throws Exception {
inputFs.close();
}

public static class RecordValidationException extends Exception {
public final List<String> errors;

public RecordValidationException(List<String> errors) {
this.errors = errors;
}
}
}

0 comments on commit 4a764f5

Please sign in to comment.