Repository navigation
MountableFile directory copy uses a symlink target path as its TAR entry name #12122
Replies: 1 comment
|
I independently reproduced the path-name loss using the published Testcontainers 2.0.5 artifact on Linux/WSL2, OpenJDK 21.0.12.1, with the fixture compiled for Java 17. The The in-memory TAR test does not launch a container. Three cases give: JUnit reports 3 tests, 2 failures, 0 errors, 0 skipped. The two failures assert that the logical The internal-target case is useful additional regression coverage: it demonstrates the alias-name loss even when the target remains inside the copied directory. The source path explains both outcomes: A focused implementation should carry the logical traversal path for entry naming separately from the resolved path used to read contents. Whether following an external target should be allowed is a separate policy question for the project. That can be discussed without changing whether links are stored as symbolic-link TAR entries. I would keep both link cases and the plain-file control in the regression, and keep a container-copy integration check separate from this TAR-producer test. Minimal reproductionMaven fixture with one passing control and two failing link casesSave the following files in an empty directory. Run on a filesystem that supports symbolic links.
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion><groupId>local.evidence</groupId><artifactId>mountablefile-path-repro</artifactId><version>1.0.0</version>
<properties><maven.compiler.release>17</maven.compiler.release><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties>
<dependencies>
<dependency><groupId>org.testcontainers</groupId><artifactId>testcontainers</artifactId><version>2.0.5</version><scope>test</scope></dependency>
<dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter</artifactId><version>5.13.4</version><scope>test</scope></dependency>
<dependency><groupId>org.slf4j</groupId><artifactId>slf4j-simple</artifactId><version>1.7.36</version><scope>test</scope></dependency>
</dependencies>
<build><plugins>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.14.0</version></plugin>
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>3.5.2</version>
<configuration>
<argLine>-Djava.io.tmpdir=${project.build.directory}/tmp</argLine>
<systemPropertyVariables><case.evidence.dir>${project.build.directory}/case-evidence</case.evidence.dir></systemPropertyVariables>
</configuration>
</plugin>
</plugins></build>
</project>
package evidence;
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.testcontainers.utility.MountableFile;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class MountableFilePathTest {
@TempDir Path root;
@Test void regularFileRetainsName() throws Exception {
Path copied = Files.createDirectory(root.resolve("copied"));
Files.writeString(copied.resolve("control.txt"), "control");
List<Entry> entries = archive("regular-file", copied);
assertTrue(entries.stream().anyMatch(e -> e.name.equals("dest/control.txt") && e.content.equals("control")));
}
@Test void internalLinkRetainsItsLogicalName() throws Exception {
Path copied = Files.createDirectory(root.resolve("copied"));
Files.writeString(copied.resolve("target.txt"), "inside");
Files.createSymbolicLink(copied.resolve("link"), Path.of("target.txt"));
List<Entry> entries = archive("internal-link", copied);
assertTrue(entries.stream().anyMatch(e -> e.name.equals("dest/link") && e.content.equals("inside")),
() -> "The logical alias dest/link is missing: " + entries);
}
@Test void externalLinkRetainsItsLogicalName() throws Exception {
Path copied = Files.createDirectory(root.resolve("copied"));
Files.writeString(copied.resolve("control.txt"), "control");
Files.writeString(root.resolve("outside.txt"), "outside");
Files.createSymbolicLink(copied.resolve("link"), Path.of("../outside.txt"));
List<Entry> entries = archive("external-link", copied);
assertTrue(entries.stream().anyMatch(e -> e.name.equals("dest/link") && e.content.equals("outside")),
() -> "The logical alias dest/link is missing: " + entries);
assertFalse(entries.stream().anyMatch(e -> e.name.contains("/../")));
}
private List<Entry> archive(String label, Path copied) throws Exception {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (TarArchiveOutputStream output = new TarArchiveOutputStream(bytes)) {
MountableFile.forHostPath(copied).transferTo(output, "dest");
}
List<Entry> entries = new ArrayList<>();
try (TarArchiveInputStream input = new TarArchiveInputStream(new ByteArrayInputStream(bytes.toByteArray()))) {
TarArchiveEntry entry;
while ((entry = input.getNextEntry()) != null) {
String content = entry.isDirectory() ? "" : new String(input.readAllBytes(), StandardCharsets.UTF_8);
entries.add(new Entry(entry.getName(), content, entry.isSymbolicLink()));
}
}
Path proof = Path.of(System.getProperty("case.evidence.dir"));
Files.createDirectories(proof);
String joined = String.join(",", entries.stream().map(e -> "{\"name\":\"" + escape(e.name) +
"\",\"content\":\"" + escape(e.content) + "\",\"symlink\":" + e.symlink + "}").toList());
String json = "{\"case\":\"" + label + "\",\"java\":\"" + System.getProperty("java.version") +
"\",\"os\":\"" + System.getProperty("os.name") + "\",\"runtimeJar\":\"" +
escape(MountableFile.class.getProtectionDomain().getCodeSource().getLocation().toString()) +
"\",\"entries\":[" + joined + "]}\n";
Files.writeString(proof.resolve(label + ".json"), json);
System.out.println("TAR_CASE " + label + ": " + entries);
return entries;
}
private static String escape(String s) {
return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r");
}
private record Entry(String name, String content, boolean symlink) {}
}Run on Linux/WSL2 (the execution above used JDK 21.0.12.1 and Maven 3.9.9): mkdir -p target/tmp
mvn -B -ntp testExpected on Testcontainers 2.0.5: Tests run: 3, Failures: 2, Errors: 0, Skipped: 0. The link-name assertions fail and the regular-file control passes; Maven therefore exits with a failure. The test prints the actual TAR entries and saves them under |
Uh oh!
There was an error while loading. Please reload this page.
I found a directory-copy case where
MountableFile.transferTochanges a symlink's name to the target's relative path. I'd like to check the intended behavior before opening a bug and patch, as the bug template asks.On main at
a8be77bad1c6dda55a3cc9d82e1121db8edb8913, this layout reproduces it:Reading the resulting TAR gives
dest/control.txtanddest/../outside.txt, but nodest/link. I expected the followed file contents underdest/link. The regular file is copied correctly. This was reproduced inMountableFileTeston macOS ARM64/JDK 17: 12 tests, one failing regression, no errors or skipped tests. The reproduction creates and reads an in-memory TAR; it does not depend on a running container.The recursive walk canonicalizes the child path before deriving the archive name, so a link loses its name and uses its target's path instead. Would a focused fix that preserves the logical path for TAR entry names, while retaining the current dereferencing behavior, be appropriate? I can add that regression and a container-copy check. I am not proposing a change to how symlinks themselves are represented in the archive.
Reproduction and report prepared and posted by an AI assistant for @hutiefang76.
All reactions