Skip to content

Commit bdc07d2

Browse files
author
Alexey Bakhtin
committedDec 4, 2024
8335912: Add an operation mode to the jar command when extracting to not overwriting existing files
Reviewed-by: mbaesken, mbalao Backport-of: 158b93d19a518d2b9d3d185e2d4c4dbff9c82aab
1 parent f23d6bf commit bdc07d2

File tree

5 files changed

+528
-2
lines changed

5 files changed

+528
-2
lines changed
 

‎src/jdk.jartool/share/classes/sun/tools/jar/GNUStyleOptions.java

+8
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,13 @@ void process(Main jartool, String opt, String arg) throws BadArgs {
212212
}
213213
},
214214

215+
// Extract options
216+
new Option(false, OptionType.EXTRACT, "--keep-old-files", "-k") {
217+
void process(Main jartool, String opt, String arg) {
218+
jartool.kflag = true;
219+
}
220+
},
221+
215222
// Hidden options
216223
new Option(false, OptionType.OTHER, "-P") {
217224
void process(Main jartool, String opt, String arg) {
@@ -254,6 +261,7 @@ enum OptionType {
254261
CREATE("create"),
255262
CREATE_UPDATE("create.update"),
256263
CREATE_UPDATE_INDEX("create.update.index"),
264+
EXTRACT("extract"),
257265
OTHER("other");
258266

259267
/** Resource lookup section prefix. */

‎src/jdk.jartool/share/classes/sun/tools/jar/Main.java

+14-1
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,9 @@ public int hashCode() {
155155
* nflag: Perform jar normalization at the end
156156
* pflag: preserve/don't strip leading slash and .. component from file name
157157
* dflag: print module descriptor
158+
* kflag: keep existing file
158159
*/
159-
boolean cflag, uflag, xflag, tflag, vflag, flag0, Mflag, iflag, pflag, dflag, validate;
160+
boolean cflag, uflag, xflag, tflag, vflag, flag0, Mflag, iflag, pflag, dflag, kflag, validate;
160161

161162
boolean suppressDeprecateMsg = false;
162163

@@ -581,6 +582,9 @@ boolean parseArgs(String args[]) {
581582
case '0':
582583
flag0 = true;
583584
break;
585+
case 'k':
586+
kflag = true;
587+
break;
584588
case 'i':
585589
if (cflag || uflag || xflag || tflag) {
586590
usageError(getMsg("error.multiple.main.operations"));
@@ -611,6 +615,9 @@ boolean parseArgs(String args[]) {
611615
usageError(getMsg("error.bad.option"));
612616
return false;
613617
}
618+
if (kflag && !xflag) {
619+
warn(formatMsg("warn.option.is.ignored", "--keep-old-files/-k/k"));
620+
}
614621

615622
/* parse file arguments */
616623
int n = args.length - count;
@@ -1451,6 +1458,12 @@ ZipEntry extractFile(InputStream is, ZipEntry e) throws IOException {
14511458
output(formatMsg("out.create", name));
14521459
}
14531460
} else {
1461+
if (f.exists() && kflag) {
1462+
if (vflag) {
1463+
output(formatMsg("out.kept", name));
1464+
}
1465+
return rc;
1466+
}
14541467
if (f.getParent() != null) {
14551468
File d = new File(f.getParent());
14561469
if (!d.exists() && !d.mkdirs() || !d.isDirectory()) {

‎src/jdk.jartool/share/classes/sun/tools/jar/resources/jar.properties

+17-1
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,8 @@ warn.release.unexpected.versioned.entry=\
137137
unexpected versioned entry {0}
138138
warn.flag.is.deprecated=\
139139
Warning: The {0} option is deprecated, and is planned for removal in a future JDK release\n
140+
warn.option.is.ignored=\
141+
Warning: The {0} option is not valid with current usage, will be ignored.
140142
out.added.manifest=\
141143
added manifest
142144
out.added.module-info=\
@@ -159,6 +161,8 @@ out.create=\
159161
\ \ created: {0}
160162
out.extracted=\
161163
extracted: {0}
164+
out.kept=\
165+
\ \ skipped: {0} exists
162166
out.inflated=\
163167
\ inflated: {0}
164168
out.size=\
@@ -236,7 +240,10 @@ main.help.opt.main.list=\
236240
main.help.opt.main.update=\
237241
\ -u, --update Update an existing jar archive
238242
main.help.opt.main.extract=\
239-
\ -x, --extract Extract named (or all) files from the archive
243+
\ -x, --extract Extract named (or all) files from the archive.\n\
244+
\ If a file with the same name appears more than once in\n\
245+
\ the archive, each copy will be extracted, with later copies\n\
246+
\ overwriting (replacing) earlier copies unless -k is specified.
240247
main.help.opt.main.describe-module=\
241248
\ -d, --describe-module Print the module descriptor, or automatic module name
242249
main.help.opt.main.validate=\
@@ -298,6 +305,15 @@ main.help.opt.create.update.index.date=\
298305
\ --date=TIMESTAMP The timestamp in ISO-8601 extended offset date-time with\n\
299306
\ optional time-zone format, to use for the timestamps of\n\
300307
\ entries, e.g. "2022-02-12T12:30:00-05:00"
308+
main.help.opt.extract=\
309+
\ Operation modifiers valid only in extract mode:\n
310+
main.help.opt.extract.keep-old-files=\
311+
\ -k, --keep-old-files Do not overwrite existing files.\n\
312+
\ If a Jar file entry with the same name exists in the target\n\
313+
\ directory, the existing file will not be overwritten.\n\
314+
\ As a result, if a file appears more than once in an\n\
315+
\ archive, later copies will not overwrite earlier copies.\n\
316+
\ Also note that some file system can be case insensitive.
301317
main.help.opt.other=\
302318
\ Other options:\n
303319
main.help.opt.other.help=\
+265
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
/*
2+
* Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
3+
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4+
*
5+
* This code is free software; you can redistribute it and/or modify it
6+
* under the terms of the GNU General Public License version 2 only, as
7+
* published by the Free Software Foundation.
8+
*
9+
* This code is distributed in the hope that it will be useful, but WITHOUT
10+
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11+
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12+
* version 2 for more details (a copy is included in the LICENSE file that
13+
* accompanied this code).
14+
*
15+
* You should have received a copy of the GNU General Public License version
16+
* 2 along with this work; if not, write to the Free Software Foundation,
17+
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18+
*
19+
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20+
* or visit www.oracle.com if you need additional information or have any
21+
* questions.
22+
*/
23+
24+
/*
25+
* @test
26+
* @bug 8335912
27+
* @summary test extract jar files overwrite existing files behavior
28+
* @library /test/lib
29+
* @modules jdk.jartool
30+
* @build jdk.test.lib.Platform
31+
* jdk.test.lib.util.FileUtils
32+
* @run junit/othervm ExtractFilesTest
33+
*/
34+
35+
import org.junit.jupiter.api.AfterAll;
36+
import org.junit.jupiter.api.Assertions;
37+
import org.junit.jupiter.api.BeforeAll;
38+
import org.junit.jupiter.api.Test;
39+
import org.junit.jupiter.api.TestInstance;
40+
import org.junit.jupiter.api.TestInstance.Lifecycle;
41+
42+
import java.io.ByteArrayOutputStream;
43+
import java.io.IOException;
44+
import java.io.PrintStream;
45+
import java.io.UncheckedIOException;
46+
import java.nio.file.Files;
47+
import java.nio.file.Path;
48+
import java.util.Arrays;
49+
import java.util.spi.ToolProvider;
50+
import java.util.stream.Stream;
51+
52+
import jdk.test.lib.util.FileUtils;
53+
54+
@TestInstance(Lifecycle.PER_CLASS)
55+
public class ExtractFilesTest {
56+
private static final ToolProvider JAR_TOOL = ToolProvider.findFirst("jar")
57+
.orElseThrow(() ->
58+
new RuntimeException("jar tool not found")
59+
);
60+
61+
private final String nl = System.lineSeparator();
62+
private final ByteArrayOutputStream baos = new ByteArrayOutputStream();
63+
private final PrintStream out = new PrintStream(baos);
64+
65+
@BeforeAll
66+
public void setupJar() throws IOException {
67+
mkdir("test1 test2");
68+
echo("testfile1", "test1/testfile1");
69+
echo("testfile2", "test2/testfile2");
70+
jar("cf test.jar -C test1 . -C test2 .");
71+
rm("test1 test2");
72+
}
73+
74+
@AfterAll
75+
public void cleanup() {
76+
rm("test.jar");
77+
}
78+
79+
/**
80+
* Regular clean extract with expected output.
81+
*/
82+
@Test
83+
public void testExtract() throws IOException {
84+
jar("xvf test.jar");
85+
println();
86+
String output = " created: META-INF/" + nl +
87+
" inflated: META-INF/MANIFEST.MF" + nl +
88+
" inflated: testfile1" + nl +
89+
" inflated: testfile2" + nl;
90+
rm("META-INF testfile1 testfile2");
91+
Assertions.assertArrayEquals(baos.toByteArray(), output.getBytes());
92+
}
93+
94+
/**
95+
* Extract should overwrite existing file as default behavior.
96+
*/
97+
@Test
98+
public void testOverwrite() throws IOException {
99+
touch("testfile1");
100+
jar("xvf test.jar");
101+
println();
102+
String output = " created: META-INF/" + nl +
103+
" inflated: META-INF/MANIFEST.MF" + nl +
104+
" inflated: testfile1" + nl +
105+
" inflated: testfile2" + nl;
106+
Assertions.assertEquals("testfile1", cat("testfile1"));
107+
rm("META-INF testfile1 testfile2");
108+
Assertions.assertArrayEquals(baos.toByteArray(), output.getBytes());
109+
}
110+
111+
/**
112+
* Extract with legacy style option `k` should preserve existing files.
113+
*/
114+
@Test
115+
public void testKeptOldFile() throws IOException {
116+
touch("testfile1");
117+
jar("xkvf test.jar");
118+
println();
119+
String output = " created: META-INF/" + nl +
120+
" inflated: META-INF/MANIFEST.MF" + nl +
121+
" skipped: testfile1 exists" + nl +
122+
" inflated: testfile2" + nl;
123+
Assertions.assertEquals("", cat("testfile1"));
124+
Assertions.assertEquals("testfile2", cat("testfile2"));
125+
rm("META-INF testfile1 testfile2");
126+
Assertions.assertArrayEquals(baos.toByteArray(), output.getBytes());
127+
}
128+
129+
/**
130+
* Extract with gnu style -k should preserve existing files.
131+
*/
132+
@Test
133+
public void testGnuOptionsKeptOldFile() throws IOException {
134+
touch("testfile1 testfile2");
135+
jar("-x -k -v -f test.jar");
136+
println();
137+
String output = " created: META-INF/" + nl +
138+
" inflated: META-INF/MANIFEST.MF" + nl +
139+
" skipped: testfile1 exists" + nl +
140+
" skipped: testfile2 exists" + nl;
141+
Assertions.assertEquals("", cat("testfile1"));
142+
Assertions.assertEquals("", cat("testfile2"));
143+
rm("META-INF testfile1 testfile2");
144+
Assertions.assertArrayEquals(baos.toByteArray(), output.getBytes());
145+
}
146+
147+
/**
148+
* Extract with gnu style long option --keep-old-files should preserve existing files.
149+
*/
150+
@Test
151+
public void testGnuLongOptionsKeptOldFile() throws IOException {
152+
touch("testfile2");
153+
jar("-x --keep-old-files -v -f test.jar");
154+
println();
155+
String output = " created: META-INF/" + nl +
156+
" inflated: META-INF/MANIFEST.MF" + nl +
157+
" inflated: testfile1" + nl +
158+
" skipped: testfile2 exists" + nl;
159+
Assertions.assertEquals("testfile1", cat("testfile1"));
160+
Assertions.assertEquals("", cat("testfile2"));
161+
rm("META-INF testfile1 testfile2");
162+
Assertions.assertArrayEquals(baos.toByteArray(), output.getBytes());
163+
}
164+
165+
/**
166+
* Test jar will issue warning when use keep option in non-extraction mode.
167+
*/
168+
@Test
169+
public void testWarningOnInvalidKeepOption() throws IOException {
170+
var err = jar("tkf test.jar");
171+
println();
172+
173+
String output = "META-INF/" + nl +
174+
"META-INF/MANIFEST.MF" + nl +
175+
"testfile1" + nl +
176+
"testfile2" + nl;
177+
178+
Assertions.assertArrayEquals(baos.toByteArray(), output.getBytes());
179+
Assertions.assertEquals("Warning: The --keep-old-files/-k/k option is not valid with current usage, will be ignored." + nl, err);
180+
}
181+
182+
private Stream<Path> mkpath(String... args) {
183+
return Arrays.stream(args).map(d -> Path.of(".", d.split("/")));
184+
}
185+
186+
private void mkdir(String cmdline) {
187+
System.out.println("mkdir -p " + cmdline);
188+
mkpath(cmdline.split(" +")).forEach(p -> {
189+
try {
190+
Files.createDirectories(p);
191+
} catch (IOException x) {
192+
throw new UncheckedIOException(x);
193+
}
194+
});
195+
}
196+
197+
private void touch(String cmdline) {
198+
System.out.println("touch " + cmdline);
199+
mkpath(cmdline.split(" +")).forEach(p -> {
200+
try {
201+
Files.createFile(p);
202+
} catch (IOException x) {
203+
throw new UncheckedIOException(x);
204+
}
205+
});
206+
}
207+
208+
private void echo(String text, String path) {
209+
System.out.println("echo '" + text + "' > " + path);
210+
try {
211+
var p = Path.of(".", path.split("/"));
212+
Files.writeString(p, text);
213+
} catch (IOException x) {
214+
throw new UncheckedIOException(x);
215+
}
216+
}
217+
218+
private String cat(String path) {
219+
System.out.println("cat " + path);
220+
try {
221+
return Files.readString(Path.of(path));
222+
} catch (IOException x) {
223+
throw new UncheckedIOException(x);
224+
}
225+
}
226+
227+
private void rm(String cmdline) {
228+
System.out.println("rm -rf " + cmdline);
229+
mkpath(cmdline.split(" +")).forEach(p -> {
230+
try {
231+
if (Files.isDirectory(p)) {
232+
FileUtils.deleteFileTreeWithRetry(p);
233+
} else {
234+
FileUtils.deleteFileIfExistsWithRetry(p);
235+
}
236+
} catch (IOException x) {
237+
throw new UncheckedIOException(x);
238+
}
239+
});
240+
}
241+
242+
private String jar(String cmdline) throws IOException {
243+
System.out.println("jar " + cmdline);
244+
baos.reset();
245+
246+
// the run method catches IOExceptions, we need to expose them
247+
ByteArrayOutputStream baes = new ByteArrayOutputStream();
248+
PrintStream err = new PrintStream(baes);
249+
PrintStream saveErr = System.err;
250+
System.setErr(err);
251+
try {
252+
int rc = JAR_TOOL.run(out, err, cmdline.split(" +"));
253+
if (rc != 0) {
254+
throw new IOException(baes.toString());
255+
}
256+
} finally {
257+
System.setErr(saveErr);
258+
}
259+
return baes.toString();
260+
}
261+
262+
private void println() throws IOException {
263+
System.out.println(new String(baos.toByteArray()));
264+
}
265+
}

1 commit comments

Comments
 (1)

openjdk-notifier[bot] commented on Dec 4, 2024

@openjdk-notifier[bot]
Please sign in to comment.