test/jdk/sun/security/util/ManifestDigester/FindSection.java
author weijun
Thu, 18 Jul 2019 08:53:06 +0800
changeset 57488 94691d8e746f
permissions -rw-r--r--
8217375: jarsigner breaks old signature with long lines in manifest Reviewed-by: jjiang, weijun Contributed-by: Philipp Kunz <philipp.kunz@paratix.ch>

/*
 * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.concurrent.Callable;
import java.util.function.Consumer;

import sun.security.util.ManifestDigester;

import org.testng.annotations.Test;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Factory;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.testng.Assert.*;

/**
 * @test
 * @bug 8217375
 * @modules java.base/sun.security.util:+open
 * @compile ../../tools/jarsigner/Utils.java
 * @run testng/othervm FindSection
 * @summary Check {@link ManifestDigester#findSection}.
 */
public class FindSection {

    /*
     * TODO:
     * FIXED_8217375 is not intended to keep. it is intended to show what
     * exactly has changed with respect to the previous version for which no
     * such test existed.
     */
    static final boolean FIXED_8217375 = true;

    /**
     * {@link ManifestDigester.Entry#digestWorkaround} should not feed the
     * trailing blank line into the digester. Before resolution of 8217375 it
     * fed the trailing blank line into the digest if the second line break
     * was at the end of the file due to <pre>
     * if (allBlank || (i == len-1)) {
     *     if (i == len-1)
     *         pos.endOfSection = i;
     *     else
     *         pos.endOfSection = last;
     * </pre> in {@link ManifestDigester#findSection}. In that case at the end
     * of the manifest file, {@link ManifestDigester.Entry#digestWorkaround}
     * would have produced the same digest as
     * {@link ManifestDigester.Entry#digest} which was wrong and without effect
     * at best.
     * <p>
     * Once this fix is accepted, this flag can be removed along with
     * {@link #actualEndOfSection8217375}.
     */
    static final boolean FIXED_8217375_EOF_ENDOFSECTION = FIXED_8217375;

    /**
     * {@link ManifestDigester.Position.endOfSection} usually points to the
     * start position of the blank line trailing a section minus one.
     * If a {@link ManifestDigester.Position} returned by
     * {@link ManifestDigester#findSection} is based on a portion that starts
     * with a blank line, above statement is (or was) not true, because of the
     * initialization of {@code last} in {@link ManifestDigester#findSection}
     * <pre>
     * int last = offset;
     * </pre>
     * which would point after incrementing it in {@code pos.endOfSection + 1}
     * on line 128 (line number before this change) or {@code int sectionLen =
     * pos.endOfSection-start+1;} on line 133 (line number before this change)
     * at one byte after the first line break character of usually two and
     * possibly (assuming "{@code \r\n}" default line break normally) in between
     * the two characters of a line break. After subtracting again the index of
     * the section start position on former line 133, the last byte would be
     * missed to be digested by {@link ManifestDigester.Entry#digestWorkaround}.
     * <p>
     * All this, however could possibly matter (or have mattered) only when
     * {@link ManifestDigester#findSection} was invoked with an offset position
     * pointing straight to a line break which happens if a manifest starts
     * with an empty line or if there are superfluous blank lines between
     * sections in both cases no useful manifest portion is identified.
     * Superfluous blank lines are not identified as sections (because they
     * don't have a name and specifically don't meet {@code if (len > 6) {} on
     * former line 136. Manifests starting with a line break are not any more
     * useful either.
     * <p>
     * Once this fix is accepted, this flag can be removed along with
     * {@link #actualEndOfSection8217375}.
     */
    static final boolean FIXED_8217375_STARTWITHBLANKLINE_ENDOFSECTION =
            FIXED_8217375;

    static Constructor<?> PositionConstructor;
    static Method findSection;
    static Field rawBytes;
    static Field endOfFirstLine;
    static Field endOfSection;
    static Field startOfNext;

    @BeforeClass
    public static void setFindSectionAccessible() throws Exception {
        Class<?> Position = Arrays.stream(ManifestDigester.class.
                getDeclaredClasses()).filter(c -> c.getSimpleName().
                        equals("Position")).findFirst().get();
        PositionConstructor = Position.getDeclaredConstructor();
        PositionConstructor.setAccessible(true);
        findSection = ManifestDigester.class.getDeclaredMethod("findSection",
            int.class, Position);
        findSection.setAccessible(true);
        rawBytes = ManifestDigester.class.getDeclaredField("rawBytes");
        rawBytes.setAccessible(true);
        endOfFirstLine = Position.getDeclaredField("endOfFirstLine");
        endOfFirstLine.setAccessible(true);
        endOfSection = Position.getDeclaredField("endOfSection");
        endOfSection.setAccessible(true);
        startOfNext = Position.getDeclaredField("startOfNext");
        startOfNext.setAccessible(true);
    }

