OptionsVerifierTest.java revision 12651:6ef01bd40ce2
1/*
2 * Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.
8 *
9 * This code is distributed in the hope that it will be useful, but WITHOUT
10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12 * version 2 for more details (a copy is included in the LICENSE file that
13 * accompanied this code).
14 *
15 * You should have received a copy of the GNU General Public License version
16 * 2 along with this work; if not, write to the Free Software Foundation,
17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18 *
19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20 * or visit www.oracle.com if you need additional information or have any
21 * questions.
22 */
23package org.graalvm.compiler.core.test;
24
25import static java.lang.String.format;
26
27import java.io.ByteArrayOutputStream;
28import java.io.File;
29import java.io.IOException;
30import java.io.InputStream;
31import java.lang.reflect.Constructor;
32import java.lang.reflect.Executable;
33import java.lang.reflect.Method;
34import java.net.URL;
35import java.net.URLClassLoader;
36import java.nio.file.Files;
37import java.util.ArrayList;
38import java.util.Arrays;
39import java.util.HashSet;
40import java.util.LinkedHashMap;
41import java.util.List;
42import java.util.Map;
43import java.util.Objects;
44import java.util.ServiceLoader;
45import java.util.Set;
46
47import org.junit.Test;
48
49import org.graalvm.compiler.options.OptionDescriptor;
50import org.graalvm.compiler.options.OptionDescriptors;
51import org.graalvm.compiler.options.OptionValue;
52import org.graalvm.compiler.test.GraalTest;
53
54import jdk.internal.org.objectweb.asm.ClassReader;
55import jdk.internal.org.objectweb.asm.ClassVisitor;
56import jdk.internal.org.objectweb.asm.Label;
57import jdk.internal.org.objectweb.asm.MethodVisitor;
58import jdk.internal.org.objectweb.asm.Opcodes;
59import jdk.internal.org.objectweb.asm.Type;
60
61/**
62 * Verifies a class declaring one or more {@linkplain OptionValue options} has a class initializer
63 * that only initializes the option(s). This sanity check mitigates the possibility of an option
64 * value being used before being set.
65 */
66public class OptionsVerifierTest {
67
68    @Test
69    public void verifyOptions() throws IOException {
70        try (Classpath cp = new Classpath()) {
71            HashSet<Class<?>> checked = new HashSet<>();
72            for (OptionDescriptors opts : ServiceLoader.load(OptionDescriptors.class, getClass().getClassLoader())) {
73                for (OptionDescriptor desc : opts) {
74                    OptionsVerifier.checkClass(desc.getDeclaringClass(), desc, checked, cp);
75                }
76            }
77        }
78    }
79
80    static class Classpath implements AutoCloseable {
81        private final Map<String, Object> entries = new LinkedHashMap<>();
82
83        Classpath() throws IOException {
84            List<String> names = new ArrayList<>(Arrays.asList(System.getProperty("java.class.path").split(File.pathSeparator)));
85            if (GraalTest.Java8OrEarlier) {
86                names.addAll(Arrays.asList(System.getProperty("sun.boot.class.path").split(File.pathSeparator)));
87            } else {
88                names.addAll(Arrays.asList(System.getProperty("jdk.module.path").split(File.pathSeparator)));
89            }
90            for (String n : names) {
91                File path = new File(n);
92                if (path.exists()) {
93                    if (path.isDirectory()) {
94                        entries.put(n, path);
95                    } else if (n.endsWith(".jar") || n.endsWith(".zip")) {
96                        URL url = new URL("jar", "", "file:" + n + "!/");
97                        entries.put(n, new URLClassLoader(new URL[]{url}));
98                    }
99                }
100            }
101        }
102
103        @Override
104        public void close() throws IOException {
105            for (Object e : entries.values()) {
106                if (e instanceof URLClassLoader) {
107                    ((URLClassLoader) e).close();
108                }
109            }
110        }
111
112        public byte[] getInputStream(String classFilePath) throws IOException {
113            for (Object e : entries.values()) {
114                if (e instanceof File) {
115                    File path = new File((File) e, classFilePath.replace('/', File.separatorChar));
116                    if (path.exists()) {
117                        return Files.readAllBytes(path.toPath());
118                    }
119                } else {
120                    assert e instanceof URLClassLoader;
121                    URLClassLoader ucl = (URLClassLoader) e;
122                    try (InputStream in = ucl.getResourceAsStream(classFilePath)) {
123                        if (in != null) {
124                            ByteArrayOutputStream buffer = new ByteArrayOutputStream();
125                            int nRead;
126                            byte[] data = new byte[1024];
127                            while ((nRead = in.read(data, 0, data.length)) != -1) {
128                                buffer.write(data, 0, nRead);
129                            }
130                            return buffer.toByteArray();
131                        }
132                    }
133                }
134            }
135            return null;
136        }
137    }
138
139    static final class OptionsVerifier extends ClassVisitor {
140
141        public static void checkClass(Class<?> cls, OptionDescriptor option, Set<Class<?>> checked, Classpath cp) throws IOException {
142            if (!checked.contains(cls)) {
143                checked.add(cls);
144                Class<?> superclass = cls.getSuperclass();
145                if (superclass != null && !superclass.equals(Object.class)) {
146                    checkClass(superclass, option, checked, cp);
147                }
148
149                String classFilePath = cls.getName().replace('.', '/') + ".class";
150                ClassReader cr = new ClassReader(Objects.requireNonNull(cp.getInputStream(classFilePath), "Could not find class file for " + cls.getName()));
151
152                ClassVisitor cv = new OptionsVerifier(cls, option);
153                cr.accept(cv, 0);
154            }
155        }
156
157        /**
158         * The option field context of the verification.
159         */
160        private final OptionDescriptor option;
161
162        /**
163         * The class in which {@link #option} is declared or a super-class of that class. This is
164         * the class whose {@code <clinit>} method is being verified.
165         */
166        private final Class<?> cls;
167
168        /**
169         * Source file context for error reporting.
170         */
171        String sourceFile = null;
172
173        /**
174         * Line number for error reporting.
175         */
176        int lineNo = -1;
177
178        final Class<?>[] boxingTypes = {Boolean.class, Byte.class, Short.class, Character.class, Integer.class, Float.class, Long.class, Double.class};
179
180        private static Class<?> resolve(String name) {
181            try {
182                return Class.forName(name.replace('/', '.'));
183            } catch (ClassNotFoundException e) {
184                throw new InternalError(e);
185            }
186        }
187
188        OptionsVerifier(Class<?> cls, OptionDescriptor desc) {
189            super(Opcodes.ASM5);
190            this.cls = cls;
191            this.option = desc;
192        }
193
194        @Override
195        public void visitSource(String source, String debug) {
196            this.sourceFile = source;
197        }
198
199        void verify(boolean condition, String message) {
200            if (!condition) {
201                error(message);
202            }
203        }
204
205        void error(String message) {
206            String errorMessage = format(
207                            "%s:%d: Illegal code in %s.<clinit> which may be executed when %s.%s is initialized:%n%n    %s%n%n" + "The recommended solution is to move " + option.getName() +
208                                            " into a separate class (e.g., %s.Options).%n",
209                            sourceFile, lineNo, cls.getSimpleName(), option.getDeclaringClass().getSimpleName(), option.getName(),
210                            message, option.getDeclaringClass().getSimpleName());
211            throw new InternalError(errorMessage);
212
213        }
214
215        @Override
216        public MethodVisitor visitMethod(int access, String name, String d, String signature, String[] exceptions) {
217            if (name.equals("<clinit>")) {
218                return new MethodVisitor(Opcodes.ASM5) {
219
220                    @Override
221                    public void visitLineNumber(int line, Label start) {
222                        lineNo = line;
223                    }
224
225                    @Override
226                    public void visitFieldInsn(int opcode, String owner, String fieldName, String fieldDesc) {
227                        if (opcode == Opcodes.PUTFIELD || opcode == Opcodes.PUTSTATIC) {
228                            verify(resolve(owner).equals(option.getDeclaringClass()), format("store to field %s.%s", resolve(owner).getSimpleName(), fieldName));
229                            verify(opcode != Opcodes.PUTFIELD, format("store to non-static field %s.%s", resolve(owner).getSimpleName(), fieldName));
230                        }
231                    }
232
233                    private Executable resolveMethod(String owner, String methodName, String methodDesc) {
234                        Class<?> declaringClass = resolve(owner);
235                        if (methodName.equals("<init>")) {
236                            for (Constructor<?> c : declaringClass.getDeclaredConstructors()) {
237                                if (methodDesc.equals(Type.getConstructorDescriptor(c))) {
238                                    return c;
239                                }
240                            }
241                        } else {
242                            Type[] argumentTypes = Type.getArgumentTypes(methodDesc);
243                            for (Method m : declaringClass.getDeclaredMethods()) {
244                                if (m.getName().equals(methodName)) {
245                                    if (Arrays.equals(argumentTypes, Type.getArgumentTypes(m))) {
246                                        if (Type.getReturnType(methodDesc).equals(Type.getReturnType(m))) {
247                                            return m;
248                                        }
249                                    }
250                                }
251                            }
252                        }
253                        throw new NoSuchMethodError(declaringClass + "." + methodName + methodDesc);
254                    }
255
256                    /**
257                     * Checks whether a given method is allowed to be called.
258                     */
259                    private boolean checkInvokeTarget(Executable method) {
260                        Class<?> holder = method.getDeclaringClass();
261                        if (method instanceof Constructor) {
262                            if (OptionValue.class.isAssignableFrom(holder)) {
263                                return true;
264                            }
265                        } else if (Arrays.asList(boxingTypes).contains(holder)) {
266                            return method.getName().equals("valueOf");
267                        } else if (method.getDeclaringClass().equals(Class.class)) {
268                            return method.getName().equals("desiredAssertionStatus");
269                        }
270                        return false;
271                    }
272
273                    @Override
274                    public void visitMethodInsn(int opcode, String owner, String methodName, String methodDesc, boolean itf) {
275                        Executable callee = resolveMethod(owner, methodName, methodDesc);
276                        verify(checkInvokeTarget(callee), "invocation of " + callee);
277                    }
278                };
279            } else {
280                return null;
281            }
282        }
283    }
284
285}
286