/* * Copyright 2012-present Facebook, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ package com.facebook.buck.android; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import com.facebook.buck.dalvik.ZipSplitter; import com.facebook.buck.io.ProjectFilesystem; import com.facebook.buck.model.BuildTarget; import com.facebook.buck.rules.FakeSourcePath; import com.google.common.base.Predicate; import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.io.CharStreams; import java.io.BufferedOutputStream; import java.io.BufferedWriter; import java.io.IOException; import java.io.StringReader; import java.io.StringWriter; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import org.easymock.EasyMock; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; public class SplitZipStepTest { @Rule public TemporaryFolder tempDir = new TemporaryFolder(); @Test public void testMetaList() throws IOException { Path outJar = tempDir.newFile("test.jar").toPath(); ZipOutputStream zipOut = new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(outJar))); Map<String, String> fileToClassName = ImmutableMap.of( "com/facebook/foo.class", "com.facebook.foo", "bar.class", "bar"); try { for (String entry : fileToClassName.keySet()) { zipOut.putNextEntry(new ZipEntry(entry)); zipOut.write(new byte[] {0}); } } finally { zipOut.close(); } StringWriter stringWriter = new StringWriter(); BufferedWriter writer = new BufferedWriter(stringWriter); ImmutableSet<APKModule> requires = ImmutableSet.of(); try { SplitZipStep.writeMetaList( writer, SplitZipStep.SECONDARY_DEX_ID, requires, ImmutableList.of(outJar), DexStore.JAR); } finally { writer.close(); } List<String> lines = CharStreams.readLines(new StringReader(stringWriter.toString())); assertEquals(1, lines.size()); String line = Iterables.getLast(lines, null); String[] data = line.split(" "); assertEquals(3, data.length); // Note that we cannot test data[1] (the hash) because zip files change their hash each // time they are written due to timestamps written into the file. assertEquals("secondary-1.dex.jar", data[0]); assertTrue( String.format("Unexpected class: %s", data[2]), fileToClassName.values().contains(data[2])); } @Test public void testMetaListApkModuule() throws IOException { Path outJar = tempDir.newFile("test.jar").toPath(); ZipOutputStream zipOut = new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(outJar))); Map<String, String> fileToClassName = ImmutableMap.of( "com/facebook/foo.class", "com.facebook.foo", "bar.class", "bar"); try { for (String entry : fileToClassName.keySet()) { zipOut.putNextEntry(new ZipEntry(entry)); zipOut.write(new byte[] {0}); } } finally { zipOut.close(); } StringWriter stringWriter = new StringWriter(); BufferedWriter writer = new BufferedWriter(stringWriter); ImmutableSet<APKModule> requires = ImmutableSet.of(APKModule.builder().setName("dependency").build()); try { SplitZipStep.writeMetaList( writer, "module", requires, ImmutableList.of(outJar), DexStore.JAR); } finally { writer.close(); } List<String> lines = CharStreams.readLines(new StringReader(stringWriter.toString())); assertEquals(3, lines.size()); assertEquals(lines.get(0), ".id module"); assertEquals(lines.get(1), ".requires dependency"); String line = Iterables.getLast(lines, null); String[] data = line.split(" "); assertEquals(3, data.length); // Note that we cannot test data[1] (the hash) because zip files change their hash each // time they are written due to timestamps written into the file. assertEquals("module-1.dex.jar", data[0]); assertTrue( String.format("Unexpected class: %s", data[2]), fileToClassName.values().contains(data[2])); } @Test public void testRequiredInPrimaryZipPredicate() throws IOException { Path primaryDexClassesFile = Paths.get("the/manifest.txt"); List<String> linesInManifestFile = ImmutableList.of( "com/google/common/collect/ImmutableSortedSet", " com/google/common/collect/ImmutableSet", "# com/google/common/collect/ImmutableMap"); ProjectFilesystem projectFilesystem = EasyMock.createMock(ProjectFilesystem.class); EasyMock.expect(projectFilesystem.readLines(primaryDexClassesFile)) .andReturn(linesInManifestFile); EasyMock.replay(projectFilesystem); SplitZipStep splitZipStep = new SplitZipStep( projectFilesystem, /* inputPathsToSplit */ ImmutableSet.of(), /* secondaryJarMetaPath */ Paths.get(""), /* primaryJarPath */ Paths.get(""), /* secondaryJarDir */ Paths.get(""), /* secondaryJarPattern */ "", /* additionalDexStoreJarMetaPath */ Paths.get(""), /* additionalDexStoreJarDir */ Paths.get(""), /* proguardFullConfigFile */ Optional.empty(), /* proguardMappingFile */ Optional.empty(), false, new DexSplitMode( /* shouldSplitDex */ true, ZipSplitter.DexSplitStrategy.MAXIMIZE_PRIMARY_DEX_SIZE, DexStore.JAR, /* linearAllocHardLimit */ 4 * 1024 * 1024, /* primaryDexPatterns */ ImmutableSet.of("List"), Optional.of(new FakeSourcePath("the/manifest.txt")), /* primaryDexScenarioFile */ Optional.empty(), /* isPrimaryDexScenarioOverflowAllowed */ false, /* secondaryDexHeadClassesFile */ Optional.empty(), /* secondaryDexTailClassesFile */ Optional.empty()), Optional.empty(), Optional.of(Paths.get("the/manifest.txt")), Optional.empty(), Optional.empty(), /* additionalDexStoreToJarPathMap */ ImmutableMultimap.of(), new APKModuleGraph(null, (BuildTarget) null, null), /* pathToReportDir */ Paths.get("")); Predicate<String> requiredInPrimaryZipPredicate = splitZipStep.createRequiredInPrimaryZipPredicate( ProguardTranslatorFactory.createForTest(Optional.empty()), Suppliers.ofInstance(ImmutableList.of())); assertTrue( "com/google/common/collect/ImmutableSortedSet.class is listed in the manifest verbatim.", requiredInPrimaryZipPredicate.apply("com/google/common/collect/ImmutableSortedSet.class")); assertTrue( "com/google/common/collect/ImmutableSet.class is in the manifest with whitespace.", requiredInPrimaryZipPredicate.apply("com/google/common/collect/ImmutableSet.class")); assertFalse( "com/google/common/collect/ImmutableSet.class cannot have whitespace as param.", requiredInPrimaryZipPredicate.apply(" com/google/common/collect/ImmutableSet.class")); assertFalse( "com/google/common/collect/ImmutableMap.class is commented out.", requiredInPrimaryZipPredicate.apply("com/google/common/collect/ImmutableMap.class")); assertFalse( "com/google/common/collect/Iterables.class is not even mentioned.", requiredInPrimaryZipPredicate.apply("com/google/common/collect/Iterables.class")); assertTrue( "java/awt/List.class matches the substring 'List'.", requiredInPrimaryZipPredicate.apply("java/awt/List.class")); assertFalse( "Substring matching is case-sensitive.", requiredInPrimaryZipPredicate.apply("shiny/Glistener.class")); EasyMock.verify(projectFilesystem); } @Test public void testRequiredInPrimaryZipPredicateWithProguard() throws IOException { List<String> linesInMappingFile = ImmutableList.of( "foo.bar.MappedPrimary -> foo.bar.a:", "foo.bar.MappedSecondary -> foo.bar.b:", "foo.bar.UnmappedPrimary -> foo.bar.UnmappedPrimary:", "foo.bar.UnmappedSecondary -> foo.bar.UnmappedSecondary:", "foo.primary.MappedPackage -> x.a:", "foo.secondary.MappedPackage -> x.b:", "foo.primary.UnmappedPackage -> foo.primary.UnmappedPackage:"); List<String> linesInManifestFile = ImmutableList.of( // Actual primary dex classes. "foo/bar/MappedPrimary", "foo/bar/UnmappedPrimary", // Red herrings! "foo/bar/b", "x/b"); Path proguardConfigFile = Paths.get("the/configuration.txt"); Path proguardMappingFile = Paths.get("the/mapping.txt"); Path primaryDexClassesFile = Paths.get("the/manifest.txt"); ProjectFilesystem projectFilesystem = EasyMock.createMock(ProjectFilesystem.class); EasyMock.expect(projectFilesystem.readLines(primaryDexClassesFile)) .andReturn(linesInManifestFile); EasyMock.expect(projectFilesystem.readLines(proguardConfigFile)).andReturn(ImmutableList.of()); EasyMock.expect(projectFilesystem.readLines(proguardMappingFile)).andReturn(linesInMappingFile); EasyMock.replay(projectFilesystem); SplitZipStep splitZipStep = new SplitZipStep( projectFilesystem, /* inputPathsToSplit */ ImmutableSet.of(), /* secondaryJarMetaPath */ Paths.get(""), /* primaryJarPath */ Paths.get(""), /* secondaryJarDir */ Paths.get(""), /* secondaryJarPattern */ "", /* additionalDexStoreJarMetaPath */ Paths.get(""), /* additionalDexStoreJarDir */ Paths.get(""), /* proguardFullConfigFile */ Optional.of(proguardConfigFile), /* proguardMappingFile */ Optional.of(proguardMappingFile), false, new DexSplitMode( /* shouldSplitDex */ true, ZipSplitter.DexSplitStrategy.MAXIMIZE_PRIMARY_DEX_SIZE, DexStore.JAR, /* linearAllocHardLimit */ 4 * 1024 * 1024, /* primaryDexPatterns */ ImmutableSet.of("/primary/", "x/"), Optional.of(new FakeSourcePath("the/manifest.txt")), /* primaryDexScenarioFile */ Optional.empty(), /* isPrimaryDexScenarioOverflowAllowed */ false, /* secondaryDexHeadClassesFile */ Optional.empty(), /* secondaryDexTailClassesFile */ Optional.empty()), Optional.empty(), Optional.of(Paths.get("the/manifest.txt")), Optional.empty(), Optional.empty(), /* additionalDexStoreToJarPathMap */ ImmutableMultimap.of(), new APKModuleGraph(null, (BuildTarget) null, null), /* pathToReportDir */ Paths.get("")); ProguardTranslatorFactory translatorFactory = ProguardTranslatorFactory.create( projectFilesystem, Optional.of(proguardConfigFile), Optional.of(proguardMappingFile), false); Predicate<String> requiredInPrimaryZipPredicate = splitZipStep.createRequiredInPrimaryZipPredicate( translatorFactory, Suppliers.ofInstance(ImmutableList.of())); assertTrue( "Mapped class from primary list should be in primary.", requiredInPrimaryZipPredicate.apply("foo/bar/a.class")); assertTrue( "Unmapped class from primary list should be in primary.", requiredInPrimaryZipPredicate.apply("foo/bar/UnmappedPrimary.class")); assertTrue( "Mapped class from substring should be in primary.", requiredInPrimaryZipPredicate.apply("x/a.class")); assertTrue( "Unmapped class from substring should be in primary.", requiredInPrimaryZipPredicate.apply("foo/primary/UnmappedPackage.class")); assertFalse( "Mapped class with obfuscated name match should not be in primary.", requiredInPrimaryZipPredicate.apply("foo/bar/b.class")); assertFalse( "Unmapped class name should not randomly be in primary.", requiredInPrimaryZipPredicate.apply("foo/bar/UnmappedSecondary.class")); assertFalse( "Map class with obfuscated name substring should not be in primary.", requiredInPrimaryZipPredicate.apply("x/b.class")); EasyMock.verify(projectFilesystem); } @Test public void testNonObfuscatedBuild() throws IOException { Path proguardConfigFile = Paths.get("the/configuration.txt"); Path proguardMappingFile = Paths.get("the/mapping.txt"); ProjectFilesystem projectFilesystem = EasyMock.createMock(ProjectFilesystem.class); EasyMock.expect(projectFilesystem.readLines(proguardConfigFile)) .andReturn(ImmutableList.of("-dontobfuscate")); EasyMock.replay(projectFilesystem); SplitZipStep splitZipStep = new SplitZipStep( projectFilesystem, /* inputPathsToSplit */ ImmutableSet.of(), /* secondaryJarMetaPath */ Paths.get(""), /* primaryJarPath */ Paths.get(""), /* secondaryJarDir */ Paths.get(""), /* secondaryJarPattern */ "", /* additionalDexStoreJarMetaPath */ Paths.get(""), /* additionalDexStoreJarDir */ Paths.get(""), /* proguardFullConfigFile */ Optional.of(proguardConfigFile), /* proguardMappingFile */ Optional.of(proguardMappingFile), false, new DexSplitMode( /* shouldSplitDex */ true, ZipSplitter.DexSplitStrategy.MAXIMIZE_PRIMARY_DEX_SIZE, DexStore.JAR, /* linearAllocHardLimit */ 4 * 1024 * 1024, /* primaryDexPatterns */ ImmutableSet.of("primary"), /* primaryDexClassesFile */ Optional.empty(), /* primaryDexScenarioFile */ Optional.empty(), /* isPrimaryDexScenarioOverflowAllowed */ false, /* secondaryDexHeadClassesFile */ Optional.empty(), /* secondaryDexTailClassesFile */ Optional.empty()), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), /* additionalDexStoreToJarPathMap */ ImmutableMultimap.of(), new APKModuleGraph(null, (BuildTarget) null, null), /* pathToReportDir */ Paths.get("")); ProguardTranslatorFactory translatorFactory = ProguardTranslatorFactory.create( projectFilesystem, Optional.of(proguardConfigFile), Optional.of(proguardMappingFile), false); Predicate<String> requiredInPrimaryZipPredicate = splitZipStep.createRequiredInPrimaryZipPredicate( translatorFactory, Suppliers.ofInstance(ImmutableList.of())); assertTrue( "Primary class should be in primary.", requiredInPrimaryZipPredicate.apply("primary.class")); assertFalse( "Secondary class should be in secondary.", requiredInPrimaryZipPredicate.apply("secondary.class")); EasyMock.verify(projectFilesystem); } @Test public void testClassFilePattern() { assertTrue( SplitZipStep.CLASS_FILE_PATTERN .matcher("com/facebook/orca/threads/ParticipantInfo.class") .matches()); assertTrue( SplitZipStep.CLASS_FILE_PATTERN .matcher("com/facebook/orca/threads/ParticipantInfo$1.class") .matches()); } }