    static class Position {
        final int endOfFirstLine; // not including newline character

        final int endOfSection; // end of section, not including the blank line
                                // between sections
        final int startOfNext;  // the start of the next section

        Position(Object pos) throws ReflectiveOperationException {
            endOfFirstLine = FindSection.endOfFirstLine.getInt(pos);
            endOfSection = FindSection.endOfSection.getInt(pos);
            startOfNext = FindSection.startOfNext.getInt(pos);
        }
    }

    Position findSection(byte[] manifestBytes)
            throws ReflectiveOperationException {
        ManifestDigester manDig = new ManifestDigester("\n\n".getBytes(UTF_8));
        FindSection.rawBytes.set(manDig, manifestBytes);
        Object pos = PositionConstructor.newInstance();
        Object result = findSection.invoke(manDig, offset, pos);
        if (Boolean.FALSE.equals(result)) {
            return null; // indicates findSection having returned false
        } else {
            return new Position(pos);
        }
    }

    @DataProvider(name = "parameters")
    public static Object[][] parameters() {
        return new Object[][] { { 0 }, { 42 } };
    }

    @Factory(dataProvider = "parameters")
    public static Object[] createTests(int offset) {
        return new Object[]{ new FindSection(offset) };
    }

    final int offset;

    FindSection(int offset) {
        this.offset = offset;
    }

    @BeforeMethod
    public void verbose() {
        System.out.println("offset = " + offset);
    }

    Position findSection(String manifestString)
            throws ReflectiveOperationException {
        byte[] manifestBytes = manifestString.getBytes(UTF_8);
        byte[] manifestWithOffset = new byte[manifestBytes.length + offset];
        System.arraycopy(manifestBytes, 0, manifestWithOffset, offset,
                manifestBytes.length);
        return findSection(manifestWithOffset);
    }

    /**
     * Surprising, but the offset actually makes a difference in
     * {@link ManifestDigester#findSection} return value.
     */
    @SuppressWarnings("unused")
    int actualEndOfFirstLine8217375(int correctPosition) {
        // if the parsed portion of the manifest starts with a blank line,
        // and offset is 0, "pos.endOfFirstLine = -1;" probably denoting a
        // yet uninitialized value coincides with the assignment by
        // "pos.endOfFirstLine = i-1;" if i == 0 and
        // "if (pos.endOfFirstLine == -1)" after "case '\n':" happens to
        // become true even though already assigned.
        if (offset == 0 && correctPosition == -1 && !FIXED_8217375) return 0;
        return correctPosition;
    }

    @SuppressWarnings("unused")
    int actualEndOfSection8217375(int correctPosition, boolean eof, int lbl) {
        // if the parsed portion of the manifest ends with a blank line and
        // just before eof, the blank line is included in Position.endOfSection/
        // Section.length (the one usually without blank line as well as in
        // Position.startOfNext/Section.lengthWithBlankLine) which is used
        // in digestWorkaround (independent of the digest without workaround)
        if (eof && !FIXED_8217375_EOF_ENDOFSECTION) {
            return correctPosition + lbl;
        } else if (correctPosition == -1
                && !FIXED_8217375_STARTWITHBLANKLINE_ENDOFSECTION) {
            return 0;
        } else {
            return correctPosition;
        }
    }

    AssertionError collectErrors(AssertionError a, Runnable run) {
        try {
            run.run();
        } catch (AssertionError e) {
            if (a == null) a = new AssertionError();
            a.addSuppressed(e);
        }
        return a;
    }

    void assertPosition(Position pos,
            int endOfFirstLine, int endOfSection, int startOfNext) {
        AssertionError a = null;
        a = collectErrors(a, () -> assertEquals(
                pos.endOfFirstLine, endOfFirstLine + offset, "endOfFirstLine"));
        a = collectErrors(a, () -> assertEquals(
                pos.endOfSection, endOfSection + offset, "endOfSection"));
        a = collectErrors(a, () -> assertEquals(
                pos.startOfNext, startOfNext + offset, "startOfNext"));
        if (a != null) throw a;
    }

