/*
* (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);
}
}