blob: 2f11175373d84ef362b6ba0fd2c44d84e347eb0d [file]
// Copyright (c) 2018, 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.dex;
import static com.android.tools.r8.graph.lens.GraphLens.getIdentityLens;
import com.android.tools.r8.DataDirectoryResource;
import com.android.tools.r8.DataEntryResource;
import com.android.tools.r8.ResourceException;
import com.android.tools.r8.graph.AppServices;
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.graph.DexString;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.graph.lens.GraphLens;
import com.android.tools.r8.naming.NamingLens;
import com.android.tools.r8.shaking.ProguardConfiguration;
import com.android.tools.r8.shaking.ProguardPathFilter;
import com.android.tools.r8.utils.DescriptorUtils;
import com.android.tools.r8.utils.ExceptionDiagnostic;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.StringDiagnostic;
import com.android.tools.r8.utils.internal.FileUtils;
import com.android.tools.r8.utils.internal.StringUtils;
import com.android.tools.r8.utils.internal.exceptions.Unreachable;
import com.google.common.io.ByteStreams;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntStack;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.function.Function;
public class ResourceAdapter {
private final AppView<?> appView;
private final DexItemFactory dexItemFactory;
private final GraphLens graphLens;
private final NamingLens namingLens;
private final InternalOptions options;
public ResourceAdapter(AppView<?> appView) {
this.appView = appView;
this.dexItemFactory = appView.dexItemFactory();
this.graphLens = appView.graphLens();
this.namingLens = appView.getNamingLens();
this.options = appView.options();
}
/**
* {@code -adaptresourcefilenames} allows resources to be moved and renamed based on code
* obfuscation.
*
* <h2>Filtering</h2>
*
* <p>A resource is only considered if it is covered by the filter given to the flag. No filter
* means that everything is considered.
*
* <h2>Finding a corresponding class</h2>
*
* <p>A resource is renamed according to its longest corresponding class, if found. When finding a
* corresponding class, first the resource path is stripped to its longest prefix containing only
* Java identifier characters and {@code /} (stopping before characters like {@code .}). The
* search for corresponding classes is then done in a sequence of shrinking prefixes delimited by
* any non-letter, non-digit character (e.g., {@code _} or {@code $}). When a candidate is the
* entire resource path (i.e., has no extension), it is skipped in the class search for ProGuard
* compatibility (e.g., {@code a/b/C} will not match {@code a/b/C.class}).
*
* <p>For {@code a/b/A$B_data.txt}, this means that these classes are checked in order:
*
* <ul>
* <li>If {@code a/b/A$B_data.class} exists and is renamed(*) to {@code q/Z.class}, the resource
* is moved to {@code q/Z.txt}.
* <li>If {@code a/b/A$B.class} exists and is renamed(*) to {@code q/Z.class}, the resource is
* moved to {@code q/Z_data.txt}.
* <li>If {@code a/b/A.class} exists and is renamed(*) to {@code q/Z.class}, the resource is
* moved to {@code q/Z$B_data.txt}.
* </ul>
*
* (*): For class matching, a non-renamed class counts as a renaming to itself.
*
* <h2>Finding a corresponding package</h2>
*
* <p>If a resource does not correspond to a class, it is renamed according to the longest
* matching package.
*
* <p>For {@code a/b/A$B_data.txt}, this means that these packages are checked in order:
*
* <ul>
* <li>If {@code a/b} exists(**) and is renamed to {@code q}, the resource is moved to {@code
* q/A$B_data.txt}.
* <li>If {@code a} exists(**) and is renamed to {@code q}, the resource is moved to {@code
* q/b/A$B_data.txt}.
* </ul>
*
* (**): For a package to exist, it has to contain code.
*/
public String adaptFileNameIfNeeded(DataEntryResource file) {
return shouldAdapt(file, options, ProguardConfiguration::getAdaptResourceFilenames)
? adaptFileName(file)
: file.getName();
}
public DataEntryResource adaptIfNeeded(DataEntryResource file) {
// Adapt name, if needed.
String name = adaptFileNameIfNeeded(file);
assert name != null;
// Adapt contents, if needed.
byte[] contents =
shouldAdapt(file, options, ProguardConfiguration::getAdaptResourceFileContents)
? adaptFileContents(file)
: null;
// Return a new resource if the name or contents changed. Otherwise return the original
// resource as it was.
if (contents != null) {
// File contents was adapted. Return a new resource that has the new contents, and a new name,
// if the filename was adapted.
return DataEntryResource.fromBytes(contents, name, file.getOrigin());
}
if (!name.equals(file.getName())) {
// File contents was not adapted, but filename was.
return file.withName(name);
}
// Neither file contents nor filename was adapted.
return file;
}
public DataDirectoryResource adaptIfNeeded(DataDirectoryResource directory) {
return DataDirectoryResource.fromName(adaptDirectoryName(directory), directory.getOrigin());
}
private boolean shouldAdapt(
DataEntryResource file,
InternalOptions options,
Function<ProguardConfiguration, ProguardPathFilter> getFilter) {
final ProguardConfiguration proguardConfiguration = options.getProguardConfiguration();
if (proguardConfiguration == null) {
assert options.testing.enableD8MetaInfServicesPassThrough;
return false;
}
ProguardPathFilter filter = getFilter.apply(proguardConfiguration);
return filter.isEnabled()
&& !StringUtils.toLowerCase(file.getName()).endsWith(FileUtils.CLASS_EXTENSION)
&& filter.matches(file.getName());
}
private String adaptFileName(DataEntryResource file) {
FileNameAdapter adapter =
file.getName().startsWith(AppServices.SERVICE_DIRECTORY_NAME)
? new ServiceFileNameAdapter(file.getName())
: new DefaultFileNameAdapter(file.getName());
if (adapter.run()) {
return adapter.getResult();
}
return file.getName();
}
public String adaptDirectoryName(DataDirectoryResource directory) {
DirectoryNameAdapter adapter = new DirectoryNameAdapter(directory.getName());
if (adapter.run()) {
return adapter.getResult();
}
return directory.getName();
}
protected String adaptPackage(String javaPackage) {
String packageName = appView.graphLens().lookupPackageName(javaPackage);
return namingLens.lookupPackageName(packageName);
}
private DexString adaptType(DexType type) {
return namingLens.lookupDescriptor(graphLens.lookupType(type, getIdentityLens()));
}
/**
* According to the Proguard documentation, the resource files should be parsed and written using
* the platform's default character set.
*
* <p>Note that there is no backtracking for -adaptresourcefilecontents.
*/
private byte[] adaptFileContents(DataEntryResource file) {
try (InputStream in = file.getByteStream()) {
byte[] bytes = ByteStreams.toByteArray(in);
String contents = new String(bytes, Charset.defaultCharset());
FileContentsAdapter adapter = new FileContentsAdapter(contents);
if (adapter.run()) {
return adapter.getResult().getBytes(Charset.defaultCharset());
}
} catch (ResourceException e) {
options.reporter.error(
new StringDiagnostic("Failed to open input: " + e.getMessage(), file.getOrigin()));
} catch (Exception e) {
options.reporter.error(new ExceptionDiagnostic(e, file.getOrigin()));
}
// Return null to signal that the file contents did not change. Otherwise we would have to copy
// the original file for no reason.
return null;
}
private abstract class StringAdapter {
protected final String contents;
private final StringBuilder result = new StringBuilder();
// If any type names in `contents` have been updated. If this flag is unchanged in the end,
// then we can simply use the resource as it was.
private boolean changed = false;
private int outputFrom = 0;
private int position = 0;
// When scanning forwards to read the
// substring "foo/bar/C$X$Y", this adapter records the positions of the two '$' characters in
// the stack `prefixEndPositionsExclusive`, such that it can easily backtrack to the previously
// valid, but shorter Java type name.
private final IntStack prefixEndPositionsExclusive;
public StringAdapter(String contents) {
this.contents = contents;
this.prefixEndPositionsExclusive = allowRenamingOfPrefixes() ? new IntArrayList() : null;
}
public boolean run() {
do {
handleMisc();
handleJavaType();
} while (!eof());
if (changed) {
// At least one type was renamed. We need to flush all characters in `contents` that follow
// the last type that was renamed.
outputRangeFromInput(outputFrom, contents.length());
} else {
// No types were renamed. In this case the adapter should simply have scanned through
// `contents`, without outputting anything to `result`.
assert outputFrom == 0;
assert result.toString().isEmpty();
}
return changed;
}
public String getResult() {
assert changed;
return result.toString();
}
// Forwards the cursor until the current character is a Java identifier part.
private void handleMisc() {
while (!eof() && !Character.isJavaIdentifierPart(contents.charAt(position))) {
position++;
}
}
// Reads a Java type from the current position in `contents`, and then checks if the given
// type has been renamed.
private void handleJavaType() {
if (eof()) {
return;
}
assert !allowRenamingOfPrefixes() || prefixEndPositionsExclusive.isEmpty();
assert Character.isJavaIdentifierPart(contents.charAt(position));
int start = position++;
while (!eof()) {
char currentChar = contents.charAt(position);
if (Character.isJavaIdentifierPart(currentChar)) {
if (allowRenamingOfPrefixes()
&& shouldRecordPrefix(currentChar)
&& isRenamingCandidate(start, position)) {
prefixEndPositionsExclusive.push(position);
}
position++;
continue;
}
if (currentChar == getClassNameSeparator()
&& !eof(position + 1)
&& Character.isJavaIdentifierPart(contents.charAt(position + 1))) {
if (allowRenamingOfPrefixes()
&& shouldRecordPrefix(currentChar)
&& isRenamingCandidate(start, position)) {
prefixEndPositionsExclusive.push(position);
}
// Consume the separator and the Java identifier part that follows the separator.
position += 2;
continue;
}
// Not a valid extension of the type name.
break;
}
if (allowRenamingOfPrefixes() && eof() && isRenamingCandidate(start, position)) {
prefixEndPositionsExclusive.push(position);
}
boolean renamingSucceeded =
isRenamingCandidate(start, position) && renameJavaTypeInRange(start, position);
if (!renamingSucceeded && allowRenamingOfPrefixes()) {
while (!prefixEndPositionsExclusive.isEmpty() && !renamingSucceeded) {
int prefixEndExclusive = prefixEndPositionsExclusive.popInt();
assert isRenamingCandidate(start, prefixEndExclusive);
renamingSucceeded = handlePrefix(start, prefixEndExclusive);
}
}
if (allowRenamingOfPrefixes()) {
while (!prefixEndPositionsExclusive.isEmpty()) {
prefixEndPositionsExclusive.popInt();
}
}
}
/** Returns true if the type is live, was merged/renamed, or is a known missing class. */
private boolean typeExists(DexType type) {
return appView.hasDefinitionFor(type)
|| appView.graphLens().lookupType(type, getIdentityLens()).isNotIdenticalTo(type)
|| (appView.hasClassHierarchy()
&& appView.appInfoWithClassHierarchy().getMissingClasses().contains(type));
}
/**
* Matches and optionally renames the Java type in the range [from; toExclusive[.
*
* <p>Returns true if a type exists for the given range, signaling to stop backtracking. If the
* type was renamed, the renamed type is written to the output.
*
* <p>Returns false if no type exists, allowing backtracking to shorter prefixes (e.g.
* packages).
*/
protected boolean renameJavaTypeInRange(int from, int toExclusive) {
String javaType = contents.substring(from, toExclusive);
if (getClassNameSeparator() != '.') {
javaType = javaType.replace(getClassNameSeparator(), '.');
}
DexString descriptor =
dexItemFactory.lookupString(
DescriptorUtils.javaTypeToDescriptorIgnorePrimitives(javaType));
DexType dexType = descriptor != null ? dexItemFactory.lookupType(descriptor) : null;
if (dexType != null && typeExists(dexType)) {
DexString renamedDescriptor = adaptType(dexType);
if (!descriptor.equals(renamedDescriptor)) {
String renamedJavaType =
DescriptorUtils.descriptorToJavaType(renamedDescriptor.toSourceString());
// Need to flush all changes up to and excluding 'from', and then output the renamed
// type.
outputRangeFromInput(outputFrom, from);
outputJavaType(
getClassNameSeparator() != '.'
? renamedJavaType.replace('.', getClassNameSeparator())
: renamedJavaType);
outputFrom = toExclusive;
changed = true;
}
return true;
}
return false;
}
// Returns true if the Java package in the range [from; toExclusive[ was renamed.
protected boolean renameJavaPackageInRange(int from, int toExclusive) {
String javaPackage = contents.substring(from, toExclusive);
if (getClassNameSeparator() != '/') {
javaPackage = javaPackage.replace(getClassNameSeparator(), '/');
}
String minifiedJavaPackage = adaptPackage(javaPackage);
if (!javaPackage.equals(minifiedJavaPackage)) {
outputRangeFromInput(outputFrom, from);
outputJavaType(
getClassNameSeparator() != '/'
? minifiedJavaPackage.replace('/', getClassNameSeparator())
: minifiedJavaPackage);
// Avoid an "a/b" -> "" renaming causing "a/b/C" being renamed to "/C".
boolean leadingSlash =
minifiedJavaPackage.isEmpty()
&& !eof(toExclusive)
&& contents.charAt(toExclusive) == getClassNameSeparator();
outputFrom = leadingSlash ? toExclusive + 1 : toExclusive;
changed = true;
return true;
}
return false;
}
protected abstract char getClassNameSeparator();
protected abstract boolean allowRenamingOfPrefixes();
protected abstract boolean shouldRecordPrefix(char c);
protected abstract boolean handlePrefix(int from, int toExclusive);
protected abstract boolean isRenamingCandidate(int from, int toExclusive);
private void outputRangeFromInput(int from, int toExclusive) {
if (from < toExclusive) {
result.append(contents, from, toExclusive);
}
}
private void outputJavaType(String s) {
result.append(s);
}
protected boolean eof() {
return eof(position);
}
protected boolean eof(int position) {
return position == contents.length();
}
}
private class FileContentsAdapter extends StringAdapter {
public FileContentsAdapter(String fileContents) {
super(fileContents);
}
@Override
public char getClassNameSeparator() {
return '.';
}
@Override
public boolean allowRenamingOfPrefixes() {
return false;
}
@Override
public boolean shouldRecordPrefix(char c) {
throw new Unreachable();
}
@Override
protected boolean handlePrefix(int from, int toExclusive) {
throw new Unreachable();
}
@Override
public boolean isRenamingCandidate(int from, int toExclusive) {
// If the Java type starts with '-' or '.', it should not be renamed.
return (from <= 0 || !isDashOrDot(contents.charAt(from - 1)))
&& (eof(toExclusive) || !isDashOrDot(contents.charAt(toExclusive)));
}
private boolean isDashOrDot(char c) {
return c == '.' || c == '-';
}
}
private abstract class FileNameAdapter extends StringAdapter {
public FileNameAdapter(String filename) {
super(filename);
}
@Override
public char getClassNameSeparator() {
return '/';
}
@Override
public boolean allowRenamingOfPrefixes() {
return true;
}
@Override
public boolean shouldRecordPrefix(char c) {
return !Character.isLetterOrDigit(c);
}
@Override
protected boolean handlePrefix(int from, int toExclusive) {
if (eof(toExclusive) || contents.charAt(toExclusive) == '/') {
return renameJavaPackageInRange(from, toExclusive);
}
return renameJavaTypeInRange(from, toExclusive);
}
}
private class DefaultFileNameAdapter extends FileNameAdapter {
public DefaultFileNameAdapter(String filename) {
super(filename);
}
@Override
public boolean isRenamingCandidate(int from, int toExclusive) {
// The EOF check stops 'a/b/C' from being associated with 'a/b/C.class' (ProGuard
// compatibility).
return from == 0 && !eof(toExclusive);
}
}
private class ServiceFileNameAdapter extends FileNameAdapter {
public ServiceFileNameAdapter(String filename) {
super(filename);
}
@Override
public char getClassNameSeparator() {
return '.';
}
@Override
public boolean allowRenamingOfPrefixes() {
return false;
}
@Override
public boolean isRenamingCandidate(int from, int toExclusive) {
return from == AppServices.SERVICE_DIRECTORY_NAME.length() && eof(toExclusive);
}
}
private class DirectoryNameAdapter extends FileNameAdapter {
public DirectoryNameAdapter(String filename) {
super(filename);
}
@Override
public boolean isRenamingCandidate(int from, int toExclusive) {
return from == 0;
}
}
}