8173970: jar tool should have a way to extract to a directory

Reviewed-by: lancea, cstein
This commit is contained in:
Jaikiran Pai 2024-10-18 00:51:39 +00:00
parent 2b03dbdac4
commit ffe60919df
5 changed files with 617 additions and 12 deletions

View File

@ -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. */

View File

@ -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<PrintWriter> 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<ZipEntry> 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<ZipEntry> 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()) {

View File

@ -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

View File

@ -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

View File

@ -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<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) + "'");
}
}