blob: 29987e7440e3525cac93d32efdb6e3f1b5bafab9 [file] [log] [blame]
// 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 com.android.tools.r8.DataDirectoryResource;
import com.android.tools.r8.DataEntryResource;
import com.android.tools.r8.ResourceException;
import com.android.tools.r8.errors.Unreachable;
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.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.FileUtils;
import com.android.tools.r8.utils.InternalOptions;
import com.android.tools.r8.utils.StringDiagnostic;
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,
DexItemFactory dexItemFactory,
GraphLens graphLens,
NamingLens namingLens,
InternalOptions options) {
this.appView = appView;
this.dexItemFactory = dexItemFactory;
this.graphLens = graphLens;
this.namingLens = namingLens;
this.options = options;
}
public DataEntryResource adaptIfNeeded(DataEntryResource file) {
// Adapt name, if needed.
String name =
shouldAdapt(file, options, ProguardConfiguration::getAdaptResourceFilenames)
? adaptFileName(file)
: file.getName();
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) {
// First check if this directory should even be in the output.
if (options.getProguardConfiguration() == null) {
assert options.testing.enableD8ResourcesPassThrough;
return null;
}
if (!options.getProguardConfiguration().getKeepDirectories().matches(directory.getName())) {
return null;
}
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.enableD8ResourcesPassThrough;
return false;
}
ProguardPathFilter filter = getFilter.apply(proguardConfiguration);
return filter.isEnabled()
&& !file.getName().toLowerCase().endsWith(FileUtils.CLASS_EXTENSION)
&& filter.matches(file.getName());
}
public boolean isService(DataEntryResource file) {
return file.getName().startsWith(AppServices.SERVICE_DIRECTORY_NAME);
}
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();
}
private String adaptDirectoryName(DataDirectoryResource file) {
DirectoryNameAdapter adapter = new DirectoryNameAdapter(file.getName());
if (adapter.run()) {
return adapter.getResult();
}
return file.getName();
}
// According to the Proguard documentation, the resource files should be parsed and written using
// the platform's default character set.
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 still true 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 renaming Java type names, the adapter always looks for the longest name to rewrite.
// For example, if there is a resource with the name "foo/bar/C$X$Y.txt", then the adapter will
// check if there is a renaming for the type "foo.bar.C$X$Y". If there is no such renaming, then
// -adaptresourcefilenames works in such a way that "foo/bar/C$X" should be rewritten if there
// is a renaming for the type "foo.bar.C$X". Therefore, 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.
//
// Note that there is no backtracking for -adaptresourcefilecontents.
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 Java type in the range [from; toExclusive[ was renamed.
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) {
DexString renamedDescriptor = namingLens.lookupDescriptor(graphLens.lookupType(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 packageName = appView.graphLens().lookupPackageName(javaPackage);
String minifiedJavaPackage = namingLens.lookupPackageName(packageName);
if (!javaPackage.equals(minifiedJavaPackage)) {
outputRangeFromInput(outputFrom, from);
outputJavaType(
getClassNameSeparator() != '/'
? minifiedJavaPackage.replace('/', getClassNameSeparator())
: minifiedJavaPackage);
outputFrom = 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) {
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;
}
}
}