| // 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; | |
| } | |
| } | |
| } |