| // Copyright 2015 The Bazel Authors. All rights reserved. |
| // |
| // 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. |
| |
| import static com.google.common.collect.ImmutableList.toImmutableList; |
| import static com.google.common.truth.Truth.assertThat; |
| import static java.nio.charset.StandardCharsets.UTF_8; |
| import static org.junit.Assert.assertFalse; |
| import static org.junit.Assert.fail; |
| |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.io.ByteStreams; |
| import org.objectweb.asm.Opcodes; |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.nio.file.Files; |
| import java.nio.file.Paths; |
| import java.time.LocalDateTime; |
| import java.time.ZoneOffset; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.Enumeration; |
| import java.util.HashMap; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.jar.Attributes; |
| import java.util.jar.JarEntry; |
| import java.util.jar.JarFile; |
| import java.util.jar.Manifest; |
| import java.util.zip.ZipEntry; |
| import java.util.zip.ZipFile; |
| import javax.annotation.processing.AbstractProcessor; |
| import javax.annotation.processing.RoundEnvironment; |
| import javax.lang.model.SourceVersion; |
| import javax.lang.model.element.TypeElement; |
| import javax.tools.Diagnostic; |
| import javax.tools.DiagnosticCollector; |
| import javax.tools.JavaCompiler; |
| import javax.tools.JavaFileObject; |
| import javax.tools.StandardJavaFileManager; |
| import javax.tools.StandardLocation; |
| import javax.tools.ToolProvider; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.junit.runners.JUnit4; |
| import org.objectweb.asm.ClassReader; |
| import org.objectweb.asm.ClassVisitor; |
| |
| /** JUnit tests for ijar tool. */ |
| @RunWith(JUnit4.class) |
| public class IjarTests { |
| |
| private static File getTmpDir() { |
| String tmpdir = System.getenv("TEST_TMPDIR"); |
| if (tmpdir == null) { |
| // Fall back on the system temporary directory |
| tmpdir = System.getProperty("java.io.tmpdir"); |
| } |
| if (tmpdir == null) { |
| fail("TEST_TMPDIR environment variable is not set!"); |
| } |
| return new File(tmpdir); |
| } |
| |
| DiagnosticCollector<JavaFileObject> diagnostics; |
| |
| private JavaCompiler.CompilationTask makeCompilationTask(String... files) throws IOException { |
| JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); |
| StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null); |
| fileManager.setLocation( |
| StandardLocation.CLASS_PATH, |
| Arrays.asList(new File("third_party/ijar/test/interface_ijar_testlib.jar"))); |
| fileManager.setLocation(StandardLocation.CLASS_OUTPUT, Arrays.asList(getTmpDir())); |
| diagnostics = new DiagnosticCollector<JavaFileObject>(); |
| return compiler.getTask( |
| null, |
| fileManager, |
| diagnostics, |
| Arrays.asList("-Xlint:deprecation"), // used for deprecation tests |
| null, |
| fileManager.getJavaFileObjects(files)); |
| } |
| |
| /** |
| * Test that the ijar tool preserves private nested classes as they may be exposed through public |
| * API. This test relies on an interface jar provided through the build rule |
| * :interface_ijar_testlib and the Java source file PrivateNestedClass.java. |
| */ |
| @Test |
| public void testPrivateNestedClass() throws IOException { |
| if (!makeCompilationTask("third_party/ijar/test/PrivateNestedClass.java").call()) { |
| fail(getFailedCompilationMessage()); |
| } |
| } |
| |
| /** Test that the ijar tool preserves annotations, especially @Target meta-annotation. */ |
| @Test |
| public void testRestrictedAnnotations() throws IOException { |
| assertFalse(makeCompilationTask("third_party/ijar/test/UseRestrictedAnnotation.java").call()); |
| } |
| |
| /** |
| * Test that the ijar tool preserves private nested classes as they may be exposed through public |
| * API. This test relies on an interface jar provided through the build rule |
| * :interface_ijar_testlib and the Java source file PrivateNestedClass.java. |
| */ |
| @Test |
| public void testDeprecatedParts() throws IOException { |
| if (!makeCompilationTask("third_party/ijar/test/UseDeprecatedParts.java").call()) { |
| fail(getFailedCompilationMessage()); |
| } |
| int deprecatedWarningCount = 0; |
| for (Diagnostic<? extends JavaFileObject> diagnostic : diagnostics.getDiagnostics()) { |
| if ((diagnostic.getKind() == Diagnostic.Kind.MANDATORY_WARNING) |
| && |
| // Java 6: |
| (diagnostic.getMessage(Locale.ENGLISH).startsWith("[deprecation]") |
| || |
| // Java 7: |
| diagnostic.getMessage(Locale.ENGLISH).contains("has been deprecated"))) { |
| deprecatedWarningCount++; |
| } |
| } |
| assertThat(deprecatedWarningCount).isAtLeast(16); |
| } |
| |
| /** |
| * Test that the ijar tool preserves EnclosingMethod attributes and doesn't prevent annotation |
| * processors from accessing all the elements in a package. |
| */ |
| @Test |
| public void testEnclosingMethod() throws IOException { |
| JavaCompiler.CompilationTask task = |
| makeCompilationTask("third_party/ijar/test/package-info.java"); |
| task.setProcessors( |
| Arrays.asList( |
| new AbstractProcessor() { |
| |
| @Override |
| public SourceVersion getSupportedSourceVersion() { |
| return SourceVersion.latestSupported(); |
| } |
| |
| @Override |
| public Set<String> getSupportedAnnotationTypes() { |
| return Collections.singleton("*"); |
| } |
| |
| @Override |
| public boolean process( |
| Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { |
| roundEnv.getElementsAnnotatedWith(java.lang.Override.class); |
| return false; |
| } |
| })); |
| if (!task.call()) { |
| fail(getFailedCompilationMessage()); |
| } |
| } |
| |
| @Test |
| public void testVerifyStripping() throws Exception { |
| ZipFile zip = new ZipFile("third_party/ijar/test/interface_ijar_testlib.jar"); |
| Enumeration<? extends ZipEntry> entries = zip.entries(); |
| while (entries.hasMoreElements()) { |
| ZipEntry entry = entries.nextElement(); |
| ClassReader reader = new ClassReader(zip.getInputStream(entry)); |
| StripVerifyingVisitor verifier = new StripVerifyingVisitor(); |
| |
| reader.accept(verifier, 0); |
| |
| if (verifier.errors.size() > 0) { |
| StringBuilder builder = new StringBuilder(); |
| builder.append("Verification of "); |
| builder.append(entry.getName()); |
| builder.append(" failed: "); |
| for (String msg : verifier.errors) { |
| builder.append(msg); |
| builder.append("\t"); |
| } |
| fail(builder.toString()); |
| } |
| } |
| } |
| |
| private String getFailedCompilationMessage() { |
| StringBuilder builder = new StringBuilder(); |
| builder.append("Build failed unexpectedly"); |
| for (Diagnostic<? extends JavaFileObject> diagnostic : diagnostics.getDiagnostics()) { |
| builder.append( |
| String.format( |
| "\t%s line %d column %d: %s", |
| diagnostic.getKind().toString(), |
| diagnostic.getLineNumber(), |
| diagnostic.getColumnNumber(), |
| diagnostic.getMessage(Locale.ENGLISH))); |
| } |
| return builder.toString(); |
| } |
| |
| @Test |
| public void localAndAnonymous() throws Exception { |
| Map<String, byte[]> lib = readJar("third_party/ijar/test/liblocal_and_anonymous_lib.jar"); |
| Map<String, byte[]> ijar = readJar("third_party/ijar/test/local_and_anonymous-interface.jar"); |
| |
| assertThat(lib.keySet()) |
| .containsExactly( |
| "LocalAndAnonymous$1.class", |
| "LocalAndAnonymous$2.class", |
| "LocalAndAnonymous$1LocalClass.class", |
| "LocalAndAnonymous.class", |
| "LocalAndAnonymous$NestedClass.class", |
| "LocalAndAnonymous$InnerClass.class", |
| "LocalAndAnonymous$PrivateInnerClass.class"); |
| assertThat(ijar.keySet()) |
| .containsExactly( |
| "LocalAndAnonymous.class", |
| "LocalAndAnonymous$NestedClass.class", |
| "LocalAndAnonymous$InnerClass.class", |
| "LocalAndAnonymous$PrivateInnerClass.class"); |
| |
| assertThat(innerClasses(lib.get("LocalAndAnonymous.class"))) |
| .isEqualTo( |
| ImmutableMap.<String, String>builder() |
| .put("LocalAndAnonymous$1", "null") |
| .put("LocalAndAnonymous$2", "null") |
| .put("LocalAndAnonymous$1LocalClass", "null") |
| .put("LocalAndAnonymous$InnerClass", "LocalAndAnonymous") |
| .put("LocalAndAnonymous$NestedClass", "LocalAndAnonymous") |
| .put("LocalAndAnonymous$PrivateInnerClass", "LocalAndAnonymous") |
| .buildOrThrow()); |
| assertThat(innerClasses(ijar.get("LocalAndAnonymous.class"))) |
| .containsExactly( |
| "LocalAndAnonymous$InnerClass", "LocalAndAnonymous", |
| "LocalAndAnonymous$NestedClass", "LocalAndAnonymous", |
| "LocalAndAnonymous$PrivateInnerClass", "LocalAndAnonymous"); |
| } |
| |
| static Map<String, byte[]> readJar(String path) throws IOException { |
| Map<String, byte[]> classes = new HashMap<>(); |
| try (JarFile jf = new JarFile(path)) { |
| Enumeration<JarEntry> entries = jf.entries(); |
| while (entries.hasMoreElements()) { |
| JarEntry je = entries.nextElement(); |
| if (!je.getName().endsWith(".class") |
| && !je.getName().endsWith(".kotlin_builtins") |
| && !je.getName().endsWith(".kotlin_module") |
| && !je.getName().endsWith(".tasty")) { |
| continue; |
| } |
| classes.put(je.getName(), ByteStreams.toByteArray(jf.getInputStream(je))); |
| } |
| } |
| return classes; |
| } |
| |
| static Map<String, String> innerClasses(byte[] bytes) { |
| final Map<String, String> innerClasses = new HashMap<>(); |
| new ClassReader(bytes) |
| .accept( |
| new ClassVisitor(Opcodes.ASM9) { |
| @Override |
| public void visitInnerClass( |
| String name, String outerName, String innerName, int access) { |
| innerClasses.put(name, String.valueOf(outerName)); |
| } |
| }, |
| /* parsingOptions= */ 0); |
| return innerClasses; |
| } |
| |
| @Test |
| public void moduleInfo() throws Exception { |
| Map<String, byte[]> lib = readJar("third_party/ijar/test/module_info-interface.jar"); |
| assertThat(lib.keySet()) |
| .containsExactly("java/lang/String.class", "module-info.class", "foo/module-info.class"); |
| // ijar passes module-infos through unmodified, so it doesn't care that these ones are bogus |
| assertThat(new String(lib.get("module-info.class"), UTF_8)).isEqualTo("hello"); |
| assertThat(new String(lib.get("foo/module-info.class"), UTF_8)).isEqualTo("goodbye"); |
| } |
| |
| @Test |
| public void kotlinModule() throws Exception { |
| Map<String, byte[]> lib = readJar("third_party/ijar/test/kotlin_module-interface.jar"); |
| assertThat(lib.keySet()) |
| .containsExactly( |
| "java/lang/String.class", |
| "kotlin/kotlin.kotlin_builtins", |
| "META-INF/bar.kotlin_module"); |
| // ijar passes kotlin modules and builtins through unmodified |
| assertThat(new String(lib.get("META-INF/bar.kotlin_module"), UTF_8)).isEqualTo("hello"); |
| assertThat(new String(lib.get("kotlin/kotlin.kotlin_builtins"), UTF_8)).isEqualTo("goodbye"); |
| } |
| |
| @Test |
| public void scalaTasty() throws Exception { |
| Map<String, byte[]> lib = readJar("third_party/ijar/test/scala_tasty-interface.jar"); |
| assertThat(lib.keySet()).containsExactly("java/lang/String.class", "Bar.tasty"); |
| // ijar passes scala tasty files through unmodified |
| assertThat(new String(lib.get("Bar.tasty"), UTF_8)).isEqualTo("hello"); |
| } |
| |
| @Test |
| public void testTargetLabel() throws Exception { |
| try (JarFile jf = |
| new JarFile("third_party/ijar/test/interface_ijar_testlib_with_target_label.jar")) { |
| ImmutableList<String> entries = jf.stream().map(JarEntry::getName).collect(toImmutableList()); |
| assertThat(entries.get(0)).isEqualTo("META-INF/"); |
| assertThat(entries.get(1)).isEqualTo("META-INF/MANIFEST.MF"); |
| Manifest manifest = jf.getManifest(); |
| Attributes attributes = manifest.getMainAttributes(); |
| assertThat(attributes.getValue("Target-Label")).isEqualTo("//foo:foo"); |
| assertThat(attributes.getValue("Injecting-Rule-Kind")).isEqualTo("foo_library"); |
| assertThat(jf.getEntry(JarFile.MANIFEST_NAME).getLastModifiedTime().toInstant()) |
| .isEqualTo( |
| LocalDateTime.of(2010, 1, 1, 0, 0, 0).atZone(ZoneOffset.systemDefault()).toInstant()); |
| } |
| } |
| |
| @Test |
| public void testEmptyWithTargetLabel() throws Exception { |
| try (JarFile jf = new JarFile("third_party/ijar/test/empty_with_target_label.jar")) { |
| Manifest manifest = jf.getManifest(); |
| Attributes attributes = manifest.getMainAttributes(); |
| assertThat(attributes.getValue("Target-Label")).isEqualTo("//empty"); |
| assertThat(jf.getEntry(JarFile.MANIFEST_NAME).getLastModifiedTime().toInstant()) |
| .isEqualTo( |
| LocalDateTime.of(2010, 1, 1, 0, 0, 0).atZone(ZoneOffset.systemDefault()).toInstant()); |
| } |
| } |
| |
| // Tests --nostrip_jar with a jar that already has a manifest, but no target label |
| @Test |
| public void testNoStripJarWithManifest() throws Exception { |
| JarFile original = new JarFile("third_party/ijar/test/jar-with-manifest.jar"); |
| JarFile stripped = new JarFile("third_party/ijar/test/jar-with-manifest-nostrip.jar"); |
| try { |
| ImmutableList<String> strippedEntries = |
| stripped.stream().map(JarEntry::getName).collect(toImmutableList()); |
| assertThat(strippedEntries.get(0)).isEqualTo("META-INF/"); |
| assertThat(strippedEntries.get(1)).isEqualTo("META-INF/MANIFEST.MF"); |
| Manifest manifest = stripped.getManifest(); |
| Attributes attributes = manifest.getMainAttributes(); |
| assertThat(attributes.getValue("Manifest-Version")).isEqualTo("1.0"); |
| // Created-By was already in manifest, doesn't get overwritten |
| assertThat(attributes.getValue("Created-By")).isEqualTo("test-code"); |
| assertThat(attributes.getValue("Target-Label")).isEqualTo("//foo:foo"); |
| assertNonManifestFilesBitIdentical(original, stripped); |
| } finally { |
| original.close(); |
| stripped.close(); |
| } |
| } |
| |
| // Tests --nostrip_jar with a jar that already has a manifest with a target label |
| @Test |
| public void testNoStripJarWithManifestAndTargetLabel() throws Exception { |
| JarFile original = new JarFile("third_party/ijar/test/jar-with-manifest-and-target-label.jar"); |
| JarFile stripped = |
| new JarFile("third_party/ijar/test/jar-with-manifest-and-target-label-nostrip.jar"); |
| try { |
| ImmutableList<String> strippedEntries = |
| stripped.stream().map(JarEntry::getName).collect(toImmutableList()); |
| assertThat(strippedEntries.get(0)).isEqualTo("META-INF/"); |
| assertThat(strippedEntries.get(1)).isEqualTo("META-INF/MANIFEST.MF"); |
| Manifest manifest = stripped.getManifest(); |
| Attributes attributes = manifest.getMainAttributes(); |
| assertThat(attributes.getValue("Manifest-Version")).isEqualTo("1.0"); |
| // Created-By was already in manifest, doesn't get overwritten |
| assertThat(attributes.getValue("Created-By")).isEqualTo("test-code"); |
| assertThat(attributes.getValue("Target-Label")).isEqualTo("//foo:foo"); |
| assertNonManifestFilesBitIdentical(original, stripped); |
| } finally { |
| original.close(); |
| stripped.close(); |
| } |
| } |
| |
| // Tests --nostrip_jar with a jar that didn't already have a manifest |
| @Test |
| public void testNoStripJarWithoutManifest() throws Exception { |
| JarFile original = new JarFile("third_party/ijar/test/jar-without-manifest.jar"); |
| JarFile stripped = new JarFile("third_party/ijar/test/jar-without-manifest-nostrip.jar"); |
| try { |
| ImmutableList<String> strippedEntries = |
| stripped.stream().map(JarEntry::getName).collect(toImmutableList()); |
| assertThat(strippedEntries.get(0)).isEqualTo("META-INF/"); |
| assertThat(strippedEntries.get(1)).isEqualTo("META-INF/MANIFEST.MF"); |
| Manifest manifest = stripped.getManifest(); |
| Attributes attributes = manifest.getMainAttributes(); |
| assertThat(attributes.getValue("Manifest-Version")).isEqualTo("1.0"); |
| assertThat(attributes.getValue("Created-By")).isEqualTo("bazel"); |
| assertThat(attributes.getValue("Target-Label")).isEqualTo("//foo:foo"); |
| assertNonManifestFilesBitIdentical(original, stripped); |
| } finally { |
| original.close(); |
| stripped.close(); |
| } |
| } |
| |
| @Test |
| public void testPreserveManifestSections() throws Exception { |
| try (JarFile original = |
| new JarFile( |
| "third_party/ijar/test/jar-with-target-label-and-manifest-sections-nostrip.jar"); |
| JarFile stripped = |
| new JarFile("third_party/ijar/test/jar-with-manifest-sections-nostrip.jar")) { |
| ImmutableList<String> strippedEntries = |
| stripped.stream().map(JarEntry::getName).collect(toImmutableList()); |
| |
| assertThat(strippedEntries.get(0)).isEqualTo("META-INF/"); |
| assertThat(strippedEntries.get(1)).isEqualTo("META-INF/MANIFEST.MF"); |
| Manifest manifest = stripped.getManifest(); |
| Attributes attributes = manifest.getMainAttributes(); |
| assertThat(attributes.getValue("Target-Label")).isEqualTo("//foo:foo"); |
| assertNonManifestFilesBitIdentical(original, stripped); |
| |
| Attributes sectionAttributes1 = manifest.getAttributes("foo"); |
| assertThat(sectionAttributes1.getValue("Foo")).isEqualTo("bar"); |
| |
| Attributes sectionAttributes2 = manifest.getAttributes("baz"); |
| assertThat(sectionAttributes2.getValue("Another")).isEqualTo("bar"); |
| } |
| } |
| |
| @Test |
| public void testPreserveManifestSectionsAndUpdateExistingTargetLabel() throws Exception { |
| try (JarFile original = |
| new JarFile( |
| "third_party/ijar/test/jar-with-target-label-and-manifest-sections-nostrip.jar"); |
| JarFile stripped = |
| new JarFile( |
| "third_party/ijar/test/jar-with-target-label-and-manifest-sections-nostrip.jar")) { |
| ImmutableList<String> strippedEntries = |
| stripped.stream().map(JarEntry::getName).collect(toImmutableList()); |
| |
| assertThat(strippedEntries.get(0)).isEqualTo("META-INF/"); |
| assertThat(strippedEntries.get(1)).isEqualTo("META-INF/MANIFEST.MF"); |
| Manifest manifest = stripped.getManifest(); |
| Attributes attributes = manifest.getMainAttributes(); |
| assertThat(attributes.getValue("Target-Label")).isEqualTo("//foo:foo"); |
| assertNonManifestFilesBitIdentical(original, stripped); |
| |
| Attributes sectionAttributes1 = manifest.getAttributes("foo"); |
| assertThat(sectionAttributes1.getValue("Foo")).isEqualTo("bar"); |
| |
| Attributes sectionAttributes2 = manifest.getAttributes("baz"); |
| assertThat(sectionAttributes2.getValue("Another")).isEqualTo("bar"); |
| } |
| } |
| |
| // Tests idempotence of --nostrip |
| @Test |
| public void testNoStripIdempotence() throws Exception { |
| byte[] original = |
| Files.readAllBytes(Paths.get("third_party/ijar/test/jar-without-manifest-nostrip.jar")); |
| byte[] stripped = |
| Files.readAllBytes( |
| Paths.get("third_party/ijar/test/jar-without-manifest-nostrip-idempotence.jar")); |
| assertThat(original).isEqualTo(stripped); |
| } |
| |
| @Test |
| public void metaInfTtransitive() throws Exception { |
| Map<String, byte[]> lib = readJar("third_party/ijar/test/meta_inf_transitive-interface.jar"); |
| assertThat(lib.keySet()).containsExactly("java/lang/String.class"); |
| } |
| |
| private static void assertNonManifestFilesBitIdentical(JarFile original, JarFile stripped) |
| throws IOException { |
| // Make sure that all other files came across bitwise equal |
| for (String classEntry : |
| original.stream() |
| .map(JarEntry::getName) |
| .filter(name -> !name.equals("META-INF/MANIFEST.MF")) |
| .collect(toImmutableList())) { |
| ZipEntry originalEntry = original.getEntry(classEntry); |
| ZipEntry strippedEntry = stripped.getEntry(classEntry); |
| InputStream originalStream = original.getInputStream(originalEntry); |
| InputStream strippedStream = stripped.getInputStream(strippedEntry); |
| assertThat(ByteStreams.toByteArray(strippedStream)) |
| .isEqualTo(ByteStreams.toByteArray(originalStream)); |
| } |
| } |
| } |