test/jdk/java/util/jar/Manifest/LineBreakLineWidth.java
author rriggs
Thu, 01 Mar 2018 15:50:26 -0500
changeset 49111 1b33025ae610
permissions -rw-r--r--
6372077: JarFile.getManifest() should handle manifest attribute name 70 bytes Reviewed-by: alanb, sherman Contributed-by: philipp.kunz@paratix.ch

/*
 * 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.Manifest;
import java.util.jar.Attributes;
import java.util.jar.Attributes.Name;

import org.testng.annotations.Test;
import static org.testng.Assert.*;

/**
 * @test
 * @bug 6372077
 * @run testng LineBreakLineWidth
 * @summary write valid manifests with respect to line breaks
 *          and read any line width
 */
public class LineBreakLineWidth {

    /**
     * maximum header name length from {@link Name#isValid(String)}
     * not including the name-value delimiter <code>": "</code>
     */
    final static int MAX_HEADER_NAME_LENGTH = 70;

    /**
     * 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
     * and to keep it somewhat independent from the actual manifest width.
     * <p>
     * bigger than 72 (maximum manifest header line with in bytes (not utf-8
     * encoded characters) but otherwise arbitrarily chosen
     */
    final static int TEST_WIDTH_RANGE = 123;

    /**
     * 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.
     * <p>
     * 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.
     */
    @Test
    public void testWriteValidManifestOrException() throws IOException {
        /*
         * 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
         * variable length utf-8 character encoding doesn't affect this test.
         */

        String name = "";
        for (int l = 1; l <= TEST_WIDTH_RANGE; l++) {
            name += "x";
            System.out.println("name = " + name + ", "
                    + "name.length = " + name.length());

            if (l <= MAX_HEADER_NAME_LENGTH) {
                writeValidManifest(name, "somevalue");
            } else {
                writeInvalidManifestThrowsException(name, "somevalue");
            }
        }
    }

    static void writeValidManifest(String name, String value)
            throws IOException {
        byte[] mfBytes = writeManifest(name, value);
        Manifest mf = new Manifest(new ByteArrayInputStream(mfBytes));
        assertMainAndSectionValues(mf, name, value);
    }

    static void writeInvalidManifestThrowsException(String name, String value)
            throws IOException {
        try {
            writeManifest(name, value);
        } catch (IllegalArgumentException e) {
            // no invalid manifest was produced which is considered acceptable
            return;
        }

        fail("no error writing manifest considered invalid");
    }

    /**
     * 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.
     * <p>
     * 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
     * additional line breaks.
     * <p>
     * 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.
     */
    @Test
    public void testReadDifferentLineWidths() throws IOException {
        /*
         * uses only one-byte utf-8 encoded characters as values.
         * correctly breaking multi-byte utf-8 encoded characters
         * would be subject of another test if there was one such.
         */

        // w: line width
        // 6 minimum required for section names starting with "Name: "
        for (int w = 6; w <= TEST_WIDTH_RANGE; w++) {

            // ln: header name length
            String name = "";
            // - 2 due to the delimiter ": " that has to fit on the same
            // line as the name
            for (int ln = 1; ln <= w - 2; ln++) {
                name += "x";

                // lv: value length
                String value = "";
                for (int lv = 1; lv <= TEST_WIDTH_RANGE; lv++) {
                    value += "y";
                }

                System.out.println("lineWidth = " + w);
                System.out.println("name = " + name + ""
                        + ", name.length = " + name.length());
                System.out.println("value = " + value + ""
                        + ", value.length = " + value.length());

                readSpecificLineWidthManifest(name, value, w);
            }
        }
    }

