Skip to content

Commit

Permalink
fix broken classpath scanning for manifest classpaths
Browse files Browse the repository at this point in the history
Tools often have an option to specify the classpath as manifest entry, i.e. to run tests the tool will create an empty JAR only containing a manifest with an `Class-Path` attribute containing the actual classpath. This is useful if the classpath is long and the platform (e.g. Windows) does not support a sufficiently long command line, thus limiting the length of the classpath supplied on the command line.
Unfortunately the recent fix for Android, switching from loading the classpath through the respective Classloader to resolving the classpath manually, did not take this case into account anymore and it slipped through the tests, since no environment would specify the classpath via manifest. Now we also resolve those manifest attributes when manually resolving the classpath.

Signed-off-by: Peter Gafert <[email protected]>
  • Loading branch information
codecholeric committed May 23, 2020
1 parent 15e5df6 commit e6d9d35
Show file tree
Hide file tree
Showing 4 changed files with 400 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,17 @@
package com.tngtech.archunit.core.importer;

import java.io.File;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
Expand All @@ -31,13 +35,19 @@
import com.google.common.base.Splitter;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.tngtech.archunit.Internal;
import com.tngtech.archunit.base.ArchUnitException.LocationException;
import com.tngtech.archunit.base.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static com.google.common.base.Strings.nullToEmpty;
import static com.google.common.collect.Iterables.concat;
import static com.tngtech.archunit.core.importer.Location.toURI;
import static java.util.Collections.emptySet;
import static java.util.jar.Attributes.Name.CLASS_PATH;