    void catchCrCausesIndexOutOfBoundsException(
            Callable<Position> test, Consumer<Position> asserts) {
        try {
            Position x = test.call();
            if (!FIXED_8217375) fail();
            asserts.accept(x);
        } catch (Exception e) {
            if (e instanceof IndexOutOfBoundsException ||
                e.getCause() instanceof IndexOutOfBoundsException) {
                if (FIXED_8217375) throw new AssertionError(e);
            } else {
                throw new AssertionError(e);
            }
        }
    }

    @Test
    public void testEmpty() throws Exception {
        assertNull(findSection(""));
    }

    @Test
    public void testOneLineBreakCr() throws Exception {
        catchCrCausesIndexOutOfBoundsException(
                () -> findSection("\r"),
                p -> assertPosition(p,
                        -1, actualEndOfSection8217375(-1, false, 1), 1)
        );
    }

    @Test
    public void testOneLineBreakLf() throws Exception {
        assertPosition(findSection("\n"),
                -1, actualEndOfSection8217375(-1, false, 1), 1);
    }

    @Test
    public void testOneLineBreakCrLf() throws Exception {
        assertPosition(findSection("\r\n"),
                actualEndOfFirstLine8217375(-1),
                actualEndOfSection8217375(-1, true, 2),
                2);
    }

    @Test
    public void testSpaceAndLineBreakCr() throws Exception {
        catchCrCausesIndexOutOfBoundsException(
                () -> findSection("   \r"),
                p -> assertPosition(p, 2, 3, 4)
        );
    }

    @Test
    public void testSpaceAndOneLineBreakLf() throws Exception {
        assertPosition(findSection("   \n"), 2, 3, 4);
    }

    @Test
    public void testSpaceAndOneLineBreakCrLf() throws Exception {
        assertPosition(findSection("   \r\n"), 2, 4, 5);
    }

    @Test
    public void testOneLineBreakCrAndSpace() throws Exception {
        assertPosition(findSection("\r   "),
                -1, actualEndOfSection8217375(-1, false, 1), 1);
    }

    @Test
    public void testOneLineBreakLfAndSpace() throws Exception {
        assertPosition(findSection("\n   "),
                -1, actualEndOfSection8217375(-1, false, 1), 1);
    }

    @Test
    public void testOneLineBreakCrLfAndSpace() throws Exception {
        assertPosition(findSection("\r\n   "),
                actualEndOfFirstLine8217375(-1),
                actualEndOfSection8217375(-1, false, 1),
                2);
    }

    @Test
    public void testCrEof() throws Exception {
        catchCrCausesIndexOutOfBoundsException(
                () -> findSection("abc\r"),
                p -> assertPosition(p, 2, 3, 4)
        );
    }

    @Test
    public void testLfEof() throws Exception {
        assertPosition(findSection("abc\n"), 2, 3, 4);
    }

    @Test
    public void testCrLfEof() throws Exception {
        assertPosition(findSection("abc\r\n"), 2, 4, 5);
    }

    @Test
    public void testCrContinued() throws Exception {
        assertPosition(findSection("abc\rxyz\r\n\r\n   "), 2, 8, 11);
    }

    @Test
    public void testLfContinued() throws Exception {
        assertPosition(findSection("abc\nxyz\r\n\r\n   "), 2, 8, 11);
    }

    @Test
    public void testCrLfContinued() throws Exception {
        assertPosition(findSection("abc\r\nxyz\r\n\r\n   "), 2, 9, 12);
    }

    @Test
    public void testCrCrEof() throws Exception {
        catchCrCausesIndexOutOfBoundsException(
                () -> findSection("abc\r\nxyz\r\r"),
                p -> assertPosition(p,
                        2, actualEndOfSection8217375(8, true, 1), 10)
        );
    }

    @Test
    public void testCrCrContinued() throws Exception {
        assertPosition(findSection("abc\r\nxyz\r\r   "), 2, 8, 10);
    }

    @Test
    public void testLfLfEof() throws Exception {
        assertPosition(findSection("abc\r\nxyz\n\n"),
                2, actualEndOfSection8217375(8, true, 1), 10);
    }

    @Test
    public void testLfLfContinued() throws Exception {
        assertPosition(findSection("abc\r\nxyz\n\n   "), 2, 8, 10);
    }

