RFR for 6443578 and 6202130: UTF-8 in Manifests (original) (raw)
Philipp Kunz philipp.kunz at paratix.ch
Wed May 2 05:21:37 UTC 2018
- Previous message: Hashing files/bytes Re: RFR(JDK11/NIO) 8202285: (fs) Add a method to Files for comparing file contents
- Next message: RFR for 6443578 and 6202130: UTF-8 in Manifests
- Messages sorted by: [ date ] [ thread ] [ subject ] [ author ]
Hi,
Recently, I tried to fix only bug 6202130 with the intention to fix bug 6443578 later with the intention to get some opportunity for feedback, but haven't got any, and propose now a fix for both together which in my opinion makes more sense.
See attached patch.
Some considerations, assumptions, and explanations
- In my opinion, the code for writing manifests was distributed in the two classes Attributes and Manifest in an elegant way but somewhat difficult to explain the coherence. I chose to group the code that writes manifests into a new class ManifestWriter. The main incentive for that was to prevent or reduce duplicated code I would have had to change twice otherwise. This also results in a source file of a suitable size.
- I could not support the assumption that the write and writeMain methods in Attributes couldn't be referenced anywhere so I deprecated them rather than having them removed.
- I assumed the patch will not make it into JDK 10 and, hence, the deprecated annotations are attributed with since = 11.
- I could not figure out any reason for the use of DataOutputStream and did not use it.
- Performance-wise I assume that the code is approximately comparable to the previous version. The biggest improvement in this respect I hope comes from removing the String that contains the byte array constructed with deprecated String(byte[], int, int, int) and then copying it over again to a StringBuffer and from there to a String again and then Characters. On the other hand, keeping whole characters together when breaking lines might make it slightly slower. I hope my changes are an overall improvement, but I haven't measured it.
- For telling first from continuation bytes of utf-8 characters apart I re-used a method isNotUtfContinuationByte from either StringCoding or UTF_8.Decoder. Unfortunately I found no way not to duplicate it.
- Where it said before "XXX Need to handle UTF8 values and break up lines longer than 72 bytes" in Attributes#writeMain I did not dare to remove the comment completely because it still does not deal correctly with version headers longer than 72 bytes and the set of allowed values. I changed it accordingly. Two similar comments are removed in the patch.
- I added two tests, WriteDeprecated and NullKeysAndValues, to demonstrate compatibility as good as I could. Might however not be desired to keep and having to maintain.
- LineBrokenMultiByteCharacter for jarsigner should not be removed or not so immediately because someone might attempt to sign an older jarfile created without that patch with a newer jarsigner that already contains it.
suggested changes or additions to the bug database: (i have no permissions to edit it myself)
- Re-combine copies of isNotUtfContinuationByte (three by now). Relates to 6184334. Worth to file another issue?
- Manifest versions have specific specifications, cannot break across lines and can contain a subset of characters only. Bug 6910466 relates but is not exactly the same. If someone else is convinced that writing a manifest should issue a warning or any other way to deal with a version that does not conform to the specification, I'd suggest to create a separate bug for that.
Now, I would be glad if someone sponsored a review. This is only my third attempt to submit a patch which is why I chose a lesser important subject to fix in order to get familiar and now I understand it's not the most attractive patch to review. Please don't hesitate to suggest what I could do better or differently.
As a bonus, with these changes, manifest files will always be displayed correctly with just any utf capable viewer even if they contain multi-byte utf characters that would have been broken across a line break with the current/previous implementation and all manifests will become also valid strings in Java.
Regards, Philipp
On 20.04.2018 00:58, Philipp Kunz wrote:
Hi,
I tried to fix bug 6202130 about manifest utf support and come up now with a test as suggested in the bug's comments that shows that utf charset actually works before removing the comments from the code. When I wanted to remove the XXX comments about utf it occurred to me that version attributes ("Signature-Version" and "Manifest-Version") would never be broken across lines and should anyway not support the whole utf character set which sounds more like related to bugs 6910466 or 4935610 but it's not a real fit. Therefore, I could not remove one such comment of Attributes#writeMain but I changed it. The first comment in bug 6202130 mentions only two comments but there are three in Attributes. In the attached patch I removed only two of three and changed the remaining third to not mention utf anymore. At the moment, at least until 6443578 is fixed, multi-byte utf characters can be broken across lines. It might be worth a consideration to test that explicitly as well but then I guess there is not much of a point in testing the current behavior that will change with 6443578, hopefully soon. There are in my opinion enough characters broken across lines in the attached test that demonstrate that this still works like it did before. I would have preferred also to remove the calls to deprecated String(byte[], int, int, int) but then figured it relates more to bug 6443578 than 6202130 and now prefer to do that in another separate patch. Bug 6202130 also states that lines are broken by String.length not by byte length. While it looks so at first glance, I could not confirm. The combination of getBytes("UTF8"), String(byte[], int, int, int), and then DataOutputStream.writeBytes(String) in that combination does not drop high-bytes because every byte (whether a whole character or only a part of a multi-byte character) becomes a character in String(...) containing that byte in its low-byte which will be read again from writeBytes(...). Or put in a different way, every utf encoded byte becomes a character and multi-byte utf characters are converted into multiple string characters containing one byte each in their lower bytes. The current solution is not nice, but at least works. With that respect I'd like to suggest to deprecate DataOutputStream.writeBytes(String) because it does something not exactly expected when guessing from its name and that would suit a byte[] parameter better very much like it has been done with String(byte[], int, int, int). Any advice about the procedure to deprecate something? I was surprised that it was not trivial to list all valid utf characters. If someone has a better idea than isValidUtfCharacter in the attached test, let me know. Altogether, I would not consider 6202130 resolved completely, unless maybe all remaining points are copied to 6443578 and maybe another bug about valid values for "Signature-Version" and "Manifest-Version" if at all desired. But still I consider the attached patch an improvement and most of the remainder can then be solved in 6443578 and so far I am looking forward to any kind of feedback. Regards, Philipp
diff -r 2ace90aec488 src/java.base/share/classes/java/util/jar/Attributes.java --- a/src/java.base/share/classes/java/util/jar/Attributes.java Mon Apr 30 21:56:54 2018 -0400 +++ b/src/java.base/share/classes/java/util/jar/Attributes.java Wed May 02 07:20:46 2018 +0200 @@ -296,27 +296,13 @@
/*
* Writes the current attributes to the specified data output stream.
* XXX Need to handle UTF8 values and break up lines longer than 72
bytes + * + * @deprecated moved to + * {@link ManifestWriter#writeSection(java.io.OutputStream)} */ - @SuppressWarnings("deprecation") - void write(DataOutputStream os) throws IOException { - for (Entry<Object, Object> e : entrySet()) { - StringBuffer buffer = new StringBuffer( - ((Name) e.getKey()).toString()); - buffer.append(": ");
String value = (String) e.getValue();
if (value != null) {
byte[] vb = value.getBytes("UTF8");
value = new String(vb, 0, 0, vb.length);
}
buffer.append(value);
Manifest.make72Safe(buffer);
buffer.append("\r\n");
os.writeBytes(buffer.toString());
}
os.writeBytes("\r\n");
- @Deprecated(since = "11")
- void write(DataOutputStream os) throws IOException {
new ManifestWriter(os).writeSection(this); } /*
@@ -324,50 +310,16 @@ * make sure to write out the MANIFEST_VERSION or SIGNATURE_VERSION * attributes first. * - * XXX Need to handle UTF8 values and break up lines longer than 72 bytes + * @deprecated moved to + * {@link ManifestWriter#writeMain(java.io.OutputStream)} */ - @SuppressWarnings("deprecation") - void writeMain(DataOutputStream out) throws IOException - { - // write out the *-Version header first, if it exists - String vername = Name.MANIFEST_VERSION.toString(); - String version = getValue(vername); - if (version == null) { - vername = Name.SIGNATURE_VERSION.toString(); - version = getValue(vername); - }
if (version != null) {
out.writeBytes(vername+": "+version+"\r\n");
}
// write out all attributes except for the version
// we wrote out earlier
for (Entry<Object, Object> e : entrySet()) {
String name = ((Name) e.getKey()).toString();
if ((version != null) && !(name.equalsIgnoreCase(vername))) {
StringBuffer buffer = new StringBuffer(name);
buffer.append(": ");
String value = (String) e.getValue();
if (value != null) {
byte[] vb = value.getBytes("UTF8");
value = new String(vb, 0, 0, vb.length);
}
buffer.append(value);
Manifest.make72Safe(buffer);
buffer.append("\r\n");
out.writeBytes(buffer.toString());
}
}
out.writeBytes("\r\n");
- @Deprecated(since = "11")
- void writeMain(DataOutputStream os) throws IOException {
new ManifestWriter(os).writeMain(this); } /* * Reads attributes from the specified input stream.
* XXX Need to handle UTF8 values. */ @SuppressWarnings("deprecation") void read(Manifest.FastInputStream is, byte[] lbuf) throws
IOException { diff -r 2ace90aec488 src/java.base/share/classes/java/util/jar/Manifest.java --- a/src/java.base/share/classes/java/util/jar/Manifest.java Mon Apr 30 21:56:54 2018 -0400 +++ b/src/java.base/share/classes/java/util/jar/Manifest.java Wed May 02 07:20:46 2018 +0200 @@ -26,7 +26,6 @@ package java.util.jar;
import java.io.FilterInputStream; -import java.io.DataOutputStream; import java.io.InputStream; import java.io.OutputStream; import java.io.IOException; @@ -143,31 +142,19 @@ * @exception IOException if an I/O error has occurred * @see #getMainAttributes */
- @SuppressWarnings("deprecation") public void write(OutputStream out) throws IOException {
DataOutputStream dos = new DataOutputStream(out);
// Write out the main attributes for the manifest
attr.writeMain(dos);
// Now write out the per-entry attributes
for (Map.Entry<String, Attributes> e : entries.entrySet()) {
StringBuffer buffer = new StringBuffer("Name: ");
String value = e.getKey();
if (value != null) {
byte[] vb = value.getBytes("UTF8");
value = new String(vb, 0, 0, vb.length);
}
buffer.append(value);
make72Safe(buffer);
buffer.append("\r\n");
dos.writeBytes(buffer.toString());
e.getValue().write(dos);
}
dos.flush();
new ManifestWriter(out).write(this); } /** * Adds line breaks to enforce a maximum 72 bytes per line.
*
* @see ManifestWriter#write72broken(String)
* @deprecated replaced by
* {@link ManifestWriter#write72broken(String)}
* which keeps whole utf character together when breaking lines */
- @Deprecated(since = "11") static void make72Safe(StringBuffer line) { int length = line.length(); int index = 72;
diff -r 2ace90aec488 src/java.base/share/classes/java/util/jar/ManifestWriter.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/java.base/share/classes/java/util/jar/ManifestWriter.java Wed May 02 07:20:46 2018 +0200 @@ -0,0 +1,224 @@ +/*
- Copyright (c) 2018, 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. Oracle designates this
- particular file as subject to the "Classpath" exception as provided
- by Oracle in the LICENSE file that accompanied this code.
- 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.
- */
- +package java.util.jar;
- +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.Map.Entry; +import java.util.jar.Attributes.Name;
- +/**
- ManifestWriter writes manifests and takes care of their structure,
- correct encoding regarding character set, and maximum line width.
- For information on the Manifest format, please see the
- Manifest format specification.
- @see Manifest#write(OutputStream)
- @since 11
- */ +class ManifestWriter {
- /**
* The utf-8 character set used for {@link Manifest}s.
*/
- private static final java.nio.charset.Charset UTF_8 =
StandardCharsets.UTF_8;
- private static final String
INDIVIDUAL_SECTION_NAME_HEADER_KEY = "Name",
KEY_VALUE_SEPARATOR = ": ",
LINE_BREAK = "\r\n",
CONTINUATION = " ";
- /**
- The <a
href="{@docRoot}/../specs/jar/jar.html#Notes_on_Manifest_and_Signature_Files">
- "Notes on Manifest and Signature Files" in the "JAR File
- Specification" state that
no line may be longer than
- Specification" state that
- 72 bytes (not characters), in its utf8-encoded
form.
- */
- private static final int MANIFEST_LINE_WIDTH = 72;
- /**
* The underlying output stream where to write a {@link Manifest} to by
* {@link #write(Manifest)}.
*/
- private final OutputStream out;
- /**
* Only constructor.
*
* @param out output stream where to write a {@link Manifest} to by
* {@link #write(Manifest)}
*/
- ManifestWriter(OutputStream out) {
this.out = out;
- }
- private static byte[] encode(String value) {
return value.getBytes(UTF_8);
- }
- private void write(String value) throws IOException {
out.write(encode(value));
- }
- private void write(byte[] value, int off, int len) throws IOException {
out.write(value, off, len);
- }
- private static final byte[] LINE_BREAK_BUF = encode(LINE_BREAK);
- private void writeLineBreak() throws IOException {
out.write(LINE_BREAK_BUF);
- }
- private static final byte[] LINE_BREAK_CONTINUATION_BUF =
encode(LINE_BREAK + CONTINUATION);
- private void writeContinuationLineBreak() throws IOException {
out.write(LINE_BREAK_CONTINUATION_BUF);
- }
- /**
* Writes a header line with added line breaks to enforce a maximum
of 72
* bytes per line with respect only to breaks whole characters and
writes
* the result to the current {@link ManifestWriter}'s underlying output
* stream.
*/
- private void write72broken(String header) throws IOException {
byte[] buf = encode(header);
int l = buf.length;
int p = 0; // start position in buf of current line to write
int n = MANIFEST_LINE_WIDTH; // number of bytes going on
current line
while (true) {
if (p + n >= l) {
n = l - p; // remainder fits on current (last) line
} else {
/*
* break whole utf characters only:
* put the current line end position on the next character
* start position / utf non-continuation byte left of and
* including n initial value / the maximum line width by
* decreasing the current line end position n until not
* followed by an utf continuation byte in order to not
break
* utf characters across line breaks
*/
while (!isNotUtfContinuationByte(buf[p + n])) n--;
}
write(buf, p, n);
p += n;
if (p == l) break;
n = MANIFEST_LINE_WIDTH - 1;
writeContinuationLineBreak();
}
- }
- /**
* @see StringCoding#isNotUtfContinuationByte(int)
* @see sun.nio.cs.UTF_8.Decoder#isNotUtfContinuationByte(int)
*/
- private static boolean isNotUtfContinuationByte(byte b) {
return (b & 0xc0) != 0x80;
- }
- private static String composeHeaderLine(String name, String value) {
return name + KEY_VALUE_SEPARATOR + value;
- }
- private void writeHeader(String name, String value) throws
IOException {
write72broken(composeHeaderLine(name, value));
writeLineBreak();
- }
- /**
* Writes the specified attributes to the current output stream.
*/
- void writeSection(Attributes attributes) throws IOException {
for (Entry<Object, Object> e : attributes.entrySet()) {
String name = ((Name) e.getKey()).toString();
writeHeader(name, (String) e.getValue());
}
writeLineBreak();
- }
- /**
* Writes the specified attributes to the current output stream,
* make sure to write out the MANIFEST_VERSION or SIGNATURE_VERSION
* attributes first.
*/
- /*
* XXX Need to handle version compliant to specification:
* - only digits and periods but no period at the beginning or end
* - not more than 72-16-1=55 characters in order not to exceed
line width
* - no space after colon or rather change specs and only 54
characters max
*/
- void writeMain(Attributes attributes) throws IOException {
// write out the *-Version header first, if it exists
String vername = Name.MANIFEST_VERSION.toString();
String version = attributes.getValue(vername);
if (version == null) {
vername = Name.SIGNATURE_VERSION.toString();
version = attributes.getValue(vername);
}
if (version != null) {
// version header cannot be continued on next line
write(composeHeaderLine(vername, version));
writeLineBreak();
}
// write out all attributes except for the version we wrote out
earlier
for (Entry<Object, Object> e : attributes.entrySet()) {
String name = ((Name) e.getKey()).toString();
if ((version != null) && !(name.equalsIgnoreCase(vername))) {
writeHeader(name, (String) e.getValue());
}
}
writeLineBreak();
- }
- /**
* Writes the given {@link Manifest} to the output stream of this
* {@link ManifestWriter} instance.
*
* @param mf the {@link Manifest} to write
*/
- void write(Manifest mf) throws IOException {
// Write out the main attributes for the manifest
writeMain(mf.getMainAttributes());
// Now write out the per-entry attributes
for (Entry<String, Attributes> e : mf.getEntries().entrySet()) {
writeHeader(INDIVIDUAL_SECTION_NAME_HEADER_KEY, e.getKey());
writeSection(e.getValue());
}
- }
- +} diff -r 2ace90aec488 test/jdk/java/util/jar/Attributes/WriteDeprecated.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/java/util/jar/Attributes/WriteDeprecated.java Wed May 02 07:20:46 2018 +0200 @@ -0,0 +1,123 @@ +/*
- Copyright (c) 2018, 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 static java.nio.charset.StandardCharsets.UTF_8;
- +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.jar.Attributes; +import java.util.jar.Attributes.Name; +import java.util.jar.Manifest; +import java.lang.reflect.Method;
- +import org.testng.annotations.Test; +import static org.testng.Assert.*;
- +/**
- @test
- @run testng/othervm --illegal-access=warn WriteDeprecated
- @summary Tests that the Attribute's write methods still work despite not
being used any longer by Manifest.
- / + at Deprecated +/
- Note to future maintainer:
- In order to actually being able to test Attributes' write methods work
- as before normal manifest and attributes manipulation through their public
- api is not sufficient but then these methods were there before and this
- way it's ensured that the behavior does not change with that respect.
- Once module isolation is enforced some test cases will not any longer be
- possible and those now tested situations will be guaranteed not to occur
- any longer at all at which point the corresponding tests can be removed
- safely without replacement unless of course another way is found to inject
- the tested null values.
- Another trick to access package private class members could be to use
- deserialization or adding a new class to the same package on the classpath.
- Here is not important how the methods are invoked because it shows that
- the behavior remains unchanged.
- */ +public class WriteDeprecated {
- static final Name KEY = new Name("some-key");
- static final String VALUE = "value";
- static class AccessibleAttributes extends Attributes {
void invoke(String name, DataOutputStream os) throws IOException {
try {
Method method = Attributes.class.getDeclaredMethod(
name, DataOutputStream.class);
method.setAccessible(true);
method.invoke(this, os);
} catch (ReflectiveOperationException e) {
fail(e.getMessage(), e);
}
}
/**
* @see Attributes#write(DataOutputStream)
*/
void write(DataOutputStream os) throws IOException {
invoke("write", os);
}
/**
* @see Attributes#writeMain(DataOutputStream)
*/
void writeMain(DataOutputStream os) throws IOException {
invoke("writeMain", os);
}
- }
- @Test
- public void testWriteMethods() throws Exception {
ByteArrayOutputStream out = new ByteArrayOutputStream();
AccessibleAttributes attributes = new AccessibleAttributes();
attributes.put(Name.MANIFEST_VERSION, "1.0");
attributes.put(KEY, VALUE);
DataOutputStream dos = new DataOutputStream(out);
attributes.writeMain(dos);
attributes.remove(Name.MANIFEST_VERSION);
dos.writeBytes("Name: " + VALUE + "\r\n");
attributes.write(dos);
dos.flush();
byte[] mfBytes = out.toByteArray();
- System.out.println("-------------------------------------------"
+ "-----------------------------");
System.out.print(new String(mfBytes, UTF_8));
- System.out.println("-------------------------------------------"
+ "-----------------------------");
Manifest mf = new Manifest(new ByteArrayInputStream(mfBytes));
assertEquals(mf.getMainAttributes().getValue(KEY), VALUE,
"main attributes header value");
Attributes attributes2 = mf.getAttributes(VALUE);
assertNotNull(attributes2, "named section not found");
assertEquals(attributes2.getValue(KEY), VALUE,
"named section attributes header value");
- }
- +} diff -r 2ace90aec488 test/jdk/java/util/jar/Manifest/LineBreakCharacter.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/jdk/java/util/jar/Manifest/LineBreakCharacter.java Wed May 02 07:20:46 2018 +0200 @@ -0,0 +1,405 @@ +/*
- Copyright (c) 2018, 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 static java.nio.charset.StandardCharsets.UTF_8;
- +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.jar.Attributes; +import java.util.jar.Manifest; +import java.util.jar.Attributes.Name; +import java.util.List; +import java.util.LinkedList;
- +import org.testng.annotations.Test; +import org.testng.annotations.DataProvider; +import static org.testng.Assert.*;
- +/**
- @test
- @bug 6443578
- @run testng/othervm --illegal-access=warn LineBreakCharacter
- @summary Tests reading and writing of jar manifests with headers broken
across lines in conjunction with multi-byte utf characters.
- Line breaks may or may not occur within utf encoded characters that are
- represented with more than one byte. Even though characters should not be
- broken across lines according to the specification the previous Manifest
- implementation did it and this test makes sure that no multi-byte characters
- are broken apart across a line break when writing manifests and manifests
- are read correctly no matter if multi-byte characters are continued after a
- line break.
- Correct support of all utf characters is the concern of
- {@link ValueUtfEncoding}.
- */ +public class LineBreakCharacter {
- static final int MANIFEST_LINE_CONTENT_WIDTH_BYTES = 72;
- static final String FILL = "x";
- /**
* By using names of four characters the same values can be used for
* testing line breaks at exact positions for both header and section
* names, header names in main and named sections, because a named
* section name is represented with a header with key "Name" that
occurs
* after a blank line and also has four characters.
*/
- static final String NAME = FILL + FILL + FILL + FILL;
- /**
* Cover main attributes headers, section names, and headers in named
* sections because an implementation might make a difference.
*/
- enum PositionInManifest {
MAIN_ATTRIBUTES, SECTION_NAME, NAMED_SECTION;
- }
- /**
* @see LineBreakLineWidth#numByteUtfCharacter(int, int)
*/
- static String numByteUtfCharacter(int numBytes, int seed) {
seed = seed < 0 ? -seed : seed;
if (numBytes == 1) {
seed %= 0x5F;
seed += 0x20; // exclude control characters (0..0x19) here
} else if (numBytes == 2) {
seed %= 0x800 - 0x80;
seed += 0x80;
} else if (numBytes == 3) {
seed %= 0x10000 - 0x800 + 0xFDD0 - 0xFDEF + 0xFFFE - 0xFFFF;
seed += 0x800
+ (seed >= 0xFDD0 ? 0xFDEF - 0xFDD0 : 0) //
non-characters
+ (seed % 0x10000) * (0xFFFF - 0xFFFE); // byte
order marks
} else {
seed %= 0x110000 - (0x10000 - 0xFFFE);
seed += 0x10000
+ (seed % 0x10000) * (0xFFFF - 0xFFFE); // byte
order marks
}
String string = new String(Character.toChars(seed));
assertEquals(string.getBytes(UTF_8).length, numBytes,
"self-test failed: unexpected utf encoded character
length");
return string;
- }
- @DataProvider(name = "lineBreakParameters")
- public static Object[][] lineBreakParameters() {
LinkedList<Object[]> params = new LinkedList<>();
// b: number of line breaks before character under test
for (int b = 0; b <= 3; b++) {
// c: character utf encoded length in bytes
for (int c = 1; c <= 4; c++) {
// p: potential break position offset in bytes
// p = 0 => before character
// p = c => after character and
// 0 < p < c => character potentially broken across
line break
// within the character
for (int p = c; p >= 0; p--) {
// a: no or one character following the one under test
// (a = 0 meaning the character under test is
followed by
// end of value which is also represented as a line
break)
for (int a = 0; a <= 1; a++) {
// offset: so many characters (actually bytes here
// filled with one byte characters) are needed
to place
// the next character into a position relative
to the
// maximum line width that it may or may not
have to be
// broken onto the next line
int offset =
// number of lines; - 1 is continuation
space
b * (MANIFEST_LINE_CONTENT_WIDTH_BYTES - 1)
// line length minus "Name: ".length()
+ MANIFEST_LINE_CONTENT_WIDTH_BYTES - 6
// position of maximum line width
relative to
// beginning of encoded character
- p;
int seed = ((a * 2 + p) * c + c) * 4 + b;
String value = "";
for (int i = 0; i < offset - 1; i++) {
value += numByteUtfCharacter(1, seed++);
}
// character before the one to test the break
value += FILL;
String character = numByteUtfCharacter(c, seed);
value += character;
for (int i = 0; i < a; i++) {
// character after the one to test the break
value += FILL;
}
for (PositionInManifest i :
PositionInManifest.values()) {
params.add(new Object[] {
b, c, p, a, i, character, value});
}
}
}
}
}
return params.toArray(new Object[0][0]);
- }
- /**
* Checks that multi-byte utf characters work well with line breaks and
* continuation lines in jar manifests.
*
* Produces otherwise arbitrary manifests so that a specific character
* will not just fit on the same line and a line break and continuation
* line are required with:<ul>
* <li>different encoded sizes of characters: one, two, three, and four
* bytes per character (including also utf bmp and surrogate
pairs)
* <li>different amount of space remaining on the same line or relative
* position of the latest possible line break position with respect
to the
* character that can be continued on the same line or will have to be
* continued on the next line after a line break</li>
* <li>different number of preceding line breaks</li>
* </ul>
* For each of these cases the break position is verified in the binary
* manifest.
*/
- @Test(dataProvider = "lineBreakParameters")
- public void testWriteLineBreaksKeepCharactersTogether(int b, int c,
int p,
int a, PositionInManifest i, String character, String value)
throws IOException {
byte[] mfBytes = writeManifest(i, NAME, value);
// expect the whole character on the next line unless it fits
// completely on the current line
boolean breakExpected = p < c;
String brokenPart = character;
if (breakExpected) {
brokenPart = "\r\n " + brokenPart;
}
// expect a line break before the next character if there is a next
// character and the previous not already broken on next line
if (a == 1) {
if (!breakExpected) {
brokenPart += "\r\n ";
}
brokenPart += FILL;
}
brokenPart = FILL + brokenPart + "\r\n";
assertOccurrence(mfBytes, brokenPart.getBytes(UTF_8));
readManifestAndAssertValue(mfBytes, i, NAME, value);
- }
- static byte[] writeManifest(PositionInManifest i, String name,
String value) throws IOException {
Manifest mf = new Manifest();
mf.getMainAttributes().put(Name.MANIFEST_VERSION, "1.0");
Attributes attributes = new Attributes();
switch (i) {
case MAIN_ATTRIBUTES:
mf.getMainAttributes().put(new Name(name), value);
break;
case SECTION_NAME:
mf.getEntries().put(value, attributes);
break;
case NAMED_SECTION:
mf.getEntries().put(NAME, attributes);
attributes.put(new Name(name), value);
break;
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
mf.write(out);
byte[] mfBytes = out.toByteArray();
- System.out.println("-------------------------------------------"
+ "-----------------------------");
System.out.print(new String(mfBytes, UTF_8));
- System.out.println("-------------------------------------------"
+ "-----------------------------");
return mfBytes;
- }
- static void assertOccurrence(byte[] mf, byte[] part) {
List<Integer> matchPos = new LinkedList<>();
for (int i = 0; i < mf.length; i++) {
for (int j = 0; j < part.length && i + j <= mf.length; j++) {
if (part[j] == 0) {
if (i + j != mf.length) {
break; // expected eof not found
}
} else if (i + j == mf.length) {
break;
} else if (mf[i + j] != part[j]) {
break;
}
if (j == part.length - 1) {
matchPos.add(i);
}
}
}
assertEquals(matchPos.size(), 1, "not "
+ (matchPos.size() < 1 ? "found" : "unique") + ": '"
+ new String(part, UTF_8) + "'");
- }
- static void readManifestAndAssertValue(
byte[] mfBytes, PositionInManifest i, String name, String
value)
throws IOException {
Manifest mf = new Manifest(new ByteArrayInputStream(mfBytes));
switch (i) {
case MAIN_ATTRIBUTES:
assertEquals(mf.getMainAttributes().getValue(name), value,
"main attributes header value");
break;
case SECTION_NAME:
Attributes attributes = mf.getAttributes(value);
assertNotNull(attributes, "named section not found");
break;
case NAMED_SECTION:
attributes =
mf.getAttributes(NAME);
assertEquals(attributes.getValue(name), value,
"named section attributes header value");
break;
}
- }
- @Test(dataProvider = "lineBreakParameters")
- public void readContinuationLines(int b, int c, int p, int a,
PositionInManifest i, String character, String value)
throws IOException {
byte[] mfBytes = writeManifestWithBrokenCharacters(i, NAME, value);
ByteArrayOutputStream buf = new ByteArrayOutputStream();
buf.write(FILL.getBytes(UTF_8));
byte[] characterBytes = character.getBytes(UTF_8);
// the portion of the character that fits on the current line
before
// a break at 72 bytes, ranges from nothing (p == 0) to the whole
// character (p == c)
for (int j = 0; j < p; j++) {
buf.write(characterBytes, j, 1);
}
// expect a line break at exactly 72 bytes from the beginning
of the
// line unless the whole character fits on that line
boolean breakExpected = p < c;
if (breakExpected) {
buf.write("\r\n ".getBytes(UTF_8));
}
// the remaining portion of the character, if any
for (int j = p; j < c; j++) {
buf.write(characterBytes, j, 1);
}
// expect another linebreak if the whole character fitted on
the same
// line and there is another character
if (a == 1) {
if (c == p) {
buf.write("\r\n ".getBytes(UTF_8));
}
buf.write(FILL.getBytes(UTF_8));
}
buf.write("\r\n".getBytes(UTF_8));
byte[] brokenPart = buf.toByteArray();
assertOccurrence(mfBytes, brokenPart);
readManifestAndAssertValue(mfBytes, i, NAME, value);
- }
- /*
* From previous Manifest implementation reduced to the minimum
required to
* demonstrate compatibility
*/
@SuppressWarnings("deprecation")
static byte[] writeManifestWithBrokenCharacters(
PositionInManifest i, String name, String value)
throws IOException {
byte[] vb = value.getBytes("UTF8");
value = new String(vb, 0, 0, vb.length);
ByteArrayOutputStream out = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(out);
String vername = Name.MANIFEST_VERSION.toString();
dos.writeBytes(vername + ": 0.1\r\n");
if (i == PositionInManifest.MAIN_ATTRIBUTES) {
StringBuffer buffer = new StringBuffer(name);
buffer.append(": ");
buffer.append(value);
make72Safe(buffer);
buffer.append("\r\n");
dos.writeBytes(buffer.toString());
}
dos.writeBytes("\r\n");
if (i == PositionInManifest.SECTION_NAME ||
i == PositionInManifest.NAMED_SECTION) {
StringBuffer buffer = new StringBuffer("Name: ");
if (i == PositionInManifest.SECTION_NAME) {
buffer.append(value);
} else {
buffer.append(NAME);
}
make72Safe(buffer);
buffer.append("\r\n");
dos.writeBytes(buffer.toString());
if (i == PositionInManifest.NAMED_SECTION) {
buffer = new StringBuffer(name);
buffer.append(": ");
buffer.append(value);
make72Safe(buffer);
buffer.append("\r\n");
dos.writeBytes(buffer.toString());
}
dos.writeBytes("\r\n");
}
dos.flush();
byte[] mfBytes = out.toByteArray();
System.out.println("-------------------------------------------"
+ "-----------------------------");
System.out.print(new String(mfBytes, UTF_8));
System.out.println("-------------------------------------------"
+ "-----------------------------");
return mfBytes;
}
/**
* Adds line breaks to enforce a maximum 72 bytes per line.
* From previous Manifest implementation.
*/
static void make72Safe(StringBuffer line) {
try {
Method method = Manifest.class.getDeclaredMethod(
"make72Safe", StringBuffer.class);
method.setAccessible(true);
method.invoke(null, line);
} catch (ReflectiveOperationException e) {
fail(e.getMessage(), e);
}
}
+} diff -r 2ace90aec488 test/jdk/java/util/jar/Manifest/LineBreakLineWidth.java --- a/test/jdk/java/util/jar/Manifest/LineBreakLineWidth.java Mon Apr 30 21:56:54 2018 -0400 +++ b/test/jdk/java/util/jar/Manifest/LineBreakLineWidth.java Wed May 02 07:20:46 2018 +0200 @@ -26,9 +26,13 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.util.Set; +import java.util.TreeSet; import java.util.jar.Manifest; import java.util.jar.Attributes; import java.util.jar.Attributes.Name; +import java.util.stream.Collectors; +import java.util.stream.IntStream;
import org.testng.annotations.Test; import static org.testng.Assert.*;
@@ -37,8 +41,10 @@
- @test
- @bug 6372077
- @run testng LineBreakLineWidth
- @summary write valid manifests with respect to line breaks
and read any line width
- @summary Write valid manifests with respect to line widths breaking longer
lines, especially for 6372077 that the lines are
sufficiently wide
to contain all possible valid header names, and read any line
width.
*/ public class LineBreakLineWidth {
@@ -49,6 +55,11 @@ final static int MAX_HEADER_NAME_LENGTH = 70;
/**
* maximum line width not including the line break
*/
final static int MAX_LINE_CONTENT_LENGTH = 72;
/**
- range of one..{@link #TEST_WIDTH_RANGE} covered in this test that
- exceeds the range of allowed header name lengths or line widths
- in order to also cover invalid cases beyond the valid boundaries @@ -60,20 +71,27 @@ final static int TEST_WIDTH_RANGE = 123;
/**
* tests if only valid manifest files can written depending on the
header
* Tests if only valid manifest files can written depending on the
header * name length or that an exception occurs already on the attempt to write * an invalid one otherwise and that no invalid manifest can be written. *
* due to bug JDK-6372077 it was possible to write a manifest that
could
* not be read again. independent of the actual manifest line
width, such
* Due to bug JDK-6372077 it was possible to write a manifest that
could
* not be read again. Independent of the actual manifest line
width, such * a situation should never happen, which is the subject of this test.
* <p>
* While this test covers header names which can contain only
one-byte utf
* encoded characters and the resulting manifest line widths,
multi-byte
* utf encoded characters can occur in header values, which is
subject of
* {@link #testManifestLineWidthMaximumRange()}. */ @Test public void testWriteValidManifestOrException() throws IOException {
assertTrue(TEST_WIDTH_RANGE > MAX_HEADER_NAME_LENGTH);
/*
* multi-byte utf-8 characters cannot occur in header names,
* Multi-byte utf-8 characters cannot occur in header names, * only in values which are not subject of this test here.
* hence, each character in a header name uses exactly one byte and
* Hence, each character in a header name uses exactly one byte and * variable length utf-8 character encoding doesn't affect
this test. */
@@ -111,18 +129,18 @@ }
/**
* tests that manifest files can be read even if the line breaks are
* Tests that manifest files can be read even if the line breaks are * placed in different positions than where the current JDK's * {@link Manifest#write(java.io.OutputStream)} would have put it
provided * the manifest is valid otherwise. *
* the <a
href="{@docRoot}/../specs/jar/jar.html#Notes_on_Manifest_and_Signature_Files">
* "Notes on Manifest and Signature Files" in the "JAR File
* Specification"</a> state that "no line may be longer than 72 bytes
* (not characters), in its utf8-encoded form." but allows for
earlier or
* The <a
href="{@docRoot}/../specs/jar/jar.html#Notes_on_Manifest_and_Signature_Files">
* "Notes on Manifest and Signature Files"</a> in the "JAR File
* Specification" state that <q>no line may be longer than 72 bytes
(not
* characters), in its utf8-encoded form.</q> but allows for earlier or * additional line breaks. * <p>
* the most important purpose of this test case is probably to make
sure
* The most important purpose of this test case is probably to make
sure * that manifest files broken at 70 bytes line width written with the * previous version of {@link Manifest} before this fix still work well. */ @@ -186,7 +204,8 @@ * form. */ String lineBrokenSectionName = breakLines(lineWidth, "Name: "
- name);
String lineBrokenNameAndValue = breakLines(lineWidth, name + ":
" + value);
String lineBrokenNameAndValue = breakLines(lineWidth, name + ": "
+ value); ByteArrayOutputStream mfBuf = new ByteArrayOutputStream(); mfBuf.write("Manifest-Version: 1.0".getBytes(UTF_8));
@@ -264,7 +283,7 @@ return mfBytes; }
- private static void printManifest(byte[] mfBytes) {
- static void printManifest(byte[] mfBytes) { final String sepLine =
"----------------------------------------------" + "---------------------|-|-|"; // |-positions: ---68-70-72 System.out.println(sepLine); @@ -272,13 +291,179 @@ System.out.println(sepLine); }
- private static void assertMainAndSectionValues(Manifest mf, String
name, + static void assertMainAndSectionValues(Manifest mf, String name, String value) { String mainValue = mf.getMainAttributes().getValue(name); - String sectionValue = mf.getAttributes(name).getValue(name);
assertEquals(value, mainValue, "value different in main section");
Attributes attributes = mf.getAttributes(name);
assertNotNull(attributes, "named section not found");
String sectionValue = attributes.getValue(name); assertEquals(value, sectionValue, "value different in named
section"); }
- @Test
- public void testWriteManifestOnOneLine() throws IOException {
testWriteVersionInfoLineWidth(Name.MANIFEST_VERSION);
- }
- @Test
- public void testWriteSignatureVersionOnOneLine() throws IOException {
testWriteVersionInfoLineWidth(Name.SIGNATURE_VERSION);
- }
- /**
* Tests that manifest versions "Manifest-Version" and
"Signature-Version" + * are not continued on a next line. + / + void testWriteVersionInfoLineWidth(Name version) throws IOException { + // e: number of characters of version header exceeding line width + for (int e = 0; e <= 1; e++) { + Manifest mf = new Manifest(); + mf.getMainAttributes().put(version, + "01234567890123456789012345678901234567890123456789012345" + .substring(0, MAX_HEADER_NAME_LENGTH + - version.toString().length() + e)); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + mf.write(out); + byte[] mfBytes = out.toByteArray(); + printManifest(mfBytes); + // maximum line width can only be exceeded if header not broken + assertEquals(MAX_LINE_CONTENT_LENGTH + e, + findMaximumLineWidth(mfBytes)); + } + } + + static int findMaximumLineWidth(byte[] mfBytes) { + int max = 0; + int b = 0; // start position of current line + for (int i = 0; i < mfBytes.length; i++) { + switch (mfBytes[i]) { + case 10: + case 13: + max = Math.max(max, i - b); + b = i + 1; + } + } + return max; + } + + /** + * @see LineBreakCharacter#numByteUtfCharacter(int, int) + */ + static String numByteUtfCharacter(int numBytes, int seed) { + seed = seed < 0 ? -seed : seed; + if (numBytes == 1) { + seed %= 0x5F; + seed += 0x20; // exclude control characters (0..0x19) here + } else if (numBytes == 2) { + seed %= 0x800 - 0x80; + seed += 0x80; + } else if (numBytes == 3) { + seed %= 0x10000 - 0x800 + 0xFDD0 - 0xFDEF + 0xFFFE - 0xFFFF; + seed += 0x800 + + (seed >= 0xFDD0 ? 0xFDEF - 0xFDD0 : 0) // non-characters + + (seed % 0x10000) * (0xFFFF - 0xFFFE); // byte order marks + } else { + seed %= 0x110000 - (0x10000 - 0xFFFE); + seed += 0x10000 + + (seed % 0x10000) * (0xFFFF - 0xFFFE); // byte order marks + } + + String string = new String(Character.toChars(seed)); + assertEquals(string.getBytes(UTF_8).length, numBytes, + "self-test failed: unexpected utf encoded character length"); + return string; + } + + /* + * Tests that the manifest line content width is between 69 (1 + + * maximum line width (without line break) 72 - maximum utf encoded + * character length 4) and 72 before continued on the next + * line, aka continuation line, depending only on the last character + * before the line break and not on those before or after in order to + * ensure the minimum number of line breaks and continuation lines + * possible. + *
+ * The
+ * "Notes on Manifest and Signature Files" in the "JAR File
+ * Specification" state that no line may be longer than 72 bytes
(not
+ * characters), in its utf8-encoded form.
and allows for earlier or
+ * additional line breaks.
+ *
+ * The number of characters a line may be less than the number of bytes + * but this is not explicitly verified because it can be assumed if some + * characters use more than one byte each and the number of bytes is as + * expected up to 72 that fewer than 72 characters were placed on the same + * line. + *
+ * While {@link #testWriteValidManifestOrException()} tests header
names
+ * lengths, this test covers the values.
+ /
+ @Test
+ public void testManifestLineWidthRange() throws Exception {
+ final int MAX_UTF_CHARACTER_ENCODED_LENGTH = 4;
+ final String TEST_NAME = "test";
+
+ // value will be filled with many possible sequences of utf
+ // characters of different encoded numbers of bytes in the form of
+ // "k times i-bytes size character" + "k times i-bytes size
character"
+ // and by prepending up to 72 "x" characters at every possible
position
+ // relative to a possible line break.
+ String value = "";
+ int seed = 0;
+ for (int i = 1; i <= MAX_UTF_CHARACTER_ENCODED_LENGTH; i++) {
+ for (int j = 1; j <= MAX_UTF_CHARACTER_ENCODED_LENGTH; j++) {
+ String valueI = "", valueJ = "";
+ for (int k = 1; k < MAX_LINE_CONTENT_LENGTH; k++) {
+ valueI += numByteUtfCharacter(i, seed++);
+ valueJ += numByteUtfCharacter(j, seed++);
+ value += valueI + valueJ;
+ }
+ }
+ }
+
+ String offset = "";
+ for (int i = 0; i <= MAX_LINE_CONTENT_LENGTH; i++) {
+ offset += "x";
+ byte[] mfBytes = writeManifest(TEST_NAME, offset + value);
+ Manifest mf = new Manifest(new ByteArrayInputStream(mfBytes));
+ assertMainAndSectionValues(mf, TEST_NAME, offset + value);
+
+ Set allowedWidths = IntStream.rangeClosed(
+ MAX_LINE_CONTENT_LENGTH -
MAX_UTF_CHARACTER_ENCODED_LENGTH + 1,
+ MAX_LINE_CONTENT_LENGTH).boxed().collect(Collectors.toSet());
+ assertEquals(allowedWidths, findContinuedLineWidths(mfBytes));
+ }
+ }
+
+ /*
+ * Counts the length of lines (without the line breaks) which are
+ * continued, not the continuation lines but those before that did
not fit
+ * on one line, whereby continuation lines may be continued again on
+ * another line, and returns their length in bytes, not characters.
+ /
+ static Set findContinuedLineWidths(byte[] mfBytes) {
+ Set widths = new TreeSet<>();
+ int previousLineWidth = -1;
+ int b = 0; // start position of current line
+ for (int i = 0; i < mfBytes.length; i++) {
+ switch (mfBytes[i]) {
+ case '\r':
+ case '\n':
+ previousLineWidth = i - b;
+ if (mfBytes[i] == '\r' &&
+ i + 1 < mfBytes.length && mfBytes[i + 1] == '\n') {
+ i++;
+ }
+ b = i + 1;
+ break;
+ case ' ':
+ if (i == b) {
+ widths.add(previousLineWidth);
+ }
+ }
+ }
+ return widths;
+ }
+
}
diff -r 2ace90aec488 test/jdk/java/util/jar/Manifest/NullKeysAndValues.java
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/util/jar/Manifest/NullKeysAndValues.java Wed May
02 07:20:46 2018 +0200
@@ -0,0 +1,149 @@
+/*
+ * Copyright (c) 2018, 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 static java.nio.charset.StandardCharsets.UTF_8;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.jar.Attributes;
+import java.util.jar.Manifest;
+import java.util.jar.Attributes.Name;
+import java.lang.reflect.Field;
+
+import org.testng.annotations.Test;
+import static org.testng.Assert.*;
+
+/**
+ * @test
+ * @run testng/othervm --illegal-access=warn NullKeysAndValues
+ * @summary Tests manifests with {@code null} values as section name,
header
+ * name, or value in both main and named attributes sections.
+ */
+/*
+ * Note to future maintainer:
+ * In order to actually being able to test all the cases where key and
values
+ * are null normal manifest and attributes manipulation through their
public
+ * api is not sufficient but then there were these null checks there before
+ * which may or may not have had their reason and this way it's ensured
that
+ * the behavior does not change with that respect.
+ * Once module isolation is enforced some test cases will not any longer be
+ * possible and those now tested situations will be guaranteed not to occur
+ * any longer at all at which point the corresponding tests can be removed
+ * safely without replacement unless of course another way is found
inject the
+ * tested null values.
+ * Another trick to access package private class members could be to use
+ * deserialization or adding a new class to the same package on the
classpath.
+ * Here is not important how the values are set to null because it
shows that
+ * the behavior remains unchanged.
+ */
+public class NullKeysAndValues {
+
+ static final String SOME_KEY = "some-key";
+ static final String SOME_VALUE = "some value";
+ static final String STRNULL = "null";
+
+ @Test
+ public void testMainAttributesHeaderNameNull() throws Exception {
+ Manifest mf = new Manifest();
+ Field attr = mf.getClass().getDeclaredField("attr");
+ attr.setAccessible(true);
+ Attributes mainAtts = new Attributes() {{
+ super.put( /* in: */ null, SOME_VALUE);
+ }};
+ attr.set(mf, mainAtts);
+ mf.getMainAttributes().put(Name.MANIFEST_VERSION, "1.0");
+ try {
+ mf = writeAndRead(mf);
+ fail();
+ } catch ( /* out: */ NullPointerException e) {
+ return; // ok
+ }
+ }
+
+ @Test
+ public void testMainAttributesHeaderValueNull() throws Exception {
+ Manifest mf = new Manifest();
+ Field attr = mf.getClass().getDeclaredField("attr");
+ attr.setAccessible(true);
+ Attributes mainAtts = new Attributes() {{
+ map.put(new Name(SOME_KEY), /* in: */ null);
+ }};
+ attr.set(mf, mainAtts);
+ mf.getMainAttributes().put(Name.MANIFEST_VERSION, "1.0");
+ mf = writeAndRead(mf);
+ assertEquals(mf.getMainAttributes().getValue(SOME_KEY),
+ /* out: */ STRNULL);
+ }
+
+ @Test
+ public void testSectionNameNull() throws IOException {
+ Manifest mf = new Manifest();
+ mf.getMainAttributes().put(Name.MANIFEST_VERSION, "1.0");
+ mf.getEntries().put( /* in: */ null, new Attributes());
+ mf = writeAndRead(mf);
+ assertNotNull(mf.getEntries().get( /* out: */ STRNULL));
+ }
+
+ @Test
+ public void testNamedSectionHeaderNameNull() throws IOException {
+ Manifest mf = new Manifest();
+ mf.getMainAttributes().put(Name.MANIFEST_VERSION, "1.0");
+ mf.getEntries().put(SOME_KEY, new Attributes() {{
+ map.put( /* in: */ null, SOME_VALUE);
+ }});
+ try {
+ mf = writeAndRead(mf);
+ fail();
+ } catch ( /* out: */ NullPointerException e) {
+ return; // ok
+ }
+ }
+
+ @Test
+ public void testNamedSectionHeaderValueNull() throws IOException {
+ Manifest mf = new Manifest();
+ mf.getMainAttributes().put(Name.MANIFEST_VERSION, "1.0");
+ mf.getEntries().put(SOME_KEY, new Attributes() {{
+ map.put(new Name(SOME_KEY), /* in: */ null);
+ }});
+ mf = writeAndRead(mf);
+ assertEquals(mf.getEntries().get(SOME_KEY).getValue(SOME_KEY),
+ /* out: */ STRNULL);
+ }
+
+ static Manifest writeAndRead(Manifest mf) throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ mf.write(out);
+ byte[] mfBytes = out.toByteArray();
+ System.out.println("-------------------------------------------"
+ + "-----------------------------");
+ System.out.print(new String(mfBytes, UTF_8));
+ System.out.println("-------------------------------------------"
+ + "-----------------------------");
+
+ ByteArrayInputStream in = new ByteArrayInputStream(mfBytes);
+ return new Manifest(in);
+ }
+
+}
diff -r 2ace90aec488 test/jdk/java/util/jar/Manifest/ValueUtfEncoding.java
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/util/jar/Manifest/ValueUtfEncoding.java Wed May
02 07:20:46 2018 +0200
@@ -0,0 +1,226 @@
+/*
+ * Copyright (c) 2018, 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 static java.nio.charset.StandardCharsets.UTF_8;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.jar.Attributes;
+import java.util.jar.Attributes.Name;
+import java.util.jar.Manifest;
+import java.util.List;
+import java.util.ArrayList;
+
+import org.testng.annotations.Test;
+import static org.testng.Assert.*;
+
+/**
+ * @test
+ * @bug 6202130
+ * @run testng ValueUtfEncoding
+ * @summary Tests complete manifest values utf encoding
+ *
+ * This test writes and reads a manifest that contains every valid utf
+ * character (three times), grouped into manifest header values with about
+ * 65535 bytes each or slightly more, resulting in a single huge
manifest with
+ * 3 * 67 + 1 values and 13703968 bytes in the manifest's encoded form in
+ * total. This way, all possible 1111995 utf characters are covered in one
+ * manifest.
+ *
+ * Every character occurs three times, once in a main attribute value,
once in
+ * a section name, and once in a named section attribute value, because
+ * implementation of writing the main section headers differs from the one
+ * writing named section headers in
+ * {@link Attributes#writeMain(java.io.DataOutputStream)} and
+ * {@link Attributes#write(java.io.DataOutputStream)} due to special
order of
+ * {@link Name#MANIFEST_VERSION} and {@link Name#SIGNATURE_VERSION}.
+ * and also {@link Manifest#read(java.io.InputStream)} treating reading the
+ * main section differently from reading named sections names headers.
+ *
+ * Only header values are tested. Characters for header names are much more
+ * limited and very simple ones are used just to get valid and
different ones.
+ *
+ * Correct support of all utf characters also has some relation to breaking
+ * characters across lines, see {@link LineBreakCharacter}.
+ /
+public class ValueUtfEncoding {
+
+ /Implementations should support 65535-byte (not character) header
+ * values, and 65535 headers per file. They might run out of memory,
+ * but there should not be hard-coded limits below these values.
+ *
+ * @see Notes
on Manifest and Signature Files
+ /
+ static final int MIN_VALUE_LENGTH_SUPPORTED = 2 << 16 - 1;
+
+ static final int MAX_UTF_CHARACTER_ENCODED_LENGTH = 4;
+
+ static boolean isValidUtfCharacter(int codePoint) {
+ if (0xFDD0 <= codePoint && codePoint <= 0xFDEF) {
+ return false; /* non-characters */
+ }
+ if ((codePoint & 0xFFFE) == 0xFFFE) {
+ return false; /* byte order marks */
+ }
+ return true;
+ }
+
+ /**
+ * returns {@code true} if {@code codePoint} is explicitly forbidden in
+ * manifest values based on a statement from the specs:
+ * otherchar: any UTF-8 character except NUL, CR and LF
+ * Produces a list of strings with all known utf characters except
those
+ * invalid in manifest header values with at least
+ * {@link #MIN_VALUE_LENGTH_SUPPORTED} utf-8 encoded bytes each
+ * except the last string which contains just the remaining characters.
+ /
+ static List produceValidManifestUtfCharacterValues() {
+ int maxLengthBytes = MIN_VALUE_LENGTH_SUPPORTED +
+ // exceed the specified limit by at least one character
+ MAX_UTF_CHARACTER_ENCODED_LENGTH + 1;
+
+ int numberOfUsedCodePoints = 0;
+ ArrayList values = new ArrayList<>();
+ byte[] valueBuf = new byte[maxLengthBytes];
+ int pos = 0;
+ for (int codePoint = Character.MIN_CODE_POINT;
+ codePoint <= Character.MAX_CODE_POINT; codePoint++) {
+ if (!isValidUtfCharacter(codePoint)) {
+ continue;
+ }
+ if (isInvalidManifestValueCharacter(codePoint)) {
+ continue;
+ }
+ numberOfUsedCodePoints++;
+
+ byte[] charBuf =
+ new
String(Character.toChars(codePoint)).getBytes(UTF_8);
+ if (pos + charBuf.length > valueBuf.length) {
+ values.add(new String(valueBuf, 0, pos, UTF_8));
+ pos = 0;
+ }
+ System.arraycopy(charBuf, 0, valueBuf, pos, charBuf.length);
+ pos += charBuf.length;
+ }
+ if (pos > 0) {
+ values.add(new String(valueBuf, 0, pos, UTF_8));
+ }
+
+ if (numberOfUsedCodePoints !=
+ (17 << 16) /* utf space */
+ - 66 /* non-characters */
+ - 3 /* nul, cr, lf */) {
+ fail("self-test: utf character set not covered exactly");
+ }
+
+ return values;
+ }
+
+ /**
+ * returns simple, valid, short, and distinct manifest header names.
+ * The returned name cannot be "{@code Manifest-Version}" because the
+ * returned string does not contain "{@code -}".
+ *
+ * @param seed seed to produce distinct names
+ */
+ static String azName(int seed) {
+ StringBuffer name = new StringBuffer();
+ do {
+ name.insert(0, (char) (seed % 26 + (seed < 26 ? 'A' : 'a')));
+ seed = seed / 26 - 1;
+ } while (seed >= 0);
+ return name.toString();
+ }
+
+ /*
+ * covers writing and reading of manifests with all known utf
characters.
+ *
+ * Because the implementation used different portions of code
depending on
+ * where the value occurs to read or write in earlier versions, each
+ * character is tested in each of the three positions:
+ *
+ * @see Jar File
Specification
+ /
+ static boolean isInvalidManifestValueCharacter(int codePoint) {
+ return codePoint == 0 / NUL /
+ || codePoint == '\r' / CR /
+ || codePoint == '\n' / LF /;
+ };
+
+ /
-
+ *
- main attribute header, + *
- named section name, which is in fact a header value after a blank + * line, and + *
- named sections header values + *
-
+ */
+ @Test
+ public void testValueUtfEncoding() throws IOException {
+ Manifest mf = new Manifest();
+ mf.getMainAttributes().put(Name.MANIFEST_VERSION, "1.0");
+
+ List values = produceValidManifestUtfCharacterValues();
+ for (int i = 0; i < values.size(); i++) {
+ String name = azName(i);
+ String value = values.get(i);
+
+ mf.getMainAttributes().put(new Name(name), value);
+ Attributes attributes = new Attributes();
+ mf.getEntries().put(value, attributes);
+ attributes.put(new Name(name), value);
+ }
+
+ mf = writeAndRead(mf);
+
+ for (int i = 0; i < values.size(); i++) {
+ String value = values.get(i);
+ String name = azName(i);
+
+ assertEquals(mf.getMainAttributes().getValue(name), value,
+ "main attributes header value");
+ Attributes attributes = mf.getAttributes(value);
+ assertNotNull(attributes, "named section not found");
+ assertEquals(attributes.getValue(name), value,
+ "named section attributes value");
+ }
+ }
+
+ static Manifest writeAndRead(Manifest mf) throws IOException {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ mf.write(out);
+ byte[] mfBytes = out.toByteArray();
+
+ System.out.println("-------------------------------------------"
+ + "-----------------------------");
+ System.out.print(new String(mfBytes, UTF_8));
+ System.out.println("-------------------------------------------"
+ + "-----------------------------");
+
+ ByteArrayInputStream in = new ByteArrayInputStream(mfBytes);
+ return new Manifest(in);
+ }
+
+}
diff -r 2ace90aec488
test/jdk/sun/security/tools/jarsigner/LineBrokenMultiByteCharacter.java
a/test/jdk/sun/security/tools/jarsigner/LineBrokenMultiByteCharacter.java Mon Apr 30 21:56:54 2018 -0400 +++ b/test/jdk/sun/security/tools/jarsigner/LineBrokenMultiByteCharacter.java Wed May 02 07:20:46 2018 +0200 @@ -21,35 +21,48 @@
- questions. */
-/* - * @test - * @bug 6695402 - * @summary verify signatures of jars containing classes with names - * with multi-byte unicode characters broken across lines - * @library /test/lib - */
import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Paths; +import java.util.HashMap; +import java.util.jar.Attributes.Name; import java.util.jar.JarFile; -import java.util.jar.Attributes.Name; import java.util.jar.JarEntry; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile;
import static java.nio.charset.StandardCharsets.UTF_8;
import jdk.test.lib.SecurityTools; import jdk.test.lib.util.JarUtils;
+import org.testng.annotations.; +import static org.testng.Assert.; + +/**
- @test
- @bug 6695402
- @library /test/lib
- @run testng LineBrokenMultiByteCharacter
- @summary verify signatures of jars containing classes with names
with multi-byte unicode characters broken across lines
- Before utf characters were kept in one piece by bug 6443578 and partially
- broken across a line break in manifests, bug 6695402 could occur resulting
- in a signature considered invalid. That can still happen after bug 6443578
- fixed when signing manifests that already contain entries with characters
- broken across a line break, presumably when an older manifest is present
- created with a different jdk.
*/ public class LineBrokenMultiByteCharacter {
/**
* this name will break across lines in MANIFEST.MF at the
* This name will break across lines in MANIFEST.MF at the * middle of a two-byte utf-8 encoded character due to its e acute
letter * at its exact position. - * - * because no file with such a name exists {@link JarUtils} will add the + *
+ * Because no file with such a name exists {@link JarUtils} will add the * name itself as contents to the jar entry which would have contained a * compiled class in the original bug. For this test, the contents of the * files contained in the jar file is not important as long as they get @@ -58,55 +71,157 @@ * @see #verifyClassNameLineBroken(JarFile, String) */ static final String testClassName =
"LineBrokenMultiByteCharacterA1234567890B1234567890C123456789D1234\u00E9xyz.class";
"LineBrokenMultiByteCharacterA1234567890B1234567890C123456789"
+ "D1234\u00E9xyz.class";
/**
* Used for a test case where an entry / class file is added to an
existing + * signed jar that already contains an entry with this name which of course + * has to be distinct from the one tested. + */ static final String anotherName =
"LineBrokenMultiByteCharacterA1234567890B1234567890C123456789D1234567890.class";
- "LineBrokenMultiByteCharacterA1234567890B1234567890C123456789"
+ "D1234567890.class"; static final String alias = "a"; static final String keystoreFileName = "test.jks";
- static final String manifestFileName = "MANIFEST.MF";
- static final String manifestFileName = JarFile.MANIFEST_NAME;
- public static void main(String[] args) throws Exception {
prepare();
- byte[] manifestNoDigest = "Manifest-Version:
1.0\r\n\r\n".getBytes(UTF_8);
- byte[] manifestWithDigest; // with character broken across line break
testSignJar("test.jar");
testSignJarNoManifest("test-no-manifest.jar");
testSignJarUpdate("test-update.jar", "test-updated.jar");
- /**
* Simulate a jar manifest as it would have been created by an
earlier jdk
* by re-arranging the line break at exactly 72 bytes content thereby
* breaking the multi-byte character under test.
*/
- static byte[] rebreak(byte[] mf0) {
byte[] mf1 = new byte[mf0.length];
int c0 = 0, c1 = 0; // bytes since last line start
for (int i0 = 0, i1 = 0; i0 < mf0.length; i0++, i1++) {
switch (mf0[i0]) {
case '\r':
if (i0 + 2 < mf0.length &&
mf0[i0 + 1] == '\n' && mf0[i0 + 2] == ' ') {
// skip line break
i0 += 2;
i1 -= 1;
} else {
mf1[i1] = mf0[i0];
c0 = c1 = 0;
}
break;
case '\n':
if (i0 + 1 < mf0.length && mf0[i0 + 1] == ' ') {
// skip line break
i0 += 1;
i1 -= 1;
} else {
mf1[i1] = mf0[i0];
c0 = c1 = 0;
}
break;
case ' ':
if (c0 == 0) {
continue;
}
default:
c0++;
if (c1 == 72) {
mf1[i1++] = '\r';
mf1[i1++] = '\n';
mf1[i1++] = ' ';
c1 = 1;
} else {
c1++;
}
mf1[i1] = mf0[i0];
}
}
return mf1; }
- static void prepare() throws Exception {
- @BeforeClass
- public void prepare() throws Exception { SecurityTools.keytool("-keystore", keystoreFileName,
"-genkeypair", "-storepass", "changeit", "-keypass", "changeit", "-storetype", "JKS", "-alias", alias, "-dname", "CN=X", "-validity", "366") .shouldHaveExitValue(0);
Files.write(Paths.get(manifestFileName), (Name.
MANIFEST_VERSION.toString() + ":
1.0\r\n").getBytes(UTF_8));
String jarFileName = "reference-manifest.jar";
createJarWithManifest(jarFileName, manifestNoDigest,
testClassName, anotherName);
SecurityTools.jarsigner("-keystore", keystoreFileName,
"-storetype",
"JKS", "-storepass", "changeit", "-debug", jarFileName,
alias)
.shouldHaveExitValue(0);
try (ZipFile refJar = new ZipFile(jarFileName);) {
ZipEntry mfEntry = refJar.getEntry(JarFile.MANIFEST_NAME);
manifestWithDigest =
refJar.getInputStream(mfEntry).readAllBytes();
}
System.out.println("manifestWithDigest before re-break = " +
new String(manifestWithDigest, UTF_8));
manifestWithDigest = rebreak(manifestWithDigest);
System.out.println("manifestWithDigest after re-break = " +
new String(manifestWithDigest, UTF_8));
// now, manifestWithDigest is accepted as unmodified by
// jdk.security.jarsigner.JarSigner#updateDigests
// (ZipEntry,ZipFile,MessageDigest[],Manifest) on line 985:
// "if (!mfDigest.equalsIgnoreCase(base64Digests[i])) {"
// and therefore left unchanged when the jar is signed and
// signature verification will check it, which is the test case.
Files.createDirectory(Paths.get("META-INF/")); }
- static void testSignJar(String jarFileName) throws Exception {
JarUtils.createJar(jarFileName, manifestFileName, testClassName);
verifyJarSignature(jarFileName);
- @Test
- public void testSignJarWithNoExistingClassEntry() throws Exception {
String jarFileName = "test-eacuteinonepiece.jar";
createJarWithManifest(jarFileName, manifestNoDigest,
testClassName);
signAndVerifyJarSignature(jarFileName, false); }
- static void testSignJarNoManifest(String jarFileName) throws
Exception {
JarUtils.createJar(jarFileName, testClassName);
verifyJarSignature(jarFileName);
- @Test
- public void testSignJarWithBrokenEAcuteClassEntry() throws Exception {
String jarFileName = "test-brokeneacute.jar";
createJarWithManifest(jarFileName, manifestWithDigest,
testClassName);
signAndVerifyJarSignature(jarFileName, true); }
- static void testSignJarUpdate(
String initialFileName, String updatedFileName) throws
Exception {
JarUtils.createJar(initialFileName, manifestFileName, anotherName);
- @Test
- public void testSignJarNoManifest() throws Exception {
String jarFileName = "test-no-manifest.jar";
JarUtils.createJar(jarFileName, testClassName);
signAndVerifyJarSignature(jarFileName, false);
- }
- @Test
- public void testSignJarUpdateWithEAcuteClassEntryInOnePiece()
throws Exception {
String initialFileName = "test-eacuteinonepiece-update.jar";
String updatedFileName = "test-eacuteinonepiece-updated.jar";
createJarWithManifest(initialFileName, manifestNoDigest,
anotherName); SecurityTools.jarsigner("-keystore", keystoreFileName, "-storetype", "JKS", "-storepass", "changeit", "-debug", initialFileName, alias).shouldHaveExitValue(0); JarUtils.updateJar(initialFileName, updatedFileName, testClassName);
verifyJarSignature(updatedFileName);
signAndVerifyJarSignature(updatedFileName, false); }
- static void verifyJarSignature(String jarFileName) throws Exception {
// actually sign the jar
- @Test
- public void testSignJarUpdateWithBrokenEAcuteClassEntry()
throws Exception {
String initialFileName = "test-brokeneacute-update.jar";
String updatedFileName = "test-brokeneacute-updated.jar";
createJarWithManifest(initialFileName, manifestWithDigest,
anotherName);
SecurityTools.jarsigner("-keystore", keystoreFileName,
"-storetype",
"JKS", "-storepass", "changeit", "-debug", initialFileName,
alias).shouldHaveExitValue(0);
JarUtils.updateJar(initialFileName, updatedFileName,
testClassName);
signAndVerifyJarSignature(updatedFileName, true);
- }
- void signAndVerifyJarSignature(String jarFileName,
boolean expectBrokenEAcute) throws Exception { SecurityTools.jarsigner("-keystore", keystoreFileName,
"-storetype", "JKS", "-storepass", "changeit", "-debug", jarFileName, alias) .shouldHaveExitValue(0); @@ -114,34 +229,34 @@ try ( JarFile jar = new JarFile(jarFileName); ) {
verifyClassNameLineBroken(jar, testClassName);
verifyClassNameLineBroken(jar, testClassName,
expectBrokenEAcute); verifyCodeSigners(jar, jar.getJarEntry(testClassName)); } }
/**
* it would be too easy to miss the actual test case by just
renaming an
* It would be too easy to miss the actual test case by just
renaming an * identifier so that the multi-byte encoded character would not any longer * be broken across a line break. *
* this check here verifies that the actual test case is tested
based on
* This check here verifies that the actual test case is tested
based on * the manifest and not based on the signature file because at the moment, * the signature file does not even contain the desired entry at all.
*
* this relies on {@link java.util.jar.Manifest} breaking lines unaware
* of bytes that belong to the same multi-byte utf characters. */
- static void verifyClassNameLineBroken(JarFile jar, String className)
throws IOException {
- static void verifyClassNameLineBroken(JarFile jar, String className,
boolean expectBrokenEAcute) throws IOException { byte[] eAcute = "\u00E9".getBytes(UTF_8); byte[] eAcuteBroken = new byte[] {eAcute[0], '\r', '\n', ' ', eAcute[1]};
if (jar.getManifest().getAttributes(className) == null) {
throw new AssertionError(className + " not found in manifest");
}
assertNotNull(jar.getManifest().getAttributes(className),
className + " not found in manifest"); JarEntry manifestEntry = jar.getJarEntry(JarFile.MANIFEST_NAME);
System.out.println("expectBrokenEAcute = " + expectBrokenEAcute);
System.out.println("Manifest = \n" + new String(
jar.getInputStream(manifestEntry).readAllBytes(), UTF_8));
try ( InputStream manifestIs = jar.getInputStream(manifestEntry); ) {
@@ -156,10 +271,12 @@ bytesMatched = 0; } }
if (bytesMatched < eAcuteBroken.length) {
throw new AssertionError("self-test failed: multi-byte "
+ "utf-8 character not broken across lines");
}
assertEquals(expectBrokenEAcute,
bytesMatched == eAcuteBroken.length,
"self-test failed: multi-byte "
+ "utf-8 character "
+ (expectBrokenEAcute ? "not " : "")
+ "broken across lines"); } }
@@ -175,11 +292,16 @@ // a check for the presence of code signers is sufficient to check // bug JDK-6695402. no need to also verify the actual code signers // attributes here.
if (jarEntry.getCodeSigners() == null
|| jarEntry.getCodeSigners().length == 0) {
throw new AssertionError(
"no signing certificate found for " +
jarEntry.getName());
}
assertTrue(jarEntry.getCodeSigners() != null
&& jarEntry.getCodeSigners().length > 0,
"no signing certificate found for " + jarEntry.getName());
- }
- static void createJarWithManifest(String jarFileName, byte[] manifest,
String... files) throws IOException {
JarUtils.createJar("yetwithoutmanifest-" + jarFileName, files);
JarUtils.updateJar("yetwithoutmanifest-" + jarFileName,
jarFileName,
}new HashMap<>() {{ put(manifestFileName, manifest); }}); }
-------------- next part -------------- A non-text attachment was scrubbed... Name: 6372077and6443578.patch Type: text/x-patch Size: 79654 bytes Desc: not available URL: <http://mail.openjdk.java.net/pipermail/core-libs-dev/attachments/20180502/3367c096/6372077and6443578-0001.patch>
- Previous message: Hashing files/bytes Re: RFR(JDK11/NIO) 8202285: (fs) Add a method to Files for comparing file contents
- Next message: RFR for 6443578 and 6202130: UTF-8 in Manifests
- Messages sorted by: [ date ] [ thread ] [ subject ] [ author ]