de.thetaphi.forbiddenapis.AsmUtils.java Source code

Java tutorial

Introduction

Here is the source code for de.thetaphi.forbiddenapis.AsmUtils.java

Source

/*
 * (C) Copyright Uwe Schindler (Generics Policeman) and others.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package de.thetaphi.forbiddenapis;

import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.PushbackInputStream;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Locale;
import java.util.regex.Pattern;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.Opcodes;

/** Some static utilities for analyzing with ASM, also constants. */
public final class AsmUtils {

    private AsmUtils() {
    }

    private static final String REGEX_META_CHARS = ".^$+{}[]|()\\";

    /** Package prefixes of documented Java API (extracted from Javadocs of Java 8). */
    private static final Pattern PORTABLE_RUNTIME_PACKAGE_PATTERN = makePkgPrefixPattern("java", "javax",
            "org.ietf.jgss", "org.omg", "org.w3c.dom", "org.xml.sax");

    /** Pattern that matches all module names, which are shipped by default in Java.
     * (see: {@code http://openjdk.java.net/projects/jigsaw/spec/sotms/}):
     * The remaining platform modules will share the 'java.' name prefix and are likely to include,
     * e.g., java.sql for database connectivity, java.xml for XML processing, and java.logging for
     * logging. Modules that are not defined in the Java SE 9 Platform Specification but instead
     * specific to the JDK will, by convention, share the 'jdk.' name prefix.
     */
    private static final Pattern RUNTIME_MODULES_PATTERN = makePkgPrefixPattern("java", "jdk");

    private static Pattern makePkgPrefixPattern(String... prefixes) {
        final StringBuilder sb = new StringBuilder();
        boolean first = true;
        for (final String p : prefixes) {
            sb.append(first ? '(' : '|').append(Pattern.quote(p));
            first = false;
        }
        sb.append(")").append(Pattern.quote(".")).append(".*");
        return Pattern.compile(sb.toString());
    }

    private static boolean isRegexMeta(char c) {
        return REGEX_META_CHARS.indexOf(c) != -1;
    }

    /** Returns true, if the given binary class name (dotted) is part of the documented and portable Java APIs. */
    public static boolean isPortableRuntimeClass(String className) {
        return PORTABLE_RUNTIME_PACKAGE_PATTERN.matcher(className).matches();
    }

    /** Returns true, if the given Java 9 module name is part of the runtime (no custom 3rd party module).
     * @param module the module name or {@code null}, if in unnamed module
     */
    public static boolean isRuntimeModule(String module) {
        return module != null && RUNTIME_MODULES_PATTERN.matcher(module).matches();
    }

    /** Converts a binary class name (dotted) to the JVM internal one (slashed). Only accepts valid class names, no arrays. */
    public static String binaryToInternal(String clazz) {
        if (clazz.indexOf('/') >= 0 || clazz.indexOf('[') >= 0) {
            throw new IllegalArgumentException(
                    String.format(Locale.ENGLISH, "'%s' is not a valid binary class name.", clazz));
        }
        return clazz.replace('.', '/');
    }

    /** Converts a binary class name to a "{@code .class}" file resource name. */
    public static String getClassResourceName(String clazz) {
        return binaryToInternal(clazz).concat(".class");
    }

    /** Returns true is a string is a glob pattern */
    public static boolean isGlob(String s) {
        return s.indexOf('*') >= 0 || s.indexOf('?') >= 0;
    }

    /** Returns a regex pattern that matches on any of the globs on class names (e.g., "sun.misc.**") */
    public static Pattern glob2Pattern(String... globs) {
        final StringBuilder regex = new StringBuilder();
        boolean needOr = false;
        for (String glob : globs) {
            if (needOr) {
                regex.append('|');
            }
            int i = 0, len = glob.length();
            while (i < len) {
                char c = glob.charAt(i++);
                switch (c) {
                case '*':
                    if (i < len && glob.charAt(i) == '*') {
                        // crosses package boundaries
                        regex.append(".*");
                        i++;
                    } else {
                        // do not cross package boundaries
                        regex.append("[^.]*");
                    }
                    break;

                case '?':
                    // do not cross package boundaries
                    regex.append("[^.]");
                    break;

                default:
                    if (isRegexMeta(c)) {
                        regex.append('\\');
                    }
                    regex.append(c);
                }
            }
            needOr = true;
        }
        return Pattern.compile(regex.toString(), 0);
    }

    /** Returns the module name from a {@code jrt:/} URL; returns null if no module given or wrong URL type. */
    public static String getModuleName(URL jrtUrl) {
        if (!"jrt".equalsIgnoreCase(jrtUrl.getProtocol())) {
            return null;
        }
        try {
            // use URI class to also decode path and remove escapes:
            String mod = jrtUrl.toURI().getPath();
            if (mod != null && mod.length() >= 1) {
                mod = mod.substring(1);
                int p = mod.indexOf('/');
                if (p >= 0) {
                    mod = mod.substring(0, p);
                }
                return mod.isEmpty() ? null : mod;
            }
            return null;
        } catch (URISyntaxException use) {
            return null;
        }
    }

    private static void patchClassMajorVersion(byte[] header, int versionFrom, int versionTo) {
        final ByteBuffer buf = ByteBuffer.wrap(header).order(ByteOrder.BIG_ENDIAN);
        if (buf.getShort(6) == versionFrom) {
            buf.putShort(6, (short) versionTo);
        }
    }

    /** Utility method to load class files of Java 9 by patching them, so ASM can read them. */
    public static ClassReader readAndPatchClass(InputStream in) throws IOException {
        final byte[] b = new byte[8];
        final PushbackInputStream pbin = new PushbackInputStream(in, b.length);
        for (int upto = 0; upto < b.length;) {
            final int read = pbin.read(b, upto, b.length - upto);
            if (read == -1)
                throw new EOFException("Not enough bytes available to read header of class file.");
            upto += read;
        }
        patchClassMajorVersion(b, Opcodes.V1_8 + 1, Opcodes.V1_8);
        pbin.unread(b);
        return new ClassReader(pbin);
    }

}