    @Test
    public void testCrLfEof2() throws Exception {
        assertPosition(findSection("abc\r\nxyz\r\n"), 2, 9, 10);
    }

    @Test
    public void testMainSectionNotTerminatedWithLineBreak() throws Exception {
        assertNull(findSection("abc\r\nxyz\r\n   "));
    }

    @Test
    public void testLfCrEof() throws Exception {
        catchCrCausesIndexOutOfBoundsException(
                () -> findSection("abc\r\nxyz\n\r"),
                p -> assertPosition(p,
                        2, actualEndOfSection8217375(8, true, 1), 10)
        );
    }

    @Test
    public void testLfCrContinued() throws Exception {
        assertPosition(findSection("abc\r\nxyz\n\r   "), 2, 8, 10);
    }

    @Test
    public void testCrLfCrEof() throws Exception {
        catchCrCausesIndexOutOfBoundsException(
                () -> findSection("abc\r\nxyz\r\n\r"),
                p -> assertPosition(p,
                        2, actualEndOfSection8217375(9, true, 2), 11)
        );
    }

    @Test
    public void testCrLfCrContinued() throws Exception {
        assertPosition(findSection("abc\r\nxyz\r\n\r   "), 2, 9, 11);
    }

    @Test
    public void testCrLfLfEof() throws Exception {
        assertPosition(findSection("abc\r\nxyz\r\n\n"),
                2, actualEndOfSection8217375(9, true, 1), 11);
    }

    @Test
    public void testCrLfLfContinued() throws Exception {
        assertPosition(findSection("abc\r\nxyz\r\n\n   "), 2, 9, 11);
    }

    @Test
    public void testCrLfCrLfEof() throws Exception {
        assertPosition(findSection("abc\r\nxyz\r\n\r\n"),
                2, actualEndOfSection8217375(9, true, 2), 12);
    }

    @Test
    public void testCrLfCfLfContinued() throws Exception {
        assertPosition(findSection("abc\r\nxyz\r\n\r\n   "), 2, 9, 12);
    }

    @Test
    public void testCrLfCrCrEof() throws Exception {
        assertPosition(findSection("abc\r\nxyz\r\n\r\r"), 2, 9, 11);
    }

    @Test
    public void testCrLfCrCrContinued() throws Exception {
        assertPosition(findSection("abc\r\nxyz\r\n\r\r   "), 2, 9, 11);
    }

    @Test
    public void testCrLfLfCrEof() throws Exception {
        assertPosition(findSection("abc\r\nxyz\r\n\n\r"), 2, 9, 11);
    }

    @Test
    public void testCrLfLfCrContinued() throws Exception {
        assertPosition(findSection("abc\r\nxyz\r\n\n\r   "), 2, 9, 11);
    }

    @Test
    public void testCrLfCrLfCrEof() throws Exception {
        assertPosition(findSection("abc\r\nxyz\r\n\r\n\r"), 2, 9, 12);
    }

    @Test
    public void testCrLfCfLfCrContinued() throws Exception {
        assertPosition(findSection("abc\r\nxyz\r\n\r\n\r   "), 2, 9, 12);
    }

    @Test
    public void testCrLfCrLfContinued() throws Exception {
        assertPosition(findSection("abc\r\nxyz\r\n\r\n   "), 2, 9, 12);
    }

    @Test
    public void testCrLfLfLfEof() throws Exception {
        assertPosition(findSection("abc\r\nxyz\r\n\n\n"), 2, 9, 11);
    }

    @Test
    public void testCrLfLfLfContinued() throws Exception {
        assertPosition(findSection("abc\r\nxyz\r\n\n\n   "), 2, 9, 11);
    }

    @Test
    public void testCrLfCrLfLfContinued() throws Exception {
        assertPosition(findSection("abc\r\nxyz\r\n\r\n\n   "), 2, 9, 12);
    }

    @Test
    public void testCrLfCrCrLfEof() throws Exception {
        assertPosition(findSection("abc\r\nxyz\r\n\r\r\n"), 2, 9, 11);
    }

    @Test
    public void testCrLfCrCrLfContinued() throws Exception {
        assertPosition(findSection("abc\r\nxyz\r\n\r\r\n   "), 2, 9, 11);
    }

