Skip to content
Open
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
2 changes: 2 additions & 0 deletions bazel-jdt-bridge/java-bridge/bnd.bnd
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ Import-Package: \
org.eclipse.core.runtime.jobs, \
org.eclipse.jdt.launching, \
org.osgi.framework, \
org.osgi.framework.hooks.weaving, \
*
Export-Package: com.bazel.jdt
Private-Package: org.objectweb.asm,org.objectweb.asm.signature
Bundle-NativeCode: \
native/linux-x86_64/libbazel_jdt_core.so; osname=Linux; processor=x86_64, \
native/linux-aarch64/libbazel_jdt_core.so; osname=Linux; processor=aarch64, \
Expand Down
12 changes: 12 additions & 0 deletions bazel-jdt-bridge/java-bridge/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,18 @@
<version>3.23.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.osgi</groupId>
<artifactId>osgi.core</artifactId>
<version>8.0.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>9.7</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,36 @@
import org.eclipse.core.runtime.Status;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceRegistration;
import org.osgi.framework.hooks.weaving.WeavingHook;

public class BazelActivator implements BundleActivator {
private static final ILog LOG = Platform.getLog(BazelActivator.class);
private static final Pattern INVISIBLE_PROJECT_PATTERN =
Pattern.compile(".+_[0-9a-f]{4,}$");

private IResourceChangeListener invisibleProjectListener;
private ServiceRegistration<WeavingHook> weavingHookRegistration;

@Override
public void start(BundleContext context) throws Exception {
LOG.log(new Status(IStatus.INFO, "com.bazel.jdt",
"Bazel JDT Bridge bundle starting"));

weavingHookRegistration = context.registerService(
WeavingHook.class, new JDTUtilsPatcher(), null);

invisibleProjectListener = this::checkForInvisibleProjects;
ResourcesPlugin.getWorkspace().addResourceChangeListener(
invisibleProjectListener, IResourceChangeEvent.POST_CHANGE);
}

@Override
public void stop(BundleContext context) throws Exception {
if (weavingHookRegistration != null) {
weavingHookRegistration.unregister();
weavingHookRegistration = null;
}
if (invisibleProjectListener != null) {
ResourcesPlugin.getWorkspace().removeResourceChangeListener(invisibleProjectListener);
invisibleProjectListener = null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package com.bazel.jdt;

import java.util.logging.Level;
import java.util.logging.Logger;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.osgi.framework.hooks.weaving.WeavingHook;
import org.osgi.framework.hooks.weaving.WovenClass;

public class JDTUtilsPatcher implements WeavingHook, Opcodes {

private static final Logger LOG = Logger.getLogger(JDTUtilsPatcher.class.getName());

static final String JDTUTILS_INTERNAL_NAME = "org/eclipse/jdt/ls/core/internal/JDTUtils";
static final String TARGET_METHOD = "searchDecompiledSources";
private static final String TARGET_BUNDLE = "org.eclipse.jdt.ls.core";
private static final String NPE_INTERNAL_NAME = "java/lang/NullPointerException";
private static final String COLLECTIONS_INTERNAL_NAME = "java/util/Collections";

@Override
public void weave(WovenClass wovenClass) {
String bundleName = wovenClass.getBundleWiring().getBundle().getSymbolicName();
if (!TARGET_BUNDLE.equals(bundleName)) {
return;
}

byte[] original = wovenClass.getBytes();
if (!containsTargetCallSite(original)) {
return;
}

try {
byte[] patched = patchCallerClass(original);
if (patched != null) {
wovenClass.setBytes(patched);
LOG.info("Patched " + wovenClass.getClassName()
+ ": wrapped searchDecompiledSources call site with NPE guard");
}
} catch (Exception e) {
LOG.log(Level.WARNING,
"Failed to patch " + wovenClass.getClassName() + ", leaving class unmodified", e);
}
}

static boolean containsTargetCallSite(byte[] classBytes) {
boolean[] found = {false};
ClassReader reader = new ClassReader(classBytes);
reader.accept(new ClassVisitor(ASM9) {
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor,
String signature, String[] exceptions) {
if (found[0]) return null;
return new MethodVisitor(ASM9) {
@Override
public void visitMethodInsn(int opcode, String owner, String mName,
String mDescriptor, boolean isInterface) {
if (opcode == INVOKESTATIC
&& JDTUTILS_INTERNAL_NAME.equals(owner)
&& TARGET_METHOD.equals(mName)) {
found[0] = true;
}
}
};
}
}, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
return found[0];
}

byte[] patchCallerClass(byte[] classBytes) {
ClassReader reader = new ClassReader(classBytes);
ClassWriter writer = new SafeClassWriter(reader, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
boolean[] patched = {false};

ClassVisitor visitor = new ClassVisitor(ASM9, writer) {
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor,
String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
return new CallSiteWrappingVisitor(mv, patched);
}
};

reader.accept(visitor, 0);
return patched[0] ? writer.toByteArray() : null;
}

static class CallSiteWrappingVisitor extends MethodVisitor {
private final boolean[] patched;

CallSiteWrappingVisitor(MethodVisitor mv, boolean[] patched) {
super(ASM9, mv);
this.patched = patched;
}

@Override
public void visitMethodInsn(int opcode, String owner, String name,
String descriptor, boolean isInterface) {
if (opcode == INVOKESTATIC
&& JDTUTILS_INTERNAL_NAME.equals(owner)
&& TARGET_METHOD.equals(name)) {

Label tryStart = new Label();
Label tryEnd = new Label();
Label catchHandler = new Label();
Label afterCatch = new Label();

mv.visitTryCatchBlock(tryStart, tryEnd, catchHandler, NPE_INTERNAL_NAME);
mv.visitLabel(tryStart);
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
mv.visitLabel(tryEnd);
mv.visitJumpInsn(GOTO, afterCatch);
mv.visitLabel(catchHandler);
mv.visitInsn(POP);
mv.visitMethodInsn(INVOKESTATIC, COLLECTIONS_INTERNAL_NAME,
"emptyList", "()Ljava/util/List;", false);
mv.visitLabel(afterCatch);

patched[0] = true;
return;
}
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
}
}

private static class SafeClassWriter extends ClassWriter {
SafeClassWriter(ClassReader classReader, int flags) {
super(classReader, flags);
}

@Override
protected String getCommonSuperClass(String type1, String type2) {
try {
return super.getCommonSuperClass(type1, type2);
} catch (Exception e) {
return "java/lang/Object";
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package com.bazel.jdt;

import static org.junit.Assert.*;

import java.lang.reflect.Method;
import java.util.List;

import org.junit.Test;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class JDTUtilsPatcherTest implements Opcodes {

private static final String SIMPLE_DESC = "()Ljava/util/List;";

@Test
public void patchCallerClass_wrapsCallSite() {
byte[] caller = buildCallerClass();
JDTUtilsPatcher patcher = new JDTUtilsPatcher();
byte[] patched = patcher.patchCallerClass(caller);

assertNotNull("should patch class that calls searchDecompiledSources", patched);
assertFalse("patched bytes should differ",
java.util.Arrays.equals(caller, patched));
}

@Test
public void patchCallerClass_skipsClassWithNoCallSite() {
byte[] unrelated = buildUnrelatedClass();
JDTUtilsPatcher patcher = new JDTUtilsPatcher();
byte[] patched = patcher.patchCallerClass(unrelated);

assertNull("should return null for class without searchDecompiledSources call", patched);
}

@Test
public void patchCallerClass_skipsWhenOwnerDiffers() {
byte[] wrongOwner = buildCallerWithDifferentOwner();
JDTUtilsPatcher patcher = new JDTUtilsPatcher();
byte[] patched = patcher.patchCallerClass(wrongOwner);

assertNull("should not patch calls to other classes", patched);
}

@Test
public void patchedCallSite_returnsEmptyListInsteadOfNPE() throws Exception {
byte[] fakeJDTUtils = buildFakeJDTUtils();
byte[] caller = buildCallerClass();

JDTUtilsPatcher patcher = new JDTUtilsPatcher();
byte[] patchedCaller = patcher.patchCallerClass(caller);
assertNotNull(patchedCaller);

TestClassLoader loader = new TestClassLoader();
loader.define("org.eclipse.jdt.ls.core.internal.JDTUtils", fakeJDTUtils);
Class<?> callerClass = loader.define("TestCaller", patchedCaller);

Method method = callerClass.getMethod("callSearch");
Object result = method.invoke(null);

assertNotNull("patched call site should return non-null", result);
assertTrue("should return a List", result instanceof List);
assertTrue("should return empty list", ((List<?>) result).isEmpty());
}

private byte[] buildCallerClass() {
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
cw.visit(V17, ACC_PUBLIC, "TestCaller", null, "java/lang/Object", null);

MethodVisitor mv = cw.visitMethod(ACC_PUBLIC | ACC_STATIC,
"callSearch", SIMPLE_DESC, null, null);
mv.visitCode();
mv.visitMethodInsn(INVOKESTATIC, JDTUtilsPatcher.JDTUTILS_INTERNAL_NAME,
JDTUtilsPatcher.TARGET_METHOD, SIMPLE_DESC, false);
mv.visitInsn(ARETURN);
mv.visitMaxs(1, 0);
mv.visitEnd();

cw.visitEnd();
return cw.toByteArray();
}

private byte[] buildFakeJDTUtils() {
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
cw.visit(V17, ACC_PUBLIC,
JDTUtilsPatcher.JDTUTILS_INTERNAL_NAME, null, "java/lang/Object", null);

MethodVisitor mv = cw.visitMethod(ACC_PUBLIC | ACC_STATIC,
JDTUtilsPatcher.TARGET_METHOD, SIMPLE_DESC, null, null);
mv.visitCode();
mv.visitTypeInsn(NEW, "java/lang/NullPointerException");
mv.visitInsn(DUP);
mv.visitLdcInsn("occurrences is null");
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/NullPointerException",
"<init>", "(Ljava/lang/String;)V", false);
mv.visitInsn(ATHROW);
mv.visitMaxs(3, 0);
mv.visitEnd();

cw.visitEnd();
return cw.toByteArray();
}

private byte[] buildUnrelatedClass() {
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
cw.visit(V17, ACC_PUBLIC, "UnrelatedClass", null, "java/lang/Object", null);

MethodVisitor mv = cw.visitMethod(ACC_PUBLIC | ACC_STATIC,
"doSomething", "()V", null, null);
mv.visitCode();
mv.visitInsn(RETURN);
mv.visitMaxs(0, 0);
mv.visitEnd();

cw.visitEnd();
return cw.toByteArray();
}

private byte[] buildCallerWithDifferentOwner() {
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
cw.visit(V17, ACC_PUBLIC, "WrongOwnerCaller", null, "java/lang/Object", null);

MethodVisitor mv = cw.visitMethod(ACC_PUBLIC | ACC_STATIC,
"callSearch", SIMPLE_DESC, null, null);
mv.visitCode();
mv.visitMethodInsn(INVOKESTATIC, "com/other/Utils",
JDTUtilsPatcher.TARGET_METHOD, SIMPLE_DESC, false);
mv.visitInsn(ARETURN);
mv.visitMaxs(1, 0);
mv.visitEnd();

cw.visitEnd();
return cw.toByteArray();
}

private static class TestClassLoader extends ClassLoader {
Class<?> define(String name, byte[] bytes) {
return defineClass(name, bytes, 0, bytes.length);
}
}
}
Loading
Loading