Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf: 2x speed up reading contents from zip/jar #761

Merged
merged 5 commits into from
Jan 19, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package software.coley.recaf.info.builder;

import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import org.objectweb.asm.*;
import software.coley.recaf.info.BasicInnerClassInfo;
import software.coley.recaf.info.BasicJvmClassInfo;
import software.coley.recaf.info.InnerClassInfo;
import software.coley.recaf.info.JvmClassInfo;
import software.coley.recaf.info.annotation.*;
import software.coley.recaf.info.member.*;
import software.coley.recaf.util.MultiMap;

import java.util.*;
import java.util.function.Consumer;
Expand All @@ -22,6 +24,9 @@
public class JvmClassInfoBuilder extends AbstractClassInfoBuilder<JvmClassInfoBuilder> {
private byte[] bytecode;
private int version = JvmClassInfo.BASE_VERSION + 8; // Java 8
private boolean skipCustomAttributeChecks = true;
@Nullable
private ClassBuilderAppender classVisitor;

/**
* Create empty builder.
Expand Down Expand Up @@ -89,7 +94,8 @@
@Nonnull
@SuppressWarnings(value = "deprecation")
public JvmClassInfoBuilder adaptFrom(@Nonnull ClassReader reader, int flags) {
reader.accept(new ClassBuilderAppender(), flags);
this.classVisitor = new ClassBuilderAppender();
reader.accept(classVisitor, flags);
return withBytecode(reader.b);
}

Expand All @@ -105,6 +111,18 @@
return this;
}

/**
* The default value is true. When {@code verify} is run it will check if there are any custom attributes.
*
* @param skipCustomAttributeChecks {@code false} if we want to verify the classes custom attribute
* @return {@code JvmClassInfoBuilder}
*/
@Nonnull
public JvmClassInfoBuilder skipCustomAttributeChecks(boolean skipCustomAttributeChecks) {
this.skipCustomAttributeChecks = skipCustomAttributeChecks;
return this;
}

public byte[] getBytecode() {
return bytecode;
}
Expand All @@ -126,6 +144,10 @@
throw new IllegalStateException("Bytecode required");
if (version < JvmClassInfo.BASE_VERSION)
throw new IllegalStateException("Version cannot be lower than 44 (v1)");
if (!this.skipCustomAttributeChecks && classVisitor != null && classVisitor.hasCustomAttributes()) {
throw new IllegalStateException("Unknown attributes found in class: " + this.getName() + "[" +
String.join(", ", classVisitor.getCustomAttributeNames()) + "]");

Check warning on line 149 in recaf-core/src/main/java/software/coley/recaf/info/builder/JvmClassInfoBuilder.java

View check run for this annotation

Codecov / codecov/patch

recaf-core/src/main/java/software/coley/recaf/info/builder/JvmClassInfoBuilder.java#L148-L149

Added lines #L148 - L149 were not covered by tests
}
}

private class ClassBuilderAppender extends ClassVisitor {
Expand All @@ -134,9 +156,20 @@
private final List<InnerClassInfo> innerClasses = new ArrayList<>();
private final List<FieldMember> fields = new ArrayList<>();
private final List<MethodMember> methods = new ArrayList<>();
/**
* Collects custom attributes in a class.
*/
private final List<Attribute> classCustomAttributes = new ArrayList<>();
private final MultiMap<String, Attribute, List<Attribute>> fieldCustomAttributes;
private final MultiMap<String, Attribute, List<Attribute>> methodCustomAttributes;


protected ClassBuilderAppender() {
super(getAsmVersion());
fieldCustomAttributes
= MultiMap.from(new HashMap<>(), ArrayList::new);
methodCustomAttributes
= MultiMap.from(new HashMap<>(), ArrayList::new);
}

@Override
Expand Down Expand Up @@ -200,9 +233,22 @@

}

@Override
public void visitAttribute(Attribute attribute) {
classCustomAttributes.add(attribute);
super.visitAttribute(attribute);
}

Check warning on line 240 in recaf-core/src/main/java/software/coley/recaf/info/builder/JvmClassInfoBuilder.java

View check run for this annotation

Codecov / codecov/patch

recaf-core/src/main/java/software/coley/recaf/info/builder/JvmClassInfoBuilder.java#L238-L240

Added lines #L238 - L240 were not covered by tests