    @Test
    public void testCrLfLfCrLfEof() throws Exception {
        assertPosition(findSection("abc\r\nxyz\r\n\n\r\n"), 2, 9, 11);
    }

    @Test
    public void testCrLfLfCrLfContinued() throws Exception {
        assertPosition(findSection("abc\r\nxyz\r\n\n\r\n   "), 2, 9, 11);
    }

    @Test
    public void testCrLfCrLfCrLfEof() throws Exception {
        assertPosition(findSection("abc\r\nxyz\r\n\r\n\r\n"), 2, 9, 12);
    }

    @Test
    public void testCrLfCfLfCrLfContinued() throws Exception {
        assertPosition(findSection("abc\r\nxyz\r\n\r\n\r\n   "), 2, 9, 12);
    }

    @Test
    public void testCrLfLfCrCrEof() throws Exception {
        assertPosition(findSection("abc\r\nxyz\r\n\n\r\r"), 2, 9, 11);
    }

    @Test
    public void testCrLfCrLfCrCrEof() throws Exception {
        assertPosition(findSection("abc\r\nxyz\r\n\r\n\r\r"), 2, 9, 12);
    }

    @Test
    public void testCrLfCrLfCrContinued() throws Exception {
        assertPosition(findSection("abc\r\nxyz\r\n\r\n\r   "), 2, 9, 12);
    }

    @Test
    public void testCrLfLfLfCrEof() throws Exception {
        assertPosition(findSection("abc\r\nxyz\r\n\n\n\r"), 2, 9, 11);
    }

    @Test
    public void testCrLfLfCrLfCrEof() throws Exception {
        assertPosition(findSection("abc\r\nxyz\r\n\n\r\n\r"), 2, 9, 11);
    }

    @Test
    public void testCrLfLfLfLfEof() throws Exception {
        assertPosition(findSection("abc\r\nxyz\r\n\n\n\n"), 2, 9, 11);
    }

    @Test
    public void testCrLfLfCrLfLfEof() throws Exception {
        assertPosition(findSection("abc\r\nxyz\r\n\n\r\n\n"), 2, 9, 11);
    }

    @Test
    public void testCrLfLfCrCrLfEof() throws Exception {
        assertPosition(findSection("abc\r\nxyz\r\n\n\r\r\n"), 2, 9, 11);
    }

    @Test
    public void testCrLfCrLfCrCrLfEof() throws Exception {
        assertPosition(findSection("abc\r\nxyz\r\n\r\n\r\r\n"), 2, 9, 12);
    }

    @Test
    public void testCrLfCrLfCrLfContinued() throws Exception {
        assertPosition(findSection("abc\r\nxyz\r\n\r\n\r\n   "), 2, 9, 12);
    }

    @Test
    public void testCrLfLfLfCrLfEof() throws Exception {
        assertPosition(findSection("abc\r\nxyz\r\n\n\n\r\n"), 2, 9, 11);
    }

    @Test
    public void testCrLfLfCrLfCrLfEof() throws Exception {
        assertPosition(findSection("abc\r\nxyz\r\n\n\r\n\r\n"), 2, 9, 11);
    }

    @Test
    public void testCrLfCrCrLfCrCrEof() throws Exception {
        assertPosition(findSection("abc\r\nxyz\r\n\r\r\n\r"), 2, 9, 11);
    }

    @Test
    public void testCrLfCrCrCrCrEof() throws Exception {
        assertPosition(findSection("abc\r\nxyz\r\n\r\r\r"), 2, 9, 11);
    }

    @Test
    public void testCrLfCrCrLfLfEof() throws Exception {
        assertPosition(findSection("abc\r\nxyz\r\n\r\r\n\n"), 2, 9, 11);
    }

    @Test
    public void testCrLfCrCrLfCrLfEof() throws Exception {
        assertPosition(findSection("abc\r\nxyz\r\n\r\r\n\r\n"), 2, 9, 11);
    }

    @Test
    public void testCrLfCrCrCrLfEof() throws Exception {
        assertPosition(findSection("abc\r\nxyz\r\n\r\r\r\n"), 2, 9, 11);
    }

    /*
     * endOfFirstLine is the same regardless of the line break delimiter
     */
    @Test
    public void testEndOfFirstLineVsLineBreak() throws Exception {
        for (String lb : new String[] { "\r", "\n", "\r\n" }) {
            Position p = findSection("abc" + lb + "xyz" + lb + lb + " ");

            // main assertion showing endOfFirstLine independent of line break
            assertEquals(p.endOfFirstLine, 2 + offset);

            // assert remaining positions as well just for completeness
            assertPosition(p, 2, 5 + 2 * lb.length(), 6 + 3 * lb.length());
        }
    }

