/* * SPDX-License-Identifier: Apache-2.0 * * The OpenSearch Contributors require contributions made to * this file be licensed under the Apache-2.0 license or a * compatible open source license. */ /* * Licensed to Elasticsearch under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch licenses this file to you 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. */ /* * Modifications Copyright OpenSearch Contributors. See * GitHub history for details. */ package org.opensearch.bootstrap; import org.opensearch.common.io.PathUtils; import org.opensearch.core.common.Strings; import org.opensearch.test.OpenSearchTestCase; import java.io.IOException; import java.lang.Runtime.Version; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Set; import java.util.jar.Attributes; import java.util.jar.JarOutputStream; import java.util.jar.Manifest; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; public class JarHellTests extends OpenSearchTestCase { URL makeJar(Path dir, String name, Manifest manifest, String... files) throws IOException { Path jarpath = dir.resolve(name); ZipOutputStream out; if (manifest == null) { out = new JarOutputStream(Files.newOutputStream(jarpath, StandardOpenOption.CREATE)); } else { out = new JarOutputStream(Files.newOutputStream(jarpath, StandardOpenOption.CREATE), manifest); } for (String file : files) { out.putNextEntry(new ZipEntry(file)); } out.close(); return jarpath.toUri().toURL(); } URL makeFile(Path dir, String name) throws IOException { Path filepath = dir.resolve(name); Files.newOutputStream(filepath, StandardOpenOption.CREATE).close(); return dir.toUri().toURL(); } public void testDifferentJars() throws Exception { Path dir = createTempDir(); Set jars = asSet(makeJar(dir, "foo.jar", null, "DuplicateClass.class"), makeJar(dir, "bar.jar", null, "DuplicateClass.class")); try { JarHell.checkJarHell(jars, logger::debug); fail("did not get expected exception"); } catch (IllegalStateException e) { assertTrue(e.getMessage().contains("jar hell!")); assertTrue(e.getMessage().contains("DuplicateClass")); assertTrue(e.getMessage().contains("foo.jar")); assertTrue(e.getMessage().contains("bar.jar")); } } public void testModuleInfo() throws Exception { Path dir = createTempDir(); JarHell.checkJarHell( asSet(makeJar(dir, "foo.jar", null, "module-info.class"), makeJar(dir, "bar.jar", null, "module-info.class")), logger::debug ); } public void testModuleInfoPackage() throws Exception { Path dir = createTempDir(); JarHell.checkJarHell( asSet(makeJar(dir, "foo.jar", null, "foo/bar/module-info.class"), makeJar(dir, "bar.jar", null, "foo/bar/module-info.class")), logger::debug ); } public void testDirsOnClasspath() throws Exception { Path dir1 = createTempDir(); Path dir2 = createTempDir(); Set dirs = asSet(makeFile(dir1, "DuplicateClass.class"), makeFile(dir2, "DuplicateClass.class")); try { JarHell.checkJarHell(dirs, logger::debug); fail("did not get expected exception"); } catch (IllegalStateException e) { assertTrue(e.getMessage().contains("jar hell!")); assertTrue(e.getMessage().contains("DuplicateClass")); assertTrue(e.getMessage().contains(dir1.toString())); assertTrue(e.getMessage().contains(dir2.toString())); } } public void testDirAndJar() throws Exception { Path dir1 = createTempDir(); Path dir2 = createTempDir(); Set dirs = asSet(makeJar(dir1, "foo.jar", null, "DuplicateClass.class"), makeFile(dir2, "DuplicateClass.class")); try { JarHell.checkJarHell(dirs, logger::debug); fail("did not get expected exception"); } catch (IllegalStateException e) { assertTrue(e.getMessage().contains("jar hell!")); assertTrue(e.getMessage().contains("DuplicateClass")); assertTrue(e.getMessage().contains("foo.jar")); assertTrue(e.getMessage().contains(dir2.toString())); } } public void testWithinSingleJar() throws Exception { // the java api for zip file does not allow creating duplicate entries (good!) so // this bogus jar had to be with https://github.com/jasontedor/duplicate-classes Set jars = Collections.singleton(JarHellTests.class.getResource("duplicate-classes.jar")); try { JarHell.checkJarHell(jars, logger::debug); fail("did not get expected exception"); } catch (IllegalStateException e) { assertTrue(e.getMessage().contains("jar hell!")); assertTrue(e.getMessage().contains("DuplicateClass")); assertTrue(e.getMessage().contains("duplicate-classes.jar")); assertTrue(e.getMessage().contains("exists multiple times in jar")); } } public void testXmlBeansLeniency() throws Exception { Set jars = Collections.singleton(JarHellTests.class.getResource("duplicate-xmlbeans-classes.jar")); JarHell.checkJarHell(jars, logger::debug); } public void testRequiredJDKVersionTooOld() throws Exception { Path dir = createTempDir(); List current = Runtime.version().version(); List target = new ArrayList<>(current.size()); for (int i = 0; i < current.size(); i++) { target.add(current.get(i) + 1); } Version targetVersion = Version.parse(Strings.collectionToDelimitedString(target, ".")); Manifest manifest = new Manifest(); Attributes attributes = manifest.getMainAttributes(); attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0.0"); attributes.put(new Attributes.Name("X-Compile-Target-JDK"), targetVersion.toString()); Set jars = Collections.singleton(makeJar(dir, "foo.jar", manifest, "Foo.class")); try { JarHell.checkJarHell(jars, logger::debug); fail("did not get expected exception"); } catch (IllegalStateException e) { assertTrue(e.getMessage().contains("requires Java " + targetVersion.toString())); assertTrue(e.getMessage().contains("your system: " + Runtime.version().toString())); } } public void testBadJDKVersionInJar() throws Exception { Path dir = createTempDir(); Manifest manifest = new Manifest(); Attributes attributes = manifest.getMainAttributes(); attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0.0"); attributes.put(new Attributes.Name("X-Compile-Target-JDK"), "bogus"); Set jars = Collections.singleton(makeJar(dir, "foo.jar", manifest, "Foo.class")); try { JarHell.checkJarHell(jars, logger::debug); fail("did not get expected exception"); } catch (IllegalStateException e) { assertTrue( e.getMessage() .equals( "version string must be a sequence of nonnegative decimal integers separated " + "by \".\"'s and may have leading zeros but was bogus" ) ); } } public void testRequiredJDKVersionIsOK() throws Exception { Path dir = createTempDir(); Manifest manifest = new Manifest(); Attributes attributes = manifest.getMainAttributes(); attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0.0"); attributes.put(new Attributes.Name("X-Compile-Target-JDK"), "1.7"); Set jars = Collections.singleton(makeJar(dir, "foo.jar", manifest, "Foo.class")); JarHell.checkJarHell(jars, logger::debug); } public void testValidVersions() { String[] versions = new String[] { "12-ea", "13.0.2.3-ea", "14-something", "11.0.2-21002", "11.0.14.1+1", "17.0.2+8" }; for (String version : versions) { try { JarHell.checkVersionFormat(version); } catch (IllegalStateException e) { fail(version + " should be accepted as a valid version format"); } } } public void testInvalidVersions() { String[] versions = new String[] { "", "1.7.0_80", "1.7.", "11.2+something-else" }; for (String version : versions) { try { JarHell.checkVersionFormat(version); fail("\"" + version + "\"" + " should be rejected as an invalid version format"); } catch (IllegalStateException e) {} } } // classpath testing is system specific, so we just write separate tests for *nix and windows cases /** * Parse a simple classpath with two elements on unix */ public void testParseClassPathUnix() throws Exception { assumeTrue("test is designed for unix-like systems only", ":".equals(System.getProperty("path.separator"))); assumeTrue("test is designed for unix-like systems only", "/".equals(System.getProperty("file.separator"))); Path element1 = createTempDir(); Path element2 = createTempDir(); Set expected = asSet(element1.toUri().toURL(), element2.toUri().toURL()); assertEquals(expected, JarHell.parseClassPath(element1.toString() + ":" + element2.toString())); } /** * Make sure an old unix classpath with an empty element (implicitly CWD: i'm looking at you 1.x ES scripts) fails */ public void testEmptyClassPathUnix() throws Exception { assumeTrue("test is designed for unix-like systems only", ":".equals(System.getProperty("path.separator"))); assumeTrue("test is designed for unix-like systems only", "/".equals(System.getProperty("file.separator"))); try { JarHell.parseClassPath(":/element1:/element2"); fail("should have hit exception"); } catch (IllegalStateException expected) { assertTrue(expected.getMessage().contains("should not contain empty elements")); } } /** * Parse a simple classpath with two elements on windows */ public void testParseClassPathWindows() throws Exception { assumeTrue("test is designed for windows-like systems only", ";".equals(System.getProperty("path.separator"))); assumeTrue("test is designed for windows-like systems only", "\\".equals(System.getProperty("file.separator"))); Path element1 = createTempDir(); Path element2 = createTempDir(); Set expected = asSet(element1.toUri().toURL(), element2.toUri().toURL()); assertEquals(expected, JarHell.parseClassPath(element1.toString() + ";" + element2.toString())); } /** * Make sure an old windows classpath with an empty element (implicitly CWD: i'm looking at you 1.x ES scripts) fails */ public void testEmptyClassPathWindows() throws Exception { assumeTrue("test is designed for windows-like systems only", ";".equals(System.getProperty("path.separator"))); assumeTrue("test is designed for windows-like systems only", "\\".equals(System.getProperty("file.separator"))); try { JarHell.parseClassPath(";c:\\element1;c:\\element2"); fail("should have hit exception"); } catch (IllegalStateException expected) { assertTrue(expected.getMessage().contains("should not contain empty elements")); } } /** * Make sure a "bogus" windows classpath element is accepted, java's classpath parsing accepts it, * therefore eclipse OSGI code does it :) */ public void testCrazyEclipseClassPathWindows() throws Exception { assumeTrue("test is designed for windows-like systems only", ";".equals(System.getProperty("path.separator"))); assumeTrue("test is designed for windows-like systems only", "\\".equals(System.getProperty("file.separator"))); Set expected = asSet( PathUtils.get("c:\\element1").toUri().toURL(), PathUtils.get("c:\\element2").toUri().toURL(), PathUtils.get("c:\\element3").toUri().toURL(), PathUtils.get("c:\\element 4").toUri().toURL() ); Set actual = JarHell.parseClassPath("c:\\element1;c:\\element2;/c:/element3;/c:/element 4"); assertEquals(expected, actual); } }