@Override
public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
return new FieldBuilderAppender(access, name, descriptor, signature, value) {

@Override
public void visitAttribute(Attribute attribute) {
fieldCustomAttributes.get(name).add(attribute);
super.visitAttribute(attribute);
}

Check warning on line 250 in recaf-core/src/main/java/software/coley/recaf/info/builder/JvmClassInfoBuilder.java

View check run for this annotation

Codecov / codecov/patch

recaf-core/src/main/java/software/coley/recaf/info/builder/JvmClassInfoBuilder.java#L248-L250

Added lines #L248 - L250 were not covered by tests

@Override
public void visitEnd() {
fields.add(getFieldMember());
Expand All @@ -213,6 +259,13 @@
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
return new MethodBuilderAppender(access, name, descriptor, signature, exceptions) {

@Override
public void visitAttribute(Attribute attribute) {
methodCustomAttributes.get(name).add(attribute);
super.visitAttribute(attribute);
}

Check warning on line 267 in recaf-core/src/main/java/software/coley/recaf/info/builder/JvmClassInfoBuilder.java

View check run for this annotation

Codecov / codecov/patch

recaf-core/src/main/java/software/coley/recaf/info/builder/JvmClassInfoBuilder.java#L265-L267

Added lines #L265 - L267 were not covered by tests

@Override
public void visitEnd() {
methods.add(getMethodMember());
Expand All @@ -232,6 +285,32 @@
withAnnotations(annotations);
withTypeAnnotations(typeAnnotations);
}

/**
* @return {@code true} when any custom attributes were found.
*/
public boolean hasCustomAttributes() {
return !classCustomAttributes.isEmpty() ||
!fieldCustomAttributes.isEmpty() ||
!methodCustomAttributes.isEmpty();
}

/**
* @return Unique names of attributes found.
*/
public Collection<String> getCustomAttributeNames() {
Set<String> names = new TreeSet<>();
classCustomAttributes.stream()
.map(a -> a.type)
.forEach(names::add);
fieldCustomAttributes.values()
.map(a -> a.type)
.forEach(names::add);
methodCustomAttributes.values()
.map(a -> a.type)
.forEach(names::add);
return names;

Check warning on line 312 in recaf-core/src/main/java/software/coley/recaf/info/builder/JvmClassInfoBuilder.java

View check run for this annotation

Codecov / codecov/patch

recaf-core/src/main/java/software/coley/recaf/info/builder/JvmClassInfoBuilder.java#L302-L312

Added lines #L302 - L312 were not covered by tests
}
}

private static class FieldBuilderAppender extends FieldVisitor {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
import jakarta.annotation.Nonnull;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.slf4j.Logger;
import software.coley.cafedude.classfile.VersionConstants;
import software.coley.recaf.analytics.logging.Logging;
Expand All @@ -15,7 +13,6 @@
import software.coley.recaf.util.IOUtil;
import software.coley.recaf.util.android.AndroidXmlUtil;
import software.coley.recaf.util.io.ByteSource;
import software.coley.recaf.util.visitors.CustomAttributeCollectingVisitor;

import java.io.IOException;

Expand Down Expand Up @@ -44,26 +41,30 @@
// Check for Java classes
if (matchesClass(data)) {
try {
// Patch if not compatible with ASM
if (!isAsmCompliantClass(data)) {
byte[] patched = classPatcher.patch(name, data);
if (config.doSkipAsmValidation()) {
Col-E marked this conversation as resolved.
Show resolved Hide resolved
return new JvmClassInfoBuilder(data).build();

Check warning on line 45 in recaf-core/src/main/java/software/coley/recaf/workspace/io/BasicInfoImporter.java

View check run for this annotation

Codecov / codecov/patch

recaf-core/src/main/java/software/coley/recaf/workspace/io/BasicInfoImporter.java#L45

Added line #L45 was not covered by tests
}

// Ensure the patch was successful
if (!isAsmCompliantClass(patched)) {
try {
return new JvmClassInfoBuilder(data)
.skipCustomAttributeChecks(false)
.build();
} catch (Throwable t) {
// Patch if not compatible with ASM
byte[] patched = classPatcher.patch(name, data);
logger.debug("CafeDude patched class: {}", name);
try {
return new JvmClassInfoBuilder(patched)
.skipCustomAttributeChecks(false)
.build();
} catch (Throwable t1) {
logger.error("CafeDude patching output is still non-compliant with ASM for file: {}", name);
return new FileInfoBuilder<>()
.withRawContent(data)
.withName(name)
.build();
} else {
logger.debug("CafeDude patched class: {}", name);
return new JvmClassInfoBuilder(patched)
.build();
.withRawContent(data)
.withName(name)
.build();
}
}

// Yield class
return new JvmClassInfoBuilder(data).build();
} catch (Throwable t) {
// Invalid class, either some new edge case we need to add to CafeDude, or the file
// isn't a class, but the structure models it close enough to look like one at a glance.
Expand Down Expand Up @@ -181,34 +182,6 @@
return cpSize >= 4;
}

/**
* Check if the class can be parsed by ASM.
*
* @param content
* The class file content.
*
* @return {@code true} if ASM can parse the class.
*/
private boolean isAsmCompliantClass(byte[] content) {
// Skip validation if configured.
if (config.doSkipAsmValidation())
return true;

// ASM should be able to write back the read class.
try {
CustomAttributeCollectingVisitor visitor = new CustomAttributeCollectingVisitor(new ClassWriter(0));
Col-E marked this conversation as resolved.
Show resolved Hide resolved
ClassReader reader = new ClassReader(content);
reader.accept(visitor, 0);
if (visitor.hasCustomAttributes()) {
throw new IllegalStateException("Unknown attributes found in class: " + reader.getClassName() + "[" +
String.join(", ", visitor.getCustomAttributeNames()) + "]");
}
return true;
} catch (Throwable t) {
return false;
}
}

@Nonnull
@Override
public String getServiceId() {
Expand Down