org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.GroovyCallSiteSelector.java Source code

Java tutorial

Introduction

Here is the source code for org.jenkinsci.plugins.scriptsecurity.sandbox.groovy.GroovyCallSiteSelector.java

Source

/*
 * The MIT License
 *
 * Copyright 2014 CloudBees, Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

package org.jenkinsci.plugins.scriptsecurity.sandbox.groovy;

import com.google.common.primitives.Primitives;
import groovy.lang.GString;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.LinkedHashSet;
import java.util.Set;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import org.apache.commons.lang.ClassUtils;

/**
 * Assists in determination of which method or other JVM element is actually about to be called by Groovy.
 * Most of this just duplicates what {@link java.lang.invoke.MethodHandles.Lookup} and {@link java.lang.invoke.MethodHandle#asType} do,
 * but {@link org.codehaus.groovy.vmplugin.v7.TypeTransformers} (Groovy 2) shows that there are Groovy-specific complications.
 * Comments in https://github.com/kohsuke/groovy-sandbox/issues/7 note that it would be great for the sandbox itself to just tell us what the call site is so we would not have to guess.
 */
class GroovyCallSiteSelector {

    private static boolean matches(@Nonnull Class<?>[] parameterTypes, @Nonnull Object[] parameters) {
        if (parameters.length != parameterTypes.length) {
            return false;
        }
        for (int i = 0; i < parameterTypes.length; i++) {
            if (parameters[i] == null) {
                if (parameterTypes[i].isPrimitive()) {
                    return false;
                } else {
                    // A null argument is assignable to any reference-typed parameter.
                    continue;
                }
            }
            if (parameterTypes[i].isInstance(parameters[i])) {
                // OK, this parameter matches.
                continue;
            }
            if (parameterTypes[i].isPrimitive() && parameters[i] != null
                    && ClassUtils.primitiveToWrapper(parameterTypes[i]).isInstance(parameters[i])) {
                // Groovy passes primitive values as objects (for example, passes 0 as Integer(0))
                // The prior test fails as int.class.isInstance(new Integer(0)) returns false.
                continue;
            }
            // TODO what about a primitive parameter type and a wrapped parameter?
            if (parameterTypes[i] == String.class && parameters[i] instanceof GString) {
                // Cf. SandboxInterceptorTest and class Javadoc.
                continue;
            }
            // Mismatch.
            return false;
        }
        return true;
    }

    /**
     * Looks up the most general possible definition of a given method call.
     * Preferentially searches for compatible definitions in supertypes.
     * @param receiver an actual receiver object
     * @param method the method name
     * @param args a set of actual arguments
     */
    public static @CheckForNull Method method(@Nonnull Object receiver, @Nonnull String method,
            @Nonnull Object[] args) {
        for (Class<?> c : types(receiver)) {
            Method candidate = findMatchingMethod(c, method, args);
            if (candidate != null) {
                return candidate;
            }
        }
        if (receiver instanceof GString) { // cf. GString.invokeMethod
            Method candidate = findMatchingMethod(String.class, method, args);
            if (candidate != null) {
                return candidate;
            }
        }
        return null;
    }

    public static @CheckForNull Constructor<?> constructor(@Nonnull Class<?> receiver, @Nonnull Object[] args) {
        for (Constructor<?> c : receiver.getDeclaredConstructors()) {
            if (matches(c.getParameterTypes(), args)) {
                return c;
            }
        }
        return null;
    }

    public static @CheckForNull Method staticMethod(@Nonnull Class<?> receiver, @Nonnull String method,
            @Nonnull Object[] args) {
        // TODO should we check for inherited static calls?
        return findMatchingMethod(receiver, method, args);
    }

    private static Method findMatchingMethod(Class<?> receiver, String method, Object[] args) {
        Method candidate = null;
        for (Method m : receiver.getDeclaredMethods()) {
            if (m.getName().equals(method) && matches(m.getParameterTypes(), args)) {
                if (candidate == null || isMoreSpecific(m, candidate)) {
                    candidate = m;
                }
            }
        }
        return candidate;
    }

    public static @CheckForNull Field field(@Nonnull Object receiver, @Nonnull String field) {
        for (Class<?> c : types(receiver)) {
            for (Field f : c.getDeclaredFields()) {
                if (f.getName().equals(field)) {
                    return f;
                }
            }
        }
        return null;
    }

    public static @CheckForNull Field staticField(@Nonnull Class<?> receiver, @Nonnull String field) {
        for (Field f : receiver.getDeclaredFields()) {
            if (f.getName().equals(field)) {
                return f;
            }
        }
        return null;
    }

    private static Iterable<Class<?>> types(@Nonnull Object o) {
        Set<Class<?>> types = new LinkedHashSet<Class<?>>();
        visitTypes(types, o.getClass());
        return types;
    }

    private static void visitTypes(@Nonnull Set<Class<?>> types, @Nonnull Class<?> c) {
        Class<?> s = c.getSuperclass();
        if (s != null) {
            visitTypes(types, s);
        }
        for (Class<?> i : c.getInterfaces()) {
            visitTypes(types, i);
        }
        // Visit supertypes first.
        types.add(c);
    }

    private static boolean isMoreSpecific(Method more, Method less) {
        Class<?>[] moreParams = more.getParameterTypes();
        Class<?>[] lessParams = less.getParameterTypes();
        assert moreParams.length == lessParams.length;
        for (int i = 0; i < moreParams.length; i++) {
            Class<?> moreParam = Primitives.wrap(moreParams[i]);
            Class<?> lessParam = Primitives.wrap(lessParams[i]);
            if (moreParam.isAssignableFrom(lessParam)) {
                return false;
            } else if (lessParam.isAssignableFrom(moreParam)) {
                return true;
            }
        }
        // Incomparable. Arbitrarily pick one of them.
        return more.toString().compareTo(less.toString()) > 0;
    }

    private GroovyCallSiteSelector() {
    }

}