diff --git a/recaf-core/src/main/java/software/coley/recaf/info/JvmClassInfo.java b/recaf-core/src/main/java/software/coley/recaf/info/JvmClassInfo.java index b2d16ec9b..c9fd5ae02 100644 --- a/recaf-core/src/main/java/software/coley/recaf/info/JvmClassInfo.java +++ b/recaf-core/src/main/java/software/coley/recaf/info/JvmClassInfo.java @@ -8,6 +8,7 @@ import software.coley.recaf.info.properties.builtin.ReferencedClassesProperty; import software.coley.recaf.info.properties.builtin.StringDefinitionsProperty; import software.coley.recaf.util.Types; +import software.coley.recaf.util.visitors.TypeVisitor; import java.util.*; import java.util.function.Consumer; @@ -66,38 +67,64 @@ default NavigableSet getReferencedClasses() { Set classNames = new HashSet<>(); ClassReader reader = getClassReader(); + + // Iterate over pool entries. Supe fast way to discover most of the referenced types. int itemCount = reader.getItemCount(); char[] buffer = new char[reader.getMaxStringLength()]; for (int i = 1; i < itemCount; i++) { int offset = reader.getItem(i); if (offset >= 10) { - int itemTag = reader.readByte(offset - 1); - if (itemTag == ConstantPoolConstants.CLASS) { - String className = reader.readUTF8(offset, buffer); - if (className.isEmpty()) - continue; - addName(className, classNames); - } else if (itemTag == ConstantPoolConstants.NAME_TYPE) { - String desc = reader.readUTF8(offset + 2, buffer); - if (desc.isEmpty()) - continue; - if (desc.charAt(0) == '(') { - Type methodType = Type.getMethodType(desc); - for (Type argumentType : methodType.getArgumentTypes()) - addType(argumentType, classNames); - Type returnType = methodType.getReturnType(); - addType(returnType, classNames); - } else { - Type type = Type.getType(desc); - addType(type, classNames); + try { + int itemTag = reader.readByte(offset - 1); + if (itemTag == ConstantPoolConstants.CLASS) { + String className = reader.readUTF8(offset, buffer); + if (className.isEmpty()) + continue; + addName(className, classNames); + } else if (itemTag == ConstantPoolConstants.NAME_TYPE) { + String desc = reader.readUTF8(offset + 2, buffer); + if (desc.isEmpty()) + continue; + if (desc.charAt(0) == '(') { + addMethodType(Type.getMethodType(desc), classNames); + } else { + Type type = Type.getType(desc); + addType(type, classNames); + } + } else if (itemTag == ConstantPoolConstants.METHOD_TYPE) { + String methodDesc = reader.readUTF8(offset, buffer); + if (methodDesc.isEmpty() || methodDesc.charAt(0) != '(') + continue; + addType(Type.getMethodType(methodDesc), classNames); } + } catch (Throwable ignored) { + // Exists only to catch situations where obfuscators put unused junk pool entries + // with malformed descriptors, which cause ASM's type parser to crash. } } } + + // In some cases like interface classes, there may be UTF8 pool entries outlining method descriptors which + // are not directly linked in NameType or MethodType pool entries. We need to iterate over fields and methods + // to get the descriptors in these cases. + reader.accept(new TypeVisitor(t -> { + if (t.getSort() == Type.METHOD) + addMethodType(t, classNames); + else + addType(t, classNames); + }), ClassReader.SKIP_DEBUG | ClassReader.SKIP_CODE); + ReferencedClassesProperty.set(this, classNames); return Objects.requireNonNull(ReferencedClassesProperty.get(this)); } + private static void addMethodType(@Nonnull Type methodType, @Nonnull Set classNames) { + for (Type argumentType : methodType.getArgumentTypes()) + addType(argumentType, classNames); + Type returnType = methodType.getReturnType(); + addType(returnType, classNames); + } + private static void addType(@Nonnull Type type, @Nonnull Set classNames) { if (type.getSort() == Type.ARRAY) type = type.getElementType(); diff --git a/recaf-core/src/main/java/software/coley/recaf/util/visitors/TypeVisitor.java b/recaf-core/src/main/java/software/coley/recaf/util/visitors/TypeVisitor.java new file mode 100644 index 000000000..f97427137 --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/util/visitors/TypeVisitor.java @@ -0,0 +1,139 @@ +package software.coley.recaf.util.visitors; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import org.objectweb.asm.*; +import org.slf4j.Logger; +import software.coley.recaf.RecafConstants; +import software.coley.recaf.analytics.logging.Logging; + +import java.util.function.Consumer; + +/** + * Visitor to accept top-level types of referenced fields, methods, annotations, and nest mates. + * + * @author Matt Coley + */ +public class TypeVisitor extends ClassVisitor { + private static final Logger logger = Logging.get(TypeVisitor.class); + private final Consumer typeConsumer; + + /** + * @param typeConsumer + * Type consumer to accept seen types. + * The same type may be visited multiple times. + * Method types are also passed in. + */ + public TypeVisitor(@Nonnull Consumer typeConsumer) { + super(RecafConstants.getAsmVersion()); + this.typeConsumer = typeConsumer; + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + if (interfaces != null) + for (String exception : interfaces) + acceptType(exception); + acceptType(superName); + } + + @Override + public void visitSource(String source, String debug) { + // no-op + } + + @Override + public ModuleVisitor visitModule(String name, int access, String version) { + return null; + } + + @Override + public void visitNestHost(String nestHost) { + // no-op + } + + @Override + public void visitOuterClass(String owner, String name, String descriptor) { + acceptDescriptor(descriptor); + } + + @Override + public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + acceptDescriptor(descriptor); + return null; + } + + @Override + public AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath, String descriptor, boolean visible) { + acceptDescriptor(descriptor); + return null; + } + + @Override + public void visitAttribute(Attribute attribute) { + // no-op + } + + @Override + public void visitNestMember(String nestMember) { + acceptType(nestMember); + } + + @Override + public void visitPermittedSubclass(String permittedSubclass) { + acceptType(permittedSubclass); + } + + @Override + public void visitInnerClass(String name, String outerName, String innerName, int access) { + // no-op + } + + @Override + public RecordComponentVisitor visitRecordComponent(String name, String descriptor, String signature) { + acceptDescriptor(descriptor); + return null; + } + + @Override + public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { + acceptDescriptor(descriptor); + return null; + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + acceptDescriptor(descriptor); + if (exceptions != null) + for (String exception : exceptions) + acceptType(exception); + return null; + } + + private void acceptType(@Nullable String internalName) { + if (internalName == null || internalName.isEmpty()) return; + + try { + Type methodType = Type.getObjectType(internalName); + typeConsumer.accept(methodType); + } catch (Throwable t) { + logger.trace("Ignored invalid internal name: {}", internalName, t); + } + } + + private void acceptDescriptor(@Nullable String descriptor) { + if (descriptor == null || descriptor.isEmpty()) return; + + try { + if (descriptor.charAt(0) == '(') { + Type methodType = Type.getMethodType(descriptor); + typeConsumer.accept(methodType); + } else { + Type type = Type.getType(descriptor); + typeConsumer.accept(type); + } + } catch (Throwable t) { + logger.trace("Ignored invalid type: {}", descriptor, t); + } + } +}