| // 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.naming; |
| |
| import static org.hamcrest.CoreMatchers.containsString; |
| import static org.hamcrest.MatcherAssert.assertThat; |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertNotEquals; |
| import static org.junit.Assert.assertNotNull; |
| import static org.junit.Assert.assertTrue; |
| |
| import com.android.tools.r8.CompilationFailedException; |
| import com.android.tools.r8.DataDirectoryResource; |
| import com.android.tools.r8.DataEntryResource; |
| import com.android.tools.r8.DataResourceConsumer; |
| import com.android.tools.r8.DataResourceProvider.Visitor; |
| import com.android.tools.r8.R8Command; |
| import com.android.tools.r8.StringConsumer; |
| import com.android.tools.r8.ToolHelper; |
| import com.android.tools.r8.origin.Origin; |
| import com.android.tools.r8.shaking.forceproguardcompatibility.ProguardCompatibilityTestBase; |
| import com.android.tools.r8.utils.AndroidApp; |
| import com.android.tools.r8.utils.ArchiveResourceProvider; |
| import com.android.tools.r8.utils.DataResourceConsumerForTesting; |
| import com.android.tools.r8.utils.FileUtils; |
| import com.android.tools.r8.utils.KeepingDiagnosticHandler; |
| import com.android.tools.r8.utils.StringUtils; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import java.io.File; |
| import java.io.IOException; |
| import java.nio.file.Path; |
| import java.nio.file.Paths; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.stream.Collectors; |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.Parameterized; |
| |
| @RunWith(Parameterized.class) |
| public class AdaptResourceFileNamesTest extends ProguardCompatibilityTestBase { |
| |
| private Backend backend; |
| |
| @Parameterized.Parameters(name = "Backend: {0}") |
| public static Backend[] data() { |
| return ToolHelper.getBackends(); |
| } |
| |
| public AdaptResourceFileNamesTest(Backend backend) { |
| this.backend = backend; |
| } |
| |
| private static final Path CF_DIR = |
| Paths.get(ToolHelper.EXAMPLES_CF_DIR).resolve("adaptresourcefilenames"); |
| private static final Path TEST_JAR = |
| Paths.get(ToolHelper.EXAMPLES_BUILD_DIR) |
| .resolve("adaptresourcefilenames" + FileUtils.JAR_EXTENSION); |
| |
| private KeepingDiagnosticHandler diagnosticsHandler; |
| private ClassNameMapper mapper = null; |
| |
| @Before |
| public void reset() { |
| diagnosticsHandler = new KeepingDiagnosticHandler(); |
| mapper = null; |
| } |
| |
| private static String getProguardConfig( |
| boolean enableAdaptResourceFileNames, String adaptResourceFileNamesPathFilter) { |
| String adaptResourceFilenamesRule; |
| if (enableAdaptResourceFileNames) { |
| adaptResourceFilenamesRule = "-adaptresourcefilenames"; |
| if (adaptResourceFileNamesPathFilter != null) { |
| adaptResourceFilenamesRule += " " + adaptResourceFileNamesPathFilter; |
| } |
| } else { |
| adaptResourceFilenamesRule = ""; |
| } |
| return String.join( |
| System.lineSeparator(), |
| adaptResourceFilenamesRule, |
| "-keep class adaptresourcefilenames.TestClass {", |
| " public static void main(...);", |
| "}"); |
| } |
| |
| private static String getProguardConfigWithNeverInline( |
| boolean enableAdaptResourceFileNames, String adaptResourceFileNamesPathFilter) { |
| return StringUtils.lines( |
| getProguardConfig(enableAdaptResourceFileNames, adaptResourceFileNamesPathFilter), |
| "-neverinline class adaptresourcefilenames.A {", |
| " public void method();", |
| "}", |
| "-neverinline class adaptresourcefilenames.B$Inner {", |
| " public void method();", |
| "}", |
| "-assumemayhavesideeffects class adaptresourcefilenames.pkg.C {", |
| " void <init>();", |
| "}", |
| "-assumemayhavesideeffects class adaptresourcefilenames.pkg.innerpkg.D {", |
| " void <init>();", |
| "}", |
| "-neverclassinline class *", |
| "-nohorizontalclassmerging class adaptresourcefilenames.pkg.C", |
| "-nohorizontalclassmerging class adaptresourcefilenames.pkg.innerpkg.D"); |
| } |
| |
| @Test |
| public void testEnabled() throws Exception { |
| DataResourceConsumerForTesting dataResourceConsumer = new DataResourceConsumerForTesting(); |
| compileWithR8( |
| getProguardConfigWithNeverInline(true, null), |
| dataResourceConsumer, |
| ToolHelper.consumeString(this::checkR8Renamings)); |
| // Check that the generated resources have the expected names. |
| for (DataEntryResource dataResource : getOriginalDataResources()) { |
| assertNotNull( |
| "Resource not renamed as expected: " + dataResource.getName(), |
| dataResourceConsumer.get(getExpectedRenamingFor(dataResource.getName(), mapper))); |
| } |
| } |
| |
| @Test |
| public void testEnabledWithFilter() throws Exception { |
| DataResourceConsumerForTesting dataResourceConsumer = new DataResourceConsumerForTesting(); |
| compileWithR8( |
| getProguardConfigWithNeverInline(true, "**.md"), |
| dataResourceConsumer, |
| ToolHelper.consumeString(this::checkR8Renamings)); |
| // Check that the generated resources have the expected names. |
| Map<String, String> expectedRenamings = |
| ImmutableMap.of("adaptresourcefilenames/B.md", "adaptresourcefilenames/b.md"); |
| for (DataEntryResource dataResource : getOriginalDataResources()) { |
| assertNotNull( |
| "Resource not renamed as expected: " + dataResource.getName(), |
| dataResourceConsumer.get( |
| expectedRenamings.getOrDefault(dataResource.getName(), dataResource.getName()))); |
| } |
| } |
| |
| @Test |
| public void testDisabled() throws Exception { |
| DataResourceConsumerForTesting dataResourceConsumer = new DataResourceConsumerForTesting(); |
| compileWithR8(getProguardConfigWithNeverInline(false, null), dataResourceConsumer); |
| // Check that none of the resources were renamed. |
| for (DataEntryResource dataResource : getOriginalDataResources()) { |
| assertNotNull( |
| "Resource not renamed as expected: " + dataResource.getName(), |
| dataResourceConsumer.get(dataResource.getName())); |
| } |
| } |
| |
| @Test |
| public void testCollisionBehavior() throws Exception { |
| DataResourceConsumerForTesting dataResourceConsumer = new DataResourceConsumerForTesting(); |
| compileWithR8( |
| getProguardConfigWithNeverInline(true, null), |
| dataResourceConsumer, |
| ToolHelper.consumeString(this::checkR8Renamings), |
| ImmutableList.<DataEntryResource>builder() |
| .addAll(getOriginalDataResources()) |
| .add( |
| DataEntryResource.fromBytes( |
| new byte[0], "adaptresourcefilenames/b.txt", Origin.unknown())) |
| .build()); |
| assertEquals(1, diagnosticsHandler.warnings.size()); |
| assertThat( |
| diagnosticsHandler.warnings.get(0).getDiagnosticMessage(), |
| containsString("Resource 'adaptresourcefilenames/b.txt' already exists.")); |
| assertEquals(getOriginalDataResources().size(), dataResourceConsumer.size()); |
| } |
| |
| @Test |
| public void testProguardBehavior() throws Exception { |
| Path inputJar = addDataResourcesToExistingJar(TEST_JAR, getOriginalDataResources()); |
| Path proguardedJar = |
| File.createTempFile("proguarded", FileUtils.JAR_EXTENSION, temp.getRoot()).toPath(); |
| Path proguardMapFile = File.createTempFile("mapping", ".txt", temp.getRoot()).toPath(); |
| runProguard6Raw(proguardedJar, inputJar, getProguardConfig(true, null), proguardMapFile); |
| // Extract the names of the generated resources. |
| Set<String> filenames = new HashSet<>(); |
| ArchiveResourceProvider.fromArchive(proguardedJar, true) |
| .accept( |
| new Visitor() { |
| @Override |
| public void visit(DataDirectoryResource directory) {} |
| |
| @Override |
| public void visit(DataEntryResource file) { |
| filenames.add(file.getName()); |
| } |
| }); |
| // Check that the generated resources have the expected names. |
| ClassNameMapper mapper = ClassNameMapper.mapperFromFile(proguardMapFile); |
| for (DataEntryResource dataResource : getOriginalDataResources()) { |
| String expectedName = getExpectedRenamingFor(dataResource.getName(), mapper); |
| assertTrue( |
| "Resource not renamed to '" + expectedName + "' as expected: " + dataResource.getName(), |
| filenames.contains(expectedName)); |
| } |
| } |
| |
| @Test |
| public void testProguardCollisionBehavior() throws Exception { |
| List<DataEntryResource> originalDataResources = getOriginalDataResources(); |
| Path inputJar = |
| addDataResourcesToExistingJar( |
| TEST_JAR, |
| ImmutableList.<DataEntryResource>builder() |
| .addAll(originalDataResources) |
| .add( |
| DataEntryResource.fromBytes( |
| new byte[0], "adaptresourcefilenames/b.txt", Origin.unknown())) |
| .build()); |
| Path proguardedJar = |
| File.createTempFile("proguarded", FileUtils.JAR_EXTENSION, temp.getRoot()).toPath(); |
| runProguard6Raw( |
| proguardedJar, |
| inputJar, |
| getProguardConfig(true, null), |
| null, |
| processResult -> { |
| assertEquals(0, processResult.exitCode); |
| assertThat( |
| processResult.stderr, |
| containsString( |
| "Warning: can't write resource [adaptresourcefilenames/b.txt] " |
| + "(Duplicate jar entry [adaptresourcefilenames/b.txt])")); |
| }); |
| assertEquals( |
| originalDataResources.size(), |
| getDataResources(ArchiveResourceProvider.fromArchive(proguardedJar, true)) |
| .stream() |
| .filter(dataResource -> !dataResource.getName().equals("META-INF/MANIFEST.MF")) |
| .count()); |
| } |
| |
| private AndroidApp compileWithR8(String proguardConfig, DataResourceConsumer dataResourceConsumer) |
| throws CompilationFailedException, IOException { |
| return compileWithR8(proguardConfig, dataResourceConsumer, null); |
| } |
| |
| private AndroidApp compileWithR8( |
| String proguardConfig, |
| DataResourceConsumer dataResourceConsumer, |
| StringConsumer proguardMapConsumer) |
| throws CompilationFailedException, IOException { |
| return compileWithR8( |
| proguardConfig, dataResourceConsumer, proguardMapConsumer, getOriginalDataResources()); |
| } |
| |
| private AndroidApp compileWithR8( |
| String proguardConfig, |
| DataResourceConsumer dataResourceConsumer, |
| StringConsumer proguardMapConsumer, |
| List<DataEntryResource> dataResources) |
| throws CompilationFailedException, IOException { |
| R8Command command = |
| ToolHelper.allowTestProguardOptions( |
| ToolHelper.prepareR8CommandBuilder( |
| getAndroidApp(dataResources), emptyConsumer(backend), diagnosticsHandler) |
| .addProguardConfiguration(ImmutableList.of(proguardConfig), Origin.unknown())) |
| .addLibraryFiles(runtimeJar(backend)) |
| .build(); |
| return ToolHelper.runR8( |
| command, |
| options -> { |
| options.dataResourceConsumer = dataResourceConsumer; |
| options.proguardMapConsumer = proguardMapConsumer; |
| }); |
| } |
| |
| private void checkR8Renamings(String proguardMap) { |
| try { |
| // Check that the renamings are as expected. These exact renamings are not important as |
| // such, but the test expectations rely on them. |
| mapper = ClassNameMapper.mapperFromString(proguardMap); |
| assertEquals( |
| "adaptresourcefilenames.TestClass", |
| mapper.deobfuscateClassName("adaptresourcefilenames.TestClass")); |
| assertEquals( |
| "adaptresourcefilenames.B", mapper.deobfuscateClassName("adaptresourcefilenames.b")); |
| assertEquals( |
| "adaptresourcefilenames.B$Inner", |
| mapper.deobfuscateClassName("adaptresourcefilenames.a")); |
| } catch (IOException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| |
| private AndroidApp getAndroidApp(List<DataEntryResource> dataResources) throws IOException { |
| AndroidApp.Builder builder = AndroidApp.builder(); |
| builder.addProgramFiles(ToolHelper.getClassFilesForTestDirectory(CF_DIR)); |
| dataResources.forEach(builder::addDataResource); |
| return builder.build(); |
| } |
| |
| private static List<DataEntryResource> getOriginalDataResources() { |
| List<String> filenames = |
| ImmutableList.of( |
| // Filename with simple name in root directory. |
| "TestClass", |
| "B", |
| // Filename with qualified name in root directory. |
| "adaptresourcefilenames.TestClass", |
| "adaptresourcefilenames.B", |
| // Filename with qualified directory name in root directory. |
| "adaptresourcefilenames/TestClass", |
| "adaptresourcefilenames/B", |
| // Filename with simple name in sub directory. |
| "foo/bar/baz/TestClass", |
| "foo/bar/baz/B", |
| // Filename with qualified name in sub directory. |
| "foo/bar/baz/adaptresourcefiles.TestClass", |
| "foo/bar/baz/adaptresourcefiles.B", |
| // Filename with qualified directory name in sub directory. |
| "foo/bar/baz/adaptresourcefilenames/TestClass", |
| "foo/bar/baz/adaptresourcefilenames/B", |
| // |
| // SUFFIX VARIANTS: |
| // |
| // Filename with simple name and extension in root directory. |
| "TestClass.txt", |
| "B.txt", |
| // Filename with qualified name and extension in root directory. |
| "adaptresourcefilenames.TestClass.txt", |
| "adaptresourcefilenames.B.txt", |
| // Filename with qualified directory name and extension in root directory. |
| "adaptresourcefilenames/TestClass.txt", |
| "adaptresourcefilenames/B.txt", |
| // Filename with simple name and extension in sub directory. |
| "foo/bar/baz/TestClass.txt", |
| "foo/bar/baz/B.txt", |
| // Filename with qualified name and extension in sub directory. |
| "foo/bar/baz/adaptresourcefiles.TestClass.txt", |
| "foo/bar/baz/adaptresourcefiles.B.txt", |
| // Filename with qualified directory name and extension in sub directory. |
| "foo/bar/baz/adaptresourcefilenames/TestClass.txt", |
| "foo/bar/baz/adaptresourcefilenames/B.txt", |
| // Filename with other extension (used to test filtering). |
| "adaptresourcefilenames/TestClass.md", |
| "adaptresourcefilenames/B.md", |
| // Filename with dot suffix only. |
| "adaptresourcefilenames/TestClass.", |
| "adaptresourcefilenames/B.", |
| // Filename with dot suffix and extension. |
| "adaptresourcefilenames/TestClass.suffix.txt", |
| "adaptresourcefilenames/B.suffix.txt", |
| // Filename with dash suffix and extension. |
| "adaptresourcefilenames/TestClass-suffix.txt", |
| "adaptresourcefilenames/B-suffix.txt", |
| // Filename with dollar suffix and extension. |
| "adaptresourcefilenames/TestClass$suffix.txt", |
| "adaptresourcefilenames/B$suffix.txt", |
| // Filename with dollar suffix matching inner class and extension. |
| "adaptresourcefilenames/TestClass$Inner.txt", |
| "adaptresourcefilenames/B$Inner.txt", |
| // Filename with underscore suffix and extension. |
| "adaptresourcefilenames/TestClass_suffix.txt", |
| "adaptresourcefilenames/B_suffix.txt", |
| // Filename with whitespace suffix and extension. |
| "adaptresourcefilenames/TestClass suffix.txt", |
| "adaptresourcefilenames/B suffix.txt", |
| // Filename with identifier suffix and extension. |
| "adaptresourcefilenames/TestClasssuffix.txt", |
| "adaptresourcefilenames/Bsuffix.txt", |
| // Filename with numeric suffix and extension. |
| "adaptresourcefilenames/TestClass42.txt", |
| "adaptresourcefilenames/B42.txt", |
| // |
| // PREFIX VARIANTS: |
| // |
| // Filename with dot prefix and extension. |
| "adaptresourcefilenames/prefix.TestClass.txt", |
| "adaptresourcefilenames/prefix.B.txt", |
| // Filename with dash prefix and extension. |
| "adaptresourcefilenames/prefix-TestClass.txt", |
| "adaptresourcefilenames/prefix-B.txt", |
| // Filename with dollar prefix and extension. |
| "adaptresourcefilenames/prefix$TestClass.txt", |
| "adaptresourcefilenames/prefix$B.txt", |
| // Filename with identifier prefix and extension. |
| "adaptresourcefilenames/prefixTestClass.txt", |
| "adaptresourcefilenames/prefixB.txt", |
| // Filename with numeric prefix and extension. |
| "adaptresourcefilenames/42TestClass.txt", |
| "adaptresourcefilenames/42B.txt", |
| // |
| // PACKAGE RENAMING TESTS: |
| // |
| // Filename that matches a type, but only the directory should be renamed. |
| "adaptresourcefilenames/pkg/C", |
| // Filename that matches a type that should be renamed. |
| "adaptresourcefilenames/pkg/C.txt", |
| // Filename that does not match a type, but where the directory should be renamed. |
| "adaptresourcefilenames/pkg/file.txt", |
| // Filename that does not match a type, but where a directory-prefix should be renamed. |
| "adaptresourcefilenames/pkg/directory/file.txt", |
| // Filename that matches a type, but only the directory should be renamed. |
| "adaptresourcefilenames/pkg/innerpkg/D", |
| // Filename that matches a type that should be renamed. |
| "adaptresourcefilenames/pkg/innerpkg/D.txt", |
| // Filename that does not match a type, but where the directory should be renamed. |
| "adaptresourcefilenames/pkg/innerpkg/file.txt", |
| // Filename that does not match a type, but where a directory-prefix should be renamed. |
| "adaptresourcefilenames/pkg/innerpkg/directory/file.txt" |
| ); |
| return filenames |
| .stream() |
| .map(filename -> DataEntryResource.fromBytes(new byte[0], filename, Origin.unknown())) |
| .collect(Collectors.toList()); |
| } |
| |
| private static String getExpectedRenamingFor(String filename, ClassNameMapper mapper) { |
| String typeName = null; |
| String suffix = null; |
| switch (filename) { |
| // Filename with dot only. |
| case "adaptresourcefilenames/B.": |
| typeName = "adaptresourcefilenames.B"; |
| suffix = "."; |
| break; |
| // Filename with extension. |
| case "adaptresourcefilenames/B.txt": |
| typeName = "adaptresourcefilenames.B"; |
| suffix = ".txt"; |
| break; |
| // Filename with other extension (used to test filtering). |
| case "adaptresourcefilenames/B.md": |
| typeName = "adaptresourcefilenames.B"; |
| suffix = ".md"; |
| break; |
| // Filename with dot suffix and extension. |
| case "adaptresourcefilenames/B.suffix.txt": |
| typeName = "adaptresourcefilenames.B"; |
| suffix = ".suffix.txt"; |
| break; |
| // Filename with dash suffix and extension. |
| case "adaptresourcefilenames/B-suffix.txt": |
| typeName = "adaptresourcefilenames.B"; |
| suffix = "-suffix.txt"; |
| break; |
| // Filename with dollar suffix and extension. |
| case "adaptresourcefilenames/B$suffix.txt": |
| typeName = "adaptresourcefilenames.B"; |
| suffix = "$suffix.txt"; |
| break; |
| // Filename with dollar suffix matching inner class and extension. |
| case "adaptresourcefilenames/B$Inner.txt": |
| typeName = "adaptresourcefilenames.B$Inner"; |
| suffix = ".txt"; |
| break; |
| // Filename with underscore suffix and extension. |
| case "adaptresourcefilenames/B_suffix.txt": |
| typeName = "adaptresourcefilenames.B"; |
| suffix = "_suffix.txt"; |
| break; |
| // Filename with whitespace suffix and extension. |
| case "adaptresourcefilenames/B suffix.txt": |
| typeName = "adaptresourcefilenames.B"; |
| suffix = " suffix.txt"; |
| break; |
| // |
| // PACKAGE RENAMING TESTS |
| // |
| case "adaptresourcefilenames/pkg/C.txt": |
| typeName = "adaptresourcefilenames.pkg.C"; |
| suffix = ".txt"; |
| break; |
| case "adaptresourcefilenames/pkg/innerpkg/D.txt": |
| typeName = "adaptresourcefilenames.pkg.innerpkg.D"; |
| suffix = ".txt"; |
| break; |
| } |
| if (typeName != null) { |
| String renamedName = mapper.getObfuscatedToOriginalMapping().inverse.get(typeName); |
| assertNotNull(renamedName); |
| assertNotEquals(typeName, renamedName); |
| return renamedName.replace('.', '/') + suffix; |
| } |
| // Renamings for files in directories that match packages that have been renamed, |
| // but where the filename itself should not be renamed. |
| String samePackageAsType = null; |
| switch (filename) { |
| case "adaptresourcefilenames/pkg/C": |
| samePackageAsType = "adaptresourcefilenames.pkg.C"; |
| suffix = "C"; |
| break; |
| case "adaptresourcefilenames/pkg/file.txt": |
| samePackageAsType = "adaptresourcefilenames.pkg.C"; |
| suffix = "file.txt"; |
| break; |
| case "adaptresourcefilenames/pkg/directory/file.txt": |
| samePackageAsType = "adaptresourcefilenames.pkg.C"; |
| suffix = "directory/file.txt"; |
| break; |
| case "adaptresourcefilenames/pkg/innerpkg/D": |
| samePackageAsType = "adaptresourcefilenames.pkg.innerpkg.D"; |
| suffix = "D"; |
| break; |
| case "adaptresourcefilenames/pkg/innerpkg/file.txt": |
| samePackageAsType = "adaptresourcefilenames.pkg.innerpkg.D"; |
| suffix = "file.txt"; |
| break; |
| case "adaptresourcefilenames/pkg/innerpkg/directory/file.txt": |
| samePackageAsType = "adaptresourcefilenames.pkg.innerpkg.D"; |
| suffix = "directory/file.txt"; |
| break; |
| } |
| if (samePackageAsType != null) { |
| String renamedName = mapper.getObfuscatedToOriginalMapping().inverse.get(samePackageAsType); |
| assertNotNull(renamedName); |
| assertNotEquals(samePackageAsType, renamedName); |
| if (renamedName.contains(".")) { |
| String renamedPackageName = renamedName.substring(0, renamedName.lastIndexOf('.')); |
| return renamedPackageName.replace('.', '/') + "/" + suffix; |
| } |
| } |
| return filename; |
| } |
| } |