diff --git a/src/jdk.jartool/share/classes/sun/tools/jar/GNUStyleOptions.java b/src/jdk.jartool/share/classes/sun/tools/jar/GNUStyleOptions.java index b526f80cb35..6bb06cee9ba 100644 --- a/src/jdk.jartool/share/classes/sun/tools/jar/GNUStyleOptions.java +++ b/src/jdk.jartool/share/classes/sun/tools/jar/GNUStyleOptions.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2022, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 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 @@ -30,6 +30,8 @@ import java.io.PrintWriter; import java.lang.module.ModuleDescriptor.Version; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.HashSet; +import java.util.Set; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import jdk.internal.module.ModulePath; @@ -245,6 +247,14 @@ class GNUStyleOptions { if (jartool.info == null) jartool.info = GNUStyleOptions::printVersion; } + }, + new Option(true, true, OptionType.EXTRACT, "--dir") { + void process(Main jartool, String opt, String arg) throws BadArgs { + if (jartool.xdestDir != null) { + throw new BadArgs("error.extract.multiple.dest.dir").showUsage(true); + } + jartool.xdestDir = arg; + } } }; @@ -254,6 +264,7 @@ class GNUStyleOptions { CREATE("create"), CREATE_UPDATE("create.update"), CREATE_UPDATE_INDEX("create.update.index"), + EXTRACT("extract"), OTHER("other"); /** Resource lookup section prefix. */ diff --git a/src/jdk.jartool/share/classes/sun/tools/jar/Main.java b/src/jdk.jartool/share/classes/sun/tools/jar/Main.java index 551568ca86f..61cd863218b 100644 --- a/src/jdk.jartool/share/classes/sun/tools/jar/Main.java +++ b/src/jdk.jartool/share/classes/sun/tools/jar/Main.java @@ -160,6 +160,9 @@ public class Main { boolean suppressDeprecateMsg = false; + // destination directory for extraction + String xdestDir = null; + /* To support additional GNU Style informational options */ Consumer info; @@ -372,6 +375,15 @@ public class Main { } } } else if (xflag) { + if (xdestDir != null) { + final Path destPath = Paths.get(xdestDir); + try { + Files.createDirectories(destPath); + } catch (IOException ioe) { + throw new IOException(formatMsg("error.create.dir", + destPath.toString()), ioe); + } + } replaceFSC(filesMap); // For the extract action, when extracting all the entries, // access using the ZipInputStream class is most efficient, @@ -631,6 +643,11 @@ public class Main { } /* change the directory */ String dir = args[++i]; + if (xflag && xdestDir != null) { + // extract option doesn't allow more than one destination directory + usageError(getMsg("error.extract.multiple.dest.dir")); + return false; + } dir = (dir.endsWith(File.separator) ? dir : (dir + File.separator)); dir = dir.replace(File.separatorChar, '/'); @@ -642,8 +659,12 @@ public class Main { if (hasUNC) { // Restore Windows UNC path. dir = "/" + dir; } - pathsMap.get(version).add(dir); - nameBuf[k++] = dir + args[++i]; + if (xflag) { + xdestDir = dir; + } else { + pathsMap.get(version).add(dir); + nameBuf[k++] = dir + args[++i]; + } } else if (args[i].startsWith("--release")) { int v = BASE_VERSION; try { @@ -702,6 +723,10 @@ public class Main { return false; } } + if (xflag && pflag && xdestDir != null) { + usageError(getMsg("error.extract.pflag.not.allowed")); + return false; + } return true; } @@ -1355,7 +1380,7 @@ public class Main { if (lastModified != -1) { String name = safeName(ze.getName().replace(File.separatorChar, '/')); if (name.length() != 0) { - File f = new File(name.replace('/', File.separatorChar)); + File f = new File(xdestDir, name.replace('/', File.separatorChar)); f.setLastModified(lastModified); } } @@ -1366,6 +1391,10 @@ public class Main { * Extracts specified entries from JAR file. */ void extract(InputStream in, String[] files) throws IOException { + if (vflag) { + output(formatMsg("out.extract.dir", Path.of(xdestDir == null ? "." : xdestDir).normalize() + .toAbsolutePath().toString())); + } ZipInputStream zis = new ZipInputStream(in); ZipEntry e; Set dirs = newDirSet(); @@ -1394,6 +1423,10 @@ public class Main { * Extracts specified entries from JAR file, via ZipFile. */ void extract(String fname, String[] files) throws IOException { + if (vflag) { + output(formatMsg("out.extract.dir", Path.of(xdestDir == null ? "." : xdestDir).normalize() + .toAbsolutePath().toString())); + } final Set dirs; try (ZipFile zf = new ZipFile(fname)) { dirs = newDirSet(); @@ -1423,16 +1456,24 @@ public class Main { */ ZipEntry extractFile(InputStream is, ZipEntry e) throws IOException { ZipEntry rc = null; - // The spec requres all slashes MUST be forward '/', it is possible + // The spec requires all slashes MUST be forward '/', it is possible // an offending zip/jar entry may uses the backwards slash in its // name. It might cause problem on Windows platform as it skips - // our "safe" check for leading slahs and dot-dot. So replace them + // our "safe" check for leading slash and dot-dot. So replace them // with '/'. String name = safeName(e.getName().replace(File.separatorChar, '/')); if (name.length() == 0) { return rc; // leading '/' or 'dot-dot' only path } - File f = new File(name.replace('/', File.separatorChar)); + // the xdestDir points to the user specified location where the jar needs to + // be extracted. By default xdestDir is null and represents current working + // directory. + // jar extraction using -P option is only allowed when the destination + // directory isn't specified (and hence defaults to current working directory). + // In such cases using this java.io.File constructor which accepts a null parent path + // allows us to extract entries that may have leading slashes and hence may need + // to be extracted outside of the current directory. + File f = new File(xdestDir, name.replace('/', File.separatorChar)); if (e.isDirectory()) { if (f.exists()) { if (!f.isDirectory()) { diff --git a/src/jdk.jartool/share/classes/sun/tools/jar/resources/jar.properties b/src/jdk.jartool/share/classes/sun/tools/jar/resources/jar.properties index 463068c517c..7d7ec89c899 100644 --- a/src/jdk.jartool/share/classes/sun/tools/jar/resources/jar.properties +++ b/src/jdk.jartool/share/classes/sun/tools/jar/resources/jar.properties @@ -1,5 +1,5 @@ # -# Copyright (c) 1999, 2023, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 1999, 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 @@ -62,6 +62,10 @@ error.incorrect.length=\ incorrect length while processing: {0} error.create.tempfile=\ Could not create a temporary file +error.extract.multiple.dest.dir=\ + You may not specify the '-C' or '--dir' option more than once with the '-x' option +error.extract.pflag.not.allowed=\ + You may not specify '-Px' with the '-C' or '--dir' options error.hash.dep=\ Hashing module {0} dependences, unable to find module {1} on module path error.module.options.without.info=\ @@ -169,6 +173,8 @@ out.inflated=\ \ inflated: {0} out.size=\ (in = {0}) (out= {1}) +out.extract.dir=\ + extracting to directory: {0} usage.compat=\ \Compatibility Interface:\ @@ -190,6 +196,7 @@ Options:\n\ \ \ -i generate index information for the specified jar files\n\ \ \ -C change to the specified directory and include the following file\n\ If any file is a directory then it is processed recursively.\n\ +When used in extract mode, extracts the jar to the specified directory\n\ The manifest file name, the archive file name and the entry point name are\n\ specified in the same order as the 'm', 'f' and 'e' flags.\n\n\ Example 1: to archive two class files into an archive called classes.jar: \n\ @@ -257,7 +264,8 @@ main.help.opt.any=\ \ Operation modifiers valid in any mode:\n\ \n\ \ -C DIR Change to the specified directory and include the\n\ -\ following file +\ following file. When used in extract mode, extracts\n\ +\ the jar to the specified directory main.help.opt.any.file=\ \ -f, --file=FILE The archive file name. When omitted, either stdin or\n\ \ stdout is used based on the operation\n\ @@ -324,3 +332,7 @@ main.help.postopt=\ \n\ \ Mandatory or optional arguments to long options are also mandatory or optional\n\ \ for any corresponding short options. +main.help.opt.extract=\ +\ Operation modifiers valid only in extract mode:\n +main.help.opt.extract.dir=\ +\ --dir Directory into which the jar will be extracted diff --git a/src/jdk.jartool/share/man/jar.1 b/src/jdk.jartool/share/man/jar.1 index 2d983eb561e..865925cd075 100644 --- a/src/jdk.jartool/share/man/jar.1 +++ b/src/jdk.jartool/share/man/jar.1 @@ -1,4 +1,4 @@ -.\" Copyright (c) 1997, 2023, Oracle and/or its affiliates. All rights reserved. +.\" Copyright (c) 1997, 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 @@ -127,13 +127,19 @@ You can use the following options to customize the actions of any operation mode included in the \f[V]jar\f[R] command. .TP \f[V]-C\f[R] \f[I]DIR\f[R] -Changes the specified directory and includes the \f[I]files\f[R] -specified at the end of the command line. +When used with the create operation mode, changes the specified +directory and includes the \f[I]files\f[R] specified at the end of the +command line. .RS .PP \f[V]jar\f[R] [\f[I]OPTION\f[R] ...] [ [\f[V]--release\f[R] \f[I]VERSION\f[R]] [\f[V]-C\f[R] \f[I]dir\f[R]] \f[I]files\f[R]] +.PP +When used with the extract operation mode, specifies the destination +directory where the JAR file will be extracted. +Unlike with the create operation mode, this option can be specified only +once with the extract operation mode. .RE .TP \f[V]-f\f[R] \f[I]FILE\f[R] or \f[V]--file=\f[R]\f[I]FILE\f[R] @@ -202,6 +208,10 @@ Stores without using ZIP compression. The timestamp in ISO-8601 extended offset date-time with optional time-zone format, to use for the timestamp of the entries, e.g. \[dq]2022-02-12T12:30:00-05:00\[dq]. +.SH OPERATION MODIFIERS VALID ONLY IN EXTRACT MODE +.TP +\f[V]--dir\f[R] \f[I]DIR\f[R] +Directory into which the JAR file will be extracted. .SH OTHER OPTIONS .PP The following options are recognized by the \f[V]jar\f[R] command and @@ -342,3 +352,17 @@ file that lists the files to include in the JAR file and pass it to the If one or more entries in the arg file cannot be found then the jar command fails without creating the JAR file. .RE +.IP \[bu] 2 +Extract the JAR file \f[V]foo.jar\f[R] to \f[V]/tmp/bar/\f[R] directory: +.RS 2 +.RS +.PP +\f[V]jar -xf foo.jar -C /tmp/bar/\f[R] +.RE +.PP +Alternatively, you can also do: +.RS +.PP +\f[V]jar --extract --file foo.jar --dir /tmp/bar/\f[R] +.RE +.RE diff --git a/test/jdk/tools/jar/JarExtractTest.java b/test/jdk/tools/jar/JarExtractTest.java new file mode 100644 index 00000000000..f1d30e678ae --- /dev/null +++ b/test/jdk/tools/jar/JarExtractTest.java @@ -0,0 +1,517 @@ +/* + * 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 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 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 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 provideAbsoluteExtractLocations() throws Exception { + final Stream 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 provideAbsoluteNormalizedExtractLocations() throws Exception { + final Stream 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 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 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 -C } 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 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 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 -C } 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 -C } 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 d4Children; + try (final Stream 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 ...} + */ + 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) + "'"); + } +} \ No newline at end of file