Skip to content

Commit

Permalink
8335912: Add an operation mode to the jar command when extracting to …
Browse files Browse the repository at this point in the history
…not overwriting existing files

Reviewed-by: henryjen, goetz, mbalao
Backport-of: 158b93d
  • Loading branch information
Alexey Bakhtin committed Dec 4, 2024
1 parent 3f648b4 commit e45287d
Show file tree
Hide file tree
Showing 5 changed files with 528 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,13 @@ void process(Main jartool, String opt, String arg) throws BadArgs {
}
},

// Extract options
new Option(false, OptionType.EXTRACT, "--keep-old-files", "-k") {
void process(Main jartool, String opt, String arg) {
jartool.kflag = true;
}
},

// Hidden options
new Option(false, OptionType.OTHER, "-P") {
void process(Main jartool, String opt, String arg) {
Expand Down Expand Up @@ -254,6 +261,7 @@ enum OptionType {
CREATE("create"),
CREATE_UPDATE("create.update"),
CREATE_UPDATE_INDEX("create.update.index"),
EXTRACT("extract"),
OTHER("other");

/** Resource lookup section prefix. */
Expand Down
15 changes: 14 additions & 1 deletion src/jdk.jartool/share/classes/sun/tools/jar/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,9 @@ public int hashCode() {
* nflag: Perform jar normalization at the end
* pflag: preserve/don't strip leading slash and .. component from file name
* dflag: print module descriptor
* kflag: keep existing file
*/
boolean cflag, uflag, xflag, tflag, vflag, flag0, Mflag, iflag, pflag, dflag, validate;
boolean cflag, uflag, xflag, tflag, vflag, flag0, Mflag, iflag, pflag, dflag, kflag, validate;

boolean suppressDeprecateMsg = false;

Expand Down Expand Up @@ -590,6 +591,9 @@ boolean parseArgs(String args[]) {
case '0':
flag0 = true;
break;
case 'k':
kflag = true;
break;
case 'i':
if (cflag || uflag || xflag || tflag) {
usageError(getMsg("error.multiple.main.operations"));
Expand Down Expand Up @@ -620,6 +624,9 @@ boolean parseArgs(String args[]) {
usageError(getMsg("error.bad.option"));
return false;
}
if (kflag && !xflag) {
warn(formatMsg("warn.option.is.ignored", "--keep-old-files/-k/k"));
}

/* parse file arguments */
int n = args.length - count;
Expand Down Expand Up @@ -1470,6 +1477,12 @@ ZipEntry extractFile(InputStream is, ZipEntry e) throws IOException {
output(formatMsg("out.create", name));
}
} else {
if (f.exists() && kflag) {
if (vflag) {
output(formatMsg("out.kept", name));
}
return rc;
}
if (f.getParent() != null) {
File d = new File(f.getParent());
if (!d.exists() && !d.mkdirs() || !d.isDirectory()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ warn.index.is.ignored=\
The JAR index (META-INF/INDEX.LIST) is ignored at run-time since JDK 18
warn.flag.is.deprecated=\
Warning: The {0} option is deprecated, and may be ignored or removed in a future release\n
warn.option.is.ignored=\
Warning: The {0} option is not valid with current usage, will be ignored.
out.added.manifest=\
added manifest
out.added.module-info=\
Expand All @@ -165,6 +167,8 @@ out.create=\
\ \ created: {0}
out.extracted=\
extracted: {0}
out.kept=\
\ \ skipped: {0} exists
out.inflated=\
\ inflated: {0}
out.size=\
Expand Down Expand Up @@ -245,7 +249,10 @@ main.help.opt.main.list=\
main.help.opt.main.update=\
\ -u, --update Update an existing jar archive
main.help.opt.main.extract=\
\ -x, --extract Extract named (or all) files from the archive
\ -x, --extract Extract named (or all) files from the archive.\n\
\ If a file with the same name appears more than once in\n\
\ the archive, each copy will be extracted, with later copies\n\
\ overwriting (replacing) earlier copies unless -k is specified.
main.help.opt.main.describe-module=\
\ -d, --describe-module Print the module descriptor, or automatic module name
main.help.opt.main.validate=\
Expand Down Expand Up @@ -307,6 +314,15 @@ main.help.opt.create.update.index.date=\
\ --date=TIMESTAMP The timestamp in ISO-8601 extended offset date-time with\n\
\ optional time-zone format, to use for the timestamps of\n\
\ entries, e.g. "2022-02-12T12:30:00-05:00"
main.help.opt.extract=\
\ Operation modifiers valid only in extract mode:\n
main.help.opt.extract.keep-old-files=\
\ -k, --keep-old-files Do not overwrite existing files.\n\
\ If a Jar file entry with the same name exists in the target\n\
\ directory, the existing file will not be overwritten.\n\
\ As a result, if a file appears more than once in an\n\
\ archive, later copies will not overwrite earlier copies.\n\
\ Also note that some file system can be case insensitive.
main.help.opt.other=\
\ Other options:\n
main.help.opt.other.help=\
Expand Down
265 changes: 265 additions & 0 deletions test/jdk/tools/jar/ExtractFilesTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
/*
* Copyright (c) 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.
*/

/*
* @test
* @bug 8335912
* @summary test extract jar files overwrite existing files behavior
* @library /test/lib
* @modules jdk.jartool
* @build jdk.test.lib.Platform
* jdk.test.lib.util.FileUtils
* @run junit/othervm ExtractFilesTest
*/

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.spi.ToolProvider;
import java.util.stream.Stream;

import jdk.test.lib.util.FileUtils;

@TestInstance(Lifecycle.PER_CLASS)
public class ExtractFilesTest {
private static final ToolProvider JAR_TOOL = ToolProvider.findFirst("jar")
.orElseThrow(() ->
new RuntimeException("jar tool not found")
);

private final String nl = System.lineSeparator();
private final ByteArrayOutputStream baos = new ByteArrayOutputStream();
private final PrintStream out = new PrintStream(baos);

@BeforeAll
public void setupJar() throws IOException {
mkdir("test1 test2");
echo("testfile1", "test1/testfile1");
echo("testfile2", "test2/testfile2");
jar("cf test.jar -C test1 . -C test2 .");
rm("test1 test2");
}

@AfterAll
public void cleanup() {
rm("test.jar");
}

/**
* Regular clean extract with expected output.
*/
@Test
public void testExtract() throws IOException {
jar("xvf test.jar");
println();
String output = " created: META-INF/" + nl +
" inflated: META-INF/MANIFEST.MF" + nl +
" inflated: testfile1" + nl +
" inflated: testfile2" + nl;
rm("META-INF testfile1 testfile2");
Assertions.assertArrayEquals(baos.toByteArray(), output.getBytes());
}

/**
* Extract should overwrite existing file as default behavior.
*/
@Test
public void testOverwrite() throws IOException {
touch("testfile1");
jar("xvf test.jar");
println();
String output = " created: META-INF/" + nl +
" inflated: META-INF/MANIFEST.MF" + nl +
" inflated: testfile1" + nl +
" inflated: testfile2" + nl;
Assertions.assertEquals("testfile1", cat("testfile1"));
rm("META-INF testfile1 testfile2");
Assertions.assertArrayEquals(baos.toByteArray(), output.getBytes());
}

/**
* Extract with legacy style option `k` should preserve existing files.
*/
@Test
public void testKeptOldFile() throws IOException {
touch("testfile1");
jar("xkvf test.jar");
println();
String output = " created: META-INF/" + nl +
" inflated: META-INF/MANIFEST.MF" + nl +
" skipped: testfile1 exists" + nl +
" inflated: testfile2" + nl;
Assertions.assertEquals("", cat("testfile1"));
Assertions.assertEquals("testfile2", cat("testfile2"));
rm("META-INF testfile1 testfile2");
Assertions.assertArrayEquals(baos.toByteArray(), output.getBytes());
}

/**
* Extract with gnu style -k should preserve existing files.
*/
@Test
public void testGnuOptionsKeptOldFile() throws IOException {
touch("testfile1 testfile2");
jar("-x -k -v -f test.jar");
println();
String output = " created: META-INF/" + nl +
" inflated: META-INF/MANIFEST.MF" + nl +
" skipped: testfile1 exists" + nl +
" skipped: testfile2 exists" + nl;
Assertions.assertEquals("", cat("testfile1"));
Assertions.assertEquals("", cat("testfile2"));
rm("META-INF testfile1 testfile2");
Assertions.assertArrayEquals(baos.toByteArray(), output.getBytes());
}

/**
* Extract with gnu style long option --keep-old-files should preserve existing files.
*/
@Test
public void testGnuLongOptionsKeptOldFile() throws IOException {
touch("testfile2");
jar("-x --keep-old-files -v -f test.jar");
println();
String output = " created: META-INF/" + nl +
" inflated: META-INF/MANIFEST.MF" + nl +
" inflated: testfile1" + nl +
" skipped: testfile2 exists" + nl;
Assertions.assertEquals("testfile1", cat("testfile1"));
Assertions.assertEquals("", cat("testfile2"));
rm("META-INF testfile1 testfile2");
Assertions.assertArrayEquals(baos.toByteArray(), output.getBytes());
}

/**
* Test jar will issue warning when use keep option in non-extraction mode.
*/
@Test
public void testWarningOnInvalidKeepOption() throws IOException {
var err = jar("tkf test.jar");
println();

String output = "META-INF/" + nl +
"META-INF/MANIFEST.MF" + nl +
"testfile1" + nl +
"testfile2" + nl;

Assertions.assertArrayEquals(baos.toByteArray(), output.getBytes());
Assertions.assertEquals("Warning: The --keep-old-files/-k/k option is not valid with current usage, will be ignored." + nl, err);
}

private Stream<Path> mkpath(String... args) {
return Arrays.stream(args).map(d -> Path.of(".", d.split("/")));
}

private void mkdir(String cmdline) {
System.out.println("mkdir -p " + cmdline);
mkpath(cmdline.split(" +")).forEach(p -> {
try {
Files.createDirectories(p);
} catch (IOException x) {
throw new UncheckedIOException(x);
}
});
}

private void touch(String cmdline) {
System.out.println("touch " + cmdline);
mkpath(cmdline.split(" +")).forEach(p -> {
try {
Files.createFile(p);
} catch (IOException x) {
throw new UncheckedIOException(x);
}
});
}

private void echo(String text, String path) {
System.out.println("echo '" + text + "' > " + path);
try {
var p = Path.of(".", path.split("/"));
Files.writeString(p, text);
} catch (IOException x) {
throw new UncheckedIOException(x);
}
}

private String cat(String path) {
System.out.println("cat " + path);
try {
return Files.readString(Path.of(path));
} catch (IOException x) {
throw new UncheckedIOException(x);
}
}

private void rm(String cmdline) {
System.out.println("rm -rf " + cmdline);
mkpath(cmdline.split(" +")).forEach(p -> {
try {
if (Files.isDirectory(p)) {
FileUtils.deleteFileTreeWithRetry(p);
} else {
FileUtils.deleteFileIfExistsWithRetry(p);
}
} catch (IOException x) {
throw new UncheckedIOException(x);
}
});
}

private String jar(String cmdline) throws IOException {
System.out.println("jar " + cmdline);
baos.reset();

// the run method catches IOExceptions, we need to expose them
ByteArrayOutputStream baes = new ByteArrayOutputStream();
PrintStream err = new PrintStream(baes);
PrintStream saveErr = System.err;
System.setErr(err);
try {
int rc = JAR_TOOL.run(out, err, cmdline.split(" +"));
if (rc != 0) {
throw new IOException(baes.toString());
}
} finally {
System.setErr(saveErr);
}
return baes.toString();
}

private void println() throws IOException {
System.out.println(new String(baos.toByteArray()));
}
}
Loading

0 comments on commit e45287d

Please sign in to comment.