interface UrlSource extends Iterable<URL> {
@Internal
Expand Down Expand Up @@ -68,10 +78,106 @@ private static Iterable<URL> unique(Iterable<URL> urls) {
}

static UrlSource classPathSystemProperties() {
return iterable(ImmutableList.<URL>builder()
List<URL> directlySpecifiedAsProperties = ImmutableList.<URL>builder()
.addAll(findUrlsForClassPathProperty(BOOT_CLASS_PATH_PROPERTY_NAME))
.addAll(findUrlsForClassPathProperty(CLASS_PATH_PROPERTY_NAME))
.build());
.build();
Iterable<URL> transitivelySpecifiedThroughManifest = readClasspathEntriesFromManifests(directlySpecifiedAsProperties);
return iterable(concat(directlySpecifiedAsProperties, transitivelySpecifiedThroughManifest));
}

private static Iterable<URL> readClasspathEntriesFromManifests(List<URL> urls) {
Set<URI> result = new HashSet<>();
readClasspathUriEntriesFromManifests(result, FluentIterable.from(urls).transform(URL_TO_URI));
return FluentIterable.from(result).transform(URI_TO_URL);
}

// Use URI because of better equals / hashcode
private static void readClasspathUriEntriesFromManifests(Set<URI> result, Iterable<URI> urls) {
for (URI url : urls) {
if (url.getScheme().equals("jar")) {
Set<URI> manifestUris = readClasspathEntriesFromManifest(url);
Set<URI> unknownSoFar = ImmutableSet.copyOf(Sets.difference(manifestUris, result));
result.addAll(unknownSoFar);
readClasspathUriEntriesFromManifests(result, unknownSoFar);
}
}
}

private static Set<URI> readClasspathEntriesFromManifest(URI url) {
Optional<Path> jarPath = findParentPathOf(url);
if (!jarPath.isPresent()) {
return emptySet();
}

Set<URI> result = new HashSet<>();
for (String classpathEntry : Splitter.on(" ").omitEmptyStrings().split(readManifestClasspath(url))) {
result.addAll(parseManifestClasspathEntry(jarPath.get(), classpathEntry).asSet());
}
return result;
}

private static Optional<Path> findParentPathOf(URI uri) {
try {
return Optional.fromNullable(Paths.get(ensureFileUrl(uri).toURI()).getParent());
} catch (Exception e) {
LOG.warn("Could not find parent folder for " + uri, e);
return Optional.absent();
}
}

private static URL ensureFileUrl(URI url) throws IOException {
return ((JarURLConnection) url.toURL().openConnection()).getJarFileURL();
}

private static String readManifestClasspath(URI uri) {
try {
String result = (String) ((JarURLConnection) uri.toURL().openConnection()).getMainAttributes().get(CLASS_PATH);
return nullToEmpty(result);
} catch (Exception e) {
return "";
}
}

private static Optional<URI> parseManifestClasspathEntry(Path parent, String classpathEntry) {
if (isUrl(classpathEntry)) {
return parseUrl(parent, classpathEntry);
} else {
return parsePath(parent, classpathEntry);
}
}

private static boolean isUrl(String classpathEntry) {
return classpathEntry.startsWith("file:") || classpathEntry.startsWith("jar:");
}

private static Optional<URI> parseUrl(Path parent, String classpathUrlEntry) {
try {
return Optional.of(convertToJarUrlIfNecessary(parent.toUri().resolve(URI.create(classpathUrlEntry).getSchemeSpecificPart())));
} catch (Exception e) {
LOG.warn("Cannot parse URL classpath entry " + classpathUrlEntry, e);
return Optional.absent();
}
}

private static Optional<URI> parsePath(Path parent, String classpathFilePathEntry) {
try {
Path path = Paths.get(classpathFilePathEntry);
if (!path.isAbsolute()) {
path = parent.resolve(path);
}
return Optional.of(convertToJarUrlIfNecessary(path.toUri()));
} catch (Exception e) {
LOG.warn("Cannot parse file path classpath entry " + classpathFilePathEntry, e);
return Optional.absent();
}
}

private static URI convertToJarUrlIfNecessary(URI uri) {
if (uri.toString().endsWith(".jar")) {
return URI.create("jar:" + uri + "!/");
}
return uri;
}

private static List<URL> findUrlsForClassPathProperty(String propertyName) {
Expand All @@ -85,7 +191,7 @@ private static List<URL> findUrlsForClassPathProperty(String propertyName) {
}

private static Optional<URL> parseClassPathEntry(String path) {
return path.endsWith(".jar") ? newJarUri(path) : newFileUri(path);
return path.endsWith(".jar") ? newJarUrl(path) : newFileUri(path);
}

private static Optional<URL> newFileUri(String path) {
Expand Down Expand Up @@ -118,7 +224,7 @@ private static Optional<URL> tryResolvePathFromUrl(String path) {
}
}

private static Optional<URL> newJarUri(String path) {
private static Optional<URL> newJarUrl(String path) {
Optional<URL> fileUri = newFileUri(path);

try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,38 @@
import java.lang.reflect.Field;
import java.util.List;

import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.tngtech.archunit.Slow;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.domain.JavaPackage;
import com.tngtech.archunit.testutil.SystemPropertiesRule;
import com.tngtech.archunit.testutil.TransientCopyRule;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.junit.rules.TemporaryFolder;

import static com.tngtech.archunit.core.domain.SourceTest.urlOf;
import static com.tngtech.archunit.core.importer.ClassFileImporterTest.jarFileOf;
import static com.tngtech.archunit.core.importer.ImportOption.Predefined.DO_NOT_INCLUDE_TESTS;
import static com.tngtech.archunit.core.importer.UrlSourceTest.JAVA_CLASS_PATH_PROP;
import static com.tngtech.archunit.testutil.Assertions.assertThat;
import static com.tngtech.archunit.testutil.Assertions.assertThatClasses;
import static java.util.jar.Attributes.Name.CLASS_PATH;

@Category(Slow.class)
public class ClassFileImporterSlowTest {
@Rule
public final TransientCopyRule copyRule = new TransientCopyRule();
@Rule
public final TemporaryFolder temporaryFolder = new TemporaryFolder();
@Rule
public final SystemPropertiesRule systemPropertiesRule = new SystemPropertiesRule();

@Test
public void imports_the_classpath() {
Expand Down Expand Up @@ -102,6 +113,31 @@ public void imports_duplicate_classes() throws IOException {
assertThat(classes.get(JavaClass.class)).isNotNull();
}

@Test
public void imports_classes_from_classpath_specified_in_manifest_file() {
String manifestClasspath = Joiner.on(" ").join(Splitter.on(File.pathSeparator).omitEmptyStrings().split(System.getProperty(JAVA_CLASS_PATH_PROP)));
String jarPath = new TestJarFile()
.withManifestAttribute(CLASS_PATH, manifestClasspath)
.create()
.getName();

System.clearProperty(JAVA_CLASS_PATH_PROP);
verifyCantLoadWithCurrentClasspath(getClass());
System.setProperty(JAVA_CLASS_PATH_PROP, jarPath);

JavaClasses javaClasses = new ClassFileImporter().importPackages(getClass().getPackage().getName());

assertThatClasses(javaClasses).contain(getClass());
}

private void verifyCantLoadWithCurrentClasspath(Class<?> clazz) {
try {
new ClassFileImporter().importClass(clazz);
Assert.fail(String.format("Should not have been able to load class %s with the current classpath", clazz.getName()));
} catch (RuntimeException ignored) {
}
}

@Test
public void creates_JavaPackages() {
JavaClasses javaClasses = importJavaBase();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,41 +5,50 @@
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;

import com.tngtech.archunit.testutil.TestUtils;

import static com.google.common.io.ByteStreams.toByteArray;
import static java.util.jar.Attributes.Name.MANIFEST_VERSION;

class TestJarFile {
private final Manifest manifest;
private final Set<String> entries = new HashSet<>();

TestJarFile withEntry(String entry) {
entries.add(entry);
TestJarFile() {
manifest = new Manifest();
manifest.getMainAttributes().put(MANIFEST_VERSION, "1.0");
}

TestJarFile withManifestAttribute(Attributes.Name name, String value) {
manifest.getMainAttributes().put(name, value);
return this;
}

TestJarFile withEntries(Iterable<String> entries) {
for (String entry : entries) {
withEntry(entry);
}
TestJarFile withEntry(String entry) {
entries.add(entry);
return this;
}

JarFile create() {
File folder = TestUtils.newTemporaryFolder();
File file = new File(folder, "test.jar");
return create(new File(folder, "test.jar"));
}

try (JarOutputStream jarOut = new JarOutputStream(new FileOutputStream(file))) {
JarFile create(File jarFile) {
try (JarOutputStream jarOut = new JarOutputStream(new FileOutputStream(jarFile), manifest)) {
for (String entry : entries) {
write(jarOut, entry);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
return newJarFile(file);
return newJarFile(jarFile);
}

private void write(JarOutputStream jarOut, String entry) throws IOException {
Expand Down
Loading

0 comments on commit e6d9d35

Please sign in to comment.