    /*
     * '\r' at the end of the bytes causes index out of bounds exception
     */
    @Test
    public void testCrLastCausesIndexOutOfBounds() throws Exception {
        catchCrCausesIndexOutOfBoundsException(
                () -> findSection("\r"),
                p -> assertPosition(p,
                        -1, actualEndOfSection8217375(-1, true, 1), 1)
        );
    }

    /*
     * endOfSection includes second line break if at end of bytes only
     */
    @Test
    public void testEndOfSectionWithLineBreakVsEof() throws Exception {
        AssertionError errors = new AssertionError("offset = " + offset);
        for (String lb : new String[] { "\r", "\n", "\r\n" }) {
            for (boolean eof : new boolean[] { false, true }) {
                Position p;
                try {
                    p = findSection("abc" + lb + lb + (eof ? "" : "xyz"));
                } catch (RuntimeException | ReflectiveOperationException e) {
                    if ((e instanceof IndexOutOfBoundsException ||
                         e.getCause() instanceof IndexOutOfBoundsException)
                          && eof && "\r".equals(lb) && !FIXED_8217375) continue;
                    throw e;
                }

                AssertionError a = new AssertionError("offset = " + offset
                        + ", lb = " + Utils.escapeStringWithNumbers(lb) + ", "
                        + "eof = " + eof);

                // main assertion showing endOfSection including second line
                // break when at end of file
                a = collectErrors(a, () -> assertEquals(
                        p.endOfSection,
                        actualEndOfSection8217375(
                                2 + lb.length() + offset, eof, lb.length()) ));

                // assert remaining positions as well just for completeness
                a = collectErrors(a, () -> assertPosition(p,
                        2,
                        actualEndOfSection8217375(
                                2 + lb.length(), eof, lb.length()),
                        3 + lb.length() * 2));

                if (a.getSuppressed().length > 0) errors.addSuppressed(a);
            }
        }
        if (errors.getSuppressed().length > 0) throw errors;
    }

    /*
     * returns position even if only one line break before end of bytes.
     * because no name will be found the result will be skipped and no entry
     * will be created.
     */
    @Test
    public void testReturnPosVsEof() throws Exception {
        for (String lb : new String[] { "\r", "\n", "\r\n" }) {
            for (boolean eof : new boolean[] { false, true }) {
                try {
                    Position p = findSection("abc" + lb + (eof ? "" : "xyz"));
                    assertTrue(p != null == eof);
                } catch (RuntimeException | ReflectiveOperationException e) {
                    if ((e instanceof IndexOutOfBoundsException ||
                         e.getCause() instanceof IndexOutOfBoundsException)
                          && eof && "\r".equals(lb) && !FIXED_8217375) continue;
                    throw e;
                }
            }
        }
    }

    /*
     * it could be normally be expected that startOfNext would point to the
     * start of the next section after a blank line but that is not the case
     * if a section ends with only one line break and no blank line immediately
     * before eof of the manifest.
     * such an entry will be digested without the trailing blank line which is
     * only fine until another section should be added afterwards.
     */
    @Test
    public void testStartOfNextPointsToEofWithNoBlankLine() throws Exception {
        for (String lb : new String[] { "\r", "\n", "\r\n" }) {
            for (boolean blank : new boolean[] { false, true }) {
                String manifest = "abc" + lb + "xyz" + lb + (blank ? lb : "");
                try {
                    Position p = findSection(manifest);

                    // assert that startOfNext points to eof in all cases
                    // whether with or without a blank line before eof
                    assertEquals(p.startOfNext, manifest.length() + offset);

                    // assert remaining positions as well just for completeness
                    assertPosition(p,
                            2,
                            actualEndOfSection8217375(
                                    5 + lb.length() * 2,
                                    true,
                                    blank ? lb.length() : 0),
                            manifest.length());
                } catch (RuntimeException | ReflectiveOperationException e) {
                    if ((e instanceof IndexOutOfBoundsException ||
                         e.getCause() instanceof IndexOutOfBoundsException)
                          && "\r".equals(lb) && !FIXED_8217375) continue;
                    throw e;
                }
            }
        }
    }

}