From c644d77a20d14b851d19049513de30036cfe38bb Mon Sep 17 00:00:00 2001 From: Alexey Bakhtin Date: Tue, 26 Nov 2024 15:24:11 -0800 Subject: [PATCH] Backport 158b93d19a518d2b9d3d185e2d4c4dbff9c82aab --- jdk/src/share/classes/sun/tools/jar/Main.java | 23 +- .../sun/tools/jar/resources/jar.properties | 16 +- jdk/test/tools/jar/ExtractFilesTest.java | 241 ++++++++++++++++++ jdk/test/tools/jar/MultipleManifestTest.java | 219 ++++++++++++++++ 4 files changed, 497 insertions(+), 2 deletions(-) create mode 100644 jdk/test/tools/jar/ExtractFilesTest.java create mode 100644 jdk/test/tools/jar/MultipleManifestTest.java diff --git a/jdk/src/share/classes/sun/tools/jar/Main.java b/jdk/src/share/classes/sun/tools/jar/Main.java index aab29ad0bbe..03891a02dd8 100644 --- a/jdk/src/share/classes/sun/tools/jar/Main.java +++ b/jdk/src/share/classes/sun/tools/jar/Main.java @@ -75,9 +75,10 @@ class Main { * iflag: generate jar index * nflag: Perform jar normalization at the end * pflag: preserve/don't strip leading slash and .. component from file name + * kflag: keep existing file * */ - boolean cflag, uflag, xflag, tflag, vflag, flag0, Mflag, iflag, nflag, pflag; + boolean cflag, uflag, xflag, tflag, vflag, flag0, Mflag, iflag, nflag, pflag, kflag; static final String MANIFEST_DIR = "META-INF/"; static final String VERSION = "1.0"; @@ -397,6 +398,9 @@ boolean parseArgs(String args[]) { case '0': flag0 = true; break; + case 'k': + kflag = true; + break; case 'i': if (cflag || uflag || xflag || tflag) { usageError(); @@ -431,6 +435,10 @@ boolean parseArgs(String args[]) { usageError(); return false; } + if (kflag && !xflag) { + warn(formatMsg("warn.option.is.ignored", "-k/k")); + } + /* parse file arguments */ int n = args.length - count; if (n > 0) { @@ -1058,6 +1066,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()) { @@ -1280,6 +1294,13 @@ protected void error(String s) { err.println(s); } + /** + * Print a warning message + */ + void warn(String s) { + err.println(s); + } + /** * Main routine to start program. */ diff --git a/jdk/src/share/classes/sun/tools/jar/resources/jar.properties b/jdk/src/share/classes/sun/tools/jar/resources/jar.properties index 3b87385c40b..d06ff5c5350 100644 --- a/jdk/src/share/classes/sun/tools/jar/resources/jar.properties +++ b/jdk/src/share/classes/sun/tools/jar/resources/jar.properties @@ -46,6 +46,8 @@ error.incorrect.length=\ incorrect length while processing: {0} error.create.tempfile=\ Could not create a temporary file +warn.option.is.ignored=\ + Warning: The {0} option is not valid with current usage, will be ignored. out.added.manifest=\ added manifest out.update.manifest=\ @@ -62,6 +64,8 @@ out.create=\ \ \ created: {0} out.extracted=\ extracted: {0} +out.kept=\ + \ \ skipped: {0} exists out.inflated=\ \ inflated: {0} out.size=\ @@ -72,7 +76,10 @@ Usage: jar {ctxui}[vfmn0PMe] [jar-file] [manifest-file] [entry-point] [-C dir] f Options:\n\ \ \ -c create new archive\n\ \ \ -t list table of contents for archive\n\ -\ \ -x extract named (or all) files from archive\n\ +\ \ -x, 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.\n\ \ \ -u update existing archive\n\ \ \ -v generate verbose output on standard output\n\ \ \ -f specify archive file name\n\ @@ -85,6 +92,13 @@ Options:\n\ \ \ -M do not create a manifest file for the entries\n\ \ \ -i generate index information for the specified jar files\n\ \ \ -C change to the specified directory and include the following file\n\ +Operation modifiers valid only in extract mode:\n\ +\ \ -k 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.\n\ If any file is a directory then it is processed recursively.\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\ diff --git a/jdk/test/tools/jar/ExtractFilesTest.java b/jdk/test/tools/jar/ExtractFilesTest.java new file mode 100644 index 00000000000..717aa7d5803 --- /dev/null +++ b/jdk/test/tools/jar/ExtractFilesTest.java @@ -0,0 +1,241 @@ +/* + * 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 /lib/testlibrary + * @build jdk.test.lib.Platform + * jdk.testlibrary.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.nio.file.Paths; +import java.util.Arrays; +import java.util.stream.Stream; + +import jdk.testlibrary.FileUtils; +import sun.tools.jar.Main; + +@TestInstance(Lifecycle.PER_CLASS) +public class ExtractFilesTest { + 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("-xkvf 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()); + } + + /** + * Test jar will issue warning when use keep option in non-extraction mode. + */ + @Test + public void testWarningOnInvalidKeepOption() throws IOException { + String 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 -k/k option is not valid with current usage, will be ignored." + nl, err); + } + + private Stream mkpath(String... args) { + return Arrays.stream(args).map(d -> Paths.get(".", 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 { + Path p = Paths.get(".", path.split("/")); + Files.write(p, text.getBytes()); + } catch (IOException x) { + throw new UncheckedIOException(x); + } + } + + private String cat(String path) { + System.out.println("cat " + path); + try { + return new String(Files.readAllBytes(Paths.get(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 { + if (!new Main(out, err, "jar").run(cmdline.split(" +"))) { + throw new IOException(baes.toString()); + } + } finally { + System.setErr(saveErr); + } + return baes.toString(); + } + + private void println() throws IOException { + System.out.println(new String(baos.toByteArray())); + } +} diff --git a/jdk/test/tools/jar/MultipleManifestTest.java b/jdk/test/tools/jar/MultipleManifestTest.java new file mode 100644 index 00000000000..c62fef47dd2 --- /dev/null +++ b/jdk/test/tools/jar/MultipleManifestTest.java @@ -0,0 +1,219 @@ +/* + * 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 with multpile manifest files + * @library /test/lib /lib/testlibrary + * @build jdk.test.lib.Platform + * jdk.testlibrary.FileUtils + * @run junit/othervm MultipleManifestTest + */ + +import java.io.ByteArrayOutputStream; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +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.IOException; +import java.io.InputStream; +import java.io.PrintStream; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import jdk.testlibrary.FileUtils; +import sun.tools.jar.Main; + +@TestInstance(Lifecycle.PER_CLASS) +class MultipleManifestTest { + private final String nl = System.lineSeparator(); + private final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + private final PrintStream jarOut = new PrintStream(baos); + + static final Path zip = Paths.get("MultipleManifestTest.jar"); + static final String jdkVendor = System.getProperty("java.vendor"); + static final String jdkVersion = System.getProperty("java.version"); + static final String MANIFEST1 = "Manifest-Version: 1.0" + + System.lineSeparator() + + "Created-By: " + jdkVersion + " (" + jdkVendor + ")"; + static final String MANIFEST2 = "Manifest-Version: 2.0" + + System.lineSeparator() + + "Created-By: " + jdkVersion + " (" + jdkVendor + ")"; + static final String MANIFEST3 = "Manifest-Version: 3.0" + + System.lineSeparator() + + "Created-By: " + jdkVersion + " (" + jdkVendor + ")"; + private static final String META_INF = "META-INF/"; + + /** + * Delete the ZIP file produced by this test + * + * @throws IOException if an unexpected IOException occurs + */ + @AfterAll + public void cleanup() throws IOException { + Files.deleteIfExists(zip); + } + + /** + * Create a JAR with the Manifest as the 1st, 2nd and 4th entry + * + * @throws IOException if an error occurs + */ + @BeforeAll + public void writeManifestAsFirstSecondAndFourthEntry() throws IOException { + int locPosA, locPosB, cenPos; + System.out.printf("%n%n*****Creating Jar with the Manifest as the 1st, 2nd and 4th entry*****%n%n"); + ByteArrayOutputStream out = new ByteArrayOutputStream(1024); + try (ZipOutputStream zos = new ZipOutputStream(out)) { + zos.putNextEntry(new ZipEntry(JarFile.MANIFEST_NAME)); + zos.write(MANIFEST1.getBytes(StandardCharsets.UTF_8)); + zos.closeEntry(); + locPosA = out.size(); + zos.putNextEntry(new ZipEntry(META_INF + "AANIFEST.MF")); + zos.write(MANIFEST2.getBytes(StandardCharsets.UTF_8)); + zos.putNextEntry(new ZipEntry("entry1.txt")); + zos.write("entry1".getBytes(StandardCharsets.UTF_8)); + zos.closeEntry(); + locPosB = out.size(); + zos.putNextEntry(new ZipEntry(META_INF + "BANIFEST.MF")); + zos.write(MANIFEST3.getBytes(StandardCharsets.UTF_8)); + zos.putNextEntry(new ZipEntry("entry2.txt")); + zos.write("hello entry2".getBytes(StandardCharsets.UTF_8)); + zos.flush(); + cenPos = out.size(); + } + byte[] template = out.toByteArray(); + // ISO_8859_1 to keep the 8-bit value + String s = new String(template, StandardCharsets.ISO_8859_1); + // change META-INF/AANIFEST.MF to META-INF/MANIFEST.MF + int loc = s.indexOf("AANIFEST.MF", locPosA); + int cen = s.indexOf("AANIFEST.MF", cenPos); + template[loc] = template[cen] = (byte) 'M'; + // change META-INF/BANIFEST.MF to META-INF/MANIFEST.MF + loc = s.indexOf("BANIFEST.MF", locPosB); + cen = s.indexOf("BANIFEST.MF", cenPos); + template[loc] = template[cen] = (byte) 'M'; + Files.write(zip, template); + } + + @AfterEach + public void removeExtractedFiles() { + rm("META-INF entry1.txt entry2.txt"); + } + + /** + * Extract by default should have the last manifest. + */ + @Test + public void testOverwrite() throws IOException { + jar("xvf " + zip.toString()); + println(); + Assertions.assertEquals("3.0", getManifestVersion()); + String output = " inflated: META-INF/MANIFEST.MF" + nl + + " inflated: META-INF/MANIFEST.MF" + nl + + " inflated: entry1.txt" + nl + + " inflated: META-INF/MANIFEST.MF" + nl + + " inflated: entry2.txt" + nl; + Assertions.assertArrayEquals(baos.toByteArray(), output.getBytes()); + } + + /** + * Extract with k option should have first manifest. + */ + @Test + public void testKeptOldFile() throws IOException { + jar("xkvf " + zip.toString()); + println(); + Assertions.assertEquals("1.0", getManifestVersion()); + String output = " inflated: META-INF/MANIFEST.MF" + nl + + " skipped: META-INF/MANIFEST.MF exists" + nl + + " inflated: entry1.txt" + nl + + " skipped: META-INF/MANIFEST.MF exists" + nl + + " inflated: entry2.txt" + nl; + Assertions.assertArrayEquals(baos.toByteArray(), output.getBytes()); + } + + private String getManifestVersion() throws IOException { + try (InputStream is = Files.newInputStream(Paths.get(JarFile.MANIFEST_NAME))) { + Manifest manifest = new Manifest(is); + return manifest.getMainAttributes().getValue(Attributes.Name.MANIFEST_VERSION); + } + } + + private void 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 { + if (!new Main(jarOut, err, "jar").run(cmdline.split(" +"))) { + throw new IOException(baes.toString()); + } + } finally { + System.setErr(saveErr); + } + } + + private void println() throws IOException { + System.out.println(new String(baos.toByteArray())); + } + + private Stream mkpath(String... args) { + return Arrays.stream(args).map(d -> Paths.get(".", d.split("/"))); + } + + 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); + } + }); + } +} \ No newline at end of file