    static void readSpecificLineWidthManifest(String name, String value,
            int lineWidth) throws IOException {
        /*
         * breaking header names is not allowed and hence cannot be reasonably
         * tested. it cannot easily be helped, that invalid manifest files
         * written by the previous Manifest version implementation are illegal
         * if the header name is 69 or 70 bytes and in that case the name/value
         * delimiter ": " was broken on a new line.
         *
         * changing the line width in Manifest#make72Safe(StringBuffer),
         * however, also affects at which positions values are broken across
         * lines (should always have affected values only and never header
         * names or the delimiter) which is tested here.
         *
         * ideally, any previous Manifest implementation would have been used
         * here to provide manifest files to test reading but these are no
         * longer available in this version's sources and there might as well
         * be other libraries writing manifests. Therefore, in order to be able
         * to test any manifest file considered valid with respect to line
         * breaks that could not possibly be produced with the current Manifest
         * implementation, this test provides its own manifests in serialized
         * form.
         */
        String lineBrokenSectionName = breakLines(lineWidth, "Name: " + name);
        String lineBrokenNameAndValue = breakLines(lineWidth, name + ": " + value);

        ByteArrayOutputStream mfBuf = new ByteArrayOutputStream();
        mfBuf.write("Manifest-Version: 1.0".getBytes(UTF_8));
        mfBuf.write("\r\n".getBytes(UTF_8));
        mfBuf.write(lineBrokenNameAndValue.getBytes(UTF_8));
        mfBuf.write("\r\n".getBytes(UTF_8));
        mfBuf.write("\r\n".getBytes(UTF_8));
        mfBuf.write(lineBrokenSectionName.getBytes(UTF_8));
        mfBuf.write("\r\n".getBytes(UTF_8));
        mfBuf.write(lineBrokenNameAndValue.getBytes(UTF_8));
        mfBuf.write("\r\n".getBytes(UTF_8));
        mfBuf.write("\r\n".getBytes(UTF_8));
        byte[] mfBytes = mfBuf.toByteArray();
        printManifest(mfBytes);

        boolean nameValid = name.length() <= MAX_HEADER_NAME_LENGTH;

        Manifest mf;
        try {
            mf = new Manifest(new ByteArrayInputStream(mfBytes));
        } catch (IOException e) {
            if (!nameValid &&
                    e.getMessage().startsWith("invalid header field")) {
                // expected because the name is not valid
                return;
            }

            throw new AssertionError(e.getMessage(), e);
        }

        assertTrue(nameValid, "failed to detect invalid manifest");

        assertMainAndSectionValues(mf, name, value);
    }

    static String breakLines(int lineWidth, String nameAndValue) {
        String lineBrokenNameAndValue = "";
        int charsOnLastLine = 0;
        for (int i = 0; i < nameAndValue.length(); i++) {
            lineBrokenNameAndValue += nameAndValue.substring(i, i + 1);
            charsOnLastLine++;
            if (0 < i && i < nameAndValue.length() - 1
                    && charsOnLastLine == lineWidth) {
                lineBrokenNameAndValue += "\r\n ";
                charsOnLastLine = 1;
            }
        }
        return lineBrokenNameAndValue;
    }

    static byte[] writeManifest(String name, String value) throws IOException {
        /*
         * writing manifest main headers is implemented separately from
         * writing named sections manifest headers:
         * - java.util.jar.Attributes.writeMain(DataOutputStream)
         * - java.util.jar.Attributes.write(DataOutputStream)
         * which is why this is also covered separately in this test by
         * always adding the same value twice, in the main attributes as
         * well as in a named section (using the header name also as the
         * section name).
         */

        Manifest mf = new Manifest();
        mf.getMainAttributes().put(Name.MANIFEST_VERSION, "1.0");
        mf.getMainAttributes().putValue(name, value);

        Attributes section = new Attributes();
        section.putValue(name, value);
        mf.getEntries().put(name, section);

        ByteArrayOutputStream out = new ByteArrayOutputStream();
        mf.write(out);
        byte[] mfBytes = out.toByteArray();
        printManifest(mfBytes);
        return mfBytes;
    }

    private static void printManifest(byte[] mfBytes) {
        final String sepLine = "----------------------------------------------"
                + "---------------------|-|-|"; // |-positions: ---68-70-72
        System.out.println(sepLine);
        System.out.print(new String(mfBytes, UTF_8));
        System.out.println(sepLine);
    }

    private 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");
        assertEquals(value, sectionValue, "value different in named section");
    }

}