Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 21 additions & 12 deletions core/src/main/java/org/testcontainers/utility/MountableFile.java
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ private String extractClassPathResourceToTempLocation(final String hostPath) {
hostPath,
tmpLocation
);
copyFromJarToLocation(jarFile, entry, internalPath, tmpLocation);
copyFromJarToLocation(jarFile, entry, tmpLocation);
}
}
} catch (IOException e) {
Expand All @@ -264,7 +264,14 @@ private String extractClassPathResourceToTempLocation(final String hostPath) {
deleteOnExit(tmpLocation.toPath());

try {
return tmpLocation.getCanonicalPath();
// Preserve the resource's own path within the JAR underneath the extraction directory
// (see #9423). A single extracted file therefore still ends up inside a directory
// created specifically for this extraction, rather than being written directly onto
// the temp directory's own path - which would leave the extracted file's parent as the
// shared system temp directory, breaking any caller that treats the resolved path's
// parent as a self-contained context (e.g. building an image from a Dockerfile loaded
// via a classpath resource).
return new File(tmpLocation, internalPath).getCanonicalPath();
} catch (IOException e) {
throw new IllegalStateException(e);
}
Expand All @@ -282,16 +289,18 @@ private File createTempDirectory() {
}

@SuppressWarnings("ResultOfMethodCallIgnored")
private void copyFromJarToLocation(
final JarFile jarFile,
final JarEntry entry,
final String fromRoot,
final File toRoot
) throws IOException {
String destinationName = entry.getName().replaceFirst(fromRoot, "");
File newFile = new File(toRoot, destinationName);

log.debug("Copying resource {} from JAR file {}", fromRoot, jarFile.getName());
private void copyFromJarToLocation(final JarFile jarFile, final JarEntry entry, final File toRoot) throws IOException {
File newFile = new File(toRoot, entry.getName());
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Guard against malformed entries (e.g. containing "../") escaping the extraction directory
Path rootPath = toRoot.getCanonicalFile().toPath();
if (!newFile.getCanonicalFile().toPath().startsWith(rootPath)) {
throw new IOException(
"JAR entry " + entry.getName() + " would be extracted outside of the target directory " + toRoot
);
}

log.debug("Copying resource {} from JAR file {}", entry.getName(), jarFile.getName());

if (!entry.isDirectory()) {
// Create parent directories
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,16 @@
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Consumer;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;

import static org.assertj.core.api.Assertions.assertThat;

Expand Down Expand Up @@ -53,6 +60,62 @@ void forClasspathResourceFromJarWithAbsolutePath() throws Exception {
performChecks(mountableFile);
}

@Test
void forClasspathResourceFileInJarIsExtractedIntoItsOwnDirectory() throws Exception {
// see #9423: a single-file classpath resource extracted from a JAR must end up inside a
// directory created specifically for this extraction (preserving the resource's own path
// within the JAR), rather than being written directly onto the temp *directory*'s own path.
// Otherwise the extracted file's parent is the shared system temp directory, which breaks
// any caller that treats the resolved path's parent as a self-contained context (e.g.
// building an image from a Dockerfile loaded via MountableFile.forClasspathResource(...)).
final Map<String, String> entries = new LinkedHashMap<>();
entries.put("nested/inside/jar/Dockerfile", "FROM postgres\n");
final Path jarFile = createJarWithEntries(entries);

withJarOnClasspath(
jarFile,
() -> {
final MountableFile mountableFile = MountableFile.forClasspathResource("nested/inside/jar/Dockerfile");
final File extractedFile = new File(mountableFile.getFilesystemPath());

assertThat(extractedFile).as("the resource was extracted to a real file").isFile();
assertThat(Files.readString(extractedFile.toPath())).isEqualTo("FROM postgres\n");

final File parentDir = extractedFile.getParentFile();
assertThat(parentDir)
.as("the extracted file's parent is not the shared system temp directory")
.isNotEqualTo(new File(System.getProperty("java.io.tmpdir")));
assertThat(parentDir.list())
.as("only the extracted resource lives in its own extraction directory")
.containsExactly("Dockerfile");
}
);
}

@Test
void forClasspathResourceDirectoryInJarPreservesRelativeStructure() throws Exception {
// Regression guard: extracting a directory resource from a JAR must still lay out its
// files relative to the resolved path exactly as before this fix.
final Map<String, String> entries = new LinkedHashMap<>();
entries.put("assets/", "");
entries.put("assets/dir/", "");
entries.put("assets/dir/sub/", "");
entries.put("assets/dir/a.txt", "a-content");
entries.put("assets/dir/sub/b.txt", "b-content");
final Path jarFile = createJarWithEntries(entries);

withJarOnClasspath(
jarFile,
() -> {
final MountableFile mountableFile = MountableFile.forClasspathResource("assets/dir");
final String resolvedPath = mountableFile.getResolvedPath();

assertThat(Files.readString(new File(resolvedPath, "a.txt").toPath())).isEqualTo("a-content");
assertThat(Files.readString(new File(resolvedPath, "sub/b.txt").toPath())).isEqualTo("b-content");
}
);
}

@Test
void forHostPath() throws Exception {
final Path file = createTempFile("somepath");
Expand Down Expand Up @@ -131,6 +194,38 @@ void noTrailingSlashesInTarEntryNames() throws Exception {
}
}

@NotNull
private Path createJarWithEntries(final Map<String, String> entries) throws IOException {
final Path jarFile = Files.createTempFile("mountable-file-test", ".jar");
jarFile.toFile().deleteOnExit();

try (JarOutputStream jos = new JarOutputStream(Files.newOutputStream(jarFile))) {
for (final Map.Entry<String, String> entry : entries.entrySet()) {
jos.putNextEntry(new JarEntry(entry.getKey()));
jos.write(entry.getValue().getBytes(StandardCharsets.UTF_8));
jos.closeEntry();
}
}

return jarFile;
}

private void withJarOnClasspath(final Path jarFile, final ThrowingRunnable runnable) throws Exception {
final ClassLoader previousContextClassLoader = Thread.currentThread().getContextClassLoader();
try (URLClassLoader jarClassLoader = new URLClassLoader(new URL[] { jarFile.toUri().toURL() }, previousContextClassLoader)) {
Thread.currentThread().setContextClassLoader(jarClassLoader);
try {
runnable.run();
} finally {
Thread.currentThread().setContextClassLoader(previousContextClassLoader);
}
}
}

private interface ThrowingRunnable {
void run() throws Exception;
}

private TarArchiveInputStream intoTarArchive(Consumer<TarArchiveOutputStream> consumer) throws IOException {
@Cleanup
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
Expand Down
Loading