Skip to content

Commit

Permalink
Use a custom JAR reader for nested jars
Browse files Browse the repository at this point in the history
  • Loading branch information
smola committed Mar 1, 2024
1 parent cf34c8b commit 6c58278
Show file tree
Hide file tree
Showing 10 changed files with 278 additions and 319 deletions.
20 changes: 0 additions & 20 deletions telemetry/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@ ext {
excludedClassesInstructionCoverage = []
}

addTestSuite('spring3Test')

dependencies {
implementation deps.slf4j

Expand All @@ -50,24 +48,6 @@ dependencies {
testImplementation project(':utils:test-utils')
testImplementation group: 'org.hamcrest', name: 'hamcrest', version: '2.2'
testImplementation group: 'org.jboss', name: 'jboss-vfs', version: '3.2.16.Final'
testImplementation group: 'org.springframework.boot', name: 'spring-boot-loader', version: '1.5.22.RELEASE'

spring3TestImplementation group: 'org.springframework.boot', name: 'spring-boot-loader', version: '3.2.2'

jmh group: 'org.springframework.boot', name: 'spring-boot-loader', version: '1.5.22.RELEASE'
}

spring3Test {
onlyIf {
def testJvm = gradle.startParameter.projectProperties.getOrDefault('testJvm', '')
def matcher = testJvm?.toLowerCase(Locale.ROOT) =~ /[A-Za-z]*([0-9]+)/
def version = matcher?.size() == 1 ? Integer.parseInt(matcher[0][1]) : -1
version >= 17
}
jacoco {
// jacoco fails with Unsupported class file major version 61
enabled = false
}
}

jmh {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,10 @@
import java.util.List;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Level;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;

Expand All @@ -36,11 +34,6 @@ public class DependencyResolverBenchmark {
+ PROJECT_DIR
+ "/src/test/resources/datadog/telemetry/dependencies/spring-boot-app.jar!/BOOT-INF/lib/opentracing-util-0.33.0.jar!/");

@Setup(Level.Trial)
public void setup() {
org.springframework.boot.loader.jar.JarFile.registerUrlProtocolHandler();
}

@Benchmark
public void resolveSimpleJar() {
final List<Dependency> result = DependencyResolver.resolve(SIMPLE_JAR_URI);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package datadog.telemetry.dependency;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
Expand All @@ -9,14 +8,11 @@
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
Expand Down Expand Up @@ -75,59 +71,45 @@ public String toString() {
+ '}';
}

public static List<Dependency> fromMavenPom(JarFile jar) {
if (jar == null) {
public static List<Dependency> fromMavenPom(
final String jar, Map<String, Properties> pomProperties) {
if (pomProperties == null) {
return Collections.emptyList();
}
List<Dependency> dependencies = new ArrayList<>(1);
Enumeration<JarEntry> entries = jar.entries();
while (entries.hasMoreElements()) {
JarEntry jarEntry = entries.nextElement();
String filename = jarEntry.getName();
if (filename.endsWith("pom.properties")) {
try (InputStream is = jar.getInputStream(jarEntry)) {
if (is == null) {
return Collections.emptyList();
}
Properties properties = new Properties();
properties.load(is);
String groupId = properties.getProperty("groupId");
String artifactId = properties.getProperty("artifactId");
String version = properties.getProperty("version");
String name = groupId + ":" + artifactId;

if (groupId == null || artifactId == null || version == null) {
log.debug(
"'pom.properties' does not have all the required properties: "
+ "jar={}, entry={}, groupId={}, artifactId={}, version={}",
jar.getName(),
jarEntry.getName(),
groupId,
artifactId,
version);
} else {
log.debug(
"dependency found in pom.properties: "
+ "jar={}, entry={}, groupId={}, artifactId={}, version={}",
jar.getName(),
jarEntry.getName(),
groupId,
artifactId,
version);
dependencies.add(
new Dependency(name, version, new File(jar.getName()).getName(), null));
}
} catch (IOException e) {
log.debug("unable to read 'pom.properties' file from {}", jar.getName(), e);
return Collections.emptyList();
}
List<Dependency> dependencies = new ArrayList<>(pomProperties.size());
for (final Map.Entry<String, Properties> entry : pomProperties.entrySet()) {
final Properties properties = entry.getValue();
final String groupId = properties.getProperty("groupId");
final String artifactId = properties.getProperty("artifactId");
final String version = properties.getProperty("version");
final String name = groupId + ":" + artifactId;

if (groupId == null || artifactId == null || version == null) {
log.debug(
"pom.properties does not have all the required properties: "
+ "jar={}, entry={}, groupId={}, artifactId={}, version={}",
jar,
entry.getKey(),
groupId,
artifactId,
version);
} else {
log.debug(
"dependency found in pom.properties: "
+ "jar={}, entry={}, groupId={}, artifactId={}, version={}",
jar,
entry.getKey(),
groupId,
artifactId,
version);
dependencies.add(new Dependency(name, version, jar, null));
}
}
return dependencies;
}

public static synchronized Dependency guessFallbackNoPom(
Manifest manifest, String source, InputStream is) throws IOException {
Attributes manifest, String source, InputStream is) throws IOException {
final int slashIndex = source.lastIndexOf('/');
if (slashIndex >= 0) {
source = source.substring(slashIndex + 1);
Expand All @@ -145,12 +127,11 @@ public static synchronized Dependency guessFallbackNoPom(
String bundleVersion = null;
String implementationVersion = null;
if (manifest != null) {
Attributes mainAttributes = manifest.getMainAttributes();
bundleSymbolicName = mainAttributes.getValue("bundle-symbolicname");
bundleName = mainAttributes.getValue("bundle-name");
bundleVersion = mainAttributes.getValue("bundle-version");
implementationTitle = mainAttributes.getValue("implementation-title");
implementationVersion = mainAttributes.getValue("implementation-version");
bundleSymbolicName = manifest.getValue("bundle-symbolicname");
bundleName = manifest.getValue("bundle-name");
bundleVersion = manifest.getValue("bundle-version");
implementationTitle = manifest.getValue("implementation-title");
implementationVersion = manifest.getValue("implementation-version");
}

// Guess from file name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,10 @@

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.JarURLConnection;
import java.net.URI;
import java.net.URL;
import java.util.Collections;
import java.util.List;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -22,60 +17,36 @@ public class DependencyResolver {
public static List<Dependency> resolve(URI uri) {
final String scheme = uri.getScheme();
try {
JarReader.Extracted metadata = null;
String path = null;
if ("file".equals(scheme)) {
File f;
if (uri.isOpaque()) {
f = new File(uri.getSchemeSpecificPart());
} else {
f = new File(uri);
}
return extractFromFile(f);
} else if ("jar".equals(scheme)) {
return extractFromJarURI(uri);
path = f.getAbsolutePath();
metadata = JarReader.readJarFile(path);
} else if ("jar".equals(scheme) && uri.getSchemeSpecificPart().startsWith("file:")) {
path = uri.getSchemeSpecificPart().substring("file:".length());
metadata = JarReader.readNestedJarFile(path);
} else {
log.debug("unsupported dependency type: {}", uri);
return Collections.emptyList();
}
final List<Dependency> dependencies =
Dependency.fromMavenPom(metadata.jarName, metadata.pomProperties);
if (!dependencies.isEmpty()) {
return dependencies;
}
try (final InputStream is = new FileInputStream(path)) {
return Collections.singletonList(
Dependency.guessFallbackNoPom(metadata.manifest, metadata.jarName, is));
}
} catch (Throwable t) {
log.debug("Failed to determine dependency for uri {}", uri, t);
}
return Collections.emptyList();
}

private static List<Dependency> extractFromFile(final File jar) throws IOException {
if (!jar.exists()) {
log.debug("unable to find dependency {} (path does not exist)", jar);
return Collections.emptyList();
} else if (!jar.getName().endsWith(JAR_SUFFIX)) {
log.debug("unsupported file dependency type : {}", jar);
return Collections.emptyList();
}

try (final JarFile file = new JarFile(jar, false /* no verify */);
final InputStream is = new FileInputStream(jar)) {
return extractFromJarFile(file, is);
}
}

/* for jar urls as handled by spring boot */
private static List<Dependency> extractFromJarURI(final URI uri) throws IOException {
final URL url = uri.toURL();
final JarURLConnection conn = (JarURLConnection) url.openConnection();
// Prevent sharing of jar file handles, which can break Spring Boot's loader.
// https://github.com/DataDog/dd-trace-java/issues/6704
conn.setUseCaches(false);
try (final JarFile jar = conn.getJarFile();
final InputStream is = conn.getInputStream()) {
return extractFromJarFile(jar, is);
}
}

private static List<Dependency> extractFromJarFile(final JarFile jar, final InputStream is)
throws IOException {
// Try Maven's pom.properties
final List<Dependency> deps = Dependency.fromMavenPom(jar);
if (!deps.isEmpty()) {
return deps;
}
// Try manifest or file name
final Manifest manifest = jar.getManifest();
return Collections.singletonList(Dependency.guessFallbackNoPom(manifest, jar.getName(), is));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package datadog.telemetry.dependency;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.NoSuchFileException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;

class JarReader {
static class Extracted {
final String jarName;
final Map<String, Properties> pomProperties;
final Attributes manifest;

public Extracted(
final String jarName,
final Map<String, Properties> pomProperties,
final Attributes manifest) {
this.jarName = jarName;
this.pomProperties = pomProperties;
this.manifest = manifest;
}
}

public static Extracted readJarFile(String jarPath) throws IOException {
try (final JarFile jar = new JarFile(jarPath, false /* no verify */)) {
final Map<String, Properties> pomProperties = new HashMap<>();
final Enumeration<? extends ZipEntry> entries = jar.entries();
while (entries.hasMoreElements()) {
final ZipEntry entry = entries.nextElement();
if (entry.getName().endsWith("pom.properties")) {
try (final InputStream is = jar.getInputStream(entry)) {
final Properties properties = new Properties();
properties.load(is);
pomProperties.put(entry.getName(), properties);
}
}
}
final Manifest manifest = jar.getManifest();
final Attributes attributes =
(manifest == null) ? new Attributes() : manifest.getMainAttributes();
return new Extracted(new File(jar.getName()).getName(), pomProperties, attributes);
}
}

public static Extracted readNestedJarFile(final String jarPath) throws IOException {
final int sepIdx = jarPath.indexOf("!/");
if (sepIdx == -1) {
throw new IllegalArgumentException("Invalid nested jar path: " + jarPath);
}
final String outerJarPath = jarPath.substring(0, sepIdx);
String innerJarPath = jarPath.substring(sepIdx + 2);
if (innerJarPath.endsWith("!/")) {
innerJarPath = innerJarPath.substring(0, innerJarPath.length() - 2);
}
try (final JarFile outerJar = new JarFile(outerJarPath, false /* no verify */)) {
final ZipEntry entry = outerJar.getEntry(innerJarPath);
if (entry == null) {
throw new NoSuchFileException("Nested jar not found: " + jarPath);
}
try (final InputStream is = outerJar.getInputStream(entry);
final JarInputStream innerJar = new JarInputStream(is, false /* no verify */)) {
final Map<String, Properties> pomProperties = new HashMap<>();
ZipEntry innerEntry;
while ((innerEntry = innerJar.getNextEntry()) != null) {
if (innerEntry.getName().endsWith("pom.properties")) {
final Properties properties = new Properties();
properties.load(innerJar);
pomProperties.put(innerEntry.getName(), properties);
}
}
final Manifest manifest = innerJar.getManifest();
final Attributes attributes =
(manifest == null) ? new Attributes() : manifest.getMainAttributes();
return new Extracted(new File(innerJarPath).getName(), pomProperties, attributes);
}
}
}
}
Loading

0 comments on commit 6c58278

Please sign in to comment.