Hi,
Manifest parsing an input stream given to the constructor or the read
method ignores the contents of the last line unless terminated with a
line break.
In course of my attempt to fix 8217375, I found that Manifest ignores
the last line when not terminated with a line break whereas
ManifestDigester throws an exception.
Without ManifestDigester's different behavior, I'd have opted for
parsing the last line also without a line break (or the last line being
empty).
But ManifestDigester cannot easily be changed that way, too, because
the line breaks are part of the digested manifest portions.
I don't think Manifest should accept any manifests that cannot later as
well be signed and should therefore raise an error when there is no
line break at the end of a manifest.
Adding a new error condition adds potential for compatibility issues.
On the other hand, I don't think the last 'ill'-terminated line should
really be ignored silently.
A good example how things can go wrong this way is
JavaClassPathTest.java. It uses a one-line manifest without a trailing
line break and m1.jar does not contain the expected Class-Path entry.
In the long term the best option would probably be to re-unite
Manifest's and ManifestDigester's parsing-related code the biggest
challenge of which is not to change digests and thereby break existing
jar signatures.
Attached is a patch with a proposed fix.
The bug number in
test/jdk/java/util/jar/Manifest/NoLineBreakAtEndOfManifestFile.java is
from another bug that lead me to it. If someone creates another bug it
can be replaced. I haven't found an existing issue that matches.
In test/jdk/tools/launcher/modules/classpath/JavaClassPathTest.java is
a test case testJARManifestClassPathAttribute/testClassPathAttribute
which I have no clue how this should really work. As far as I can tell
the test is and was wrong but nevertheless passes.
Is there a chance to find a sponsor for fixing this?
Regards,
Philipp
diff -r 7c17199fa37d src/java.base/share/classes/java/util/jar/Attributes.java
--- a/src/java.base/share/classes/java/util/jar/Attributes.java Fri Feb 15 08:21:08 2019 -0500
+++ b/src/java.base/share/classes/java/util/jar/Attributes.java Fri Feb 15 21:57:03 2019 +0100
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 1997, 2018, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1997, 2019, 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
@@ -367,7 +367,7 @@
byte[] lastline = null;
int len;
- while ((len = is.readLine(lbuf)) != -1) {
+ while ((len = is.readLine(lbuf)) > 0) {
boolean lineContinued = false;
byte c = lbuf[--len];
lineNumber++;
diff -r 7c17199fa37d src/java.base/share/classes/java/util/jar/Manifest.java
--- a/src/java.base/share/classes/java/util/jar/Manifest.java Fri Feb 15 08:21:08 2019 -0500
+++ b/src/java.base/share/classes/java/util/jar/Manifest.java Fri Feb 15 21:57:03 2019 +0100
@@ -299,12 +299,13 @@
boolean skipEmptyLines = true;
byte[] lastline = null;
- while ((len = fis.readLine(lbuf)) != -1) {
+ while ((len = fis.readLine(lbuf)) > 0) {
byte c = lbuf[--len];
lineNumber++;
if (c != '\n' && c != '\r') {
- throw new IOException("manifest line too long ("
+ throw new IOException("manifest line too long"
+ + " or no line break on last line ("
+ getErrorPosition(jarFilename, lineNumber) + ")");
}
if (len > 0 && lbuf[len-1] == '\r') {
@@ -461,7 +462,10 @@
/*
* Reads 'len' bytes from the input stream, or until an end-of-line
- * is reached. Returns the number of bytes read.
+ * or the end of the input stream is reached.
+ * Returns the number of bytes read. If that returned number of bytes
+ * read is zero, the end of the stream has been reached
+ * and zero is returned as the number of read bytes.
*/
public int readLine(byte[] b, int off, int len) throws IOException {
byte[] tbuf = this.buf;
@@ -472,7 +476,7 @@
fill();
avail = count - pos;
if (avail <= 0) {
- return -1;
+ break;
}
}
int n = len - total;
diff -r 7c17199fa37d test/jdk/java/util/jar/Manifest/IncludeInExceptionsTest.java
--- a/test/jdk/java/util/jar/Manifest/IncludeInExceptionsTest.java Fri Feb 15 08:21:08 2019 -0500
+++ b/test/jdk/java/util/jar/Manifest/IncludeInExceptionsTest.java Fri Feb 15 21:57:03 2019 +0100
@@ -21,7 +21,6 @@
* questions.
*/
-import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
diff -r 7c17199fa37d test/jdk/java/util/jar/Manifest/NoLineBreakAtEndOfManifestFile.java
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/util/jar/Manifest/NoLineBreakAtEndOfManifestFile.java Fri Feb 15 21:57:03 2019 +0100
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2019, 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.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.jar.Manifest;
+import java.util.jar.Attributes.Name;
+
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.testng.Assert.*;
+
+/**
+ * @test
+ * @bug 8217375
+ * @run testng NoLineBreakAtEndOfManifestFile
+ * @summary Check that manifests don't just ignore the last line if not
+ * properly terminated with a line break.
+ */
+public class NoLineBreakAtEndOfManifestFile {
+
+ @DataProvider(name = "invalidManifests")
+ public static Object[][] invalidManifests() {
+ return new Object[][] {
+ { Name.MANIFEST_VERSION + ": 1.0" },
+ { Name.MANIFEST_VERSION + ": 1.0\r\n" +
+ "\r\n" +
+ "Name: Section Name\r\n" +
+ "X: Y" }
+ };
+ }
+
+ @Test(dataProvider = "invalidManifests")
+ public void testConstructor(String manifest) {
+ assertThrows(IOException.class, () -> {
+ new Manifest(
+ new ByteArrayInputStream(manifest.getBytes(UTF_8)));
+ });
+ }
+
+ @Test(dataProvider = "invalidManifests")
+ public void testRead(String manifest) {
+ assertThrows(IOException.class, () -> {
+ new Manifest().read(
+ new ByteArrayInputStream(manifest.getBytes(UTF_8)));
+ });
+ }
+
+}
diff -r 7c17199fa37d test/jdk/tools/launcher/modules/classpath/JavaClassPathTest.java
--- a/test/jdk/tools/launcher/modules/classpath/JavaClassPathTest.java Fri Feb 15 08:21:08 2019 -0500
+++ b/test/jdk/tools/launcher/modules/classpath/JavaClassPathTest.java Fri Feb 15 21:57:03 2019 +0100
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2016, 2019, 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
@@ -21,13 +21,14 @@
* questions.
*/
-import java.io.BufferedWriter;
+import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.jar.Attributes;
import java.util.spi.ToolProvider;
import java.util.stream.Stream;
@@ -86,13 +87,13 @@
file.toString()) == 0);
Path manifest = LIB_DIR.resolve("manifest");
- try (BufferedWriter writer = Files.newBufferedWriter(manifest)) {
- writer.write("CLASS-PATH: lib/m.jar");
+ try (PrintWriter writer = new PrintWriter(manifest.toFile())) {
+ writer.println(Attributes.Name.CLASS_PATH + ": lib/m.jar");
}
- jarfile = LIB_DIR.resolve("m1.jar");
+ Path cpJarfile = LIB_DIR.resolve("m-cp.jar");
assertTrue(jartool.run(System.out, System.err, "cfme",
- jarfile.toString(), manifest.toString(), TEST_MAIN,
- file.toString()) == 0);
+ cpJarfile.toString(), manifest.toString(),
+ TEST_MAIN, file.toString()) == 0);
}
@DataProvider(name = "classpath")
@@ -144,7 +145,6 @@
args.add(Boolean.toString(false));
args.add(expected);
-
assertTrue(execute(args).getExitValue() == 0);
}
@@ -175,17 +175,17 @@
}
/*
- * Test CLASS-PATH attribute in manifest
+ * Test "Class-Path" attribute in manifest
*/
@Test
- public void testClassPathAttribute() throws Throwable {
- String jarfile = LIB_DIR.resolve("m1.jar").toString();
+ public void testJARManifestClassPathAttribute() throws Throwable {
+ String jarfile = LIB_DIR.resolve("m-cp.jar").toString();
List<String> args = new ArrayList<>();
args.add("-jar");
args.add(jarfile);
args.add(Boolean.toString(false));
- args.add(jarfile);
+ args.add(jarfile); // not the class path specified in the manifest
assertTrue(execute(args).getExitValue() == 0);
@@ -197,6 +197,10 @@
args.add(jarfile);
assertTrue(execute(args).getExitValue() == 0);
+
+ // TODO: How exactly does this test show that the JAR manifest
+ // Class-Path makes a difference or has any effect?
+ // I haven't understood that...
}
private OutputAnalyzer execute(List<String> options) throws Throwable {
diff -r 7c17199fa37d test/jdk/tools/launcher/modules/classpath/src/m/jdk/test/Main.java
--- a/test/jdk/tools/launcher/modules/classpath/src/m/jdk/test/Main.java Fri Feb 15 08:21:08 2019 -0500
+++ b/test/jdk/tools/launcher/modules/classpath/src/m/jdk/test/Main.java Fri Feb 15 21:57:03 2019 +0100
@@ -1,5 +1,5 @@
-/**
- * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
+/*
+ * Copyright (c) 2016, 2019, 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
@@ -29,9 +29,10 @@
static final String JAVA_CLASS_PATH = "java.class.path";
public static void main(String[] args) throws Exception {
- boolean expected = args[0].equals("true");
+ boolean expected = Boolean.parseBoolean(args[0]);
String cpath = args.length > 1 ? args[1] : "";
String value = System.getProperty(JAVA_CLASS_PATH);
+ System.out.println(JAVA_CLASS_PATH + " = " + value);
if (!value.equals(cpath)) {
throw new RuntimeException(JAVA_CLASS_PATH + "=" + value +
" expected=" + cpath);
@@ -39,8 +40,11 @@
ClassLoader loader = ClassLoader.getSystemClassLoader();
URL url = loader.getResource("jdk/test/res.properties");
+ System.out.println("jdk/test/res.properties " +
+ (url != null ? "present" : "not present") + " in class path");
if ((expected && url == null) || (!expected && url != null)) {
- throw new RuntimeException("URL: " + url + " expected non-null: " + expected);
+ throw new RuntimeException("URL: " + url +
+ " expected non-null: " + expected);
}
}
}