Skip to content
This repository was archived by the owner on Oct 28, 2020. It is now read-only.

Add test showing stricter handling of Symbolic References #9

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
.idea
*.iml

.project
.settings
.classpath

out
generated
generated-*
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ So far we have "trouble" with...
* [Noto Sans](noto-sans)
* [weird Tycho/JDT behavior](tycho-jdt)
* [XML transformations](xml-transformer)
* [Symbolic References](symbolic-references)

## Observe!

Expand Down
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
<module>noto-sans</module>
<module>tycho-jdt</module>
<module>xml-transformer</module>
<module>symbolic-references</module>
</modules>

<dependencies>
Expand Down
32 changes: 32 additions & 0 deletions symbolic-references/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Symbolic References

This is related to the class loading tests, and really falls into the "Should Break" category,
as previous behaviour was not strictly to-spec.

## Stricter interpretation of Symbolic References in .class files

Java 9 appears to be more strict in its interpretation of the term "Symbolic Reference" in
class files. The spec says:

> For a nonarray class or an interface, the name is the fully qualified name of the class or interface.

(This is expected in "binary format", with forward-slashes instead of periods, for example
`java/lang/String`.)

In Java 8, you could get away with using a _Type Descriptor_ when really you should have
used a symbolic reference. A good example is CHECKCAST (which is used in this test).
Under Java 8, the instruction could have referenced a constant pool entry with either
`java/lang/String` (to spec) or `Ljava/lang/String;` (a descriptor, and not strictly
to spec) and it would work fine.

Under Java 9, such classes will not load. You'll instead receive a `ClassFormatError`:

```
[ERROR] loadClassWithDescriptorCheckcast Time elapsed: 0.031 s <<< ERROR!
java.lang.RuntimeException: Unrecoverable Error
at wtf.java9.symbolic_references.CheckCastTest.loadClassWithDescriptorCheckcast(CheckCastTest.java:13)
Caused by: java.lang.reflect.InvocationTargetException
at wtf.java9.symbolic_references.CheckCastTest.loadClassWithDescriptorCheckcast(CheckCastTest.java:13)
Caused by: java.lang.ClassFormatError: Illegal class name "Ljava/lang/String;" in class file TestClassImpl
at wtf.java9.symbolic_references.CheckCastTest.loadClassWithDescriptorCheckcast(CheckCastTest.java:13)
```
30 changes: 30 additions & 0 deletions symbolic-references/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>wtf.java9</groupId>
<artifactId>symbolic-references</artifactId>
<version>1.0-SNAPSHOT</version>

<parent>
<groupId>wtf.java9</groupId>
<artifactId>seriously-wtf</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

<dependencies>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>6.2</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-tree</artifactId>
<version>6.2</version>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package wtf.java9.symbolic_references;

public interface TestClass {
public String generatedMethod(Object o);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package wtf.java9.symbolic_references;

import static org.objectweb.asm.Opcodes.*;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.tree.ClassNode;

public class TestClassGenerator {
private static final Method defineClass;
static {
try {
defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
} catch (NoSuchMethodException e) {
throw new IllegalStateException("Unrecoverable Error: NoSuchMethodException 'defineClass' on ClassLoader.\n"
+ "This is most likely an environment issue.", e);
}
defineClass.setAccessible(true);
}

public Class<? extends TestClass> generateClass() {
return defineClass(getClass().getClassLoader());

}

byte[] generateBytecode() {
ClassNode cv = new ClassNode();

cv.visit(V1_8,
ACC_PUBLIC | ACC_SYNTHETIC | ACC_SUPER,
"TestClassImpl",
null,
"java/lang/Object",
new String[] { "wtf/java9/symbolic_references/TestClass" });

generateConstructor(cv);
generateGeneratedMethod(cv);

cv.visitEnd();

ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
cv.accept(writer);
return writer.toByteArray();
}

void generateConstructor(ClassVisitor cv) {
MethodVisitor mv = cv.visitMethod(ACC_PUBLIC | ACC_SYNTHETIC,
"<init>",
"()V",
"",
null);

mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
mv.visitInsn(RETURN);
mv.visitEnd();
}

void generateGeneratedMethod(ClassVisitor cv) {
MethodVisitor mv = cv.visitMethod(ACC_PUBLIC | ACC_SYNTHETIC,
"generatedMethod",
"(Ljava/lang/Object;)Ljava/lang/String;",
"",
null);

mv.visitCode();
mv.visitVarInsn(ALOAD, 1);

// The following line initiates the failure. Java 9 is
// more strict here and requires an internal name
// rather than a descriptor. So change to "java/lang/String".
mv.visitTypeInsn(CHECKCAST, "Ljava/lang/String;");

mv.visitInsn(ARETURN);
mv.visitEnd();
}

@SuppressWarnings("unchecked")
Class<? extends TestClass> defineClass(ClassLoader loader) {
byte[] code = generateBytecode();
try {
return (Class<? extends TestClass>)defineClass.invoke(loader, "TestClassImpl", code, 0, code.length);
} catch (InvocationTargetException e) {
System.err.println("InvocationTargetException: in defineClass: " + e.getMessage());
throw new RuntimeException("Unrecoverable Error", e);
} catch (IllegalAccessException e) {
System.err.println("IllegalAccessException: in defineClass: " + e.getMessage());
throw new RuntimeException("Unrecoverable Error", e);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package wtf.java9.symbolic_references;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Test;

import wtf.java9.symbolic_references.TestClass;
import wtf.java9.symbolic_references.TestClassGenerator;

public class CheckCastTest {

/* Works under Java < 9, fails with ClassFormatError under Java 9. */
@Test
public void loadClassWithDescriptorCheckcast() throws Exception {
Class<? extends TestClass> clz = new TestClassGenerator().generateClass();
TestClass testClass = clz.newInstance();
assertThat(testClass.generatedMethod("hello")).isEqualTo("hello");
}
}