diff --git a/components/native-loader/build.gradle.kts b/components/native-loader/build.gradle.kts new file mode 100644 index 00000000000..a4178348a71 --- /dev/null +++ b/components/native-loader/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + `java-library` +} + +apply(from = "$rootDir/gradle/java.gradle") + +dependencies { + implementation(project(":components:environment")) +} diff --git a/components/native-loader/src/main/java/datadog/nativeloader/ClassLoaderResourcePathLocator.java b/components/native-loader/src/main/java/datadog/nativeloader/ClassLoaderResourcePathLocator.java new file mode 100644 index 00000000000..4563facfb81 --- /dev/null +++ b/components/native-loader/src/main/java/datadog/nativeloader/ClassLoaderResourcePathLocator.java @@ -0,0 +1,34 @@ +package datadog.nativeloader; + +import java.net.URL; +import java.util.Objects; + +/** ClassLoaderResourcePathLocator locates library paths inside a {@link ClassLoader} */ +final class ClassLoaderResourcePathLocator implements PathLocator { + private final ClassLoader classLoader; + private final String baseResource; + + public ClassLoaderResourcePathLocator(final ClassLoader classLoader, final String baseResource) { + this.classLoader = classLoader; + this.baseResource = baseResource; + } + + @Override + public URL locate(String component, String path) { + return this.classLoader.getResource(PathUtils.concatPath(component, this.baseResource, path)); + } + + @Override + public int hashCode() { + return Objects.hash(this.classLoader, this.baseResource); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ClassLoaderResourcePathLocator)) return false; + + ClassLoaderResourcePathLocator that = (ClassLoaderResourcePathLocator) obj; + return this.classLoader.equals(that.classLoader) + && Objects.equals(this.baseResource, that.baseResource); + } +} diff --git a/components/native-loader/src/main/java/datadog/nativeloader/FlatDirLibraryResolver.java b/components/native-loader/src/main/java/datadog/nativeloader/FlatDirLibraryResolver.java new file mode 100644 index 00000000000..29b8f5e16c5 --- /dev/null +++ b/components/native-loader/src/main/java/datadog/nativeloader/FlatDirLibraryResolver.java @@ -0,0 +1,55 @@ +package datadog.nativeloader; + +import java.net.URL; + +/** + * FlatDirLibraryResolver - uses flat directories to provide more specific libraries to load + * {os}-{arch}-{libc/musl} + */ +public final class FlatDirLibraryResolver implements LibraryResolver { + public static final FlatDirLibraryResolver INSTANCE = new FlatDirLibraryResolver(); + + private FlatDirLibraryResolver() {} + + @Override + public final URL resolve( + PathLocator pathLocator, String component, PlatformSpec platformSpec, String libName) + throws Exception { + PathLocatorHelper pathLocatorHelper = new PathLocatorHelper(libName, pathLocator); + + String libFileName = PathUtils.libFileName(platformSpec, libName); + + String osPath = PathUtils.osPartOf(platformSpec); + String archPath = PathUtils.archPartOf(platformSpec); + String libcPath = PathUtils.libcPartOf(platformSpec); + + URL url; + String regularPath = osPath + "-" + archPath; + + if (libcPath != null) { + String specializedPath = regularPath + "-" + libcPath; + url = pathLocatorHelper.locate(component, specializedPath + "/" + libFileName); + if (url != null) return url; + } + + url = pathLocatorHelper.locate(component, regularPath + "/" + libFileName); + if (url != null) return url; + + url = pathLocatorHelper.locate(component, osPath + "/" + libFileName); + if (url != null) return url; + + // fallback to searching at top-level, mostly concession to good out-of-box behavior + // with java.library.path + url = pathLocatorHelper.locate(component, libFileName); + if (url != null) return url; + + if (component != null) { + url = pathLocatorHelper.locate(null, libFileName); + if (url != null) return url; + } + + pathLocatorHelper.tryThrow(); + + return null; + } +} diff --git a/components/native-loader/src/main/java/datadog/nativeloader/IntrospectPlatformSpec.java b/components/native-loader/src/main/java/datadog/nativeloader/IntrospectPlatformSpec.java new file mode 100644 index 00000000000..00ef82d6d69 --- /dev/null +++ b/components/native-loader/src/main/java/datadog/nativeloader/IntrospectPlatformSpec.java @@ -0,0 +1,65 @@ +package datadog.nativeloader; + +import datadog.environment.OperatingSystem; +import datadog.environment.OperatingSystem.Architecture; + +/* + * Default PlatformSpec used in dd-trace-java -- wraps detection code in component:environment + */ +final class IntrospectPlatformSpec extends PlatformSpec { + static final PlatformSpec INSTANCE = new IntrospectPlatformSpec(); + + @Override + public boolean isLinux() { + return OperatingSystem.isLinux(); + } + + @Override + public boolean isMac() { + return OperatingSystem.isMacOs(); + } + + @Override + public boolean isWindows() { + return OperatingSystem.isWindows(); + } + + @Override + public boolean isMusl() { + return OperatingSystem.isMusl(); + } + + @Override + public boolean isAarch64() { + return isArch(Architecture.ARM64); + } + + @Override + public boolean isArm32() { + return isArch(Architecture.ARM); + } + + @Override + public boolean isX86_32() { + return isArch(Architecture.X86); + } + + @Override + public boolean isX86_64() { + return isArch(Architecture.X64); + } + + static final boolean isArch(OperatingSystem.Architecture arch) { + return (OperatingSystem.architecture() == arch); + } + + @Override + public int hashCode() { + return IntrospectPlatformSpec.class.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return (obj instanceof IntrospectPlatformSpec); + } +} diff --git a/components/native-loader/src/main/java/datadog/nativeloader/LibDirBasedPathLocator.java b/components/native-loader/src/main/java/datadog/nativeloader/LibDirBasedPathLocator.java new file mode 100644 index 00000000000..5199ae0fc3a --- /dev/null +++ b/components/native-loader/src/main/java/datadog/nativeloader/LibDirBasedPathLocator.java @@ -0,0 +1,49 @@ +package datadog.nativeloader; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Arrays; + +/** LibDirBasedPathLocator locates libraries inside a list of library directories */ +final class LibDirBasedPathLocator implements PathLocator { + private final File[] libDirs; + + public LibDirBasedPathLocator(File... libDirs) { + this.libDirs = libDirs; + } + + @Override + public URL locate(String component, String path) { + String fullPath = PathUtils.concatPath(component, path); + + for (File libDir : this.libDirs) { + File libFile = new File(libDir, fullPath); + if (libFile.exists()) return toUrl(libFile); + } + + return null; + } + + @SuppressWarnings("deprecation") + private static final URL toUrl(File file) { + try { + return file.toURL(); + } catch (MalformedURLException e) { + return null; + } + } + + @Override + public int hashCode() { + return Arrays.hashCode(this.libDirs); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof LibDirBasedPathLocator)) return false; + + LibDirBasedPathLocator that = (LibDirBasedPathLocator) obj; + return Arrays.equals(this.libDirs, that.libDirs); + } +} diff --git a/components/native-loader/src/main/java/datadog/nativeloader/LibFile.java b/components/native-loader/src/main/java/datadog/nativeloader/LibFile.java new file mode 100644 index 00000000000..fb2e27d61d4 --- /dev/null +++ b/components/native-loader/src/main/java/datadog/nativeloader/LibFile.java @@ -0,0 +1,81 @@ +package datadog.nativeloader; + +import java.io.File; +import java.nio.file.Path; + +/** + * Represents a resolved library + * + * + */ +public final class LibFile implements AutoCloseable { + static final boolean NO_CLEAN_UP = false; + static final boolean CLEAN_UP = true; + + static final LibFile preloaded(String libName) { + return new LibFile(libName, null, NO_CLEAN_UP); + } + + static final LibFile fromFile(String libName, File file) { + return new LibFile(libName, file, NO_CLEAN_UP); + } + + static final LibFile fromTempFile(String libName, File file) { + return new LibFile(libName, file, CLEAN_UP); + } + + final String libName; + + final File file; + final boolean needsCleanup; + + LibFile(String libName, File file, boolean needsCleanup) { + this.libName = libName; + + this.file = file; + this.needsCleanup = needsCleanup; + } + + /** Indicates if this library was "preloaded" */ + public boolean isPreloaded() { + return (this.file == null); + } + + /** Loads the underlying library into the JVM */ + public void load() throws LibraryLoadException { + if (this.isPreloaded()) return; + + try { + Runtime.getRuntime().load(this.getAbsolutePath()); + } catch (Throwable t) { + throw new LibraryLoadException(this.libName, t); + } + } + + /** Provides a File to the library -- returns null for pre-loaded libraries */ + public final File toFile() { + return this.file; + } + + /** Provides a Path to the library -- return null for pre-loaded libraries */ + public final Path toPath() { + return this.file == null ? null : this.file.toPath(); + } + + /** Provides the an absolute path to the library -- returns null for pre-loaded libraries */ + public final String getAbsolutePath() { + return this.file == null ? null : this.file.getAbsolutePath(); + } + + /** Schedules clean-up of underlying file -- if the file is a temp file */ + @Override + public void close() { + if (this.needsCleanup) { + NativeLoader.delete(this.file); + } + } +} diff --git a/components/native-loader/src/main/java/datadog/nativeloader/LibraryLoadException.java b/components/native-loader/src/main/java/datadog/nativeloader/LibraryLoadException.java new file mode 100644 index 00000000000..12a8779958e --- /dev/null +++ b/components/native-loader/src/main/java/datadog/nativeloader/LibraryLoadException.java @@ -0,0 +1,29 @@ +package datadog.nativeloader; + +/** Exception raised when NativeLoader fails to resolve or load a library */ +public class LibraryLoadException extends Exception { + static final String UNSUPPORTED_OS = "Unsupported OS"; + static final String UNSUPPORTED_ARCH = "Unsupported arch"; + + private static final long serialVersionUID = 1L; + + public LibraryLoadException(String libName) { + super(message(libName)); + } + + public LibraryLoadException(String libName, Throwable cause) { + this(message(libName), cause.getMessage(), cause); + } + + public LibraryLoadException(String libName, String message) { + super(message(libName) + " - " + message); + } + + public LibraryLoadException(String libName, String message, Throwable cause) { + super(message(libName) + " - " + message, cause); + } + + static final String message(String libName) { + return "Unable to resolve library " + libName; + } +} diff --git a/components/native-loader/src/main/java/datadog/nativeloader/LibraryResolver.java b/components/native-loader/src/main/java/datadog/nativeloader/LibraryResolver.java new file mode 100644 index 00000000000..4d28aae2794 --- /dev/null +++ b/components/native-loader/src/main/java/datadog/nativeloader/LibraryResolver.java @@ -0,0 +1,20 @@ +package datadog.nativeloader; + +import java.net.URL; + +/** + * LibraryResolver encapsulates a library resolution strategy + * + *

The LibraryResolver should use the provided {@link PathLocator} to locate the desired + * resources. The LibraryResolver may try multiple locations to find the best possible library to + * use. + */ +@FunctionalInterface +public interface LibraryResolver { + default boolean isPreloaded(PlatformSpec platform, String libName) { + return false; + } + + URL resolve(PathLocator pathLocator, String component, PlatformSpec platformSpec, String libName) + throws Exception; +} diff --git a/components/native-loader/src/main/java/datadog/nativeloader/LibraryResolvers.java b/components/native-loader/src/main/java/datadog/nativeloader/LibraryResolvers.java new file mode 100644 index 00000000000..6ef8cec3c4b --- /dev/null +++ b/components/native-loader/src/main/java/datadog/nativeloader/LibraryResolvers.java @@ -0,0 +1,44 @@ +package datadog.nativeloader; + +import java.net.URL; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +public final class LibraryResolvers { + private LibraryResolvers() {} + + public static final LibraryResolver defaultLibraryResolver() { + return flatDirs(); + } + + public static final LibraryResolver withPreloaded( + LibraryResolver baseResolver, String... preloadedLibNames) { + return withPreloaded(baseResolver, new HashSet<>(Arrays.asList(preloadedLibNames))); + } + + public static final LibraryResolver withPreloaded( + LibraryResolver baseResolver, Set preloadedLibNames) { + return new LibraryResolver() { + @Override + public boolean isPreloaded(PlatformSpec platform, String libName) { + return preloadedLibNames.contains(libName); + } + + @Override + public URL resolve( + PathLocator pathLocator, String component, PlatformSpec platformSpec, String libName) + throws Exception { + return baseResolver.resolve(pathLocator, component, platformSpec, libName); + } + }; + } + + public static final LibraryResolver flatDirs() { + return FlatDirLibraryResolver.INSTANCE; + } + + public static final LibraryResolver nestedDirs() { + return NestedDirLibraryResolver.INSTANCE; + } +} diff --git a/components/native-loader/src/main/java/datadog/nativeloader/NativeLoader.java b/components/native-loader/src/main/java/datadog/nativeloader/NativeLoader.java new file mode 100644 index 00000000000..17af0b6f7ab --- /dev/null +++ b/components/native-loader/src/main/java/datadog/nativeloader/NativeLoader.java @@ -0,0 +1,304 @@ +package datadog.nativeloader; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.Set; + +/** + * NativeLoader is intended as more feature rich replacement for calling {@link + * System#loadLibrary(String)} directly. NativeLoader can be used to find the corresponding platform + * specific library using pluggable strategies -- for both path determination {@link + * LibraryResolver} and path resolution {@link PathLocator} + */ +public final class NativeLoader { + public static final class Builder { + private PlatformSpec platformSpec; + private Path tempDir; + private String[] preloadedLibNames; + private LibraryResolver libResolver; + private PathLocator pathLocator; + + Builder() {} + + /** Sets the default {@link PlatformSpec} used by the {@link NativeLoader} */ + public Builder platformSpec(PlatformSpec platform) { + this.platformSpec = platform; + return this; + } + + /** Uses a nested directory layout -- {@link LibraryResolvers#nestedDirs()} */ + public Builder nestedLayout() { + return this.libResolver(LibraryResolvers.nestedDirs()); + } + + /** Uses a flat directory layout -- {@link LibraryResolvers#flatDirs()} */ + public Builder flatLayout() { + return this.libResolver(LibraryResolvers.flatDirs()); + } + + /** + * Indicates that libraries (or signatures from those libraries) are already loaded into the JVM + * {@link LibraryResolver#isPreloaded} + * + * @param libNames - lib names + */ + public Builder preloaded(String... libNames) { + this.preloadedLibNames = libNames; + return this; + } + + /** + * Uses the specified {@link LibraryResolver} can be used to implement an alternate file layout + */ + public Builder libResolver(LibraryResolver libResolver) { + this.libResolver = libResolver; + return this; + } + + /** Searches for the native libraries in the provided {@link ClassLoader} */ + public Builder fromClassLoader(ClassLoader classLoader) { + return this.pathLocator(PathLocators.fromClassLoader(classLoader)); + } + + /** + * Searches for the native libraries in the provided {@link ClassLoader} using the specified + * baseResource + */ + public Builder fromClassLoader(ClassLoader classLoader, String baseResource) { + return this.pathLocator(PathLocators.fromClassLoader(classLoader, baseResource)); + } + + /** Searches for the native libraries in the specified directory */ + public Builder fromDir(String includeDir) { + return this.pathLocator(PathLocators.fromLibDirs(includeDir)); + } + + /** Searches for the native libraries in the specified directories */ + public Builder fromDirs(String... includeDirs) { + return this.pathLocator(PathLocators.fromLibDirs(includeDirs)); + } + + /** Searches for the native libraries in the specified directory */ + public Builder fromDir(File includeDir) { + return this.pathLocator(PathLocators.fromLibDirs(includeDir)); + } + + /** Searches for the native libraries in the specified directories */ + public Builder fromDirs(File... includeDirs) { + return this.pathLocator(PathLocators.fromLibDirs(includeDirs)); + } + + /** Searches for the native libraries in the specified directory */ + public Builder fromDir(Path includeDir) { + return this.pathLocator(PathLocators.fromLibDirs(includeDir)); + } + + /** Searches for the native libraries in the specified directories */ + public Builder fromDirs(Path... paths) { + return this.pathLocator(PathLocators.fromLibDirs(paths)); + } + + /** Searches for the native libraries using the provided {@link PathLocator} */ + public Builder pathLocator(PathLocator pathLocator) { + this.pathLocator = pathLocator; + return this; + } + + /** + * Specifies temporary directory where native libraries are copied if the {@link PathLocator} + * returns a non-file {@link URL} + */ + public Builder tempDir(File tempDir) { + return this.tempDir(tempDir.toPath()); + } + + /** + * Specifies temporary directory where native libraries are copied if the {@link PathLocator} + * returns a non-file {@link URL} + */ + public Builder tempDir(Path tempPath) { + this.tempDir = tempPath; + return this; + } + + /** + * Specifies temporary directory where native libraries are copied if the {@link PathLocator} + * returns a non-file {@link URL} + */ + public Builder tempDir(String tmpPath) { + return this.tempDir(Paths.get(tmpPath)); + } + + /** Constructs and returns the {@link NativeLoader} */ + public NativeLoader build() { + return new NativeLoader(this); + } + + PlatformSpec platformSpec() { + return (this.platformSpec == null) ? PlatformSpec.defaultPlatformSpec() : this.platformSpec; + } + + PathLocator pathLocator() { + return (this.pathLocator == null) ? PathLocators.defaultPathLocator() : this.pathLocator; + } + + LibraryResolver libResolver() { + LibraryResolver baseResolver = + (this.libResolver == null) ? LibraryResolvers.defaultLibraryResolver() : this.libResolver; + + return (this.preloadedLibNames == null) + ? baseResolver + : LibraryResolvers.withPreloaded(baseResolver, this.preloadedLibNames); + } + + Path tempDir() { + return this.tempDir; + } + } + + public static final Builder builder() { + return new Builder(); + } + + private final PlatformSpec defaultPlatformSpec; + private final LibraryResolver libResolver; + private final PathLocator pathResolver; + private final Path tempDir; + + private NativeLoader(Builder builder) { + this.defaultPlatformSpec = builder.platformSpec(); + this.libResolver = builder.libResolver(); + this.pathResolver = builder.pathLocator(); + this.tempDir = builder.tempDir(); + } + + /** Indicates if a library is considered "pre-loaded" */ + public boolean isPreloaded(String libName) { + return this.libResolver.isPreloaded(this.defaultPlatformSpec, libName); + } + + /** Indicates if a library is considered "pre-loaded" for the specified {@link PlatformSpec} */ + public boolean isPreloaded(PlatformSpec platformSpec, String libName) { + return this.libResolver.isPreloaded(platformSpec, libName); + } + + /** Loads a library */ + public void load(String libName) throws LibraryLoadException { + this.load(null, libName); + } + + /** Loads a library associated with an associated component */ + public void load(String component, String libName) throws LibraryLoadException { + try (LibFile libFile = this.resolveDynamic(component, libName)) { + libFile.load(); + } + } + + /** Resolves a library to a LibFile - creating a temporary file if necessary */ + public LibFile resolveDynamic(String libName) throws LibraryLoadException { + return this.resolveDynamic((String) null, libName); + } + + /** Resolves a library with an associated component */ + public LibFile resolveDynamic(String component, String libName) throws LibraryLoadException { + return this.resolveDynamic(component, this.defaultPlatformSpec, libName); + } + + /** + * Resolves a library using a different {@link PlatformSpec} than the default for this {@link + * NativeLoader} + */ + public LibFile resolveDynamic(PlatformSpec platformSpec, String libName) + throws LibraryLoadException { + return this.resolveDynamic(null, platformSpec, libName); + } + + /** + * Resolves a library with an associated component with a different {@link PlatformSpec} than the + * default + */ + public LibFile resolveDynamic(String component, PlatformSpec platformSpec, String libName) + throws LibraryLoadException { + if (platformSpec.isUnknownOs() || platformSpec.isUnknownArch()) { + throw new LibraryLoadException(libName, "Unsupported platform"); + } + + if (this.isPreloaded(platformSpec, libName)) { + return LibFile.preloaded(libName); + } + + URL url; + try { + url = this.libResolver.resolve(this.pathResolver, component, platformSpec, libName); + } catch (LibraryLoadException e) { + // don't wrap if it is already a LibraryLoadException + throw e; + } catch (Throwable t) { + throw new LibraryLoadException(libName, t); + } + + if (url == null) { + throw new LibraryLoadException(libName); + } + return toLibFile(platformSpec, libName, url); + } + + private LibFile toLibFile(PlatformSpec platformSpec, String libName, URL url) + throws LibraryLoadException { + if (url.getProtocol().equals("file")) { + return LibFile.fromFile(libName, new File(url.getPath())); + } else { + String libExt = PathUtils.dynamicLibExtension(platformSpec); + + try { + Path tempFile = TempFileHelper.createTempFile(this.tempDir, libName, libExt); + + try (InputStream in = url.openStream()) { + Files.copy(in, tempFile, StandardCopyOption.REPLACE_EXISTING); + } + + return LibFile.fromTempFile(libName, tempFile.toFile()); + } catch (Throwable t) { + throw new LibraryLoadException(libName, t); + } + } + } + + static void delete(File tempFile) { + TempFileHelper.delete(tempFile); + } + + static final class TempFileHelper { + private TempFileHelper() {} + + static Path createTempFile(Path tempDir, String libname, String libExt) + throws IOException, SecurityException { + FileAttribute> permAttrs = + PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------")); + + if (tempDir == null) { + return Files.createTempFile(libname, "." + libExt, permAttrs); + } else { + Files.createDirectories( + tempDir, + PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------"))); + + return Files.createTempFile(tempDir, libname, "." + libExt, permAttrs); + } + } + + static void delete(File tempFile) { + boolean deleted = tempFile.delete(); + if (!deleted) tempFile.deleteOnExit(); + } + } +} diff --git a/components/native-loader/src/main/java/datadog/nativeloader/NestedDirLibraryResolver.java b/components/native-loader/src/main/java/datadog/nativeloader/NestedDirLibraryResolver.java new file mode 100644 index 00000000000..966fd2c03e8 --- /dev/null +++ b/components/native-loader/src/main/java/datadog/nativeloader/NestedDirLibraryResolver.java @@ -0,0 +1,54 @@ +package datadog.nativeloader; + +import java.net.URL; + +/** + * NestedDirLibraryResolver - uses nested directories to provide more specific libraries to load + * {os} / {arch} / {libc} + */ +final class NestedDirLibraryResolver implements LibraryResolver { + public static final NestedDirLibraryResolver INSTANCE = new NestedDirLibraryResolver(); + + @Override + public final URL resolve( + PathLocator pathLocator, String component, PlatformSpec platformSpec, String libName) + throws Exception { + PathLocatorHelper pathLocatorHelper = new PathLocatorHelper(libName, pathLocator); + + String libFileName = PathUtils.libFileName(platformSpec, libName); + + String osPath = PathUtils.osPartOf(platformSpec); + String archPath = PathUtils.archPartOf(platformSpec); + + String libcPath = PathUtils.libcPartOf(platformSpec); + + URL url; + String regularPath = osPath + "/" + archPath; + + if (libcPath != null) { + String specializedPath = regularPath + "/" + libcPath; + url = pathLocatorHelper.locate(component, specializedPath + "/" + libFileName); + if (url != null) return url; + } + + url = pathLocatorHelper.locate(component, regularPath + "/" + libFileName); + if (url != null) return url; + + url = pathLocatorHelper.locate(component, osPath + "/" + libFileName); + if (url != null) return url; + + // fallback to searching at top-level, mostly concession to good out-of-box behavior + // with java.library.path + url = pathLocatorHelper.locate(component, libFileName); + if (url != null) return url; + + if (component != null) { + url = pathLocatorHelper.locate(null, libFileName); + if (url != null) return url; + } + + pathLocatorHelper.tryThrow(); + + return null; + } +} diff --git a/components/native-loader/src/main/java/datadog/nativeloader/PathLocator.java b/components/native-loader/src/main/java/datadog/nativeloader/PathLocator.java new file mode 100644 index 00000000000..e7cc2e14f20 --- /dev/null +++ b/components/native-loader/src/main/java/datadog/nativeloader/PathLocator.java @@ -0,0 +1,18 @@ +package datadog.nativeloader; + +import java.net.URL; + +/** Resolves a component / path pair to a {@link URL} - called by a {@link LibraryResolver} */ +@FunctionalInterface +public interface PathLocator { + /** + * URL to the requested resource -- or null if the resource could not be found + * + *

If the returned URL uses file protocol, then {@link NativeLoader} will provide direct access + * to the file + * + *

If the returned URL uses a non-file protocol, then {@link NativeLoader} will call {@link + * URL#openStream()} and copy the contents to a temporary file + */ + URL locate(String component, String path) throws Exception; +} diff --git a/components/native-loader/src/main/java/datadog/nativeloader/PathLocatorHelper.java b/components/native-loader/src/main/java/datadog/nativeloader/PathLocatorHelper.java new file mode 100644 index 00000000000..38c976c5c86 --- /dev/null +++ b/components/native-loader/src/main/java/datadog/nativeloader/PathLocatorHelper.java @@ -0,0 +1,38 @@ +package datadog.nativeloader; + +import java.net.URL; + +/** + * Helper {@link PathLocator} that can be useful when doing multiple look-ups. PathLocatorHelper can + * be used to wrap another {@link PathLocator} and make the exception handling easier. + */ +public final class PathLocatorHelper implements PathLocator { + final String libName; + final PathLocator locator; + + private Throwable firstCause; + + public PathLocatorHelper(String libName, PathLocator locator) { + this.libName = libName; + this.locator = locator; + } + + @Override + public URL locate(String component, String path) { + try { + return this.locator.locate(component, path); + } catch (Throwable t) { + if (this.firstCause == null) this.firstCause = t; + return null; + } + } + + /** Raises a LibraryLoadException if an exception occurred during a prior call to locate */ + public void tryThrow() throws LibraryLoadException { + if (this.firstCause instanceof LibraryLoadException) { + throw (LibraryLoadException) this.firstCause; + } else if (this.firstCause != null) { + throw new LibraryLoadException(this.libName, this.firstCause); + } + } +} diff --git a/components/native-loader/src/main/java/datadog/nativeloader/PathLocators.java b/components/native-loader/src/main/java/datadog/nativeloader/PathLocators.java new file mode 100644 index 00000000000..f3b368f98ea --- /dev/null +++ b/components/native-loader/src/main/java/datadog/nativeloader/PathLocators.java @@ -0,0 +1,58 @@ +package datadog.nativeloader; + +import java.io.File; +import java.nio.file.Path; +import java.util.regex.Pattern; + +/** Helper factory class for common {@link PathLocator} */ +public final class PathLocators { + private PathLocators() {} + + public static final PathLocator defaultPathLocator() { + return fromJavaLibraryPath(); + } + + public static final PathLocator fromLibDirs(Path... libDirs) { + File[] libDirFiles = new File[libDirs.length]; + for (int i = 0; i < libDirs.length; ++i) { + libDirFiles[i] = libDirs[i].toFile(); + } + return fromLibDirs(libDirFiles); + } + + public static final PathLocator fromLibDirs(String... libDirs) { + File[] libDirFiles = new File[libDirs.length]; + for (int i = 0; i < libDirs.length; ++i) { + libDirFiles[i] = new File(libDirs[i]); + } + return fromLibDirs(libDirFiles); + } + + public static final PathLocator fromLibDirs(File... libDirs) { + return new LibDirBasedPathLocator(libDirs); + } + + public static final PathLocator fromJavaLibraryPath() { + String libPaths; + try { + libPaths = System.getProperty("java.library.path"); + } catch (SecurityException e) { + return new LibDirBasedPathLocator(); + } + return fromLibPathString(libPaths); + } + + public static final PathLocator fromLibPathString(String javaLibPath) { + // Typically, this method will be called at most once per run, + // so not storing pattern in a static because we don't want memory consumed forever + return fromLibDirs(Pattern.compile("\\:").split(javaLibPath)); + } + + public static final PathLocator fromClassLoader(ClassLoader classLoader) { + return new ClassLoaderResourcePathLocator(classLoader, null); + } + + public static final PathLocator fromClassLoader(ClassLoader classLoader, String baseResource) { + return new ClassLoaderResourcePathLocator(classLoader, baseResource); + } +} diff --git a/components/native-loader/src/main/java/datadog/nativeloader/PathUtils.java b/components/native-loader/src/main/java/datadog/nativeloader/PathUtils.java new file mode 100644 index 00000000000..66e69ed357f --- /dev/null +++ b/components/native-loader/src/main/java/datadog/nativeloader/PathUtils.java @@ -0,0 +1,98 @@ +package datadog.nativeloader; + +import static datadog.nativeloader.LibraryLoadException.UNSUPPORTED_ARCH; +import static datadog.nativeloader.LibraryLoadException.UNSUPPORTED_OS; + +/** Utilities for generating library file paths to be requested from a {@link PathLocator} */ +public final class PathUtils { + private PathUtils() {} + + public static final String libPrefix(PlatformSpec platformSpec) { + if (platformSpec.isMac() || platformSpec.isLinux()) { + return "lib"; + } else if (platformSpec.isWindows()) { + return ""; + } else { + throw new IllegalArgumentException(UNSUPPORTED_OS); + } + } + + public static final String libFileName(PlatformSpec platformSpec, String libName) { + return libPrefix(platformSpec) + libName + "." + dynamicLibExtension(platformSpec); + } + + public static final String dynamicLibExtension(PlatformSpec platformSpec) { + if (platformSpec.isLinux()) { + return "so"; + } else if (platformSpec.isWindows()) { + return "dll"; + } else if (platformSpec.isMac()) { + return "dylib"; + } else { + throw new IllegalArgumentException(UNSUPPORTED_OS); + } + } + + public static final String osPartOf(PlatformSpec platformSpec) { + if (platformSpec.isLinux()) { + return "linux"; + } else if (platformSpec.isWindows()) { + return "win"; + } else if (platformSpec.isMac()) { + return "macos"; + } else { + throw new IllegalArgumentException(UNSUPPORTED_OS); + } + } + + public static final String archPartOf(PlatformSpec platformSpec) { + if (platformSpec.isX86_64()) { + return "x86_64"; + } else if (platformSpec.isAarch64()) { + return "aarch64"; + } else if (platformSpec.isX86_32()) { + return "x86_32"; + } else if (platformSpec.isArm32()) { + return "arm32"; + } else { + throw new IllegalArgumentException(UNSUPPORTED_ARCH); + } + } + + public static final String libcPartOf(PlatformSpec platformSpec) { + if (!platformSpec.isLinux()) { + return null; + } else if (platformSpec.isMusl()) { + return "musl"; + } else { + return "libc"; + } + } + + /** Helper for concatenating paths with / Handles null & empty for both parts */ + public static final String concatPath(String pathPart1, String pathPart2) { + if (isEmpty(pathPart1)) { + return pathPart2; + } else if (isEmpty(pathPart2)) { + return pathPart1; + } else { + return pathPart1 + "/" + pathPart2; + } + } + + /** Helper for concatenating parts with / Handles null & empty anywhere in the var-arg array */ + public static final String concatPath(String... pathParts) { + StringBuilder builder = new StringBuilder(); + for (String pathPart : pathParts) { + if (isEmpty(pathPart)) continue; + + if (builder.length() != 0) builder.append('/'); + builder.append(pathPart); + } + return builder.toString(); + } + + static final boolean isEmpty(String pathPart) { + return (pathPart == null) || pathPart.isEmpty(); + } +} diff --git a/components/native-loader/src/main/java/datadog/nativeloader/PlatformSpec.java b/components/native-loader/src/main/java/datadog/nativeloader/PlatformSpec.java new file mode 100644 index 00000000000..c8e0a9e5902 --- /dev/null +++ b/components/native-loader/src/main/java/datadog/nativeloader/PlatformSpec.java @@ -0,0 +1,45 @@ +package datadog.nativeloader; + +/** + * PlatformSpec describes a native library "platform" -- including operating system, architecture, + * and libc variation + */ +public abstract class PlatformSpec { + public static final PlatformSpec defaultPlatformSpec() { + return IntrospectPlatformSpec.INSTANCE; + } + + /** Is the target OS MacOS? */ + public abstract boolean isMac(); + + /** Is the target OS Windows? */ + public abstract boolean isWindows(); + + /** Is the target OS Linux? */ + public abstract boolean isLinux(); + + /** Is the target OS unknown / not handled by {@link NativeLoader}? */ + public final boolean isUnknownOs() { + return !this.isLinux() && !this.isMac() && !this.isWindows(); + } + + /** Is the target architecture x86-32 bit? */ + public abstract boolean isX86_32(); + + /** Is the target architecture x86-64 bit? */ + public abstract boolean isX86_64(); + + /** Is the target architecture ARM 32-bit? */ + public abstract boolean isArm32(); + + /** Is the target architecture ARM 64-bit? */ + public abstract boolean isAarch64(); + + /** Is the target architecture unknown / not handled by {@link NativeLoader}? */ + public final boolean isUnknownArch() { + return !this.isX86_64() && !this.isAarch64() && !this.isX86_32() && !this.isArm32(); + } + + /** Is the target using MUSL libc? */ + public abstract boolean isMusl(); +} diff --git a/components/native-loader/src/test/java/datadog/nativeloader/CapturingPathLocator.java b/components/native-loader/src/test/java/datadog/nativeloader/CapturingPathLocator.java new file mode 100644 index 00000000000..257e5ba9958 --- /dev/null +++ b/components/native-loader/src/test/java/datadog/nativeloader/CapturingPathLocator.java @@ -0,0 +1,143 @@ +package datadog.nativeloader; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Deque; +import java.util.LinkedList; + +public final class CapturingPathLocator implements PathLocator { + public static final boolean WITH_OMIT_COMP_FALLBACK = true; + public static final boolean WITHOUT_OMIT_COMP_FALLBACK = false; + + public static final void testFailOnExceptions( + LibraryResolver resolver, + PlatformSpec platformSpec, + boolean withSkipCompFallback, + String... expectedPaths) { + try { + test(resolver, platformSpec, withSkipCompFallback, expectedPaths); + } catch (Exception e) { + throw new IllegalStateException("unexpected exception", e); + } + } + + public static final void test( + LibraryResolver resolver, + PlatformSpec platformSpec, + boolean withSkipCompFallback, + String... expectedPaths) + throws Exception { + String comp = "comp"; + + CapturingPathLocator fullCaptureLocator = new CapturingPathLocator(Integer.MAX_VALUE); + resolver.resolve(fullCaptureLocator, comp, platformSpec, "test"); + + for (int i = 0; !fullCaptureLocator.isEmpty(); ++i) { + if (i >= expectedPaths.length) { + // checking the final fallback here was confusing when debugging tests + + if (!withSkipCompFallback) { + fullCaptureLocator.assertDone(); + } else { + // checked at at the end of the method + fullCaptureLocator.nextRequest(); + fullCaptureLocator.assertDone(); + } + } else { + fullCaptureLocator.assertRequested(comp, expectedPaths[i]); + } + } + + for (int i = 0; i < expectedPaths.length; ++i) { + CapturingPathLocator fallbackLocator = new CapturingPathLocator(i); + resolver.resolve(fallbackLocator, comp, platformSpec, "test"); + + for (int j = 0; j <= i; ++j) { + fallbackLocator.assertRequested(comp, expectedPaths[j]); + } + fallbackLocator.assertDone(); + } + + if (withSkipCompFallback) { + CapturingPathLocator fallbackLocator = new CapturingPathLocator(expectedPaths.length); + resolver.resolve(fallbackLocator, comp, platformSpec, "test"); + + for (int j = 0; j < expectedPaths.length; ++j) { + fallbackLocator.assertRequested(comp, expectedPaths[j]); + } + fallbackLocator.assertRequested(null, expectedPaths[expectedPaths.length - 1]); + fallbackLocator.assertDone(); + } + } + + final int simulateNotFoundCount; + int numRequests; + + final Deque locateRequests = new LinkedList<>(); + + public CapturingPathLocator() { + this(0); + } + + public CapturingPathLocator(int simulateNotFoundCount) { + this.numRequests = 0; + this.simulateNotFoundCount = simulateNotFoundCount; + } + + @Override + public URL locate(String component, String path) { + this.locateRequests.addLast(new LocateRequest(component, path)); + + if (this.numRequests++ < this.simulateNotFoundCount) return null; + try { + return new URL("http://localhost"); + } catch (MalformedURLException e) { + throw new IllegalStateException(e); + } + } + + int numLocateRequests() { + return this.locateRequests.size(); + } + + boolean isEmpty() { + return this.locateRequests.isEmpty(); + } + + LocateRequest nextRequest() { + return this.locateRequests.removeFirst(); + } + + void assertRequested(LocateRequest request) { + this.assertRequested(request.requestedComponent, request.requestedPath); + } + + void assertRequested(String expectedComponent, String expectedPath) { + this.nextRequest().assertRequested(expectedComponent, expectedPath); + } + + void assertDone() { + // written this way to aid in debugging and fleshing out the test + if (!this.isEmpty()) { + this.assertRequested("", ""); + } + } + + final class LocateRequest { + private final String requestedComponent; + private final String requestedPath; + + LocateRequest(String requestedComponent, String requestedPath) { + this.requestedComponent = requestedComponent; + this.requestedPath = requestedPath; + } + + void assertRequested(String expectedComponent, String expectedPath) { + // order is inverted, since usually test writer is worrying about comp directly + assertEquals(expectedPath, this.requestedPath); + assertEquals(expectedComponent, this.requestedComponent); + } + } +} diff --git a/components/native-loader/src/test/java/datadog/nativeloader/FlatDirLibraryResolverTest.java b/components/native-loader/src/test/java/datadog/nativeloader/FlatDirLibraryResolverTest.java new file mode 100644 index 00000000000..2438770288e --- /dev/null +++ b/components/native-loader/src/test/java/datadog/nativeloader/FlatDirLibraryResolverTest.java @@ -0,0 +1,64 @@ +package datadog.nativeloader; + +import static datadog.nativeloader.TestPlatformSpec.AARCH64; +import static datadog.nativeloader.TestPlatformSpec.GLIBC; +import static datadog.nativeloader.TestPlatformSpec.LINUX; +import static datadog.nativeloader.TestPlatformSpec.MAC; +import static datadog.nativeloader.TestPlatformSpec.MUSL; +import static datadog.nativeloader.TestPlatformSpec.WINDOWS; +import static datadog.nativeloader.TestPlatformSpec.X86_64; + +import org.junit.jupiter.api.Test; + +public class FlatDirLibraryResolverTest { + @Test + public void linux_x86_64_libc() { + test( + TestPlatformSpec.of(LINUX, X86_64, GLIBC), + "linux-x86_64-libc/libtest.so", + "linux-x86_64/libtest.so", + "linux/libtest.so", + "libtest.so"); + } + + @Test + public void linux_x86_64_musl() { + test( + TestPlatformSpec.of(LINUX, X86_64, MUSL), + "linux-x86_64-musl/libtest.so", + "linux-x86_64/libtest.so", + "linux/libtest.so", + "libtest.so"); + } + + @Test + public void osx_x86_64() { + test( + TestPlatformSpec.of(MAC, X86_64), + "macos-x86_64/libtest.dylib", + "macos/libtest.dylib", + "libtest.dylib"); + } + + @Test + public void osx_aarch64() { + test( + TestPlatformSpec.of(MAC, AARCH64), + "macos-aarch64/libtest.dylib", + "macos/libtest.dylib", + "libtest.dylib"); + } + + @Test + public void windows_x86_64() { + test(TestPlatformSpec.of(WINDOWS, X86_64), "win-x86_64/test.dll", "win/test.dll", "test.dll"); + } + + static final void test(PlatformSpec platformSpec, String... expectedPaths) { + CapturingPathLocator.testFailOnExceptions( + FlatDirLibraryResolver.INSTANCE, + platformSpec, + CapturingPathLocator.WITH_OMIT_COMP_FALLBACK, + expectedPaths); + } +} diff --git a/components/native-loader/src/test/java/datadog/nativeloader/IntrospectPlatformSpecTest.java b/components/native-loader/src/test/java/datadog/nativeloader/IntrospectPlatformSpecTest.java new file mode 100644 index 00000000000..75da5ae18cd --- /dev/null +++ b/components/native-loader/src/test/java/datadog/nativeloader/IntrospectPlatformSpecTest.java @@ -0,0 +1,50 @@ +package datadog.nativeloader; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import datadog.environment.OperatingSystem; +import datadog.environment.OperatingSystem.Architecture; +import org.junit.jupiter.api.Test; + +public class IntrospectPlatformSpecTest { + @Test + public void os() { + PlatformSpec platformSpec = IntrospectPlatformSpec.INSTANCE; + + // a bit silly since this just mirrors the underlying implementation + assertEquals(OperatingSystem.isMacOs(), platformSpec.isMac()); + assertEquals(OperatingSystem.isLinux(), platformSpec.isLinux()); + assertEquals(OperatingSystem.isWindows(), platformSpec.isWindows()); + } + + @Test + public void arch() { + PlatformSpec platformSpec = IntrospectPlatformSpec.INSTANCE; + + assertEquals(OperatingSystem.architecture() == Architecture.X86, platformSpec.isX86_32()); + assertEquals(OperatingSystem.architecture() == Architecture.X64, platformSpec.isX86_64()); + assertEquals(OperatingSystem.architecture() == Architecture.ARM, platformSpec.isArm32()); + assertEquals(OperatingSystem.architecture() == Architecture.ARM64, platformSpec.isAarch64()); + } + + @Test + public void musl() { + PlatformSpec platformSpec = IntrospectPlatformSpec.INSTANCE; + + assertEquals(OperatingSystem.isMusl(), platformSpec.isMusl()); + } + + @Test + public void equals() { + // just a sanity check, since assertEquals is used in other tests + assertTrue(IntrospectPlatformSpec.INSTANCE.equals(IntrospectPlatformSpec.INSTANCE)); + } + + @Test + public void notEquals_diffType() { + // just a sanity check, since assertEquals is used in other tests + assertFalse(IntrospectPlatformSpec.INSTANCE.equals(TestPlatformSpec.unsupportedOs())); + } +} diff --git a/components/native-loader/src/test/java/datadog/nativeloader/NativeLoaderBuilderTest.java b/components/native-loader/src/test/java/datadog/nativeloader/NativeLoaderBuilderTest.java new file mode 100644 index 00000000000..db04ea859e1 --- /dev/null +++ b/components/native-loader/src/test/java/datadog/nativeloader/NativeLoaderBuilderTest.java @@ -0,0 +1,141 @@ +package datadog.nativeloader; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.nio.file.Paths; +import org.junit.jupiter.api.Test; + +public class NativeLoaderBuilderTest { + @Test + public void defaultPlatformSpec() { + NativeLoader.Builder builder = NativeLoader.builder(); + + assertEquals(PlatformSpec.defaultPlatformSpec(), builder.platformSpec()); + } + + @Test + public void customPlatformSpec() { + PlatformSpec platformSpec = + TestPlatformSpec.of(TestPlatformSpec.LINUX, TestPlatformSpec.ARM32, TestPlatformSpec.GLIBC); + + NativeLoader.Builder builder = NativeLoader.builder().platformSpec(platformSpec); + assertSame(platformSpec, builder.platformSpec()); + } + + @Test + public void defaultLibraryResolver() { + NativeLoader.Builder builder = NativeLoader.builder(); + + assertEquals(LibraryResolvers.defaultLibraryResolver(), builder.libResolver()); + } + + @Test + public void flatLayout() { + NativeLoader.Builder builder = NativeLoader.builder().flatLayout(); + + assertEquals(LibraryResolvers.flatDirs(), builder.libResolver()); + } + + @Test + public void nestedLayout() { + NativeLoader.Builder builder = NativeLoader.builder().nestedLayout(); + + assertEquals(LibraryResolvers.nestedDirs(), builder.libResolver()); + } + + @Test + public void preloaded() { + PlatformSpec platformSpec = + TestPlatformSpec.of(TestPlatformSpec.LINUX, TestPlatformSpec.ARM32, TestPlatformSpec.GLIBC); + + NativeLoader.Builder builder = + NativeLoader.builder().platformSpec(platformSpec).preloaded("foo", "bar"); + + LibraryResolver libResolver = builder.libResolver(); + assertTrue(libResolver.isPreloaded(platformSpec, "foo")); + assertTrue(libResolver.isPreloaded(platformSpec, "bar")); + + assertFalse(libResolver.isPreloaded(platformSpec, "not-preloaded")); + } + + @Test + public void defaultPathLocator() { + NativeLoader.Builder builder = NativeLoader.builder(); + + assertEquals(PathLocators.defaultPathLocator(), builder.pathLocator()); + } + + @Test + public void dirBasedLocator_string() { + NativeLoader.Builder builder = NativeLoader.builder().fromDir("libs"); + + assertEquals(PathLocators.fromLibDirs("libs"), builder.pathLocator()); + } + + @Test + public void dirBasedLocator_strings_multiple() { + NativeLoader.Builder builder = NativeLoader.builder().fromDirs("libs1", "libs2"); + + assertEquals(PathLocators.fromLibDirs("libs1", "libs2"), builder.pathLocator()); + } + + @Test + public void dirBasedLocator_file() { + NativeLoader.Builder builder = NativeLoader.builder().fromDir(new File("libs")); + + assertEquals(PathLocators.fromLibDirs("libs"), builder.pathLocator()); + } + + @Test + public void dirBasedLocator_files_multiple() { + NativeLoader.Builder builder = + NativeLoader.builder().fromDirs(new File("libs1"), new File("libs2")); + + assertEquals(PathLocators.fromLibDirs("libs1", "libs2"), builder.pathLocator()); + } + + @Test + public void dirBasedLocator_path() { + NativeLoader.Builder builder = NativeLoader.builder().fromDir(Paths.get("libs")); + + assertEquals(PathLocators.fromLibDirs("libs"), builder.pathLocator()); + } + + @Test + public void dirBasedLocator_paths_multiple() { + NativeLoader.Builder builder = + NativeLoader.builder().fromDirs(Paths.get("libs1"), Paths.get("libs2")); + + assertEquals(PathLocators.fromLibDirs("libs1", "libs2"), builder.pathLocator()); + } + + @Test + public void classLoaderBasedLocator() { + ClassLoader sysClassLoader = ClassLoader.getSystemClassLoader(); + NativeLoader.Builder builder = NativeLoader.builder().fromClassLoader(sysClassLoader); + + assertEquals(PathLocators.fromClassLoader(sysClassLoader), builder.pathLocator()); + } + + @Test + public void tempDir_string() { + NativeLoader.Builder builder = NativeLoader.builder().tempDir("tmp"); + assertEquals(builder.tempDir(), Paths.get("tmp")); + } + + @Test + public void tempDir_file() { + NativeLoader.Builder builder = NativeLoader.builder().tempDir(new File("tmp")); + assertEquals(builder.tempDir(), Paths.get("tmp")); + } + + @Test + public void tempDir_path() { + NativeLoader.Builder builder = NativeLoader.builder().tempDir(Paths.get("tmp")); + assertEquals(builder.tempDir(), Paths.get("tmp")); + } +} diff --git a/components/native-loader/src/test/java/datadog/nativeloader/NativeLoaderTest.java b/components/native-loader/src/test/java/datadog/nativeloader/NativeLoaderTest.java new file mode 100644 index 00000000000..a122e88bea4 --- /dev/null +++ b/components/native-loader/src/test/java/datadog/nativeloader/NativeLoaderTest.java @@ -0,0 +1,392 @@ +package datadog.nativeloader; + +import static datadog.nativeloader.TestPlatformSpec.AARCH64; +import static datadog.nativeloader.TestPlatformSpec.LINUX; +import static datadog.nativeloader.TestPlatformSpec.UNSUPPORTED_ARCH; +import static datadog.nativeloader.TestPlatformSpec.UNSUPPORTED_OS; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.PosixFilePermissions; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import org.junit.jupiter.api.Test; + +public class NativeLoaderTest { + @Test + public void preloaded() throws LibraryLoadException { + NativeLoader loader = NativeLoader.builder().preloaded("dne1", "dne2").build(); + + assertTrue(loader.isPreloaded("dne1")); + assertTrue(loader.isPreloaded("dne2")); + + assertFalse(loader.isPreloaded("dne3")); + + try (LibFile lib = loader.resolveDynamic("dne1")) { + assertPreloaded(lib); + + // already considered loaded -- so this is a nop + lib.load(); + } + + // already considered loaded -- so this is a nop + loader.load("dne2"); + + // not already loaded - so passes through to underlying resolver + assertThrows(LibraryLoadException.class, () -> loader.load("dne3")); + } + + @Test + public void unsupportedPlatform() { + PlatformSpec unsupportedOsSpec = TestPlatformSpec.of(UNSUPPORTED_OS, AARCH64); + NativeLoader loader = NativeLoader.builder().platformSpec(unsupportedOsSpec).build(); + + assertThrows(LibraryLoadException.class, () -> loader.resolveDynamic("dummy")); + } + + @Test + public void unsupportArch() { + PlatformSpec unsupportedOsSpec = TestPlatformSpec.of(LINUX, UNSUPPORTED_ARCH); + NativeLoader loader = NativeLoader.builder().platformSpec(unsupportedOsSpec).build(); + + assertThrows(LibraryLoadException.class, () -> loader.resolveDynamic("dummy")); + } + + @Test + public void loadFailure() throws LibraryLoadException { + NativeLoader loader = NativeLoader.builder().build(); + + // test libraries are just text files, so they shouldn't load & link properly + // NativeLoader is supposed to wrap the loading failures, so that we + // remember to handle them + assertThrows(LibraryLoadException.class, () -> loader.load("dummy")); + } + + @Test + public void fromDir() throws LibraryLoadException { + NativeLoader loader = NativeLoader.builder().fromDir("test-data").build(); + + try (LibFile lib = loader.resolveDynamic("dummy")) { + // loaded directly from directory, so no clean-up required + assertRegularFile(lib); + + // file isn't actually a dynamic library + assertThrows(LibraryLoadException.class, () -> lib.load()); + } + } + + @Test + public void fromDir_override_windows() throws LibraryLoadException { + NativeLoader loader = NativeLoader.builder().fromDir("test-data").build(); + + try (LibFile lib = loader.resolveDynamic(TestPlatformSpec.windows(), "dummy")) { + // loaded directly from directory, so no clean-up required + assertRegularFile(lib); + assertTrue(lib.getAbsolutePath().endsWith("dummy.dll")); + } + } + + @Test + public void fromDir_override_mac() throws LibraryLoadException { + NativeLoader loader = NativeLoader.builder().fromDir("test-data").build(); + + try (LibFile lib = loader.resolveDynamic(TestPlatformSpec.mac(), "dummy")) { + // loaded directly from directory, so no clean-up required + assertRegularFile(lib); + assertTrue(lib.getAbsolutePath().endsWith("libdummy.dylib")); + } + } + + @Test + public void fromDir_override_linux() throws LibraryLoadException { + NativeLoader loader = NativeLoader.builder().fromDir("test-data").build(); + + try (LibFile lib = loader.resolveDynamic(TestPlatformSpec.linux(), "dummy")) { + // loaded directly from directory, so no clean-up required + assertRegularFile(lib); + assertTrue(lib.getAbsolutePath().endsWith("libdummy.so")); + } + } + + @Test + public void fromDirList() throws LibraryLoadException { + NativeLoader loader = NativeLoader.builder().fromDirs("dne1", "dne2", "test-data").build(); + + try (LibFile lib = loader.resolveDynamic("dummy")) { + // loaded directly from directory, so no clean-up required + assertRegularFile(lib); + } + } + + @Test + public void fromDir_with_component() throws LibraryLoadException { + NativeLoader loader = NativeLoader.builder().fromDir("test-data").build(); + + try (LibFile lib = loader.resolveDynamic("comp1", "dummy")) { + assertRegularFile(lib); + assertTrue(lib.getAbsolutePath().contains("comp1")); + } + + try (LibFile lib = loader.resolveDynamic("comp2", "dummy")) { + assertRegularFile(lib); + assertTrue(lib.getAbsolutePath().contains("comp2")); + } + } + + @Test + public void fromDirBackedClassLoader() throws IOException, LibraryLoadException { + // ClassLoader pulling from a directory, so there's still a normal file + URL[] urls = {new File("test-data").toURL()}; + + try (URLClassLoader classLoader = new URLClassLoader(urls)) { + NativeLoader loader = NativeLoader.builder().fromClassLoader(classLoader).build(); + try (LibFile lib = loader.resolveDynamic("dummy")) { + // since there's a normal file, no need to copy to a temp file and clean-up + assertRegularFile(lib); + } + } + } + + @Test + public void fromDirBackedClassLoader_with_component() throws IOException, LibraryLoadException { + // ClassLoader pulling from a directory, so there's still a normal file + URL[] urls = {new File("test-data").toURL()}; + + try (URLClassLoader classLoader = new URLClassLoader(urls)) { + NativeLoader loader = NativeLoader.builder().fromClassLoader(classLoader).build(); + + try (LibFile lib = loader.resolveDynamic("comp1", "dummy")) { + assertRegularFile(lib); + assertTrue(lib.getAbsolutePath().contains("comp1")); + } + + try (LibFile lib = loader.resolveDynamic("comp2", "dummy")) { + assertRegularFile(lib); + assertTrue(lib.getAbsolutePath().contains("comp2")); + } + } + } + + @Test + public void fromDirBackedClassLoader_with_subResource() throws IOException, LibraryLoadException { + // ClassLoader pulling from a directory, so there's still a normal file + URL[] urls = {new File("test-data").toURL()}; + + try (URLClassLoader classLoader = new URLClassLoader(urls)) { + NativeLoader loader = NativeLoader.builder().fromClassLoader(classLoader, "resource").build(); + try (LibFile lib = loader.resolveDynamic("dummy")) { + // since there's a normal file, no need to copy to a temp file and clean-up + assertRegularFile(lib); + assertTrue(lib.getAbsolutePath().contains("resource")); + } + } + } + + @Test + public void fromDirBackedClassLoader_with_subResource_and_comp() + throws IOException, LibraryLoadException { + // ClassLoader pulling from a directory, so there's still a normal file + try (URLClassLoader classLoader = createClassLoader(Paths.get("test-data"))) { + NativeLoader loader = NativeLoader.builder().fromClassLoader(classLoader, "resource").build(); + try (LibFile lib = loader.resolveDynamic("comp1", "dummy")) { + // since there's a normal file, no need to copy to a temp file and clean-up + assertRegularFile(lib); + assertTrue(lib.getAbsolutePath().contains("comp1")); + assertTrue(lib.getAbsolutePath().contains("resource")); + } + } + } + + @Test + public void fromJarBackedClassLoader() throws IOException, LibraryLoadException { + Path jar = jar("test-data"); + try { + try (URLClassLoader classLoader = createClassLoader(jar)) { + NativeLoader loader = NativeLoader.builder().fromClassLoader(classLoader).build(); + try (LibFile lib = loader.resolveDynamic("dummy")) { + // loaded from a jar, so copied to temp file + assertTempFile(lib); + } + } + } finally { + deleteHelper(jar); + } + } + + @Test + public void fromJarBackedClassLoader_with_tempDir() throws IOException, LibraryLoadException { + Path jar = jar("test-data"); + try { + Path tempDir = Paths.get("temp"); + deleteHelper(tempDir); + + try (URLClassLoader classLoader = createClassLoader(jar)) { + NativeLoader loader = + NativeLoader.builder().fromClassLoader(classLoader).tempDir(tempDir).build(); + try (LibFile lib = loader.resolveDynamic("dummy")) { + // loaded from a jar, so copied to temp file + assertTempFile(lib); + } + } finally { + deleteHelper(tempDir); + } + } finally { + deleteHelper(jar); + } + } + + @Test + public void fromJarBackedClassLoader_with_unwritable_tempDir() + throws IOException, LibraryLoadException { + Path jar = jar("test-data"); + try { + Path noWriteDir = Paths.get("no-write-temp"); + deleteHelper(noWriteDir); + Files.createDirectories(noWriteDir, posixAttr("r-x------")); + + try (URLClassLoader classLoader = createClassLoader(jar)) { + NativeLoader loader = + NativeLoader.builder().fromClassLoader(classLoader).tempDir(noWriteDir).build(); + + // unable to resolve to a File because tempDir isn't writable + assertThrows(LibraryLoadException.class, () -> loader.resolveDynamic("dummy")); + } finally { + deleteHelper(noWriteDir); + } + } finally { + deleteHelper(jar); + } + } + + @Test + public void fromJarBackedClassLoader_with_locked_file() throws IOException, LibraryLoadException { + Path jar = jar("test-data"); + try { + Path tempDir = Paths.get("temp"); + deleteHelper(tempDir); + + try (URLClassLoader classLoader = createClassLoader(jar)) { + NativeLoader loader = + NativeLoader.builder().fromClassLoader(classLoader).tempDir(tempDir).build(); + try (LibFile lib = loader.resolveDynamic("dummy")) { + // loaded from a jar, so copied to temp file + assertTempFile(lib); + + // simulating lock - by blocking ability to delete from parent dir + // forces fallback to deleteOnExit + Files.setPosixFilePermissions(lib.toPath().getParent(), posixPerms("r-x------")); + } + } finally { + deleteHelper(tempDir); + } + } finally { + deleteHelper(jar); + } + } + + void deleteHelper(Path dir) { + try { + Files.setPosixFilePermissions(dir, posixPerms("rwx------")); + Files.delete(dir); + } catch (IOException e) { + } + } + + static URLClassLoader createClassLoader(Path... paths) { + return new URLClassLoader(urls(paths)); + } + + static URL[] urls(Path... paths) { + URL[] urls = new URL[paths.length]; + for (int i = 0; i < urls.length; ++i) { + try { + urls[i] = paths[i].toUri().toURL(); + } catch (MalformedURLException e) { + throw new IllegalStateException(e); + } + } + return urls; + } + + static Path jar(String dirName) { + return jar(Paths.get(dirName)); + } + + static Path jar(Path dir) { + try { + return jarHelper(dir); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + static Path jarHelper(Path dir) throws IOException { + Path jarPath = Files.createTempFile(dir.toFile().getName(), ".jar", posixAttr("rwx------")); + + try (JarOutputStream jarStream = new JarOutputStream(Files.newOutputStream(jarPath))) { + Files.walk(dir) + .filter(path -> !Files.isDirectory(path)) + .forEach( + path -> { + try { + JarEntry jarEntry = new JarEntry(dir.relativize(path).toString()); + jarStream.putNextEntry(jarEntry); + Files.copy(path, jarStream); + jarStream.closeEntry(); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + } + return jarPath; + } + + static FileAttribute> posixAttr(String posixStr) { + return PosixFilePermissions.asFileAttribute(posixPerms(posixStr)); + } + + static Set posixPerms(String posixStr) { + return PosixFilePermissions.fromString(posixStr); + } + + static void assertPreloaded(LibFile lib) { + assertTrue(lib.isPreloaded()); + assertNull(lib.toFile()); + assertNull(lib.toPath()); + assertNull(lib.getAbsolutePath()); + assertFalse(lib.needsCleanup); + } + + static void assertRegularFile(LibFile lib) { + assertFalse(lib.isPreloaded()); + assertNotNull(lib.toFile()); + assertNotNull(lib.toPath()); + assertTrue(lib.toFile().exists()); + assertNotNull(lib.getAbsolutePath()); + assertFalse(lib.needsCleanup); + } + + static void assertTempFile(LibFile lib) { + assertFalse(lib.isPreloaded()); + assertNotNull(lib.toFile()); + assertNotNull(lib.toPath()); + assertTrue(lib.toFile().exists()); + assertNotNull(lib.getAbsolutePath()); + assertTrue(lib.needsCleanup); + } +} diff --git a/components/native-loader/src/test/java/datadog/nativeloader/NestedDirLibraryResolverTest.java b/components/native-loader/src/test/java/datadog/nativeloader/NestedDirLibraryResolverTest.java new file mode 100644 index 00000000000..a85e68a269a --- /dev/null +++ b/components/native-loader/src/test/java/datadog/nativeloader/NestedDirLibraryResolverTest.java @@ -0,0 +1,64 @@ +package datadog.nativeloader; + +import static datadog.nativeloader.TestPlatformSpec.AARCH64; +import static datadog.nativeloader.TestPlatformSpec.GLIBC; +import static datadog.nativeloader.TestPlatformSpec.LINUX; +import static datadog.nativeloader.TestPlatformSpec.MAC; +import static datadog.nativeloader.TestPlatformSpec.MUSL; +import static datadog.nativeloader.TestPlatformSpec.WINDOWS; +import static datadog.nativeloader.TestPlatformSpec.X86_64; + +import org.junit.jupiter.api.Test; + +public class NestedDirLibraryResolverTest { + @Test + public void linux_x86_64_libc() { + test( + TestPlatformSpec.of(LINUX, X86_64, GLIBC), + "linux/x86_64/libc/libtest.so", + "linux/x86_64/libtest.so", + "linux/libtest.so", + "libtest.so"); + } + + @Test + public void linux_x86_64_musl() { + test( + TestPlatformSpec.of(LINUX, X86_64, MUSL), + "linux/x86_64/musl/libtest.so", + "linux/x86_64/libtest.so", + "linux/libtest.so", + "libtest.so"); + } + + @Test + public void osx_x86_64() { + test( + TestPlatformSpec.of(MAC, X86_64), + "macos/x86_64/libtest.dylib", + "macos/libtest.dylib", + "libtest.dylib"); + } + + @Test + public void osx_aarch() { + test( + TestPlatformSpec.of(MAC, AARCH64), + "macos/aarch64/libtest.dylib", + "macos/libtest.dylib", + "libtest.dylib"); + } + + @Test + public void windows_x86_64() { + test(TestPlatformSpec.of(WINDOWS, X86_64), "win/x86_64/test.dll", "win/test.dll", "test.dll"); + } + + static final void test(PlatformSpec platformSpec, String... expectedPaths) { + CapturingPathLocator.testFailOnExceptions( + NestedDirLibraryResolver.INSTANCE, + platformSpec, + CapturingPathLocator.WITH_OMIT_COMP_FALLBACK, + expectedPaths); + } +} diff --git a/components/native-loader/src/test/java/datadog/nativeloader/PathLocationHelperTest.java b/components/native-loader/src/test/java/datadog/nativeloader/PathLocationHelperTest.java new file mode 100644 index 00000000000..dae51056efd --- /dev/null +++ b/components/native-loader/src/test/java/datadog/nativeloader/PathLocationHelperTest.java @@ -0,0 +1,105 @@ +package datadog.nativeloader; + +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.fail; + +import java.net.MalformedURLException; +import java.net.URL; +import org.junit.jupiter.api.Test; + +public final class PathLocationHelperTest { + @Test + public void happyPath() throws MalformedURLException { + URL expected = new URL("http://localhost"); + + PathLocator pathLocator = (comp, path) -> expected; + PathLocatorHelper helper = new PathLocatorHelper("test", pathLocator); + + URL result = helper.locate(null, "path"); + assertSame(expected, result); + } + + @Test + public void throwingException() { + Exception expectedCause = new IllegalStateException("wrong!"); + PathLocator throwingPathLocator = + (comp, path) -> { + throw expectedCause; + }; + + PathLocatorHelper helper = new PathLocatorHelper("test", throwingPathLocator); + + // on exception, PathLocator returns null, but stores the exception for use when tryThrow is + // called later + URL result = helper.locate(null, "path"); + assertNull(result); + + try { + // should throw, since an exception occurred earlier + helper.tryThrow(); + + fail("should raise a LibraryLoadException"); + } catch (LibraryLoadException e) { + assertSame(expectedCause, e.getCause()); + } + } + + @Test + public void throwingLibraryLoadException() { + Exception expectedCause = new LibraryLoadException("test", "wrong!"); + PathLocator throwingPathLocator = + (comp, path) -> { + throw expectedCause; + }; + + PathLocatorHelper helper = new PathLocatorHelper("test", throwingPathLocator); + + // on exception, PathLocator returns null, but stores the exception for use when tryThrow is + // called later + URL result = helper.locate(null, "path"); + assertNull(result); + + try { + // should throw, since an exception occurred earlier + helper.tryThrow(); + + fail("should raise a LibraryLoadException"); + } catch (LibraryLoadException e) { + assertSame(expectedCause, e); + } + } + + @Test + public void throwingMultipleExceptions() { + Exception firstCause = new IllegalStateException("wrong!"); + Exception secondCause = new IllegalStateException("wrong again!"); + + PathLocator throwingPathLocator = + (comp, path) -> { + if (path.equals("firstPath")) { + throw firstCause; + } else { + throw secondCause; + } + }; + + // on exception, PathLocator returns null, but stores the first exception + PathLocatorHelper helper = new PathLocatorHelper("test", throwingPathLocator); + URL firstResult = helper.locate(null, "firstPath"); + assertNull(firstResult); + + // still retgurns null, but doesn't store exception + URL secondResult = helper.locate(null, "secondPath"); + assertNull(secondResult); + + try { + helper.tryThrow(); + + fail("should raise a LibraryLoadException"); + } catch (LibraryLoadException e) { + // expect just the first cause to be captured + assertSame(firstCause, e.getCause()); + } + } +} diff --git a/components/native-loader/src/test/java/datadog/nativeloader/PathLocatorsTest.java b/components/native-loader/src/test/java/datadog/nativeloader/PathLocatorsTest.java new file mode 100644 index 00000000000..84fb1134a70 --- /dev/null +++ b/components/native-loader/src/test/java/datadog/nativeloader/PathLocatorsTest.java @@ -0,0 +1,112 @@ +package datadog.nativeloader; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Paths; +import org.junit.jupiter.api.Test; + +/* + * These tests mostly exist to satisfy coverage checks. + * Although equals is used in other tests & usual contract obliges defining hashCode, too + */ +public class PathLocatorsTest { + @Test + public void dirBased_equals() { + PathLocator dirLocator1 = PathLocators.fromLibDirs("foo"); + PathLocator dirLocator2 = PathLocators.fromLibDirs(new File("foo")); + PathLocator dirLocator3 = PathLocators.fromLibDirs(Paths.get("foo")); + + assertEqualsAndHashCode(dirLocator1, dirLocator2); + assertEqualsAndHashCode(dirLocator1, dirLocator3); + } + + @Test + public void dirBased_notEquals() { + PathLocator dirLocator1 = PathLocators.fromLibDirs("foo1"); + PathLocator dirLocator2 = PathLocators.fromLibDirs(new File("foo2")); + PathLocator dirLocator3 = PathLocators.fromLibDirs(Paths.get("foo3")); + + assertNotEquals(dirLocator1, dirLocator2); + assertNotEquals(dirLocator1, dirLocator3); + } + + @Test + public void dirBased_diffType_notEquals() { + PathLocator dirLocator = PathLocators.fromLibDirs("foo1"); + PathLocator otherLocator = (comp, path) -> null; + + // be explicit about which equals is being used + assertFalse(dirLocator.equals(otherLocator)); + } + + @Test + public void libPath() { + PathLocator dirLocator1 = PathLocators.fromLibPathString("foo:bar:baz"); + PathLocator dirLocator2 = PathLocators.fromLibDirs("foo", "bar", "baz"); + + assertEqualsAndHashCode(dirLocator1, dirLocator2); + } + + @Test + public void classLoaderBased_equals() { + ClassLoader loader = ClassLoader.getSystemClassLoader(); + + PathLocator locator1 = PathLocators.fromClassLoader(loader); + PathLocator locator2 = PathLocators.fromClassLoader(loader); + + assertEqualsAndHashCode(locator1, locator2); + } + + @Test + public void classLoaderBased_with_resource_equals() { + ClassLoader loader = ClassLoader.getSystemClassLoader(); + + PathLocator locator1 = PathLocators.fromClassLoader(loader, "resource"); + PathLocator locator2 = PathLocators.fromClassLoader(loader, "resource"); + + assertEqualsAndHashCode(locator1, locator2); + } + + @Test + public void classLoaderBased_notEequals_loader() { + ClassLoader loader1 = ClassLoader.getSystemClassLoader(); + ClassLoader loader2 = new URLClassLoader(new URL[0]); + + PathLocator locator1 = PathLocators.fromClassLoader(loader1); + PathLocator locator2 = PathLocators.fromClassLoader(loader2); + + assertNotEquals(locator1, locator2); + } + + @Test + public void classLoaderBased_notEequals_resource() { + ClassLoader loader = ClassLoader.getSystemClassLoader(); + + PathLocator locator1 = PathLocators.fromClassLoader(loader, "resource1"); + PathLocator locator2 = PathLocators.fromClassLoader(loader, "resource2"); + + assertNotEquals(locator1, locator2); + } + + @Test + public void classLoaderBased_diffType_notEequals() { + ClassLoader loader = ClassLoader.getSystemClassLoader(); + + PathLocator locator1 = PathLocators.fromClassLoader(loader, "resource1"); + PathLocator locator2 = (comp, path) -> null; + + // be explicit about which equals is being used + assertFalse(locator1.equals(locator2)); + } + + static final void assertEqualsAndHashCode(Object lhs, Object rhs) { + assertEquals(lhs.hashCode(), rhs.hashCode()); + assertTrue(lhs.equals(rhs)); + } +} diff --git a/components/native-loader/src/test/java/datadog/nativeloader/PathUtilsTest.java b/components/native-loader/src/test/java/datadog/nativeloader/PathUtilsTest.java new file mode 100644 index 00000000000..f2432eaabdb --- /dev/null +++ b/components/native-loader/src/test/java/datadog/nativeloader/PathUtilsTest.java @@ -0,0 +1,166 @@ +package datadog.nativeloader; + +import static datadog.nativeloader.TestPlatformSpec.linux; +import static datadog.nativeloader.TestPlatformSpec.linux_arm32; +import static datadog.nativeloader.TestPlatformSpec.linux_arm64; +import static datadog.nativeloader.TestPlatformSpec.linux_glibc; +import static datadog.nativeloader.TestPlatformSpec.linux_musl; +import static datadog.nativeloader.TestPlatformSpec.linux_x86_32; +import static datadog.nativeloader.TestPlatformSpec.linux_x86_64; +import static datadog.nativeloader.TestPlatformSpec.mac; +import static datadog.nativeloader.TestPlatformSpec.unsupportedArch; +import static datadog.nativeloader.TestPlatformSpec.unsupportedOs; +import static datadog.nativeloader.TestPlatformSpec.windows; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +public class PathUtilsTest { + @Test + public void libFileName_mac() { + assertEquals("libtest.dylib", PathUtils.libFileName(mac(), "test")); + } + + @Test + public void libFileName_linux() { + assertEquals("libtest.so", PathUtils.libFileName(linux(), "test")); + } + + @Test + public void libFileName_windows() { + assertEquals("test.dll", PathUtils.libFileName(windows(), "test")); + } + + @Test + public void libPrefix_mac() { + assertEquals("lib", PathUtils.libPrefix(mac())); + } + + @Test + public void libPrefix_linux() { + assertEquals("lib", PathUtils.libPrefix(linux())); + } + + @Test + public void libPrefix_windows() { + assertEquals("", PathUtils.libPrefix(windows())); + } + + @Test + public void libPrefix_unsupportedOs() { + assertThrows(IllegalArgumentException.class, () -> PathUtils.libPrefix(unsupportedOs())); + } + + @Test + public void dynamicLibExtension_mac() { + assertEquals("dylib", PathUtils.dynamicLibExtension(mac())); + } + + @Test + public void dynamicLibExtension_linux() { + assertEquals("so", PathUtils.dynamicLibExtension(linux())); + } + + @Test + public void dynamicLibExtension_windows() { + assertEquals("dll", PathUtils.dynamicLibExtension(windows())); + } + + @Test + public void dynamicLibExtension_unsupportedOs() { + assertThrows( + IllegalArgumentException.class, () -> PathUtils.dynamicLibExtension(unsupportedOs())); + } + + @Test + public void osPart_linux() { + assertEquals("linux", PathUtils.osPartOf(linux())); + } + + @Test + public void osPart_mac() { + assertEquals("macos", PathUtils.osPartOf(mac())); + } + + @Test + public void osPart_windows() { + assertEquals("win", PathUtils.osPartOf(windows())); + } + + @Test + public void osPart_unsupportedOs() { + assertThrows(IllegalArgumentException.class, () -> PathUtils.osPartOf(unsupportedOs())); + } + + @Test + public void archPart_x86_32() { + assertEquals("x86_32", PathUtils.archPartOf(linux_x86_32())); + } + + @Test + public void archPart_x86_64() { + assertEquals("x86_64", PathUtils.archPartOf(linux_x86_64())); + } + + @Test + public void archPart_arm32() { + assertEquals("arm32", PathUtils.archPartOf(linux_arm32())); + } + + @Test + public void archPart_arm64() { + assertEquals("aarch64", PathUtils.archPartOf(linux_arm64())); + } + + @Test + public void archPart_unsupportedArch() { + assertThrows(IllegalArgumentException.class, () -> PathUtils.archPartOf(unsupportedArch())); + } + + @Test + public void libcPart_linux_glibc() { + assertEquals("libc", PathUtils.libcPartOf(linux_glibc())); + } + + @Test + public void libcPart_linux_musl() { + assertEquals("musl", PathUtils.libcPartOf(linux_musl())); + } + + @Test + public void libcPart_mac() { + assertNull(PathUtils.libcPartOf(mac())); + } + + @Test + public void libcPart_windows() { + assertNull(PathUtils.libcPartOf(windows())); + } + + @Test + public void concat_nonEmpty_nonEmpty() { + assertEquals("foo/bar", PathUtils.concatPath("foo", "bar")); + } + + @Test + public void concat_null_nonEmpty() { + assertEquals("bar", PathUtils.concatPath(null, "bar")); + } + + @Test + public void concat_nonEmpty_null() { + assertEquals("foo", PathUtils.concatPath("foo", null)); + } + + @Test + public void concat_empty_nonEmpty() { + assertEquals("bar", PathUtils.concatPath("", "bar")); + } + + @Test + public void concat_null_empty_nonEmpty() { + assertEquals("bar", PathUtils.concatPath(null, "", "bar")); + } +} diff --git a/components/native-loader/src/test/java/datadog/nativeloader/PlatformSpecTest.java b/components/native-loader/src/test/java/datadog/nativeloader/PlatformSpecTest.java new file mode 100644 index 00000000000..3dce67b4d03 --- /dev/null +++ b/components/native-loader/src/test/java/datadog/nativeloader/PlatformSpecTest.java @@ -0,0 +1,76 @@ +package datadog.nativeloader; + +import static datadog.nativeloader.TestPlatformSpec.linux; +import static datadog.nativeloader.TestPlatformSpec.linux_arm32; +import static datadog.nativeloader.TestPlatformSpec.linux_arm64; +import static datadog.nativeloader.TestPlatformSpec.linux_x86_32; +import static datadog.nativeloader.TestPlatformSpec.linux_x86_64; +import static datadog.nativeloader.TestPlatformSpec.mac; +import static datadog.nativeloader.TestPlatformSpec.unsupportedArch; +import static datadog.nativeloader.TestPlatformSpec.unsupportedOs; +import static datadog.nativeloader.TestPlatformSpec.windows; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +public class PlatformSpecTest { + @Test + public void macSpec() { + assertTrue(mac().isMac()); + assertFalse(mac().isUnknownOs()); + } + + @Test + public void linuxSpec() { + assertTrue(linux().isLinux()); + assertFalse(linux().isUnknownOs()); + } + + @Test + public void windowsSpec() { + assertTrue(windows().isWindows()); + assertFalse(windows().isUnknownOs()); + } + + @Test + public void unsupportedOsSpec() { + assertFalse(unsupportedOs().isMac()); + assertFalse(unsupportedOs().isLinux()); + assertFalse(unsupportedOs().isWindows()); + assertTrue(unsupportedOs().isUnknownOs()); + } + + @Test + public void arm32Spec() { + assertTrue(linux_arm32().isArm32()); + assertFalse(linux_arm32().isUnknownArch()); + } + + @Test + public void arm64Spec() { + assertTrue(linux_arm64().isAarch64()); + assertFalse(linux_arm64().isUnknownArch()); + } + + @Test + public void x86_32Spec() { + assertTrue(linux_x86_32().isX86_32()); + assertFalse(linux_x86_32().isUnknownArch()); + } + + @Test + public void x86_64Spec() { + assertTrue(linux_x86_64().isX86_64()); + assertFalse(linux_x86_64().isUnknownArch()); + } + + @Test + public void unsupportedArchSpec() { + assertFalse(unsupportedArch().isArm32()); + assertFalse(unsupportedArch().isAarch64()); + assertFalse(unsupportedArch().isX86_32()); + assertFalse(unsupportedArch().isX86_64()); + assertTrue(unsupportedArch().isUnknownArch()); + } +} diff --git a/components/native-loader/src/test/java/datadog/nativeloader/TestPlatformSpec.java b/components/native-loader/src/test/java/datadog/nativeloader/TestPlatformSpec.java new file mode 100644 index 00000000000..dd875453045 --- /dev/null +++ b/components/native-loader/src/test/java/datadog/nativeloader/TestPlatformSpec.java @@ -0,0 +1,130 @@ +package datadog.nativeloader; + +public class TestPlatformSpec extends PlatformSpec { + public static final String MAC = "mac"; + public static final String WINDOWS = "win"; + public static final String LINUX = "linux"; + + public static final String UNSUPPORTED_OS = "unsupported"; + + public static final String X86_32 = "x86_32"; + public static final String X86_64 = "x86_64"; + + public static final String ARM32 = "arm32"; + public static final String AARCH64 = "aarch64"; + + public static final String UNSUPPORTED_ARCH = "unsupported"; + + public static final boolean GLIBC = false; + public static final boolean MUSL = true; + + public static final PlatformSpec of(String os, String arch) { + return new TestPlatformSpec(os, arch, false); + } + + public static final TestPlatformSpec of(String os, String arch, boolean isMusl) { + return new TestPlatformSpec(os, arch, isMusl); + } + + public static final PlatformSpec mac() { + return TestPlatformSpec.of(MAC, AARCH64); + } + + public static final PlatformSpec linux() { + return TestPlatformSpec.of(LINUX, X86_64); + } + + public static final PlatformSpec windows() { + return TestPlatformSpec.of(WINDOWS, X86_64); + } + + public static final PlatformSpec unsupportedOs() { + return TestPlatformSpec.of(UNSUPPORTED_OS, AARCH64); + } + + public static final PlatformSpec linux_x86_32() { + return TestPlatformSpec.of(WINDOWS, X86_32); + } + + public static final PlatformSpec linux_x86_64() { + return TestPlatformSpec.of(MAC, X86_64); + } + + public static final PlatformSpec linux_arm32() { + return TestPlatformSpec.of(LINUX, ARM32); + } + + public static final PlatformSpec linux_arm64() { + return TestPlatformSpec.of(LINUX, AARCH64); + } + + public static final PlatformSpec linux_glibc() { + return TestPlatformSpec.of(LINUX, AARCH64, GLIBC); + } + + public static final PlatformSpec linux_musl() { + return TestPlatformSpec.of(LINUX, AARCH64, MUSL); + } + + public static final PlatformSpec unsupportedArch() { + return TestPlatformSpec.of(LINUX, UNSUPPORTED_ARCH); + } + + private final String os; + private final String arch; + private final boolean isMusl; + + private TestPlatformSpec(String os, String arch, boolean isMusl) { + this.os = os; + this.arch = arch; + this.isMusl = isMusl; + } + + @Override + public boolean isLinux() { + return this.isOs(LINUX); + } + + @Override + public boolean isMac() { + return this.isOs(MAC); + } + + @Override + public boolean isWindows() { + return this.isOs(WINDOWS); + } + + private boolean isOs(String osConst) { + return osConst.equals(this.os); + } + + @Override + public boolean isX86_32() { + return this.isArch(X86_32); + } + + @Override + public boolean isAarch64() { + return this.isArch(AARCH64); + } + + @Override + public boolean isArm32() { + return this.isArch(ARM32); + } + + @Override + public boolean isX86_64() { + return this.isArch(X86_64); + } + + @Override + public boolean isMusl() { + return this.isMusl; + } + + private boolean isArch(String archConst) { + return archConst.equals(this.arch); + } +} diff --git a/components/native-loader/test-data/comp1/dummy.dll b/components/native-loader/test-data/comp1/dummy.dll new file mode 100644 index 00000000000..ccbb965f72a --- /dev/null +++ b/components/native-loader/test-data/comp1/dummy.dll @@ -0,0 +1 @@ +not a library \ No newline at end of file diff --git a/components/native-loader/test-data/comp1/libdummy.dylib b/components/native-loader/test-data/comp1/libdummy.dylib new file mode 100644 index 00000000000..ccbb965f72a --- /dev/null +++ b/components/native-loader/test-data/comp1/libdummy.dylib @@ -0,0 +1 @@ +not a library \ No newline at end of file diff --git a/components/native-loader/test-data/comp1/libdummy.so b/components/native-loader/test-data/comp1/libdummy.so new file mode 100644 index 00000000000..ccbb965f72a --- /dev/null +++ b/components/native-loader/test-data/comp1/libdummy.so @@ -0,0 +1 @@ +not a library \ No newline at end of file diff --git a/components/native-loader/test-data/comp1/resource/dummy.dll b/components/native-loader/test-data/comp1/resource/dummy.dll new file mode 100644 index 00000000000..ccbb965f72a --- /dev/null +++ b/components/native-loader/test-data/comp1/resource/dummy.dll @@ -0,0 +1 @@ +not a library \ No newline at end of file diff --git a/components/native-loader/test-data/comp1/resource/libdummy.dylib b/components/native-loader/test-data/comp1/resource/libdummy.dylib new file mode 100644 index 00000000000..ccbb965f72a --- /dev/null +++ b/components/native-loader/test-data/comp1/resource/libdummy.dylib @@ -0,0 +1 @@ +not a library \ No newline at end of file diff --git a/components/native-loader/test-data/comp1/resource/libdummy.so b/components/native-loader/test-data/comp1/resource/libdummy.so new file mode 100644 index 00000000000..ccbb965f72a --- /dev/null +++ b/components/native-loader/test-data/comp1/resource/libdummy.so @@ -0,0 +1 @@ +not a library \ No newline at end of file diff --git a/components/native-loader/test-data/comp2/dummy.dll b/components/native-loader/test-data/comp2/dummy.dll new file mode 100644 index 00000000000..ccbb965f72a --- /dev/null +++ b/components/native-loader/test-data/comp2/dummy.dll @@ -0,0 +1 @@ +not a library \ No newline at end of file diff --git a/components/native-loader/test-data/comp2/libdummy.dylib b/components/native-loader/test-data/comp2/libdummy.dylib new file mode 100644 index 00000000000..ccbb965f72a --- /dev/null +++ b/components/native-loader/test-data/comp2/libdummy.dylib @@ -0,0 +1 @@ +not a library \ No newline at end of file diff --git a/components/native-loader/test-data/comp2/libdummy.so b/components/native-loader/test-data/comp2/libdummy.so new file mode 100644 index 00000000000..ccbb965f72a --- /dev/null +++ b/components/native-loader/test-data/comp2/libdummy.so @@ -0,0 +1 @@ +not a library \ No newline at end of file diff --git a/components/native-loader/test-data/dummy.dll b/components/native-loader/test-data/dummy.dll new file mode 100644 index 00000000000..ccbb965f72a --- /dev/null +++ b/components/native-loader/test-data/dummy.dll @@ -0,0 +1 @@ +not a library \ No newline at end of file diff --git a/components/native-loader/test-data/libdummy.dylib b/components/native-loader/test-data/libdummy.dylib new file mode 100644 index 00000000000..ccbb965f72a --- /dev/null +++ b/components/native-loader/test-data/libdummy.dylib @@ -0,0 +1 @@ +not a library \ No newline at end of file diff --git a/components/native-loader/test-data/libdummy.so b/components/native-loader/test-data/libdummy.so new file mode 100644 index 00000000000..ccbb965f72a --- /dev/null +++ b/components/native-loader/test-data/libdummy.so @@ -0,0 +1 @@ +not a library \ No newline at end of file diff --git a/components/native-loader/test-data/resource/dummy.dll b/components/native-loader/test-data/resource/dummy.dll new file mode 100644 index 00000000000..ccbb965f72a --- /dev/null +++ b/components/native-loader/test-data/resource/dummy.dll @@ -0,0 +1 @@ +not a library \ No newline at end of file diff --git a/components/native-loader/test-data/resource/libdummy.dylib b/components/native-loader/test-data/resource/libdummy.dylib new file mode 100644 index 00000000000..ccbb965f72a --- /dev/null +++ b/components/native-loader/test-data/resource/libdummy.dylib @@ -0,0 +1 @@ +not a library \ No newline at end of file diff --git a/components/native-loader/test-data/resource/libdummy.so b/components/native-loader/test-data/resource/libdummy.so new file mode 100644 index 00000000000..ccbb965f72a --- /dev/null +++ b/components/native-loader/test-data/resource/libdummy.so @@ -0,0 +1 @@ +not a library \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 62807b078f9..4820489d970 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -104,6 +104,7 @@ include( ":components:context", ":components:environment", ":components:json", + ":components:native-loader", ":components:yaml", ":telemetry", ":remote-config:remote-config-api",