jdk-24/test/jdk/tools/jar/JarExtractTest.java
2024-10-18 00:51:39 +00:00

517 lines
25 KiB
Java

/*
* Copyright (c) 2021, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.spi.ToolProvider;
import java.util.stream.Stream;
import jdk.test.lib.util.JarBuilder;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
/*
* @test
* @bug 8173970
* @summary jar tool should allow extracting to specific directory
* @library /test/lib
* @comment The test relies on verification of error messages generated by jar tool, so we use
* a fixed en_US locale for this test.
* @run junit/othervm -Duser.language=en -Duser.country=US JarExtractTest
*/
public class JarExtractTest {
private static final ToolProvider JAR_TOOL = ToolProvider.findFirst("jar")
.orElseThrow(() ->
new RuntimeException("jar tool not found")
);
private static final byte[] FILE_CONTENT = "Hello world!!!".getBytes(StandardCharsets.UTF_8);
// the jar that will get extracted in the tests
private Path testJarPath;
private static Collection<Path> filesToDelete = new ArrayList<>();
@BeforeEach
public void createTestJar() throws Exception {
final String tmpDir = Files.createTempDirectory("8173970-").toString();
testJarPath = Path.of(tmpDir, "8173970-test.jar");
final JarBuilder builder = new JarBuilder(testJarPath.toString());
// d1
// |--- d2
// | |--- d3
// | | |--- f2.txt
// |
// |--- d4
// ...
// f1.txt
builder.addEntry("d1/", new byte[0]);
builder.addEntry("f1.txt", FILE_CONTENT);
builder.addEntry("d1/d2/d3/f2.txt", FILE_CONTENT);
builder.addEntry("d1/d4/", new byte[0]);
builder.build();
}
@AfterEach
public void cleanup() {
for (final Path p : filesToDelete) {
try {
System.out.println("Deleting file/dir " + p);
Files.delete(p);
} catch (IOException ioe) {
//ignore
System.err.println("ignoring exception: " + ioe
+ " that happened when deleting: " + p);
}
}
}
/**
* Creates and returns various relative paths, to which the jar will be extracted in the tests
*/
static Stream<Arguments> provideRelativeExtractLocations() throws Exception {
// create some dirs so that they already exist when the jar is being extracted
final String existing1 = "." + File.separator + "8173970-existing-1";
Files.createDirectories(Path.of(existing1));
final String existing2 = "." + File.separator + "foo" + File.separator + "8173970-existing-2";
Files.createDirectories(Path.of(existing2));
final Path dirOutsideScratchDir = Files.createTempDirectory(Path.of(".."), "8173970");
// we need to explicitly delete this dir after the tests end
filesToDelete.add(dirOutsideScratchDir);
final String existing3 = dirOutsideScratchDir.toString() + File.separator + "8173970-existing-3";
Files.createDirectories(Path.of(existing3));
final String anotherDirOutsideScratchDir = ".." + File.separator + "8173970-non-existent";
filesToDelete.add(Path.of(anotherDirOutsideScratchDir));
final List<Arguments> args = new ArrayList<>();
args.add(Arguments.of(".")); // current dir
// (explicitly) relative to current dir
args.add(Arguments.of("." + File.separator + "8173970-extract-1"));
// (implicitly) relative to current dir
args.add(Arguments.of("8173970-extract-2"));
// sibling to current dir
args.add(Arguments.of(anotherDirOutsideScratchDir));
// some existing dirs
args.add(Arguments.of(existing1));
args.add(Arguments.of(existing2));
args.add(Arguments.of(existing3));
// a non-existent dir within an existing dir
args.add(Arguments.of(existing1 + File.separator
+ "non-existing" + File.separator + "foo"));
return args.stream();
}
/**
* Creates and returns various absolute paths, to which the jar will be extracted in the tests
*/
static Stream<Arguments> provideAbsoluteExtractLocations() throws Exception {
final Stream<Arguments> relative = provideRelativeExtractLocations();
return relative.map((arg) -> {
final String relPath = (String) arg.get()[0];
return Arguments.of(Path.of(relPath).toAbsolutePath().toString());
});
}
/**
* Creates and returns various normalized paths, to which the jar will be extracted in the tests
*/
static Stream<Arguments> provideAbsoluteNormalizedExtractLocations() throws Exception {
final Stream<Arguments> relative = provideRelativeExtractLocations();
return relative.map((arg) -> {
final String relPath = (String) arg.get()[0];
return Arguments.of(Path.of(relPath).toAbsolutePath().normalize().toString());
});
}
/**
* Extracts a jar to various relative paths, using the -C/--dir option and then
* verifies that the extracted content is at the expected locations with the correct
* content
*/
@ParameterizedTest
@MethodSource("provideRelativeExtractLocations")
public void testExtractToRelativeDir(final String dest) throws Exception {
testLongFormExtract(dest);
testExtract(dest);
}
/**
* Extracts a jar to various absolute paths, using the -C/--dir option and then
* verifies that the extracted content is at the expected locations with the correct
* content
*/
@ParameterizedTest
@MethodSource("provideAbsoluteExtractLocations")
public void testExtractToAbsoluteDir(final String dest) throws Exception {
testExtract(dest);
testLongFormExtract(dest);
}
/**
* Extracts a jar to various normalized paths (i.e. no {@code .} or @{code ..} in the path components),
* using the -C/--dir option and then verifies that the extracted content is at the expected locations
* with the correct content
*/
@ParameterizedTest
@MethodSource("provideAbsoluteNormalizedExtractLocations")
public void testExtractToAbsoluteNormalizedDir(final String dest) throws Exception {
testExtract(dest);
testLongFormExtract(dest);
}
/**
* Test that extracting a jar with {@code jar -x -f --dir} works as expected
*/
@Test
public void testExtractLongFormDir() throws Exception {
final String dest = "foo-bar";
System.out.println("Extracting " + testJarPath + " to " + dest);
final int exitCode = JAR_TOOL.run(System.out, System.err, "-x", "-f", testJarPath.toString(),
"--dir", dest);
assertEquals(0, exitCode, "Failed to extract " + testJarPath + " to " + dest);
verifyExtractedContent(dest);
}
/**
* Verifies that the {@code jar --help} output contains the --dir option
*/
@Test
public void testHelpOutput() {
final ByteArrayOutputStream outStream = new ByteArrayOutputStream();
final int exitCode = JAR_TOOL.run(new PrintStream(outStream), System.err, "--help");
assertEquals(0, exitCode, "jar --help command failed");
final String output = outStream.toString();
// this message is expected to be the one from the jar --help output which is sourced from
// jar.properties
final String expectedMsg = "--dir Directory into which the jar will be extracted";
assertTrue(output.contains(expectedMsg), "jar --help didn't contain --dir option");
}
/**
* Tests that {@code jar -x -f} command works fine even when the -C or --dir option
* isn't specified
*/
@Test
public void testExtractWithoutOutputDir() throws Exception {
final int exitCode = JAR_TOOL.run(System.out, System.err, "-x", "-f", testJarPath.toString());
assertEquals(0, exitCode, "Failed to extract " + testJarPath);
// the content would have been extracted to current dir
verifyExtractedContent(".");
}
/**
* Tests that {@code jar --extract -f} command works fine even when the -C or --dir option
* isn't specified
*/
@Test
public void testLongFormExtractWithoutOutputDir() throws Exception {
final int exitCode = JAR_TOOL.run(System.out, System.err, "--extract", "-f", testJarPath.toString());
assertEquals(0, exitCode, "Failed to extract " + testJarPath);
// the content would have been extracted to current dir
verifyExtractedContent(".");
}
/**
* Tests that when the destination directory specified for jar extract is actually a file
* or one of the path component in the specified destination path is a file, then the
* extraction fails.
*/
@Test
public void testExtractToNonDirectory() throws Exception {
final String expectedErrMsg = "could not create directory";
final Path notADir1 = Files.createTempFile(Path.of("."), "8173970", ".txt");
final Path notADir2 = notADir1.resolve("foobar");
for (final Path dest : List.of(notADir1, notADir2)) {
final String[] args = {"-x", "-f", testJarPath.toString(), "-C", dest.toString()};
final ByteArrayOutputStream err = new ByteArrayOutputStream();
printJarCommand(args);
int exitCode = JAR_TOOL.run(System.out, new PrintStream(err), args);
assertNotEquals(0, exitCode, "jar extraction was expected to fail but didn't");
// verify it did indeed fail due to the right reason
assertTrue(err.toString(StandardCharsets.UTF_8).contains(expectedErrMsg));
}
}
/**
* Tests that extracting a jar using {@code -P} flag and without any explicit destination
* directory works correctly if the jar contains entries with leading slashes and/or {@code ..}
* parts preserved.
*/
@Test
public void testExtractNoDestDirWithPFlag() throws Exception {
// run this test only on those systems where "/tmp" directory is available and we
// can write to it
Assumptions.assumeTrue(Files.isDirectory(Path.of("/tmp")),
"skipping test, since /tmp isn't a directory");
// try and write into "/tmp"
final Path tmpDir;
try {
tmpDir = Files.createTempDirectory(Path.of("/tmp"), "8173970-").toAbsolutePath();
} catch (IOException ioe) {
Assumptions.abort("skipping test, since /tmp cannot be written to: " + ioe);
return;
}
final String leadingSlashEntryName = tmpDir.toString() + "/foo/f1.txt";
// create a jar which has leading slash (/) and dot-dot (..) preserved in entry names
final Path jarPath = createJarWithPFlagSemantics(leadingSlashEntryName);
final List<String[]> cmdArgs = new ArrayList<>();
cmdArgs.add(new String[]{"-xvfP", jarPath.toString()});
cmdArgs.add(new String[]{"--extract", "-v", "-P", "-f", jarPath.toString()});
try {
for (final String[] args : cmdArgs) {
printJarCommand(args);
final int exitCode = JAR_TOOL.run(System.out, System.err, args);
assertEquals(0, exitCode, "Failed to extract " + jarPath);
final String dest = ".";
assertTrue(Files.isDirectory(Path.of(dest)), dest + " is not a directory");
final Path d1 = Path.of(dest, "d1");
assertTrue(Files.isDirectory(d1), d1 + " directory is missing or not a directory");
final Path d2 = Path.of(dest, "d1", "d2");
assertTrue(Files.isDirectory(d2), d2 + " directory is missing or not a directory");
final Path f1 = Path.of(leadingSlashEntryName);
assertTrue(Files.isRegularFile(f1), f1 + " is missing or not a file");
assertArrayEquals(FILE_CONTENT, Files.readAllBytes(f1),
"Unexpected content in file " + f1);
final Path f2 = Path.of("d1/d2/../f2.txt");
assertTrue(Files.isRegularFile(f2), f2 + " is missing or not a file");
assertArrayEquals(FILE_CONTENT, Files.readAllBytes(f2),
"Unexpected content in file " + f2);
}
} finally {
// clean up the file that might have been extracted into "/tmp/...." directory
Files.deleteIfExists(Path.of(leadingSlashEntryName));
}
}
/**
* Tests that the {@code -P} option cannot be used during jar extraction when the {@code -C} and/or
* {@code --dir} option is used
*/
@Test
public void testExtractWithDirPFlagNotAllowed() throws Exception {
// this error message is expected to be the one from the jar --help output which is sourced from
// jar.properties
final String expectedErrMsg = "You may not specify '-Px' with the '-C' or '--dir' options";
final String tmpDir = Files.createTempDirectory(Path.of("."), "8173970-").toString();
final List<String[]> cmdArgs = new ArrayList<>();
cmdArgs.add(new String[]{"-x", "-f", testJarPath.toString(), "-P", "-C", tmpDir});
cmdArgs.add(new String[]{"-x", "-f", testJarPath.toString(), "-P", "--dir", tmpDir});
cmdArgs.add(new String[]{"-x", "-f", testJarPath.toString(), "-P", "-C", "."});
cmdArgs.add(new String[]{"-x", "-f", testJarPath.toString(), "-P", "--dir", "."});
cmdArgs.add(new String[]{"-xvfP", testJarPath.toString(), "-C", tmpDir});
cmdArgs.add(new String[]{"--extract", "-f", testJarPath.toString(), "-P", "-C", tmpDir});
cmdArgs.add(new String[]{"--extract", "-f", testJarPath.toString(), "-P", "--dir", tmpDir});
cmdArgs.add(new String[]{"--extract", "-f", testJarPath.toString(), "-P", "-C", "."});
cmdArgs.add(new String[]{"--extract", "-f", testJarPath.toString(), "-P", "--dir", "."});
for (final String[] args : cmdArgs) {
final ByteArrayOutputStream err = new ByteArrayOutputStream();
printJarCommand(args);
int exitCode = JAR_TOOL.run(System.out, new PrintStream(err), args);
assertNotEquals(0, exitCode, "jar extraction was expected to fail but didn't");
// verify it did indeed fail due to the right reason
assertTrue(err.toString(StandardCharsets.UTF_8).contains(expectedErrMsg));
}
}
/**
* Tests that {@code jar -xvf <jarname> -C <dir>} works fine too
*/
@Test
public void testLegacyCompatibilityMode() throws Exception {
final String tmpDir = Files.createTempDirectory(Path.of("."), "8173970-").toString();
final String[] args = new String[]{"-xvf", testJarPath.toString(), "-C", tmpDir};
printJarCommand(args);
final int exitCode = JAR_TOOL.run(System.out, System.err, args);
assertEquals(0, exitCode, "Failed to extract " + testJarPath);
verifyExtractedContent(tmpDir);
}
/**
* Tests that when multiple directories are specified for extracting the jar, the jar extraction
* fails
*/
@Test
public void testExtractFailWithMultipleDir() throws Exception {
// this error message is expected to be the one from the jar --help output which is sourced from
// jar.properties
final String expectedErrMsg = "You may not specify the '-C' or '--dir' option more than once with the '-x' option";
final String tmpDir = Files.createTempDirectory(Path.of("."), "8173970-").toString();
final List<String[]> cmdArgs = new ArrayList<>();
cmdArgs.add(new String[]{"-x", "-f", testJarPath.toString(), "-C", tmpDir, "-C", tmpDir});
cmdArgs.add(new String[]{"-x", "-f", testJarPath.toString(), "--dir", tmpDir, "--dir", tmpDir});
cmdArgs.add(new String[]{"-x", "-f", testJarPath.toString(), "--dir", tmpDir, "-C", tmpDir});
cmdArgs.add(new String[]{"--extract", "-f", testJarPath.toString(), "-C", tmpDir, "-C", tmpDir});
cmdArgs.add(new String[]{"--extract", "-f", testJarPath.toString(), "--dir", tmpDir, "--dir", tmpDir});
cmdArgs.add(new String[]{"--extract", "-f", testJarPath.toString(), "--dir", tmpDir, "-C", tmpDir});
for (final String[] args : cmdArgs) {
final ByteArrayOutputStream err = new ByteArrayOutputStream();
printJarCommand(args);
int exitCode = JAR_TOOL.run(System.out, new PrintStream(err), args);
assertNotEquals(0, exitCode, "jar extraction was expected to fail but didn't");
// verify it did indeed fail due to the right reason
assertTrue(err.toString(StandardCharsets.UTF_8).contains(expectedErrMsg));
}
}
/**
* Tests that extracting only specific files from a jar, into a specific destination directory,
* works as expected
*/
@Test
public void testExtractPartialContent() throws Exception {
String tmpDir = Files.createTempDirectory(Path.of("."), "8173970-").toString();
String[] cmdArgs = new String[]{"-x", "-f", testJarPath.toString(), "--dir", tmpDir,
"f1.txt", "d1/d2/d3/f2.txt"};
testExtractPartialContent(tmpDir, cmdArgs);
tmpDir = Files.createTempDirectory(Path.of("."), "8173970-").toString();
cmdArgs = new String[]{"--extract", "-f", testJarPath.toString(), "--dir", tmpDir,
"f1.txt", "d1/d2/d3/f2.txt"};
testExtractPartialContent(tmpDir, cmdArgs);
}
/**
* Extract to destDir using the passed command arguments and verify the extracted content
*/
private void testExtractPartialContent(final String destDir, final String[] extractArgs) throws Exception {
printJarCommand(extractArgs);
final int exitCode = JAR_TOOL.run(System.out, System.err, extractArgs);
assertEquals(0, exitCode, "Failed to extract " + testJarPath);
// make sure only the specific files were extracted
final Stream<Path> paths = Files.walk(Path.of(destDir));
// files/dirs count expected to be found when the location to which the jar was extracted
// is walked.
// 1) The top level dir being walked 2) f1.txt file 3) d1 dir 4) d1/d2 dir
// 5) d1/d2/d3 dir 6) d1/d2/d3/f2.txt file
final int numExpectedFiles = 6;
assertEquals(numExpectedFiles, paths.count(), "Unexpected number of files/dirs in " + destDir);
final Path f1 = Path.of(destDir, "f1.txt");
assertTrue(Files.isRegularFile(f1), f1.toString() + " wasn't extracted from " + testJarPath);
assertArrayEquals(FILE_CONTENT, Files.readAllBytes(f1), "Unexpected content in file " + f1);
final Path d1 = Path.of(destDir, "d1");
assertTrue(Files.isDirectory(d1), d1.toString() + " wasn't extracted from " + testJarPath);
assertEquals(2, Files.walk(d1, 1).count(), "Unexpected number " +
"of files/dirs in " + d1);
final Path d2 = Path.of(d1.toString(), "d2");
assertTrue(Files.isDirectory(d2), d2.toString() + " wasn't extracted from " + testJarPath);
assertEquals(2, Files.walk(d2, 1).count(), "Unexpected number " +
"of files/dirs in " + d2);
final Path d3 = Path.of(d2.toString(), "d3");
assertTrue(Files.isDirectory(d3), d3.toString() + " wasn't extracted from " + testJarPath);
assertEquals(2, Files.walk(d3, 1).count(), "Unexpected number " +
"of files/dirs in " + d3);
final Path f2 = Path.of(d3.toString(), "f2.txt");
assertTrue(Files.isRegularFile(f2), f2.toString() + " wasn't extracted from " + testJarPath);
assertArrayEquals(FILE_CONTENT, Files.readAllBytes(f2), "Unexpected content in file " + f2);
}
/**
* Extracts the jar file using {@code jar -x -f <jarfile> -C <dest>} and verifies the extracted content
*/
private void testExtract(final String dest) throws Exception {
final String[] args = new String[]{"-x", "-f", testJarPath.toString(), "-C", dest};
printJarCommand(args);
final int exitCode = JAR_TOOL.run(System.out, System.err, args);
assertEquals(0, exitCode, "Failed to extract " + testJarPath + " to " + dest);
verifyExtractedContent(dest);
}
/**
* Extracts the jar file using {@code jar --extract -f <jarfile> -C <dest>} and verifies the
* extracted content
*/
private void testLongFormExtract(final String dest) throws Exception {
final String[] args = new String[]{"--extract", "-f", testJarPath.toString(), "-C", dest};
printJarCommand(args);
final int exitCode = JAR_TOOL.run(System.out, System.err, args);
assertEquals(0, exitCode, "Failed to extract " + testJarPath + " to " + dest);
verifyExtractedContent(dest);
}
/**
* Verifies that the extracted jar content matches what was present in the original jar
*/
private void verifyExtractedContent(final String dest) throws IOException {
assertTrue(Files.isDirectory(Path.of(dest)), dest + " is not a directory");
final Path d1 = Path.of(dest, "d1");
assertTrue(Files.isDirectory(d1), d1 + " directory is missing or not a directory");
final Path d2 = Path.of(dest, "d1", "d2");
assertTrue(Files.isDirectory(d2), d2 + " directory is missing or not a directory");
final Path d3 = Path.of(dest, "d1", "d2", "d3");
assertTrue(Files.isDirectory(d3), d3 + " directory is missing or not a directory");
final Path d4 = Path.of(dest, "d1", "d4");
assertTrue(Files.isDirectory(d4), d4 + " directory is missing or not a directory");
// d1/d4 is expected to be empty directory
final List<Path> d4Children;
try (final Stream<Path> s = Files.walk(d4, 1)) {
d4Children = s.toList();
}
assertEquals(1, d4Children.size(), "Directory " + d4
+ " has unexpected files/dirs: " + d4Children);
final Path f1 = Path.of(dest, "f1.txt");
assertTrue(Files.isRegularFile(f1), f1 + " is missing or not a file");
assertArrayEquals(FILE_CONTENT, Files.readAllBytes(f1), "Unexpected content in file " + f1);
final Path f2 = Path.of(d3.toString(), "f2.txt");
assertTrue(Files.isRegularFile(f2), f2 + " is missing or not a file");
assertArrayEquals(FILE_CONTENT, Files.readAllBytes(f2), "Unexpected content in file " + f2);
}
/**
* Creates a jar whose entries have a leading slash and the dot-dot character preserved.
* This is the same as creating a jar using {@code jar -cfP somejar.jar <file1> <file2> ...}
*/
private static Path createJarWithPFlagSemantics(String leadingSlashEntryName)
throws IOException {
final Path tmpDir = Files.createTempDirectory(Path.of("."), "8173970-").toAbsolutePath();
final Path jarPath = tmpDir.resolve("8173970-test-withpflag.jar");
final JarBuilder builder = new JarBuilder(jarPath.toString());
builder.addEntry("d1/", new byte[0]);
builder.addEntry("d1/d2/", new byte[0]);
builder.addEntry(leadingSlashEntryName, FILE_CONTENT);
builder.addEntry("d1/d2/../f2.txt", FILE_CONTENT);
builder.build();
return jarPath;
}
private static void printJarCommand(final String[] cmdArgs) {
System.out.println("Running 'jar " + String.join(" ", cmdArgs) + "'");
}
}