blob: bfd8334e0fd46a433cd207f832b939918fa495e8 [file] [log] [blame]
// Copyright (c) 2016, the R8 project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
package com.android.tools.r8.utils;
import static com.android.tools.r8.utils.FileUtils.CLASS_EXTENSION;
import static com.android.tools.r8.utils.FileUtils.MODULES_PREFIX;
import com.android.tools.r8.errors.CompilationError;
import com.android.tools.r8.errors.InvalidDescriptorException;
import com.android.tools.r8.errors.Unreachable;
import com.android.tools.r8.graph.DexString;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.naming.ClassNameMapper;
import com.google.common.collect.ImmutableMap;
import java.io.File;
import java.nio.file.Path;
import java.util.Map;
public class DescriptorUtils {
public static final char DESCRIPTOR_PACKAGE_SEPARATOR = '/';
public static final char JAVA_PACKAGE_SEPARATOR = '.';
public static final char INNER_CLASS_SEPARATOR = '$';
private static final Map<String, String> typeNameToLetterMap =
ImmutableMap.<String, String>builder()
.put("void", "V")
.put("boolean", "Z")
.put("byte", "B")
.put("short", "S")
.put("char", "C")
.put("int", "I")
.put("long", "J")
.put("float", "F")
.put("double", "D")
.build();
private static String internalToDescriptor(
String typeName, boolean shorty, boolean ignorePrimitives) {
String descriptor = null;
if (!ignorePrimitives) {
descriptor = typeNameToLetterMap.get(typeName);
}
if (descriptor != null) {
return descriptor;
}
// Must be some array or object type.
if (shorty) {
return "L";
}
if (typeName.endsWith("[]")) {
return "[" + internalToDescriptor(
typeName.substring(0, typeName.length() - 2), shorty, ignorePrimitives);
}
// Must be an object type.
return "L" + typeName.replace(JAVA_PACKAGE_SEPARATOR, DESCRIPTOR_PACKAGE_SEPARATOR) + ";";
}
/**
* Convert a Java type name to a descriptor string.
*
* @param typeName the java type name
* @return the descriptor string
*/
public static String javaTypeToDescriptor(String typeName) {
assert typeName.indexOf(DESCRIPTOR_PACKAGE_SEPARATOR) == -1;
return internalToDescriptor(typeName, false, false);
}
/**
* Convert a Java type name to a descriptor string ignoring primitive types.
*
* Ignoring primitives mean that type named like int and long are considered class names, will
* return Lint; and Llong; respectively instead of I and J. These are not legal Java class names,
* but valid on the JVM and minification/obfuscation can generate them.
*
* @param typeName the java type name
* @return the descriptor string
*/
public static String javaTypeToDescriptorIgnorePrimitives(String typeName) {
assert typeName.indexOf(DESCRIPTOR_PACKAGE_SEPARATOR) == -1;
return internalToDescriptor(typeName, false, true);
}
/**
* Convert a Java type name to a descriptor string only if the given {@param typeName} is valid.
*
* @param typeName the java type name
* @return the descriptor string if {@param typeName} is not valid or null otherwise
*/
public static String javaTypeToDescriptorIfValidJavaType(String typeName) {
if (isValidJavaType(typeName)) {
return javaTypeToDescriptor(typeName);
}
return null;
}
/**
* Produces an array descriptor having the number of dimensions specified and the
* baseTypeDescriptor as base.
*
* @param dimensions number of dimensions
* @param baseTypeDescriptor the base type
* @return the descriptor string
*/
public static String toArrayDescriptor(int dimensions, String baseTypeDescriptor) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < dimensions; i++) {
sb.append('[');
}
sb.append(baseTypeDescriptor);
return sb.toString();
}
/**
* Determine the given {@param typeName} is a valid jvms binary name or not (jvms 4.2.1).
*
* @param typeName the jvms binary name
* @return true if and only if the given type name is valid jvms binary name
*/
public static boolean isValidJavaType(String typeName) {
if (typeName.length() == 0) {
return false;
}
char last = 0;
for (int i = 0; i < typeName.length(); i++) {
char c = typeName.charAt(i);
if (c == ';' ||
c == '[' ||
c == '/') {
return false;
}
if (c == '.' && (i == 0 || last == '.')) {
return false;
}
last = c;
}
return true;
}
/**
* Convert a Java type name to a shorty descriptor string.
*
* @param typeName the java type name
* @return the shorty descriptor string
*/
public static String javaTypeToShorty(String typeName) {
return internalToDescriptor(typeName, true, false);
}
/**
* Convert a type descriptor to a Java type name.
*
* @param descriptor type descriptor
* @return Java type name
*/
public static String descriptorToJavaType(String descriptor) {
return descriptorToJavaType(descriptor, null);
}
/**
* Convert a class type descriptor to an ASM internal name.
*
* @param descriptor type descriptor
* @return Java type name
*/
public static String descriptorToInternalName(String descriptor) {
switch (descriptor.charAt(0)) {
case '[':
return descriptor;
case 'L':
return descriptor.substring(1, descriptor.length() - 1);
default:
throw new Unreachable("Not array or class type");
}
}
/**
* Convert a descriptor to a classifier in Kotlin metadata
* @param descriptor like "Lorg/foo/bar/Baz$Nested;"
* @return className "org/foo/bar/Baz.Nested"
*/
public static String descriptorToKotlinClassifier(String descriptor) {
final String classifier =
getBinaryNameFromDescriptor(descriptor)
.replace(INNER_CLASS_SEPARATOR, JAVA_PACKAGE_SEPARATOR);
if (descriptor.startsWith("Lj$/")) {
assert classifier.startsWith("j./");
return "j$/" + classifier.substring(3);
}
return classifier;
}
/**
* Convert a type descriptor to a Java type name. Will also deobfuscate class names if a
* class mapper is provided.
*
* @param descriptor type descriptor
* @param classNameMapper class name mapper for mapping obfuscated class names
* @return Java type name
*/
public static String descriptorToJavaType(String descriptor, ClassNameMapper classNameMapper) {
char c = descriptor.charAt(0);
switch (c) {
case 'L':
assert descriptor.charAt(descriptor.length() - 1) == ';';
String clazz = descriptor.substring(1, descriptor.length() - 1)
.replace(DESCRIPTOR_PACKAGE_SEPARATOR, JAVA_PACKAGE_SEPARATOR);
String originalName =
classNameMapper == null ? clazz : classNameMapper.deobfuscateClassName(clazz);
return originalName;
case '[':
return descriptorToJavaType(descriptor.substring(1), classNameMapper) + "[]";
default:
return primitiveDescriptorToJavaType(c);
}
}
public static boolean isPrimitiveDescriptor(String descriptor) {
if (descriptor.length() != 1) {
return false;
}
return isPrimitiveType(descriptor.charAt(0));
}
public static boolean isPrimitiveType(char c) {
return c == 'Z' || c == 'B' || c == 'S' || c == 'C' || c == 'I' || c == 'F' || c == 'J'
|| c == 'D';
}
public static boolean isArrayDescriptor(String descriptor) {
if (descriptor.length() < 2) {
return false;
}
if (descriptor.charAt(0) == '[') {
return isDescriptor(descriptor.substring(1));
}
return false;
}
public static boolean isDescriptor(String descriptor) {
return isClassDescriptor(descriptor)
|| isPrimitiveDescriptor(descriptor)
|| isArrayDescriptor(descriptor);
}
public static String primitiveDescriptorToJavaType(char primitive) {
switch (primitive) {
case 'V':
return "void";
case 'Z':
return "boolean";
case 'B':
return "byte";
case 'S':
return "short";
case 'C':
return "char";
case 'I':
return "int";
case 'J':
return "long";
case 'F':
return "float";
case 'D':
return "double";
default:
throw new Unreachable("Unknown type " + primitive);
}
}
/**
* Get unqualified class name from its descriptor.
*
* @param classDescriptor a class descriptor i.e. "Ljava/lang/Object;" or "La/b/C$D;"
* @return class name i.e. "Object" or "C$D" (not "D")
*/
public static String getUnqualifiedClassNameFromDescriptor(String classDescriptor) {
return getUnqualifiedClassNameFromBinaryName(getClassBinaryNameFromDescriptor(classDescriptor));
}
/**
* Get class name from its descriptor.
*
* @param classDescriptor a class descriptor i.e. "Ljava/lang/Object;"
* @return full class name i.e. "java.lang.Object"
*/
public static String getClassNameFromDescriptor(String classDescriptor) {
return getClassBinaryNameFromDescriptor(classDescriptor)
.replace(DESCRIPTOR_PACKAGE_SEPARATOR, JAVA_PACKAGE_SEPARATOR);
}
/**
* Get the simple class name from its descriptor.
*
* @param classDescriptor a class descriptor i.e. "Ljava/lang/Object;"
* @return simple class name i.e. "Object"
*/
public static String getSimpleClassNameFromDescriptor(String classDescriptor) {
return classDescriptor.substring(
getSimpleClassNameIndex(classDescriptor), classDescriptor.length() - 1);
}
/**
* Replace the simple class name from its descriptor with a new simple name.
*
* @param classDescriptor a class descriptor i.e. "Ljava/lang/Object;"
* @param newSimpleName a new simple name e.g. "NewObject"
* @return updated class descriptor i.e. "Ljava/lang/NewObject;"
*/
public static String replaceSimpleClassNameInDescriptor(
String classDescriptor, String newSimpleName) {
return "L"
+ classDescriptor.substring(1, getSimpleClassNameIndex(classDescriptor))
+ newSimpleName
+ ";";
}
/**
* Finds the index of the simple class name in its descriptor.
*
* @param classDescriptor a class descriptor i.e. "Ljava/lang/Object;"
* @return the index of the simple name i.e. 11.
*/
private static int getSimpleClassNameIndex(String classDescriptor) {
return Integer.max(classDescriptor.lastIndexOf("/"), 0) + 1;
}
/**
* Get canonical class name from its descriptor.
*
* @param classDescriptor a class descriptor i.e. "La/b/C$D;"
* @return canonical class name i.e. "a.b.C.D"
*/
public static String getCanonicalNameFromDescriptor(String classDescriptor) {
return getClassNameFromDescriptor(classDescriptor)
.replace(INNER_CLASS_SEPARATOR, JAVA_PACKAGE_SEPARATOR);
}
/**
* Convert class to a binary name.
*
* @param clazz a java.lang.Class reference
* @return class binary name i.e. "java/lang/Object"
*/
public static String getClassBinaryName(Class<?> clazz) {
return getBinaryNameFromJavaType(clazz.getTypeName());
}
/**
* Get package java name from a class descriptor.
*
* @param descriptor a class descriptor i.e. "Ljava/lang/Object;"
* @return java package name i.e. "java.lang"
*/
public static String getPackageNameFromDescriptor(String descriptor) {
return getPackageNameFromBinaryName(getClassBinaryNameFromDescriptor(descriptor));
}
/**
* Convert class descriptor to a binary name.
*
* @param classDescriptor a class descriptor i.e. "Ljava/lang/Object;"
* @return class binary name i.e. "java/lang/Object"
*/
public static String getClassBinaryNameFromDescriptor(String classDescriptor) {
assert isClassDescriptor(classDescriptor) : "Invalid class descriptor "
+ classDescriptor;
return classDescriptor.substring(1, classDescriptor.length() - 1);
}
/**
* Convert package name to a binary name.
*
* @param packageName a package name i.e., "java.lang"
* @return java package name in a binary name format, i.e., java/lang
*/
public static String getPackageBinaryNameFromJavaType(String packageName) {
return packageName.replace(JAVA_PACKAGE_SEPARATOR, DESCRIPTOR_PACKAGE_SEPARATOR);
}
/**
* Convert class name to a binary name.
*
* @param className a package name i.e., "java.lang.Object"
* @return java class name in a binary name format, i.e., java/lang/Object
*/
public static String getBinaryNameFromJavaType(String className) {
return className.replace(JAVA_PACKAGE_SEPARATOR, DESCRIPTOR_PACKAGE_SEPARATOR);
}
public static String getJavaTypeFromBinaryName(String className) {
return className.replace(DESCRIPTOR_PACKAGE_SEPARATOR, JAVA_PACKAGE_SEPARATOR);
}
public static String getBinaryNameFromDescriptor(String classDescriptor) {
assert isClassDescriptor(classDescriptor);
return classDescriptor.substring(1, classDescriptor.length() - 1);
}
/**
* Convert a class binary name to a descriptor.
*
* @param typeBinaryName class binary name i.e. "java/lang/Object"
* @return a class descriptor i.e. "Ljava/lang/Object;"
*/
public static String getDescriptorFromClassBinaryName(String typeBinaryName) {
assert typeBinaryName != null;
return 'L' + typeBinaryName + ';';
}
/**
* Convert a fully qualified name of a classifier in Kotlin metadata to a descriptor.
* @param className "org/foo/bar/Baz.Nested"
* @return a class descriptor like "Lorg/foo/bar/Baz$Nested;"
*/
public static String getDescriptorFromKotlinClassifier(String className) {
assert className != null;
assert !className.contains("[") : className;
return 'L' + className.replace(JAVA_PACKAGE_SEPARATOR, INNER_CLASS_SEPARATOR) + ';';
}
/**
* Get unqualified class name from its binary name.
*
* @param classBinaryName a class binary name i.e. "java/lang/Object" or "a/b/C$Inner"
* @return class name i.e. "Object" or "C$Inner" (not "Inner")
*
* Note that we cannot rely on $ separator in binary name or descriptor because a class, which is
* not a member or local class, can still contain $ in its name. For the correct retrieval of the
* simple name of member or local classes, use the inner name in the inner-class attribute (or
* refer to ReflectionOptimizer#computeClassName as an example).
*/
public static String getUnqualifiedClassNameFromBinaryName(String classBinaryName) {
int simpleNameIndex = classBinaryName.lastIndexOf(DESCRIPTOR_PACKAGE_SEPARATOR);
return (simpleNameIndex < 0) ? classBinaryName : classBinaryName.substring(simpleNameIndex + 1);
}
public static String computeInnerClassSeparator(
DexType outerClass, DexType innerClass, DexString innerName) {
assert innerClass != null;
// Filter out non-member classes ahead.
if (outerClass == null || innerName == null) {
return String.valueOf(INNER_CLASS_SEPARATOR);
}
return computeInnerClassSeparator(
outerClass.getInternalName(), innerClass.getInternalName(), innerName.toString());
}
public static String computeInnerClassSeparator(
String outerDescriptor, String innerDescriptor, String innerName) {
assert innerName != null && !innerName.isEmpty();
// outer-internal<separator>inner-name == inner-internal
if (outerDescriptor.length() + innerName.length() > innerDescriptor.length()) {
return null;
}
String separator =
innerDescriptor.substring(
outerDescriptor.length(), innerDescriptor.length() - innerName.length());
// Any non-$ separator results in a runtime exception in getCanonicalName.
if (!separator.startsWith(String.valueOf(INNER_CLASS_SEPARATOR))) {
return null;
}
return separator;
}
public static boolean isClassDescriptor(String descriptor) {
char[] buffer = descriptor.toCharArray();
int length = buffer.length;
if (length < 3 || buffer[0] != 'L') {
return false;
}
int pos = 1;
char ch;
do {
// First letter of an Ident (an Ident can't be empty)
if (pos >= length) {
return false;
}
ch = buffer[pos++];
if (isInvalidChar(ch) || ch == DESCRIPTOR_PACKAGE_SEPARATOR || ch == ';') {
return false;
}
// Next letters of an Ident
do {
if (pos >= length) {
return false;
}
ch = buffer[pos++];
if (isInvalidChar(ch)) {
return false;
}
} while (ch != DESCRIPTOR_PACKAGE_SEPARATOR && ch != ';');
} while (ch != ';');
return pos == length;
}
/**
* Get package java name from a class binary name
*
* @param classBinaryName a class binary name i.e. "java/lang/Object"
* @return java package name i.e. "java.lang"
*/
public static String getPackageNameFromBinaryName(String classBinaryName) {
int nameIndex = classBinaryName.lastIndexOf(DESCRIPTOR_PACKAGE_SEPARATOR);
return (nameIndex < 0) ? "" : classBinaryName.substring(0, nameIndex)
.replace(DESCRIPTOR_PACKAGE_SEPARATOR, JAVA_PACKAGE_SEPARATOR);
}
private static boolean isInvalidChar(char ch) {
switch (ch) {
case JAVA_PACKAGE_SEPARATOR:
case '[':
return true;
default:
return false;
}
}
/**
* Guess class descriptor from location of the class file on the file system
*
* @param name Path of the file to convert to the corresponding descriptor
* @return java class descriptor
*/
public static String guessTypeDescriptor(Path name) {
String fileName = name.toString();
if (File.separatorChar != '/') {
fileName = fileName.replace(File.separatorChar, '/');
}
return guessTypeDescriptor(fileName);
}
/**
* Guess class descriptor from location of the class file. This method assumes that the
* name uses '/' as the separator. Therefore, this should not be the name of a file
* on a file system.
*
* @param name the location of the class file to convert to descriptor
* @return java class descriptor
*/
public static String guessTypeDescriptor(String name) {
assert name != null;
assert name.endsWith(CLASS_EXTENSION) :
"Name " + name + " must have " + CLASS_EXTENSION + " suffix";
String descriptor = name.substring(0, name.length() - CLASS_EXTENSION.length());
if (descriptor.indexOf(JAVA_PACKAGE_SEPARATOR) != -1) {
throw new CompilationError("Unexpected class file name: " + name);
}
return 'L' + descriptor + ';';
}
public static boolean isValidBinaryName(String binaryName) {
return isValidJavaType(
binaryName.replace(DESCRIPTOR_PACKAGE_SEPARATOR, JAVA_PACKAGE_SEPARATOR));
}
/**
* Computes the inner name from the outer- and inner descriptors. If outer is not a prefix of the
* inner descriptor null is returned. Do not use this method if the relationship between inner and
* outer is not reflected in the name.
*
* @param outerDescriptor the outer descriptor, such as Lfoo/bar/Baz;
* @param innerDescriptor the inner descriptor, such as Lfoo/bar/Baz$Qux;
* @return the inner name or null, i.e. Qux in the example above
*/
public static String getInnerClassName(String outerDescriptor, String innerDescriptor) {
if (innerDescriptor.length() <= outerDescriptor.length()) {
return null;
}
String prefix =
outerDescriptor.substring(0, outerDescriptor.length() - 1) + INNER_CLASS_SEPARATOR;
if (innerDescriptor.startsWith(prefix)) {
return innerDescriptor.substring(prefix.length(), innerDescriptor.length() - 1);
}
return null;
}
public static class ModuleAndDescriptor {
private final String module;
private final String descriptor;
ModuleAndDescriptor(String module, String descriptor) {
this.module = module;
this.descriptor = descriptor;
}
public String getModule() {
return module;
}
public String getDescriptor() {
return descriptor;
}
}
/**
* Guess module and class descriptor from the location of a class file in a jrt file system.
*
* @param name the location in a jrt file system of the class file to convert to descriptor
* @return module and java class descriptor
*/
public static ModuleAndDescriptor guessJrtModuleAndTypeDescriptor(String name) {
assert name != null;
assert name.endsWith(CLASS_EXTENSION)
: "Name " + name + " must have " + CLASS_EXTENSION + " suffix";
assert name.startsWith(MODULES_PREFIX)
: "Name " + name + " must have " + MODULES_PREFIX + " prefix";
assert name.charAt(MODULES_PREFIX.length()) == '/';
int moduleNameEnd = name.indexOf('/', MODULES_PREFIX.length() + 1);
String module = name.substring(MODULES_PREFIX.length() + 1, moduleNameEnd);
String descriptor = name.substring(moduleNameEnd + 1, name.length() - CLASS_EXTENSION.length());
if (descriptor.indexOf(JAVA_PACKAGE_SEPARATOR) != -1) {
throw new CompilationError("Unexpected class file name: " + name);
}
return new ModuleAndDescriptor(module, 'L' + descriptor + ';');
}
public static String getPathFromDescriptor(String descriptor) {
// We are quite loose on names here to support testing illegal names, too.
assert descriptor.startsWith("L");
assert descriptor.endsWith(";");
return descriptor.substring(1, descriptor.length() - 1) + ".class";
}
public static String getPathFromJavaType(Class<?> clazz) {
return getPathFromJavaType(clazz.getTypeName());
}
public static String getPathFromJavaType(String typeName) {
assert isValidJavaType(typeName);
return typeName.replace(JAVA_PACKAGE_SEPARATOR, DESCRIPTOR_PACKAGE_SEPARATOR) + ".class";
}
public static String getClassFileName(String classDescriptor) {
assert classDescriptor != null && isClassDescriptor(classDescriptor);
return getClassBinaryNameFromDescriptor(classDescriptor) + CLASS_EXTENSION;
}
public static String getReturnTypeDescriptor(final String methodDescriptor) {
assert methodDescriptor.indexOf(')') != -1;
return methodDescriptor.substring(methodDescriptor.indexOf(')') + 1);
}
public static String getShortyDescriptor(String descriptor) {
if (descriptor.length() == 1) {
return descriptor;
}
assert descriptor.charAt(0) == 'L' || descriptor.charAt(0) == '[';
return "L";
}
public static String[] getArgumentTypeDescriptors(final String methodDescriptor) {
String[] argDescriptors = new String[getArgumentCount(methodDescriptor)];
int charIdx = 1;
char c;
int argIdx = 0;
int startType;
while ((c = methodDescriptor.charAt(charIdx)) != ')') {
switch (c) {
case 'V':
throw new InvalidDescriptorException(methodDescriptor);
case 'Z':
case 'C':
case 'B':
case 'S':
case 'I':
case 'F':
case 'J':
case 'D':
argDescriptors[argIdx++] = Character.toString(c);
break;
case '[':
startType = charIdx;
while (methodDescriptor.charAt(++charIdx) == '[')
;
if (methodDescriptor.charAt(charIdx) == 'L') {
while (methodDescriptor.charAt(++charIdx) != ';')
;
}
argDescriptors[argIdx++] = methodDescriptor.substring(startType, charIdx + 1);
break;
case 'L':
startType = charIdx;
while (methodDescriptor.charAt(++charIdx) != ';')
;
argDescriptors[argIdx++] = methodDescriptor.substring(startType, charIdx + 1);
break;
default:
throw new InvalidDescriptorException(methodDescriptor);
}
charIdx++;
}
return argDescriptors;
}
public static int getArgumentCount(final String methodDescriptor) {
int length = methodDescriptor.length();
int charIdx = 1;
char c;
int argCount = 0;
while (charIdx < length && (c = methodDescriptor.charAt(charIdx++)) != ')') {
if (c == 'L') {
while (charIdx < length && methodDescriptor.charAt(charIdx++) != ';')
;
// Check if the inner loop found ';' within the boundary.
if (charIdx >= length || methodDescriptor.charAt(charIdx - 1) != ';') {
throw new InvalidDescriptorException(methodDescriptor);
}
argCount++;
} else if (c != '[') {
argCount++;
}
}
// Check if the outer loop found ')' within the boundary.
if (charIdx >= length || methodDescriptor.charAt(charIdx - 1) != ')') {
throw new InvalidDescriptorException(methodDescriptor);
}
return argCount;
}
}