package com.oracle.truffle.api.test.polyglot;
import static org.hamcrest.CoreMatchers.containsString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Arrays;
import java.util.function.Function;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.HostAccess;
import org.graalvm.polyglot.HostAccess.Export;
import org.graalvm.polyglot.HostAccess.Implementable;
import org.graalvm.polyglot.PolyglotException;
import org.graalvm.polyglot.Value;
import org.junit.Assert;
import org.junit.Test;
import com.oracle.truffle.api.interop.ArityException;
import com.oracle.truffle.api.interop.InteropException;
import com.oracle.truffle.api.interop.InteropLibrary;
import com.oracle.truffle.api.interop.UnknownIdentifierException;
import com.oracle.truffle.api.interop.UnsupportedMessageException;
import com.oracle.truffle.api.interop.UnsupportedTypeException;
public class ExposeToGuestTest {
@Test
public void byDefaultOnlyAnnotatedMethodsCanBeAccessed() {
Context context = Context.create();
Value readValue = context.eval("sl", "" + "function readValue(x) {\n" + " return x.value;\n" + "}\n" + "function main() {\n" + " return readValue;\n" + "}\n");
Assert.assertEquals(42, readValue.execute(new ExportedValue()).asInt());
assertPropertyUndefined("PublicValue isn't enough by default", readValue, new PublicValue());
}
private static void assertPropertyUndefined(String msg, Value readValue, Object value) {
assertPropertyUndefined(msg, "value", readValue, value);
}
static void assertPropertyUndefined(String msg, String propName, Value readValue, Object value) {
try {
readValue.execute(value);
fail(msg);
} catch (PolyglotException ex) {
assertEquals("Undefined property: " + propName, ex.getMessage());
}
}
public static class PublicValue {
public int value = 42;
}
public static class ExportedValue {
@HostAccess.Export public int value = 42;
}
@Test
public void exportingAllPublicIsEasy() {
Context context = Context.newBuilder().allowHostAccess(HostAccess.ALL).build();
Value readValue = context.eval("sl", "" + "function readValue(x) {\n" + " return x.value;\n" + "}\n" + "function main() {\n" + " return readValue;\n" + "}\n");
Assert.assertEquals(42, readValue.execute(new PublicValue()).asInt());
Assert.assertEquals(42, readValue.execute(new ExportedValue()).asInt());
}
@Test
public void customExportedAnnotation() {
HostAccess accessMeConfig = HostAccess.newBuilder().allowAccessAnnotatedBy(AccessMe.class).build();
Context context = Context.newBuilder().allowHostAccess(accessMeConfig).build();
Value readValue = context.eval("sl", "" + "function readValue(x) {\n" + " return x.value;\n" + "}\n" + "function main() {\n" + " return readValue;\n" + "}\n");
Assert.assertEquals(42, readValue.execute(new AccessibleValue()).asInt());
assertPropertyUndefined("Default annotation isn't enough", readValue, new ExportedValue());
assertPropertyUndefined("Public isn't enough by default", readValue, new PublicValue());
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD})
public @interface AccessMe {
}
public static class AccessibleValue {
@AccessMe public int value = 42;
}
@Test
public void explicitlyEnumeratingField() throws Exception {
HostAccess explictConfig = HostAccess.newBuilder().allowAccess(AccessibleValue.class.getField("value")).build();
Context context = Context.newBuilder().allowHostAccess(explictConfig).build();
Value readValue = context.eval("sl", "" + "function readValue(x) {\n" + " return x.value;\n" + "}\n" + "function main() {\n" + " return readValue;\n" + "}\n");
Assert.assertEquals(42, readValue.execute(new AccessibleValue()).asInt());
assertPropertyUndefined("Default annotation isn't enough", readValue, new ExportedValue());
assertPropertyUndefined("Public isn't enough by default", readValue, new PublicValue());
}
public static class Foo<T extends Number> {
@HostAccess.Export
@SuppressWarnings("unused")
public Object foo(T x) {
return "basic foo";
}
}
static class Bar extends Foo<Number> {
@Override
@SuppressWarnings("unused")
public Object foo(Number x) {
return "enhanced bar";
}
}
static class PackagePrivateBar {
@SuppressWarnings("unused")
public Object foo(Number x) {
fail("Never called");
return "hidden bar";
}
}
static class PrivateFoo<T extends Number> extends Foo<T> {
@SuppressWarnings("all")
private Object foo(Integer y) {
fail("Never called");
return "hidden foo";
}
}
@SuppressWarnings("all")
public static class PrivateChangedFoo<T extends Integer> extends PrivateFoo<T> {
@HostAccess.Export
@Override
public Object foo(T x) {
return "overriden foo";
}
}
@Test
public void fooBarExposedByInheritance() throws Exception {
Context context = Context.newBuilder().allowHostAccess(HostAccess.EXPLICIT).build();
Value readValue = context.eval("sl", "" + "function callFoo(x) {\n" + " return x.foo(1);\n" + "}\n" + "function main() {\n" + " return callFoo;\n" + "}\n");
Assert.assertEquals("basic foo", readValue.execute(new Foo<>()).asString());
Assert.assertEquals("enhanced bar", readValue.execute(new Bar()).asString());
assertPropertyUndefined("Cannot call public method in package private class", "foo", readValue, new PackagePrivateBar());
Assert.assertEquals("basic foo", readValue.execute(new PrivateFoo<>()).asString());
Assert.assertEquals("overriden foo", readValue.execute(new PrivateChangedFoo<>()).asString());
}
@FunctionalInterface
public interface FooInterface<T extends Number> {
@HostAccess.Export
Object foo(T value);
}
@Test
public void functionalInterfaceCall() throws Exception {
Context context = Context.newBuilder().allowHostAccess(HostAccess.EXPLICIT).build();
Value readValue = context.eval("sl", "" + "function callFoo(x) {\n" + " return x.foo(1);\n" + "}\n" + "function main() {\n" + " return callFoo;\n" + "}\n");
FooInterface<Number> foo = (ignore) -> "functional foo";
Assert.assertEquals("functional foo", readValue.execute(foo).asString());
}
@Test
public void listAccessAllowedInPublicHostAccess() throws Exception {
doAccessAllowedInPublicHostAccess(true);
}
@Test
public void arrayAccessAllowedInPublicHostAccess() throws Exception {
doAccessAllowedInPublicHostAccess(false);
}
private static void doAccessAllowedInPublicHostAccess(boolean asList) throws Exception {
Context context = Context.newBuilder().allowHostAccess(HostAccess.ALL).build();
Value readValue = context.eval("sl", "" + "function callFoo(x) {\n" + " return x.foo(1)[0];\n" + "}\n" + "function main() {\n" + " return callFoo;\n" + "}\n");
boolean[] gotIn = {false};
FooInterface<Number> foo = returnAsArrayOrList(gotIn, asList);
final Value arrayRead = readValue.execute(foo);
Assert.assertTrue("Foo lamda called", gotIn[0]);
Assert.assertEquals(1, arrayRead.asInt());
}
@Test
public void listAccessForbiddenInExplicit() throws Exception {
doAccessForbiddenInExplicit(true);
}
@Test
public void arrayAccessForbiddenInExplicit() throws Exception {
doAccessForbiddenInExplicit(false);
}
private static void doAccessForbiddenInExplicit(boolean asList) throws Exception {
Context context = Context.newBuilder().allowHostAccess(HostAccess.EXPLICIT).build();
Value readValue = context.eval("sl", "" + "function callFoo(x) {\n" + " return x.foo(1)[0];\n" + "}\n" + "function main() {\n" + " return callFoo;\n" + "}\n");
boolean[] gotIn = {false};
FooInterface<Number> foo = returnAsArrayOrList(gotIn, asList);
final Value arrayRead;
try {
arrayRead = readValue.execute(foo);
} catch (Exception ex) {
assertEquals("Expecting an exception", PolyglotException.class, ex.getClass());
Assert.assertTrue("Foo lamda called", gotIn[0]);
return;
}
fail("The read shouldn't succeed: " + arrayRead);
}
@Test
public void listAccessForbiddenInManual() throws Exception {
doAccessForbiddenInManual(true);
}
@Test
public void arrayAccessForbiddenInManual() throws Exception {
doAccessForbiddenInManual(false);
}
private static void doAccessForbiddenInManual(boolean asList) throws Exception {
HostAccess config = HostAccess.newBuilder().allowAccess(FooInterface.class.getMethod("foo", Number.class)).build();
Context context = Context.newBuilder().allowHostAccess(config).build();
Value readValue = context.eval("sl", "" + "function callFoo(x) {\n" + " return x.foo(1)[0];\n" + "}\n" + "function main() {\n" + " return callFoo;\n" + "}\n");
boolean[] gotIn = {false};
FooInterface<Number> foo = returnAsArrayOrList(gotIn, asList);
final Value arrayRead;
try {
arrayRead = readValue.execute(foo);
} catch (Exception ex) {
assertEquals("Expecting an exception", PolyglotException.class, ex.getClass());
Assert.assertTrue("Foo lamda called", gotIn[0]);
return;
}
fail("The read shouldn't succeed: " + arrayRead);
}
private static FooInterface<Number> returnAsArrayOrList(boolean[] gotIn, boolean asList) {
FooInterface<Number> foo = (n) -> {
gotIn[0] = true;
if (asList) {
return Arrays.asList(n);
} else {
return new Number[]{n};
}
};
return foo;
}
public static class FieldAccess {
public static Object staticField = "42";
public static final Object finalField = "42";
@Export public static Object exportedStaticField = "42";
@Export public static final Object exportedField = "42";
}
@Test
public void staticFieldAccessIsForbidden() throws InteropException {
Context.Builder builder = Context.newBuilder();
builder.allowHostClassLookup((c) -> c.endsWith("FieldAccess"));
Context c = builder.build();
c.initialize(ProxyLanguage.ID);
c.enter();
try {
Object hostLookup = ProxyLanguage.getCurrentContext().getEnv().lookupHostSymbol(FieldAccess.class.getName());
assertMember(hostLookup, "staticField", false, false);
assertMember(hostLookup, "finalField", false, false);
assertMember(hostLookup, "exportedStaticField", true, true);
assertMember(hostLookup, "exportedField", true, false);
} finally {
c.leave();
c.close();
}
}
private static void assertMember(Object object, String member, boolean readable, boolean modifiable) throws InteropException {
InteropLibrary interop = InteropLibrary.getFactory().getUncached();
assertTrue(interop.hasMembers(object));
assertEquals(readable, interop.isMemberReadable(object, member));
assertEquals(modifiable, interop.isMemberModifiable(object, member));
assertFalse(interop.isMemberInsertable(object, member));
assertFalse(interop.isMemberRemovable(object, member));
if (readable) {
assertEquals("42", interop.readMember(object, member));
} else {
try {
interop.readMember(object, member);
fail();
} catch (UnknownIdentifierException e) {
}
}
if (modifiable) {
interop.writeMember(object, member, "42");
} else {
try {
interop.writeMember(object, member, "43");
fail();
} catch (UnknownIdentifierException e) {
}
}
try {
interop.removeMember(object, member);
fail();
} catch (UnsupportedMessageException e) {
}
}
@SuppressWarnings("unused")
public static class AllowedConstructorAccess {
@HostAccess.Export
public AllowedConstructorAccess(String s) {
}
public AllowedConstructorAccess() {
}
AllowedConstructorAccess(int c) {
}
}
public static class DeniedConstructorAccess {
public DeniedConstructorAccess() {
}
}
@Test
public void staticConstructorAccessIsForbidden() throws InteropException {
Context.Builder builder = Context.newBuilder();
builder.allowHostClassLookup((c) -> c.endsWith("ConstructorAccess"));
Context c = builder.build();
c.initialize(ProxyLanguage.ID);
c.enter();
try {
Object allowed = ProxyLanguage.getCurrentContext().getEnv().lookupHostSymbol(AllowedConstructorAccess.class.getName());
InteropLibrary library = InteropLibrary.getFactory().getUncached();
assertTrue(library.isInstantiable(allowed));
try {
library.instantiate(allowed);
fail();
} catch (ArityException e) {
}
try {
library.instantiate(allowed, 42);
fail();
} catch (UnsupportedTypeException e) {
}
assertNotNull(library.instantiate(allowed, "asdf"));
Object denied = ProxyLanguage.getCurrentContext().getEnv().lookupHostSymbol(DeniedConstructorAccess.class.getName());
assertFalse(library.isInstantiable(denied));
try {
library.instantiate(denied);
fail();
} catch (UnsupportedMessageException e) {
}
} finally {
c.leave();
c.close();
}
}
interface EmptyInterface {
}
interface UnmarkedInterface {
Object exported(String arg);
}
@Implementable
interface MarkedInterface {
String exported(String arg);
}
@FunctionalInterface
interface MarkedFunctional {
int f();
}
interface UnmarkedFunctional {
int f();
}
public static class Impl {
@Export
public Object exported(Object arg) {
return arg;
}
@Export
public Object noArg() {
return 42;
}
}
@SuppressWarnings("unused")
public static class Overloaded {
@Export
public Object overloaded(MarkedFunctional arg) {
return "MarkedFunctional";
}
@Export
public Object overloaded(MarkedInterface arg) {
return "MarkedInterface";
}
@Export
public Object overloaded(String arg) {
return arg;
}
}
@Implementable
public abstract static class MarkedClass {
public abstract String exported(String arg);
}
@Implementable
public abstract static class NoDefaultConstructor {
public NoDefaultConstructor(@SuppressWarnings("unused") String dummy) {
}
public abstract String exported(String arg);
}
@SuppressWarnings("unchecked")
@Test
public void testProxyOverloads() {
HostAccess access = HostAccess.EXPLICIT;
try (Context c = Context.newBuilder().allowHostAccess(access).build()) {
c.initialize(ProxyLanguage.ID);
Value v = c.asValue(new Overloaded());
Value arg = c.asValue(new Impl());
try {
v.invokeMember("overloaded", arg);
fail();
} catch (IllegalArgumentException e) {
assertTrue(e.getMessage(), e.getMessage().contains("Multiple applicable overloads"));
}
assertEquals("42", v.invokeMember("overloaded", "42").asString());
}
access = HostAccess.newBuilder().allowAccessAnnotatedBy(Export.class).build();
try (Context c = Context.newBuilder().allowHostAccess(access).build()) {
c.initialize(ProxyLanguage.ID);
Value v = c.asValue(new Overloaded());
Value arg = c.asValue(new Impl());
try {
v.invokeMember("overloaded", arg);
fail();
} catch (IllegalArgumentException e) {
assertTrue(e.getMessage(), e.getMessage().contains("no applicable overload found"));
}
assertEquals("42", v.invokeMember("overloaded", "42").asString());
}
access = HostAccess.newBuilder().allowAccessAnnotatedBy(Export.class).allowImplementationsAnnotatedBy(FunctionalInterface.class).build();
try (Context c = Context.newBuilder().allowHostAccess(access).build()) {
c.initialize(ProxyLanguage.ID);
Value v = c.asValue(new Overloaded());
Value arg = c.asValue(new Impl());
assertEquals("MarkedFunctional", v.invokeMember("overloaded", arg.getMember("noArg")).asString());
assertEquals("42", v.invokeMember("overloaded", "42").asString());
}
access = HostAccess.newBuilder().allowAccessAnnotatedBy(Export.class).allowImplementations(MarkedInterface.class).build();
try (Context c = Context.newBuilder().allowHostAccess(access).build()) {
c.initialize(ProxyLanguage.ID);
Value v = c.asValue(new Overloaded());
Value arg = c.asValue(new Impl());
assertEquals("MarkedInterface", v.invokeMember("overloaded", arg).asString());
assertEquals("42", v.invokeMember("overloaded", "42").asString());
}
}
@SuppressWarnings("unchecked")
@Test
public void testProxyExplicit() {
HostAccess access = HostAccess.EXPLICIT;
try (Context c = Context.newBuilder().allowHostAccess(access).build()) {
c.initialize(ProxyLanguage.ID);
Value v = c.asValue(new Impl());
Value f = v.getMember("noArg");
assertEquals("42", v.as(MarkedInterface.class).exported("42"));
try {
v.as(EmptyInterface.class);
fail();
} catch (ClassCastException e) {
}
try {
v.as(UnmarkedInterface.class);
fail();
} catch (ClassCastException e) {
}
assertEquals(42, f.as(MarkedFunctional.class).f());
try {
f.as(UnmarkedFunctional.class);
fail();
} catch (ClassCastException e) {
}
assertEquals(42, f.as(Function.class).apply(null));
}
}
@SuppressWarnings("unchecked")
@Test
public void testProxyMarked() {
HostAccess access = HostAccess.newBuilder().allowAccessAnnotatedBy(Export.class).
allowImplementations(UnmarkedInterface.class).allowImplementations(UnmarkedFunctional.class).build();
try (Context c = Context.newBuilder().allowHostAccess(access).build()) {
c.initialize(ProxyLanguage.ID);
Value v = c.asValue(new Impl());
Value f = v.getMember("noArg");
assertEquals("42", v.as(UnmarkedInterface.class).exported("42"));
try {
v.as(EmptyInterface.class);
fail();
} catch (ClassCastException e) {
}
try {
v.as(MarkedInterface.class);
fail();
} catch (ClassCastException e) {
}
assertEquals(42, f.as(UnmarkedFunctional.class).f());
try {
f.as(MarkedFunctional.class);
fail();
} catch (ClassCastException e) {
}
assertEquals(42, f.as(Function.class).apply(null));
}
}
@SuppressWarnings("unchecked")
@Test
public void testProxyNone() {
HostAccess access = HostAccess.newBuilder().allowAccessAnnotatedBy(Export.class).build();
try (Context c = Context.newBuilder().allowHostAccess(access).build()) {
c.initialize(ProxyLanguage.ID);
Value v = c.asValue(new Impl());
Value f = v.getMember("exported");
try {
v.as(MarkedInterface.class);
fail();
} catch (ClassCastException e) {
}
try {
v.as(EmptyInterface.class);
fail();
} catch (ClassCastException e) {
}
try {
v.as(UnmarkedInterface.class);
fail();
} catch (ClassCastException e) {
}
try {
f.as(MarkedFunctional.class);
fail();
} catch (ClassCastException e) {
}
try {
f.as(UnmarkedFunctional.class);
fail();
} catch (ClassCastException e) {
}
assertEquals("42", f.as(Function.class).apply("42"));
}
}
@SuppressWarnings("unchecked")
@Test
public void testProxyManualAll() {
HostAccess access = HostAccess.newBuilder().allowAccessAnnotatedBy(Export.class).allowAllImplementations(true).build();
try (Context c = Context.newBuilder().allowHostAccess(access).build()) {
c.initialize(ProxyLanguage.ID);
Value v = c.asValue(new Impl());
Value f = v.getMember("noArg");
assertEquals("42", v.as(MarkedInterface.class).exported("42"));
assertEquals("42", v.as(UnmarkedInterface.class).exported("42"));
assertNotNull(v.as(EmptyInterface.class));
assertEquals(42, f.as(MarkedFunctional.class).f());
assertEquals(42, f.as(UnmarkedFunctional.class).f());
assertEquals(42, f.as(Function.class).apply(null));
}
}
@SuppressWarnings("unchecked")
@Test
public void testProxyAll() {
HostAccess access = HostAccess.ALL;
try (Context c = Context.newBuilder().allowHostAccess(access).build()) {
c.initialize(ProxyLanguage.ID);
Value v = c.asValue(new Impl());
Value f = v.getMember("noArg");
assertEquals("42", v.as(MarkedInterface.class).exported("42"));
assertEquals("42", v.as(UnmarkedInterface.class).exported("42"));
assertNotNull(v.as(EmptyInterface.class));
assertEquals(42, f.as(MarkedFunctional.class).f());
assertEquals(42, f.as(UnmarkedFunctional.class).f());
assertEquals(42, f.as(Function.class).apply(null));
}
}
@SuppressWarnings("unchecked")
@Test
public void testAdapterClass() {
HostAccess access = HostAccess.newBuilder().allowAccessAnnotatedBy(Export.class).allowImplementations(MarkedClass.class).build();
try (Context c = Context.newBuilder().allowHostAccess(access).build()) {
c.initialize(ProxyLanguage.ID);
Value v = c.asValue(new Impl());
Value f = v.getMember("noArg");
MarkedClass markedClass = v.as(MarkedClass.class);
assertEquals("42", markedClass.exported("42"));
assertSame("adapter class should be cached", markedClass.getClass(), v.as(MarkedClass.class).getClass());
assertEquals(42, f.as(Function.class).apply(null));
}
}
@Test
public void testAdapterNoDefaultConstructor() {
HostAccess access = HostAccess.newBuilder().allowAccessAnnotatedBy(Export.class).allowImplementations(NoDefaultConstructor.class).build();
try (Context c = Context.newBuilder().allowHostAccess(access).build()) {
c.initialize(ProxyLanguage.ID);
Value v = c.asValue(new Impl());
try {
v.as(NoDefaultConstructor.class);
fail();
} catch (ClassCastException e) {
assertThat(e.getMessage(), containsString("Unsupported target type"));
}
}
}
@Test
public void testAdapterClassImplementationsNotAllowed() {
HostAccess access = HostAccess.newBuilder().allowAccessAnnotatedBy(Export.class).build();
try (Context c = Context.newBuilder().allowHostAccess(access).build()) {
c.initialize(ProxyLanguage.ID);
Value v = c.asValue(new Impl());
try {
v.as(MarkedClass.class);
fail();
} catch (ClassCastException e) {
assertThat(e.getMessage(), containsString("Unsupported target type"));
}
}
}
}