Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Expand Up @@ -2,19 +2,13 @@

import io.github.eisop.runtimeframework.core.RuntimeChecker;
import io.github.eisop.runtimeframework.core.RuntimeInstrumenter;
import io.github.eisop.runtimeframework.filter.AnnotatedForFilter;
import io.github.eisop.runtimeframework.filter.ClassInfo;
import io.github.eisop.runtimeframework.filter.Filter;
import io.github.eisop.runtimeframework.qual.AnnotatedFor;
import java.io.InputStream;
import java.lang.classfile.Annotation;
import java.lang.classfile.AnnotationValue;
import java.lang.classfile.Attributes;
import java.lang.classfile.ClassFile;
import java.lang.classfile.ClassModel;
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class RuntimeTransformer implements ClassFileTransformer {

Expand All @@ -23,9 +17,7 @@ public class RuntimeTransformer implements ClassFileTransformer {
private final RuntimeChecker checker;
private final boolean trustAnnotatedFor;
private final boolean isGlobalMode;

private final Map<String, Boolean> packageCache = new ConcurrentHashMap<>();
private static final String ANNOTATED_FOR_DESC = AnnotatedFor.class.descriptorString();
private final AnnotatedForFilter annotatedForFilter;

public RuntimeTransformer(
Filter<ClassInfo> scanFilter,
Expand All @@ -38,6 +30,7 @@ public RuntimeTransformer(
this.checker = checker;
this.trustAnnotatedFor = trustAnnotatedFor;
this.isGlobalMode = isGlobalMode;
this.annotatedForFilter = trustAnnotatedFor ? new AnnotatedForFilter(checker.getName()) : null;
}

@Override
Expand Down Expand Up @@ -71,14 +64,10 @@ public byte[] transform(

boolean isChecked = policyFilter.test(info);

if (!isChecked && trustAnnotatedFor) {
String targetSystem = checker.getName();

if (hasAnnotatedFor(classModel, targetSystem)) {
System.out.println("[RuntimeFramework] Auto-detected Checked Class: " + className);
isChecked = true;
} else if (hasPackageLevelAnnotation(className, loader, targetSystem)) {
System.out.println("[RuntimeFramework] Auto-detected Checked Package: " + className);
if (!isChecked && trustAnnotatedFor && annotatedForFilter != null) {
if (annotatedForFilter.test(classModel, loader)) {
System.out.println(
"[RuntimeFramework] Auto-detected Checked Class/Package: " + className);
isChecked = true;
}
}
Expand All @@ -90,10 +79,22 @@ public byte[] transform(
boolean finalIsChecked = isChecked;
Filter<ClassInfo> dynamicFilter =
ctx -> {
if (ctx.internalName().equals(className)) {
ClassLoader effectiveLoader = ctx.loader();
if (effectiveLoader == null) {
effectiveLoader = loader;
}
ClassInfo effectiveCtx =
new ClassInfo(ctx.internalName(), effectiveLoader, ctx.module());

if (effectiveCtx.internalName().equals(className)) {
return finalIsChecked;
}
return policyFilter.test(ctx);
if (trustAnnotatedFor
&& annotatedForFilter != null
&& annotatedForFilter.test(effectiveCtx)) {
return true;
}
return policyFilter.test(effectiveCtx);
};

RuntimeInstrumenter instrumenter = checker.getInstrumenter(dynamicFilter);
Expand All @@ -105,62 +106,4 @@ public byte[] transform(
return null;
}
}

private boolean hasPackageLevelAnnotation(String className, ClassLoader loader, String system) {
int lastSlash = className.lastIndexOf('/');
if (lastSlash == -1) return false;

String packageName = className.substring(0, lastSlash);

if (packageCache.containsKey(packageName)) {
return packageCache.get(packageName);
}

String packageInfoPath = packageName + "/package-info.class";
boolean found = false;

try (InputStream is =
(loader != null)
? loader.getResourceAsStream(packageInfoPath)
: ClassLoader.getSystemResourceAsStream(packageInfoPath)) {

if (is != null) {
ClassModel packageModel = ClassFile.of().parse(is.readAllBytes());
if (hasAnnotatedFor(packageModel, system)) {
found = true;
}
}
} catch (Exception e) {
System.out.println("Cannot get package info");
}

packageCache.put(packageName, found);
return found;
}

private boolean hasAnnotatedFor(ClassModel model, String system) {
return model
.findAttribute(Attributes.runtimeVisibleAnnotations())
.map(
attr -> {
for (Annotation anno : attr.annotations()) {
if (anno.classSymbol().descriptorString().equals(ANNOTATED_FOR_DESC)) {
for (var element : anno.elements()) {
if (element.name().stringValue().equals("value")) {
if (element.value() instanceof AnnotationValue.OfArray arr) {
for (AnnotationValue v : arr.values()) {
if (v instanceof AnnotationValue.OfString s
&& s.stringValue().equals(system)) {
return true;
}
}
}
}
}
}
}
return false;
})
.orElse(false);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package io.github.eisop.runtimeframework.filter;

import io.github.eisop.runtimeframework.qual.AnnotatedFor;
import java.io.IOException;
import java.io.InputStream;
import java.lang.classfile.Annotation;
import java.lang.classfile.AnnotationValue;
import java.lang.classfile.Attributes;
import java.lang.classfile.ClassFile;
import java.lang.classfile.ClassModel;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
* A filter that checks if a class or its package is annotated with {@link AnnotatedFor} for a
* specific type system (e.g., "nullness").
*
* <p>This filter maintains a cache of results to avoid repeated bytecode parsing. It supports
* checking both a pre-parsed {@link ClassModel} (for the class currently being transformed) and
* loading bytecode on-demand (for dependencies).
*/
public class AnnotatedForFilter implements Filter<ClassInfo> {

private final String targetSystem;
private final Map<String, Boolean> cache = new ConcurrentHashMap<>();
private static final String ANNOTATED_FOR_DESC = AnnotatedFor.class.descriptorString();

public AnnotatedForFilter(String targetSystem) {
this.targetSystem = targetSystem;
}

/**
* Checks if the class represented by the given ClassModel is annotated for the a target type
* system. This also checks the package-level annotation if the class itself is not annotated.
*
* @param model The ClassModel of the class to check.
* @param loader The ClassLoader to use for loading package-info.
* @return true if the class or its package is annotated for the target system.
*/
public boolean test(ClassModel model, ClassLoader loader) {
String className = model.thisClass().asInternalName();

if (cache.containsKey(className)) {
return cache.get(className);
}

boolean result = hasAnnotatedFor(model);
if (!result) {
result = hasPackageLevelAnnotation(className, loader);
}

cache.put(className, result);
return result;
}

@Override
public boolean test(ClassInfo info) {
String className = info.internalName();
if (className == null) return false;

if (cache.containsKey(className)) {
return cache.get(className);
}

boolean result = false;
String resourcePath = className + ".class";

try (InputStream is =
(info.loader() != null)
? info.loader().getResourceAsStream(resourcePath)
: ClassLoader.getSystemResourceAsStream(resourcePath)) {

if (is != null) {
ClassModel model = ClassFile.of().parse(is.readAllBytes());
result = test(model, info.loader());
}
} catch (IOException e) {
System.err.println("[AnnotatedForFilter] Failed to load bytecode for: " + className);
}

cache.put(className, result);
return result;
}

private boolean hasPackageLevelAnnotation(String className, ClassLoader loader) {
int lastSlash = className.lastIndexOf('/');
if (lastSlash == -1) return false;

String packageName = className.substring(0, lastSlash);
String packageInfoClass = packageName + "/package-info";

return test(new ClassInfo(packageInfoClass, loader, null));
}

private boolean hasAnnotatedFor(ClassModel model) {
return model
.findAttribute(Attributes.runtimeVisibleAnnotations())
.map(
attr -> {
for (Annotation anno : attr.annotations()) {
if (anno.classSymbol().descriptorString().equals(ANNOTATED_FOR_DESC)) {
for (var element : anno.elements()) {
if (element.name().stringValue().equals("value")) {
if (element.value() instanceof AnnotationValue.OfArray arr) {
for (AnnotationValue v : arr.values()) {
if (v instanceof AnnotationValue.OfString s
&& s.stringValue().equals(targetSystem)) {
return true;
}
}
}
}
}
}
}
return false;
})
.orElse(false);
}
}