1 /*
2  * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved.
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * This code is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License version 2 only, as
7  * published by the Free Software Foundation.
8  *
9  * This code is distributed in the hope that it will be useful, but WITHOUT
10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12  * version 2 for more details (a copy is included in the LICENSE file that
13  * accompanied this code).
14  *
15  * You should have received a copy of the GNU General Public License version
16  * 2 along with this work; if not, write to the Free Software Foundation,
17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18  *
19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20  * or visit www.oracle.com if you need additional information or have any
21  * questions.
22  */
23 
24 import static java.nio.charset.StandardCharsets.UTF_8;
25 
26 import java.io.ByteArrayInputStream;
27 import java.io.ByteArrayOutputStream;
28 import java.io.IOException;
29 import java.util.jar.Manifest;
30 import java.util.jar.Attributes;
31 import java.util.jar.Attributes.Name;
32 
33 import org.testng.annotations.Test;
34 import static org.testng.Assert.*;
35 
36 /**
37  * @test
38  * @bug 6372077
39  * @run testng LineBreakLineWidth
40  * @summary write valid manifests with respect to line breaks
41  *          and read any line width
42  */
43 public class LineBreakLineWidth {
44 
45     /**
46      * maximum header name length from {@link Name#isValid(String)}
47      * not including the name-value delimiter <code>": "</code>
48      */
49     final static int MAX_HEADER_NAME_LENGTH = 70;
50 
51     /**
52      * range of one..{@link #TEST_WIDTH_RANGE} covered in this test that
53      * exceeds the range of allowed header name lengths or line widths
54      * in order to also cover invalid cases beyond the valid boundaries
55      * and to keep it somewhat independent from the actual manifest width.
56      * <p>
57      * bigger than 72 (maximum manifest header line with in bytes (not utf-8
58      * encoded characters) but otherwise arbitrarily chosen
59      */
60     final static int TEST_WIDTH_RANGE = 123;
61 
62     /**
63      * tests if only valid manifest files can written depending on the header
64      * name length or that an exception occurs already on the attempt to write
65      * an invalid one otherwise and that no invalid manifest can be written.
66      * <p>
67      * due to bug JDK-6372077 it was possible to write a manifest that could
68      * not be read again. independent of the actual manifest line width, such
69      * a situation should never happen, which is the subject of this test.
70      */
71     @Test
testWriteValidManifestOrException()72     public void testWriteValidManifestOrException() throws IOException {
73         /*
74          * multi-byte utf-8 characters cannot occur in header names,
75          * only in values which are not subject of this test here.
76          * hence, each character in a header name uses exactly one byte and
77          * variable length utf-8 character encoding doesn't affect this test.
78          */
79 
80         String name = "";
81         for (int l = 1; l <= TEST_WIDTH_RANGE; l++) {
82             name += "x";
83             System.out.println("name = " + name + ", "
84                     + "name.length = " + name.length());
85 
86             if (l <= MAX_HEADER_NAME_LENGTH) {
87                 writeValidManifest(name, "somevalue");
88             } else {
89                 writeInvalidManifestThrowsException(name, "somevalue");
90             }
91         }
92     }
93 
writeValidManifest(String name, String value)94     static void writeValidManifest(String name, String value)
95             throws IOException {
96         byte[] mfBytes = writeManifest(name, value);
97         Manifest mf = new Manifest(new ByteArrayInputStream(mfBytes));
98         assertMainAndSectionValues(mf, name, value);
99     }
100 
writeInvalidManifestThrowsException(String name, String value)101     static void writeInvalidManifestThrowsException(String name, String value)
102             throws IOException {
103         try {
104             writeManifest(name, value);
105         } catch (IllegalArgumentException e) {
106             // no invalid manifest was produced which is considered acceptable
107             return;
108         }
109 
110         fail("no error writing manifest considered invalid");
111     }
112 
113     /**
114      * tests that manifest files can be read even if the line breaks are
115      * placed in different positions than where the current JDK's
116      * {@link Manifest#write(java.io.OutputStream)} would have put it provided
117      * the manifest is valid otherwise.
118      * <p>
119      * the <a href="{@docRoot}/../specs/jar/jar.html#Notes_on_Manifest_and_Signature_Files">
120      * "Notes on Manifest and Signature Files" in the "JAR File
121      * Specification"</a> state that "no line may be longer than 72 bytes
122      * (not characters), in its utf8-encoded form." but allows for earlier or
123      * additional line breaks.
124      * <p>
125      * the most important purpose of this test case is probably to make sure
126      * that manifest files broken at 70 bytes line width written with the
127      * previous version of {@link Manifest} before this fix still work well.
128      */
129     @Test
testReadDifferentLineWidths()130     public void testReadDifferentLineWidths() throws IOException {
131         /*
132          * uses only one-byte utf-8 encoded characters as values.
133          * correctly breaking multi-byte utf-8 encoded characters
134          * would be subject of another test if there was one such.
135          */
136 
137         // w: line width
138         // 6 minimum required for section names starting with "Name: "
139         for (int w = 6; w <= TEST_WIDTH_RANGE; w++) {
140 
141             // ln: header name length
142             String name = "";
143             // - 2 due to the delimiter ": " that has to fit on the same
144             // line as the name
145             for (int ln = 1; ln <= w - 2; ln++) {
146                 name += "x";
147 
148                 // lv: value length
149                 String value = "";
150                 for (int lv = 1; lv <= TEST_WIDTH_RANGE; lv++) {
151                     value += "y";
152                 }
153 
154                 System.out.println("lineWidth = " + w);
155                 System.out.println("name = " + name + ""
156                         + ", name.length = " + name.length());
157                 System.out.println("value = " + value + ""
158                         + ", value.length = " + value.length());
159 
160                 readSpecificLineWidthManifest(name, value, w);
161             }
162         }
163     }
164 
readSpecificLineWidthManifest(String name, String value, int lineWidth)165     static void readSpecificLineWidthManifest(String name, String value,
166             int lineWidth) throws IOException {
167         /*
168          * breaking header names is not allowed and hence cannot be reasonably
169          * tested. it cannot easily be helped, that invalid manifest files
170          * written by the previous Manifest version implementation are illegal
171          * if the header name is 69 or 70 bytes and in that case the name/value
172          * delimiter ": " was broken on a new line.
173          *
174          * changing the line width in Manifest#make72Safe(StringBuffer),
175          * however, also affects at which positions values are broken across
176          * lines (should always have affected values only and never header
177          * names or the delimiter) which is tested here.
178          *
179          * ideally, any previous Manifest implementation would have been used
180          * here to provide manifest files to test reading but these are no
181          * longer available in this version's sources and there might as well
182          * be other libraries writing manifests. Therefore, in order to be able
183          * to test any manifest file considered valid with respect to line
184          * breaks that could not possibly be produced with the current Manifest
185          * implementation, this test provides its own manifests in serialized
186          * form.
187          */
188         String lineBrokenSectionName = breakLines(lineWidth, "Name: " + name);
189         String lineBrokenNameAndValue = breakLines(lineWidth, name + ": " + value);
190 
191         ByteArrayOutputStream mfBuf = new ByteArrayOutputStream();
192         mfBuf.write("Manifest-Version: 1.0".getBytes(UTF_8));
193         mfBuf.write("\r\n".getBytes(UTF_8));
194         mfBuf.write(lineBrokenNameAndValue.getBytes(UTF_8));
195         mfBuf.write("\r\n".getBytes(UTF_8));
196         mfBuf.write("\r\n".getBytes(UTF_8));
197         mfBuf.write(lineBrokenSectionName.getBytes(UTF_8));
198         mfBuf.write("\r\n".getBytes(UTF_8));
199         mfBuf.write(lineBrokenNameAndValue.getBytes(UTF_8));
200         mfBuf.write("\r\n".getBytes(UTF_8));
201         mfBuf.write("\r\n".getBytes(UTF_8));
202         byte[] mfBytes = mfBuf.toByteArray();
203         printManifest(mfBytes);
204 
205         boolean nameValid = name.length() <= MAX_HEADER_NAME_LENGTH;
206 
207         Manifest mf;
208         try {
209             mf = new Manifest(new ByteArrayInputStream(mfBytes));
210         } catch (IOException e) {
211             if (!nameValid &&
212                     e.getMessage().startsWith("invalid header field")) {
213                 // expected because the name is not valid
214                 return;
215             }
216 
217             throw new AssertionError(e.getMessage(), e);
218         }
219 
220         assertTrue(nameValid, "failed to detect invalid manifest");
221 
222         assertMainAndSectionValues(mf, name, value);
223     }
224 
breakLines(int lineWidth, String nameAndValue)225     static String breakLines(int lineWidth, String nameAndValue) {
226         String lineBrokenNameAndValue = "";
227         int charsOnLastLine = 0;
228         for (int i = 0; i < nameAndValue.length(); i++) {
229             lineBrokenNameAndValue += nameAndValue.substring(i, i + 1);
230             charsOnLastLine++;
231             if (0 < i && i < nameAndValue.length() - 1
232                     && charsOnLastLine == lineWidth) {
233                 lineBrokenNameAndValue += "\r\n ";
234                 charsOnLastLine = 1;
235             }
236         }
237         return lineBrokenNameAndValue;
238     }
239 
writeManifest(String name, String value)240     static byte[] writeManifest(String name, String value) throws IOException {
241         /*
242          * writing manifest main headers is implemented separately from
243          * writing named sections manifest headers:
244          * - java.util.jar.Attributes.writeMain(DataOutputStream)
245          * - java.util.jar.Attributes.write(DataOutputStream)
246          * which is why this is also covered separately in this test by
247          * always adding the same value twice, in the main attributes as
248          * well as in a named section (using the header name also as the
249          * section name).
250          */
251 
252         Manifest mf = new Manifest();
253         mf.getMainAttributes().put(Name.MANIFEST_VERSION, "1.0");
254         mf.getMainAttributes().putValue(name, value);
255 
256         Attributes section = new Attributes();
257         section.putValue(name, value);
258         mf.getEntries().put(name, section);
259 
260         ByteArrayOutputStream out = new ByteArrayOutputStream();
261         mf.write(out);
262         byte[] mfBytes = out.toByteArray();
263         printManifest(mfBytes);
264         return mfBytes;
265     }
266 
printManifest(byte[] mfBytes)267     private static void printManifest(byte[] mfBytes) {
268         final String sepLine = "----------------------------------------------"
269                 + "---------------------|-|-|"; // |-positions: ---68-70-72
270         System.out.println(sepLine);
271         System.out.print(new String(mfBytes, UTF_8));
272         System.out.println(sepLine);
273     }
274 
assertMainAndSectionValues(Manifest mf, String name, String value)275     private static void assertMainAndSectionValues(Manifest mf, String name,
276             String value) {
277         String mainValue = mf.getMainAttributes().getValue(name);
278         String sectionValue = mf.getAttributes(name).getValue(name);
279 
280         assertEquals(value, mainValue, "value different in main section");
281         assertEquals(value, sectionValue, "value different in named section");
282     }
283 
284 }
285