This is an automated email from the ASF dual-hosted git repository.

pkarwasz pushed a commit to branch fix/attribute-layout-parser
in repository https://gitbox.apache.org/repos/asf/commons-compress.git

commit 1c14156e0a3db9942b8023df4a6785f1ae3ae91a
Author: Piotr P. Karwasz <[email protected]>
AuthorDate: Tue Nov 11 17:36:52 2025 +0100

    Pack200: enforce strict attribute layout parsing
    
    The parser for the [Pack200 attribute layout 
definitions](https://docs.oracle.com/en/java/javase/11/docs/specs/pack-spec.html#attribute-layout-definitions)
 micro-language was previously overly permissive, allowing invalid layouts and 
masking archive corruption.
    
    This change implements strict parsing in accordance with the [Java 11 
specification](https://docs.oracle.com/en/java/javase/11/docs/specs/pack-spec.html)
 and removes duplication of the micro-language parsing logic between 
`pack200.NewAttributeBands` and `unpack200.NewAttributeBands`.
---
 pom.xml                                            |  35 +-
 src/changes/changes.xml                            |   1 +
 .../harmony/archive/internal/nls/package-info.java |   6 +-
 .../harmony/internal/AttributeLayoutParser.java    | 362 ++++++++++++++++++++
 .../harmony/internal/AttributeLayoutUtils.java     | 252 ++++++++++++++
 .../internal/nls => internal}/package-info.java    |   9 +-
 .../harmony/pack200/NewAttributeBands.java         | 367 ++++++--------------
 .../harmony/unpack200/NewAttributeBands.java       | 375 ++++++---------------
 .../internal/AttributeLayoutParserTest.java        | 149 ++++++++
 .../harmony/internal/AttributeLayoutUtilsTest.java |  79 +++++
 .../compress/harmony/pack200/Compress626Test.java  |   6 +-
 .../compress/harmony/pack200/Compress628Test.java  |   6 +-
 .../harmony/pack200/NewAttributeBandsTest.java     |   2 +-
 .../harmony/unpack200/NewAttributeBandsTest.java   |  29 +-
 14 files changed, 1132 insertions(+), 546 deletions(-)

diff --git a/pom.xml b/pom.xml
index 987774995..c709cc20d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -50,6 +50,11 @@ Brotli, Zstandard and ar, cpio, jar, tar, zip, dump, 7z, arj.
     
<commons.distSvnStagingUrl>scm:svn:https://dist.apache.org/repos/dist/dev/commons/${commons.componentid}</commons.distSvnStagingUrl>
     
<commons.manifestlocation>${project.build.outputDirectory}/META-INF</commons.manifestlocation>
     
<commons.manifestfile>${commons.manifestlocation}/MANIFEST.MF</commons.manifestfile>
+    <!-- Don't export internal packages in OSGi manifest -->
+    <commons.osgi.export>
+      !org.apache.commons.*.internal.*,
+      org.apache.commons.*;version=${project.version};-noimport:=true
+    </commons.osgi.export>
     <commons.osgi.import>
       org.tukaani.xz;resolution:=optional,
       org.brotli.dec;resolution:=optional,
@@ -540,7 +545,8 @@ Brotli, Zstandard and ar, cpio, jar, tar, zip, dump, 7z, 
arj.
       </build>
     </profile>
     <profile>
-      <id>java9+</id>
+      <!-- Same id as the `commons-parent` profile it extends -->
+      <id>java-9-up</id>
       <activation>
         <jdk>[9,)</jdk>
       </activation>
@@ -548,6 +554,33 @@ Brotli, Zstandard and ar, cpio, jar, tar, zip, dump, 7z, 
arj.
         <maven.compiler.release>8</maven.compiler.release>
         <animal.sniffer.skip>true</animal.sniffer.skip>
       </properties>
+      <!--
+        Excludes internal packages from the list of exported packages.
+      -->
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.moditect</groupId>
+            <artifactId>moditect-maven-plugin</artifactId>
+            <executions>
+              <execution>
+                <id>add-module-infos</id>
+                <configuration>
+                  <module>
+                    <moduleInfo>
+                      <exports>
+                        !org.apache.commons.compress.harmony.internal.*;
+                        
!org.apache.commons.compress.harmony.archive.internal.*;
+                        *;
+                      </exports>
+                    </moduleInfo>
+                  </module>
+                </configuration>
+              </execution>
+            </executions>
+          </plugin>
+        </plugins>
+      </build>
     </profile>
     <profile>
       <id>java17</id>
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 20c0c855b..d192ff9c2 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -86,6 +86,7 @@ The <action> type attribute can be add,update,fix,remove.
       <action type="fix" dev="pkarwasz" due-to="Piotr P. Karwasz">Add strict 
header validation in ARJ input stream and `selfExtracting` option.</action>
       <!-- FIX unpack200 -->
       <action type="fix" dev="ggregory" due-to="Gary Gregory, Stanislav 
Fort">org.apache.commons.compress.harmony.unpack200 now throws 
Pack200Exception, IllegalArgumentException, and IllegalStateException instead 
of other runtime exceptions and Error.</action>
+      <action type="fix" dev="pkarwasz" due-to="Piotr P. Karwasz">Enforce 
strict attribute layout parsing in Pack200.</action>
       <!-- FIX pack200 -->      
       <action type="fix" dev="ggregory" due-to="Gary Gregory, Igor 
Morgenstern">org.apache.commons.compress.harmony.pack200 now throws 
Pack200Exception, IllegalArgumentException, IllegalStateException, instead of 
other runtime exceptions and Error.</action>
       <action type="fix" dev="ppkarwasz" due-to="Raeps">Extract duplicate code 
in org.apache.commons.compress.harmony.pack200.IntList.</action>
diff --git 
a/src/main/java/org/apache/commons/compress/harmony/archive/internal/nls/package-info.java
 
b/src/main/java/org/apache/commons/compress/harmony/archive/internal/nls/package-info.java
index 672547a16..9b2dbdcd9 100644
--- 
a/src/main/java/org/apache/commons/compress/harmony/archive/internal/nls/package-info.java
+++ 
b/src/main/java/org/apache/commons/compress/harmony/archive/internal/nls/package-info.java
@@ -16,8 +16,10 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-
 /**
- * Internal package.
+ * Internal implementation details.
+ *
+ * <p>This package is not exported and must not be used by external code.
+ * Its contents may change or be removed at any time without notice.</p>
  */
 package org.apache.commons.compress.harmony.archive.internal.nls;
diff --git 
a/src/main/java/org/apache/commons/compress/harmony/internal/AttributeLayoutParser.java
 
b/src/main/java/org/apache/commons/compress/harmony/internal/AttributeLayoutParser.java
new file mode 100644
index 000000000..b1d9f7a83
--- /dev/null
+++ 
b/src/main/java/org/apache/commons/compress/harmony/internal/AttributeLayoutParser.java
@@ -0,0 +1,362 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.commons.compress.harmony.internal;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.commons.compress.harmony.pack200.Pack200Exception;
+import org.apache.commons.lang3.Range;
+
+/**
+ * Encapsulates parsing of attribute layout definitions.
+ *
+ * <p>Previously the parsing logic was duplicated in {@link 
org.apache.commons.compress.harmony.pack200.NewAttributeBands} and
+ * {@link org.apache.commons.compress.harmony.unpack200.NewAttributeBands}.</p>
+ *
+ * @param <LAYOUT_ELEMENT>           the type corresponding to the {@code 
layout_element} production
+ * @param <ATTRIBUTE_LAYOUT_ELEMENT> the type corresponding to the elements of 
the {@code attribute_layout} production: either {@code layout_element} or
+ *                                   {@code callable}
+ */
+public final class AttributeLayoutParser<ATTRIBUTE_LAYOUT_ELEMENT, 
LAYOUT_ELEMENT extends ATTRIBUTE_LAYOUT_ELEMENT> {
+
+    /**
+     * Factory interface for creating attribute layout elements.
+     */
+    public interface Factory<ATTRIBUTE_LAYOUT_ELEMENT, LAYOUT_ELEMENT extends 
ATTRIBUTE_LAYOUT_ELEMENT> {
+        LAYOUT_ELEMENT createCall(int callableIndex);
+
+        ATTRIBUTE_LAYOUT_ELEMENT createCallable(String body) throws 
Pack200Exception;
+
+        LAYOUT_ELEMENT createIntegral(String tag);
+
+        LAYOUT_ELEMENT createReference(String tag);
+
+        LAYOUT_ELEMENT createReplication(String unsignedInt, String body) 
throws Pack200Exception;
+
+        LAYOUT_ELEMENT createUnion(String anyInt, List<UnionCaseData> cases, 
String body) throws Pack200Exception;
+    }
+
+    /**
+     * Data class representing a union case in an attribute layout definition.
+     */
+    public static final class UnionCaseData {
+        public final String body;
+        public final List<Range<Integer>> tagRanges;
+
+        private UnionCaseData(final List<Range<Integer>> tagRanges, final 
String body) {
+            this.tagRanges = Collections.unmodifiableList(tagRanges);
+            this.body = body;
+        }
+    }
+
+    private final CharSequence definition;
+    private final Factory<ATTRIBUTE_LAYOUT_ELEMENT, LAYOUT_ELEMENT> factory;
+    private int p;
+
+    public AttributeLayoutParser(final CharSequence definition, final 
Factory<ATTRIBUTE_LAYOUT_ELEMENT, LAYOUT_ELEMENT> factory) {
+        this.definition = definition;
+        this.factory = factory;
+    }
+
+    private void ensureNotEof() throws Pack200Exception {
+        if (eof()) {
+            throw new Pack200Exception("Incomplete attribute layout 
definition: " + definition);
+        }
+    }
+
+    private boolean eof() {
+        return p >= definition.length();
+    }
+
+    private char expect(char... expected) throws Pack200Exception {
+        final char c = next();
+        for (char e : expected) {
+            if (c == e) {
+                return c;
+            }
+        }
+        throw new Pack200Exception("Invalid attribute layout definition: 
expected one of " + new String(expected) + ", found '" + c + "'");
+    }
+
+    private char next() throws Pack200Exception {
+        ensureNotEof();
+        return definition.charAt(p++);
+    }
+
+    private char peek() throws Pack200Exception {
+        ensureNotEof();
+        return definition.charAt(p);
+    }
+
+    private String readAnyInt() throws Pack200Exception {
+        final char first = next();
+        return AttributeLayoutUtils.checkAnyIntTag(first == 'S' ? "S" + next() 
: String.valueOf(first));
+    }
+
+    /**
+     * Reads the next {@code attribute_layout_element} from the stream.
+     *
+     * <pre>
+     * attribute_layout_element:
+     *       ( callable | layout_element )
+     * callable:
+     *       '[' (body)? ']'
+     * </pre>
+     *
+     * @return next AttributeLayoutElement from the stream or {@code null} if 
end of stream is reached.
+     * @throws Pack200Exception If the layout definition is invalid.
+     */
+    public ATTRIBUTE_LAYOUT_ELEMENT readAttributeLayoutElement() throws 
Pack200Exception {
+        if (eof()) {
+            return null;
+        }
+        final char first = peek();
+        if (first == '[') {
+            try {
+                final String body = readBody();
+                if (body.isEmpty()) {
+                    throw new Pack200Exception("Corrupted Pack200 archive: 
Callable body cannot be empty.");
+                }
+                return factory.createCallable(body);
+            } catch (final Exception ex) {
+                throw new Pack200Exception(String.format("Corrupted Pack200 
archive: Invalid layout '%s' at position %d.", definition, p), ex);
+            }
+        }
+        return readLayoutElement();
+    }
+
+    private String readBody() throws Pack200Exception {
+        expect('[');
+        int depth = 1;
+        final StringBuilder body = new StringBuilder();
+        char c;
+        while (true) {
+            c = next();
+            if (c == '[') {
+                depth++;
+            } else if (c == ']') {
+                depth--;
+                if (depth == 0) {
+                    break;
+                }
+            }
+            body.append(c);
+        }
+        return body.toString();
+    }
+
+    private String readIntegralTag() throws Pack200Exception {
+        char c = next();
+        final char[] buf = new char[3];
+        int len = 0;
+        buf[len++] = c;
+        if (c == 'F' || c == 'O' || c == 'P' || c == 'S') {
+            c = next();
+            buf[len++] = c;
+            if (c == 'O' || c == 'S') {
+                c = next();
+                buf[len++] = c;
+            }
+        }
+        return AttributeLayoutUtils.checkIntegralTag(new String(buf, 0, len));
+    }
+
+    /**
+     * Reads the next {@code layout_element} from the stream.
+     *
+     * <pre>
+     * layout_element:
+     *       ( integral | replication | union | call | reference )
+     * </pre>
+     *
+     * @return next LayoutElement from the stream or {@code null} if end of 
stream is reached.
+     * @throws Pack200Exception If the layout definition is invalid.
+     */
+    public LAYOUT_ELEMENT readLayoutElement() throws Pack200Exception {
+        if (eof()) {
+            return null;
+        }
+        try {
+            switch (peek()) {
+                // Integrals
+                case 'B':
+                case 'H':
+                case 'I':
+                case 'V':
+                case 'S':
+                case 'F':
+                case 'O':
+                case 'P':
+                    return factory.createIntegral(readIntegralTag());
+                // Replication
+                case 'N': {
+                    next();
+                    final String integral = readUnsignedInt();
+                    final String body = readBody();
+                    if (body.isEmpty()) {
+                        throw new Pack200Exception("Corrupted Pack200 archive: 
Replication body cannot be empty.");
+                    }
+                    return factory.createReplication(integral, body);
+                }
+                // Union
+                case 'T': {
+                    next();
+                    final String anyInt = readAnyInt();
+                    final List<UnionCaseData> unionCases = new ArrayList<>();
+                    UnionCaseData data;
+                    while ((data = readUnionCase()) != null) {
+                        unionCases.add(data);
+                    }
+                    expect('(');
+                    expect(')');
+                    final String body = readBody();
+                    return factory.createUnion(anyInt, unionCases, body);
+                }
+                // Call
+                case '(': {
+                    next();
+                    final int callNumber = readNumeral();
+                    expect(')');
+                    return factory.createCall(callNumber);
+                }
+                // Reference
+                case 'K':
+                case 'R': {
+                    return factory.createReference(readReferenceTag());
+                }
+                default: {
+                    throw new Pack200Exception("Unexpected character '" + 
peek() + "' in attribute layout definition.");
+                }
+            }
+        } catch (final Exception ex) {
+            throw new Pack200Exception(String.format("Corrupted Pack200 
archive: Invalid layout '%s' at position %d.", definition, p), ex);
+        }
+    }
+
+    /**
+     * Reads a number from the stream.
+     *
+     * <p>Stops reading at the first character, which is not a digit.</p>
+     *
+     * <p><strong>Note:</strong> there is a typo in the
+     * <a 
href="https://docs.oracle.com/en/java/javase/11/docs/specs/pack-spec.html#attribute-layout-definitions";>official
 {@code numeral} definition</a>.
+     * Parentheses should <strong>not</strong> be part of the numeral 
definition.</p>
+     *
+     * <pre>
+     * numeral:
+     *       ('-')? (digit)+
+     * </pre>
+     *
+     * @return A number from the stream.
+     * @throws IOException If an I/O error occurs.
+     */
+    private int readNumeral() throws IOException {
+        // Determine if the number is negative
+        final char first = peek();
+        final boolean negative = first == '-';
+        if (negative) {
+            next();
+        }
+        // Read the number
+        final char firstDigit = next();
+        if (!Character.isDigit(firstDigit)) {
+            throw new Pack200Exception("Corrupted Pack200 archive: numeral 
must start with a digit.");
+        }
+        long result = firstDigit - '0';
+        while (!eof()) {
+            if (!Character.isDigit(peek())) {
+                break;
+            }
+            result = result * 10 + (next() - '0');
+            // Check for overflow
+            if (result > Integer.MAX_VALUE) {
+                throw new Pack200Exception("Corrupted Pack200 archive: numeral 
value out of range.");
+            }
+        }
+        return (int) (negative ? -result : result);
+    }
+
+    private String readReferenceTag() throws Pack200Exception {
+        final char[] buf = new char[4];
+        int len = 0;
+        buf[len++] = next();
+        buf[len++] = next();
+        char c = next();
+        buf[len++] = c;
+        if (c == 'N') {
+            c = next();
+            buf[len++] = c;
+        }
+        return AttributeLayoutUtils.checkReferenceTag(new String(buf, 0, len));
+    }
+
+    /**
+     * Reads a non default {@code union_case} from the stream
+     *
+     * <p>Reads a non default {@code union_case} or returns {@code null} if 
the default case {@code ()} is encountered.</p>
+     *
+     * <pre>
+     * union_case:
+     *       '(' union_case_tag (',' union_case_tag)* ')' '[' (body)? ']'
+     * union_case_tag:
+     *       ( numeral | numeral '-' numeral )
+     * </pre>
+     *
+     * @return A UnionCase from the stream or {@code null} if the default case 
is encountered.
+     * @throws IOException If an I/O error occurs.
+     */
+    private UnionCaseData readUnionCase() throws IOException {
+        // Check for default case
+        expect('(');
+        char c = peek();
+        if (c == ')') {
+            // Default case
+            p--;
+            return null;
+        }
+        // Read the tag ranges
+        final List<Range<Integer>> tagRanges = new ArrayList<>();
+        int nextTag;
+        Integer startTag = null;
+        do {
+            nextTag = readNumeral();
+            c = expect('-', ',', ')');
+            if (startTag == null) { // First number of a range
+                if (c == '-') {
+                    startTag = nextTag;
+                } else {
+                    tagRanges.add(Range.of(nextTag, nextTag));
+                }
+            } else { // Second number of a range
+                tagRanges.add(Range.of(startTag, nextTag));
+                startTag = null;
+            }
+        } while (c != ')');
+        // Read the body
+        final String body = readBody();
+        return new UnionCaseData(tagRanges, body);
+    }
+
+    private String readUnsignedInt() throws Pack200Exception {
+        return 
AttributeLayoutUtils.checkUnsignedIntTag(String.valueOf(next()));
+    }
+}
diff --git 
a/src/main/java/org/apache/commons/compress/harmony/internal/AttributeLayoutUtils.java
 
b/src/main/java/org/apache/commons/compress/harmony/internal/AttributeLayoutUtils.java
new file mode 100644
index 000000000..bbe1d1910
--- /dev/null
+++ 
b/src/main/java/org/apache/commons/compress/harmony/internal/AttributeLayoutUtils.java
@@ -0,0 +1,252 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.commons.compress.harmony.internal;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.apache.commons.compress.harmony.pack200.Pack200Exception;
+import org.apache.commons.lang3.Range;
+
+public final class AttributeLayoutUtils {
+
+    /**
+     * <pre>
+     * integral:
+     *       ( unsigned_int | signed_int | bc_index | bc_offset | flag )
+     * signed_int:
+     *       'S' uint_type
+     * any_int:
+     *       ( unsigned_int | signed_int )
+     * bc_index:
+     *       ( 'P' uint_type | 'PO' uint_type )
+     * bc_offset:
+     *       'O' any_int
+     * flag:
+     *       'F' uint_type
+     * uint_type:
+     *       ( 'B' | 'H' | 'I' | 'V' )
+     * </pre>
+     */
+    private static final Set<String> INTEGRAL_TAGS;
+
+    /**
+     * <pre>
+     * reference:
+     *       reference_type ( 'N' )? uint_type
+     * reference_type:
+     *       ( constant_ref | schema_ref | utf8_ref | untyped_ref )
+     * constant_ref:
+     *       ( 'KI' | 'KJ' | 'KF' | 'KD' | 'KS' | 'KQ' | 'KM' | 'KT' | 'KL' )
+     * schema_ref:
+     *       ( 'RC' | 'RS' | 'RD' | 'RF' | 'RM' | 'RI' | 'RY' | 'RB' | 'RN' )
+     * utf8_ref:
+     *       'RU'
+     * untyped_ref:
+     *       'RQ'
+     * uint_type:
+     *       ( 'B' | 'H' | 'I' | 'V' )
+     * </pre>
+     */
+    private static final Set<String> REFERENCE_TAGS;
+
+    /**
+     * <pre>
+     * uint_type:
+     *       ( 'B' | 'H' | 'I' | 'V' )
+     * </pre>
+     */
+    private static final Set<String> UNSIGNED_INT_TAGS;
+
+    static {
+        final Set<String> unsignedIntTags = new HashSet<>();
+        Collections.addAll(unsignedIntTags,
+                // unsigned_int
+                "B", "H", "I", "V");
+        UNSIGNED_INT_TAGS = Collections.unmodifiableSet(unsignedIntTags);
+        final Set<String> integralTags = new HashSet<>();
+        UNSIGNED_INT_TAGS.forEach(tag -> Collections.addAll(integralTags,
+                // unsigned_int
+                tag,
+                // signed_int
+                "S" + tag,
+                // bc_index
+                "P" + tag, "PO" + tag,
+                // bc_offset
+                "O" + tag, "OS" + tag,
+                // flag
+                "F" + tag)); INTEGRAL_TAGS = 
Collections.unmodifiableSet(integralTags);
+        final Set<String> referenceTags = new HashSet<>();
+        Collections.addAll(referenceTags,
+                // constant_ref
+                "KI", "KJ", "KF", "KD", "KS", "KQ", "KM", "KT", "KL",
+                // schema_ref
+                "RC", "RS", "RD", "RF", "RM", "RI", "RY", "RB", "RN",
+                // utf8_ref
+                "RU",
+                // untyped_ref
+                "RQ");
+        REFERENCE_TAGS = Collections.unmodifiableSet(referenceTags);
+    }
+
+    /**
+     * Validates that the given tag matches the {@code unsigned_int} layout 
definition production rule.
+     *
+     * @param tag the layout tag to validate
+     * @return the validated tag
+     * @throws IllegalArgumentException if the tag is invalid
+     */
+    static String checkAnyIntTag(final String tag) {
+        if (UNSIGNED_INT_TAGS.contains(tag) || tag.startsWith("S") && 
UNSIGNED_INT_TAGS.contains(tag.substring(1))) {
+            return tag;
+        }
+        throw new IllegalArgumentException("Invalid unsigned int layout tag: " 
+ tag);
+    }
+
+    /**
+     * Validates that the given tag matches the {@code integral} layout 
definition production rule.
+     *
+     * @param tag the layout tag to validate
+     * @return the validated tag
+     * @throws IllegalArgumentException if the tag is invalid
+     */
+    public static String checkIntegralTag(final String tag) {
+        if (INTEGRAL_TAGS.contains(tag)) {
+            return tag;
+        }
+        throw new IllegalArgumentException("Invalid integral layout tag: " + 
tag);
+    }
+
+    /**
+     * Validates that the given tag matches the {@code reference} layout 
definition production rule.
+     *
+     * @param tag the layout tag to validate
+     * @return the validated tag
+     * @throws IllegalArgumentException if the tag is invalid
+     */
+    public static String checkReferenceTag(final String tag) {
+        if (tag.length() >= 3) {
+            final String baseTag = tag.substring(0, 2);
+            final String uintType = tag.substring(tag.length() - 1);
+            if (REFERENCE_TAGS.contains(baseTag) && 
UNSIGNED_INT_TAGS.contains(uintType)
+                    && (tag.length() == 3 || tag.length() == 4 && 
tag.charAt(2) == 'N')) {
+                return tag;
+            }
+        }
+        throw new IllegalArgumentException("Invalid reference layout tag: " + 
tag);
+    }
+
+    /**
+     * Validates that the given tag matches the {@code unsigned_int} layout 
definition production rule.
+     *
+     * @param tag the layout tag to validate
+     * @return the validated tag
+     * @throws IllegalArgumentException if the tag is invalid
+     */
+    static String checkUnsignedIntTag(final String tag) {
+        if (UNSIGNED_INT_TAGS.contains(tag)) {
+            return tag;
+        }
+        throw new IllegalArgumentException("Invalid unsigned int layout tag: " 
+ tag);
+    }
+
+    /**
+     * Reads a {@code attribute_layout} from the stream.
+     *
+     * <p>The returned list <strong>may</strong> be empty if the stream is 
empty.</p>
+     *
+     * <pre>
+     * attribute_layout:
+     *       ( layout_element )* | ( callable )+
+     * </pre>
+     *
+     * @param definition the attribute layout definition body.
+     * @param factory the factory to create AttributeLayoutElements.
+     * @param <ALE>> the type of AttributeLayoutElement.
+     * @param <LE> the type of LayoutElement.
+     * @return not empty list of LayoutElements from the body or, if the 
stream is empty, an empty list.
+     * @throws Pack200Exception If the layout definition is invalid.
+     */
+    public static <ALE, LE extends ALE> List<ALE> readAttributeLayout(final 
String definition,
+            final AttributeLayoutParser.Factory<ALE, LE> factory) throws 
Pack200Exception {
+        final List<ALE> layoutElements = new ArrayList<>();
+        final AttributeLayoutParser<ALE, LE> parser = new 
AttributeLayoutParser<>(definition, factory);
+        ALE e;
+        while ((e = parser.readAttributeLayoutElement()) != null) {
+            layoutElements.add(e);
+        }
+        return layoutElements;
+    }
+
+    /**
+     * Reads a {@code body} from the stream.
+     *
+     * <p>The returned list <strong>may</strong> be empty if the stream is 
empty.</p>
+     *
+     * <pre>
+     * body:
+     *       ( layout_element )+
+     * </pre>
+     *
+     * @param body the attribute layout definition body.
+     * @param factory the factory to create LayoutElements.
+     * @param <ALE>> the type of AttributeLayoutElement.
+     * @param <LE> the type of LayoutElement.
+     * @return not empty list of LayoutElements from the body or, if the 
stream is empty, an empty list.
+     * @throws Pack200Exception If the layout definition is invalid.
+     */
+    public static <ALE, LE extends ALE> List<LE> readBody(final String body, 
final AttributeLayoutParser.Factory<ALE, LE> factory) throws Pack200Exception {
+        final List<LE> layoutElements = new ArrayList<>();
+        final AttributeLayoutParser<ALE, LE> parser = new 
AttributeLayoutParser<>(body, factory);
+        LE e;
+        while ((e = parser.readLayoutElement()) != null) {
+            layoutElements.add(e);
+        }
+        return layoutElements;
+    }
+
+    /**
+     * Converts a list of integers to a list of ranges where each range 
represents a single integer.
+     *
+     * @param tags the list of integer tags
+     * @return a list of ranges representing the tags
+     */
+    public static List<Range<Integer>> toRanges(final List<Integer> tags) {
+        return tags.stream().map(n -> Range.of(n, 
n)).collect(Collectors.toList());
+    }
+
+    /**
+     * Checks if any of the given tag ranges contains the specified tag.
+     *
+     * @param tagRanges the list of tag ranges
+     * @param tag       the tag to check
+     * @return {@code true} if any range contains the tag, {@code false} 
otherwise
+     */
+    public static boolean unionCaseMatches(final List<Range<Integer>> 
tagRanges, final int tag) {
+        return tagRanges.stream().anyMatch(r -> r.contains(tag));
+    }
+
+    private AttributeLayoutUtils() {
+        // Utility class
+    }
+}
diff --git 
a/src/main/java/org/apache/commons/compress/harmony/archive/internal/nls/package-info.java
 b/src/main/java/org/apache/commons/compress/harmony/internal/package-info.java
similarity index 77%
copy from 
src/main/java/org/apache/commons/compress/harmony/archive/internal/nls/package-info.java
copy to 
src/main/java/org/apache/commons/compress/harmony/internal/package-info.java
index 672547a16..d139a4f4e 100644
--- 
a/src/main/java/org/apache/commons/compress/harmony/archive/internal/nls/package-info.java
+++ 
b/src/main/java/org/apache/commons/compress/harmony/internal/package-info.java
@@ -16,8 +16,11 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-
 /**
- * Internal package.
+ * Internal implementation details.
+ *
+ * <p>This package is not exported and must not be used by external code.
+ * Its contents may change or be removed at any time without notice.</p>
  */
-package org.apache.commons.compress.harmony.archive.internal.nls;
+
+package org.apache.commons.compress.harmony.internal;
diff --git 
a/src/main/java/org/apache/commons/compress/harmony/pack200/NewAttributeBands.java
 
b/src/main/java/org/apache/commons/compress/harmony/pack200/NewAttributeBands.java
index 8b32099ac..ca2680ed1 100644
--- 
a/src/main/java/org/apache/commons/compress/harmony/pack200/NewAttributeBands.java
+++ 
b/src/main/java/org/apache/commons/compress/harmony/pack200/NewAttributeBands.java
@@ -22,15 +22,17 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.io.StringReader;
 import java.io.UncheckedIOException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 
+import org.apache.commons.compress.harmony.internal.AttributeLayoutParser;
+import 
org.apache.commons.compress.harmony.internal.AttributeLayoutParser.UnionCaseData;
+import org.apache.commons.compress.harmony.internal.AttributeLayoutUtils;
 import 
org.apache.commons.compress.harmony.pack200.AttributeDefinitionBands.AttributeDefinition;
-import org.apache.commons.compress.utils.ParsingUtils;
+import org.apache.commons.lang3.Range;
 import org.objectweb.asm.Label;
 
 /**
@@ -55,6 +57,43 @@ public interface AttributeLayoutElement {
 
     }
 
+    private class AttributeLayoutFactory implements 
AttributeLayoutParser.Factory<AttributeLayoutElement, LayoutElement> {
+
+        @Override
+        public LayoutElement createCall(int callableIndex) {
+            return new Call(callableIndex);
+        }
+
+        @Override
+        public AttributeLayoutElement createCallable(String body) throws 
Pack200Exception {
+            return new Callable(AttributeLayoutUtils.readBody(body, 
attributeLayoutFactory));
+        }
+
+        @Override
+        public LayoutElement createIntegral(String tag) {
+            return new Integral(tag);
+        }
+
+        @Override
+        public LayoutElement createReference(String tag) {
+            return new Reference(tag);
+        }
+
+        @Override
+        public LayoutElement createReplication(String unsignedInt, String 
body) throws Pack200Exception {
+            return new Replication(unsignedInt, body);
+        }
+
+        @Override
+        public LayoutElement createUnion(String anyInt, List<UnionCaseData> 
cases, String body) throws Pack200Exception {
+            final List<UnionCase> unionCases = new ArrayList<>();
+            for (final UnionCaseData unionCaseData : cases) {
+                unionCases.add(new UnionCase(unionCaseData.tagRanges, 
AttributeLayoutUtils.readBody(unionCaseData.body, attributeLayoutFactory), 
false));
+            }
+            return new Union(anyInt, unionCases, 
AttributeLayoutUtils.readBody(body, attributeLayoutFactory));
+        }
+    }
+
     public class Call extends LayoutElement {
 
         private final int callableIndex;
@@ -106,7 +145,16 @@ public class Callable implements AttributeLayoutElement {
 
         private int backwardsCallableIndex;
 
-        public Callable(final List<LayoutElement> body) {
+        /**
+         * Constructs a new Callable layout element with the given body.
+         *
+         * @param body the body of the callable.
+         * @throws Pack200Exception If the body is empty.
+         */
+        public Callable(final List<LayoutElement> body) throws 
Pack200Exception {
+            if (body.isEmpty()) {
+                throw new Pack200Exception("Corrupted Pack200 archive: 
Callable body is empty");
+            }
             this.body = body;
         }
 
@@ -166,13 +214,26 @@ public class Integral extends LayoutElement {
         private Integral previousIntegral;
         private int previousPValue;
 
+        /**
+         * Constructs a new Integral layout element with the given tag.
+         *
+         * @param tag The tag.
+         * @throws IllegalArgumentException If the tag is invalid.
+         */
         public Integral(final String tag) {
-            this.tag = tag;
+            this.tag = AttributeLayoutUtils.checkIntegralTag(tag);
             this.defaultCodec = getCodec(tag);
         }
 
+        /**
+         * Constructs a new Integral layout element with the given tag.
+         *
+         * @param tag The tag.
+         * @param previousIntegral The previous integral (for PO and OS types).
+         * @throws IllegalArgumentException If the tag is invalid.
+         */
         public Integral(final String tag, final Integral previousIntegral) {
-            this.tag = tag;
+            this.tag = AttributeLayoutUtils.checkIntegralTag(tag);
             this.defaultCodec = getCodec(tag);
             this.previousIntegral = previousIntegral;
         }
@@ -320,8 +381,14 @@ public class Reference extends LayoutElement {
 
         private final boolean nullsAllowed;
 
+        /**
+         * Constructs a new Reference layout element with the given tag.
+         *
+         * @param tag The tag.
+         * @throws IllegalArgumentException If the tag is invalid.
+         */
         public Reference(final String tag) {
-            this.tag = tag;
+            this.tag = AttributeLayoutUtils.checkReferenceTag(tag);
             nullsAllowed = tag.indexOf('N') != -1;
         }
 
@@ -371,14 +438,21 @@ public class Replication extends LayoutElement {
 
         private final Integral countElement;
 
-        private final List<LayoutElement> layoutElements = new ArrayList<>();
+        private final List<LayoutElement> layoutElements;
 
-        public Replication(final String tag, final String contents) throws 
IOException {
+        /**
+         * Constructs a new Replication layout element.
+         *
+         * @param tag the tag of the Integral element.
+         * @param contents the contents of the replication.
+         * @throws IllegalArgumentException If the tag is invalid or the 
contents are empty.
+         * @throws Pack200Exception If the contents are invalid.
+         */
+        public Replication(final String tag, final String contents) throws 
Pack200Exception {
             this.countElement = new Integral(tag);
-            final StringReader stream = new StringReader(contents);
-            LayoutElement e;
-            while ((e = readNextLayoutElement(stream)) != null) {
-                layoutElements.add(e);
+            this.layoutElements = AttributeLayoutUtils.readBody(contents, 
attributeLayoutFactory);
+            if (layoutElements.isEmpty()) {
+                throw new Pack200Exception("Corrupted Pack200 archive: 
Replication body is empty");
             }
         }
 
@@ -426,6 +500,14 @@ public class Union extends LayoutElement {
         private final List<UnionCase> unionCases;
         private final List<LayoutElement> defaultCaseBody;
 
+        /**
+         * Constructs a new Union layout element.
+         *
+         * @param tag the tag of the Integral element.
+         * @param unionCases the union cases.
+         * @param body the default case body.
+         * @throws IllegalArgumentException If the tag is invalid.
+         */
         public Union(final String tag, final List<UnionCase> unionCases, final 
List<LayoutElement> body) {
             this.unionTag = new Integral(tag);
             this.unionCases = unionCases;
@@ -489,17 +571,19 @@ public void renumberBci(final IntList bciRenumbering, 
final Map<Label, Integer>
      */
     public class UnionCase extends LayoutElement {
 
+        private final List<Range<Integer>> tagRanges;
         private final List<LayoutElement> body;
 
-        private final List<Integer> tags;
-
         public UnionCase(final List<Integer> tags) {
-            this.tags = tags;
-            this.body = Collections.EMPTY_LIST;
+            this(tags, Collections.emptyList());
         }
 
         public UnionCase(final List<Integer> tags, final List<LayoutElement> 
body) {
-            this.tags = tags;
+            this(AttributeLayoutUtils.toRanges(tags), body, false);
+        }
+
+        private UnionCase(final List<Range<Integer>> tagRanges, final 
List<LayoutElement> body, final boolean ignored) {
+            this.tagRanges = tagRanges;
             this.body = body;
         }
 
@@ -515,7 +599,7 @@ public List<LayoutElement> getBody() {
         }
 
         public boolean hasTag(final long l) {
-            return tags.contains(Integer.valueOf((int) l));
+            return AttributeLayoutUtils.unionCaseMatches(tagRanges, (int) l);
         }
 
         @Override
@@ -546,6 +630,8 @@ public void renumberBci(final IntList bciRenumbering, final 
Map<Label, Integer>
     // used when parsing
     private Integral lastPIntegral;
 
+    private final AttributeLayoutFactory attributeLayoutFactory = new 
AttributeLayoutFactory();
+
     public NewAttributeBands(final int effort, final CpBands cpBands, final 
SegmentHeader header, final AttributeDefinition def) throws IOException {
         super(effort, header);
         this.def = def;
@@ -591,35 +677,6 @@ public int getFlagIndex() {
         return def.index;
     }
 
-    /**
-     * Utility method to get the contents of the given stream, up to the next 
{@code ]}, (ignoring pairs of brackets {@code [} and {@code ]})
-     *
-     * @param reader
-     * @return
-     * @throws IOException If an I/O error occurs.
-     */
-    private StringReader getStreamUpToMatchingBracket(final StringReader 
reader) throws IOException {
-        final StringBuilder sb = new StringBuilder();
-        int foundBracket = -1;
-        while (foundBracket != 0) {
-            final int read = reader.read();
-            if (read == -1) {
-                break;
-            }
-            final char c = (char) read;
-            if (c == ']') {
-                foundBracket++;
-            }
-            if (c == '[') {
-                foundBracket--;
-            }
-            if (!(foundBracket == 0)) {
-                sb.append(c);
-            }
-        }
-        return new StringReader(sb.toString());
-    }
-
     public boolean isUsedAtLeastOnce() {
         return usedAtLeastOnce;
     }
@@ -638,32 +695,11 @@ public void pack(final OutputStream outputStream) throws 
IOException, Pack200Exc
     private void parseLayout() throws IOException {
         final String layout = def.layout.getUnderlyingString();
         if (attributeLayoutElements == null) {
-            attributeLayoutElements = new ArrayList<>();
-            final StringReader reader = new StringReader(layout);
-            AttributeLayoutElement e;
-            while ((e = readNextAttributeElement(reader)) != null) {
-                attributeLayoutElements.add(e);
-            }
+            attributeLayoutElements = 
AttributeLayoutUtils.readAttributeLayout(layout, attributeLayoutFactory);
             resolveCalls();
         }
     }
 
-    /**
-     * Reads a 'body' section of the layout from the given stream
-     *
-     * @param reader
-     * @return List of LayoutElements
-     * @throws IOException If an I/O error occurs.
-     */
-    private List<LayoutElement> readBody(final StringReader reader) throws 
IOException {
-        final List<LayoutElement> layoutElements = new ArrayList<>();
-        LayoutElement e;
-        while ((e = readNextLayoutElement(reader)) != null) {
-            layoutElements.add(e);
-        }
-        return layoutElements;
-    }
-
     private int readInteger(final int i, final InputStream inputStream) {
         int result = 0;
         for (int j = 0; j < i; j++) {
@@ -683,199 +719,6 @@ private int readInteger(final int i, final InputStream 
inputStream) {
         return result;
     }
 
-    private AttributeLayoutElement readNextAttributeElement(final StringReader 
reader) throws IOException {
-        reader.mark(1);
-        final int next = reader.read();
-        if (next == -1) {
-            return null;
-        }
-        if (next == '[') {
-            return new 
Callable(readBody(getStreamUpToMatchingBracket(reader)));
-        }
-        reader.reset();
-        return readNextLayoutElement(reader);
-    }
-
-    private LayoutElement readNextLayoutElement(final StringReader reader) 
throws IOException {
-        final int nextChar = reader.read();
-        if (nextChar == -1) {
-            return null;
-        }
-
-        switch (nextChar) {
-        // Integrals
-        case 'B':
-        case 'H':
-        case 'I':
-        case 'V':
-            return new Integral(new String(new char[] { (char) nextChar }));
-        case 'S':
-        case 'F':
-            return new Integral(new String(new char[] { (char) nextChar, 
(char) reader.read() }));
-        case 'P':
-            reader.mark(1);
-            if (reader.read() != 'O') {
-                reader.reset();
-                lastPIntegral = new Integral("P" + (char) reader.read());
-                return lastPIntegral;
-            }
-            lastPIntegral = new Integral("PO" + (char) reader.read(), 
lastPIntegral);
-            return lastPIntegral;
-        case 'O':
-            reader.mark(1);
-            if (reader.read() != 'S') {
-                reader.reset();
-                return new Integral("O" + (char) reader.read(), lastPIntegral);
-            }
-            return new Integral("OS" + (char) reader.read(), lastPIntegral);
-
-        // Replication
-        case 'N':
-            final char uint_type = (char) reader.read();
-            reader.read(); // '['
-            final String str = readUpToMatchingBracket(reader);
-            return new Replication("" + uint_type, str);
-
-        // Union
-        case 'T':
-            String int_type = String.valueOf((char) reader.read());
-            if (int_type.equals("S")) {
-                int_type += (char) reader.read();
-            }
-            final List<UnionCase> unionCases = new ArrayList<>();
-            UnionCase c;
-            while ((c = readNextUnionCase(reader)) != null) {
-                unionCases.add(c);
-            }
-            reader.read(); // '('
-            reader.read(); // ')'
-            reader.read(); // '['
-            List<LayoutElement> body = null;
-            reader.mark(1);
-            final char next = (char) reader.read();
-            if (next != ']') {
-                reader.reset();
-                body = readBody(getStreamUpToMatchingBracket(reader));
-            }
-            return new Union(int_type, unionCases, body);
-
-        // Call
-        case '(':
-            final int number = readNumber(reader).intValue();
-            reader.read(); // ')'
-            return new Call(number);
-        // Reference
-        case 'K':
-        case 'R':
-            final StringBuilder string = new StringBuilder("").append((char) 
nextChar).append((char) reader.read());
-            final char nxt = (char) reader.read();
-            string.append(nxt);
-            if (nxt == 'N') {
-                string.append((char) reader.read());
-            }
-            return new Reference(string.toString());
-        }
-        return null;
-    }
-
-    /**
-     * Reads a UnionCase from the stream
-     *
-     * @param reader
-     * @return
-     * @throws IOException If an I/O error occurs.
-     */
-    private UnionCase readNextUnionCase(final StringReader reader) throws 
IOException {
-        reader.mark(2);
-        reader.read(); // '('
-        final int next = reader.read();
-        char ch = (char) next;
-        if (ch == ')' || next == -1) {
-            reader.reset();
-            return null;
-        }
-        reader.reset();
-        reader.read(); // '('
-        final List<Integer> tags = new ArrayList<>();
-        Integer nextTag;
-        do {
-            nextTag = readNumber(reader);
-            if (nextTag != null) {
-                tags.add(nextTag);
-                reader.read(); // ',' or ')'
-            }
-        } while (nextTag != null);
-        reader.read(); // '['
-        reader.mark(1);
-        ch = (char) reader.read();
-        if (ch == ']') {
-            return new UnionCase(tags);
-        }
-        reader.reset();
-        return new UnionCase(tags, 
readBody(getStreamUpToMatchingBracket(reader)));
-    }
-
-    /**
-     * Reads a number from the stream and return it
-     *
-     * @param stream
-     * @return
-     * @throws IOException If an I/O error occurs.
-     */
-    private Integer readNumber(final StringReader stream) throws IOException {
-        stream.mark(1);
-        final char first = (char) stream.read();
-        final boolean negative = first == '-';
-        if (!negative) {
-            stream.reset();
-        }
-        stream.mark(100);
-        int i;
-        int length = 0;
-        while ((i = stream.read()) != -1 && Character.isDigit((char) i)) {
-            length++;
-        }
-        stream.reset();
-        if (length == 0) {
-            return null;
-        }
-        final char[] digits = new char[length];
-        final int read = stream.read(digits);
-        if (read != digits.length) {
-            throw new Pack200Exception("Error reading from the input stream");
-        }
-        return ParsingUtils.parseIntValue((negative ? "-" : "") + new 
String(digits));
-    }
-
-    /**
-     * Utility method to get the contents of the given stream, up to the next 
']', (ignoring pairs of brackets '[' and ']')
-     *
-     * @param reader
-     * @return
-     * @throws IOException If an I/O error occurs.
-     */
-    private String readUpToMatchingBracket(final StringReader reader) throws 
IOException {
-        final StringBuilder sb = new StringBuilder();
-        int foundBracket = -1;
-        while (foundBracket != 0) {
-            final int read = reader.read();
-            if (read == -1) {
-                break;
-            }
-            final char c = (char) read;
-            if (c == ']') {
-                foundBracket++;
-            }
-            if (c == '[') {
-                foundBracket--;
-            }
-            if (!(foundBracket == 0)) {
-                sb.append(c);
-            }
-        }
-        return sb.toString();
-    }
-
     /**
      * Renumber any bytecode indexes or offsets as described in section 5.5.2 
of the pack200 specification
      *
diff --git 
a/src/main/java/org/apache/commons/compress/harmony/unpack200/NewAttributeBands.java
 
b/src/main/java/org/apache/commons/compress/harmony/unpack200/NewAttributeBands.java
index a6bd2d333..7dc2a18b9 100644
--- 
a/src/main/java/org/apache/commons/compress/harmony/unpack200/NewAttributeBands.java
+++ 
b/src/main/java/org/apache/commons/compress/harmony/unpack200/NewAttributeBands.java
@@ -20,11 +20,13 @@
 
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.StringReader;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
+import org.apache.commons.compress.harmony.internal.AttributeLayoutParser;
+import 
org.apache.commons.compress.harmony.internal.AttributeLayoutParser.UnionCaseData;
+import org.apache.commons.compress.harmony.internal.AttributeLayoutUtils;
 import org.apache.commons.compress.harmony.pack200.BHSDCodec;
 import org.apache.commons.compress.harmony.pack200.Codec;
 import org.apache.commons.compress.harmony.pack200.Pack200Exception;
@@ -41,7 +43,7 @@
 import org.apache.commons.compress.harmony.unpack200.bytecode.CPString;
 import org.apache.commons.compress.harmony.unpack200.bytecode.CPUTF8;
 import org.apache.commons.compress.harmony.unpack200.bytecode.NewAttribute;
-import org.apache.commons.compress.utils.ParsingUtils;
+import org.apache.commons.lang3.Range;
 
 /**
  * Sets of bands relating to a non-predefined attribute
@@ -76,6 +78,43 @@ private interface AttributeLayoutElement {
 
     }
 
+    private class AttributeLayoutFactory implements 
AttributeLayoutParser.Factory<AttributeLayoutElement, LayoutElement> {
+
+        @Override
+        public LayoutElement createCall(int callableIndex) {
+            return new Call(callableIndex);
+        }
+
+        @Override
+        public AttributeLayoutElement createCallable(String body) throws 
Pack200Exception {
+            return new Callable(AttributeLayoutUtils.readBody(body, 
attributeLayoutFactory));
+        }
+
+        @Override
+        public LayoutElement createIntegral(String tag) {
+            return new Integral(tag);
+        }
+
+        @Override
+        public LayoutElement createReference(String tag) {
+            return new Reference(tag);
+        }
+
+        @Override
+        public LayoutElement createReplication(String unsignedInt, String 
body) throws Pack200Exception {
+            return new Replication(unsignedInt, body);
+        }
+
+        @Override
+        public LayoutElement createUnion(String anyInt, List<UnionCaseData> 
cases, String body) throws Pack200Exception {
+            final List<UnionCase> unionCases = new ArrayList<>();
+            for (final UnionCaseData unionCaseData : cases) {
+                unionCases.add(new UnionCase(unionCaseData.tagRanges, 
AttributeLayoutUtils.readBody(unionCaseData.body, attributeLayoutFactory), 
false));
+            }
+            return new Union(anyInt, unionCases, 
AttributeLayoutUtils.readBody(body, attributeLayoutFactory));
+        }
+    }
+
     public class Call extends LayoutElement {
 
         private final int callableIndex;
@@ -129,7 +168,16 @@ public static class Callable implements 
AttributeLayoutElement {
 
         private int index;
 
-        public Callable(final List<LayoutElement> body) {
+        /**
+         * Constructs a new Callable layout element with the given body.
+         *
+         * @param body the body of the callable.
+         * @throws Pack200Exception If the body is empty.
+         */
+        public Callable(final List<LayoutElement> body) throws  
Pack200Exception {
+            if (body.isEmpty()) {
+                throw new Pack200Exception("Corrupted Pack200 archive: 
Callable body is empty");
+            }
             this.body = body;
         }
 
@@ -203,8 +251,14 @@ public class Integral extends LayoutElement {
 
         private int[] band;
 
+        /**
+         * Constructs a new Integral layout element with the given tag.
+         *
+         * @param tag The tag.
+         * @throws IllegalArgumentException If the tag is invalid.
+         */
         public Integral(final String tag) {
-            this.tag = tag;
+            this.tag = AttributeLayoutUtils.checkIntegralTag(tag);
         }
 
         @Override
@@ -317,8 +371,14 @@ public class Reference extends LayoutElement {
 
         private final int length;
 
+        /**
+         * Constructs a new Reference layout element with the given tag.
+         *
+         * @param tag The tag.
+         * @throws IllegalArgumentException If the tag is invalid.
+         */
         public Reference(final String tag) {
-            this.tag = tag;
+            this.tag = AttributeLayoutUtils.checkReferenceTag(tag);
             length = getLength(tag.charAt(tag.length() - 1));
         }
 
@@ -393,14 +453,21 @@ public class Replication extends LayoutElement {
 
         private final Integral countElement;
 
-        private final List<LayoutElement> layoutElements = new ArrayList<>();
+        private final List<LayoutElement> layoutElements;
 
-        public Replication(final String tag, final String contents) throws 
IOException {
+        /**
+         * Constructs a new Replication layout element.
+         *
+         * @param tag the tag of the Integral element.
+         * @param contents the contents of the replication.
+         * @throws IllegalArgumentException If the tag is invalid or the 
contents are empty.
+         * @throws Pack200Exception If the contents are invalid.
+         */
+        public Replication(final String tag, final String contents) throws 
Pack200Exception {
             this.countElement = new Integral(tag);
-            final StringReader stream = new StringReader(contents);
-            LayoutElement e;
-            while ((e = readNextLayoutElement(stream)) != null) {
-                layoutElements.add(e);
+            this.layoutElements = AttributeLayoutUtils.readBody(contents, 
attributeLayoutFactory);
+            if (layoutElements.isEmpty()) {
+                throw new Pack200Exception("Corrupted Pack200 archive: 
Replication body is empty");
             }
         }
 
@@ -454,6 +521,14 @@ public class Union extends LayoutElement {
         private int[] caseCounts;
         private int defaultCount;
 
+        /**
+         * Constructs a new Union layout element.
+         *
+         * @param tag the tag of the Integral element.
+         * @param unionCases the union cases.
+         * @param body the default case body.
+         * @throws IllegalArgumentException If the tag is invalid.
+         */
         public Union(final String tag, final List<UnionCase> unionCases, final 
List<LayoutElement> body) {
             this.unionTag = new Integral(tag);
             this.unionCases = unionCases;
@@ -553,56 +628,58 @@ public void readBands(final InputStream in, final int 
count) throws IOException,
      */
     public class UnionCase extends LayoutElement {
 
-        private List<LayoutElement> body;
+        private final List<Range<Integer>> tagRanges;
+        private final List<LayoutElement> body;
 
-        private final List<Integer> tags;
+        public UnionCase(final List<Integer> tags) throws IOException {
+            this(tags, Collections.emptyList());
+        }
 
-        public UnionCase(final List<Integer> tags) {
-            this.tags = tags;
+        public UnionCase(final List<Integer> tags, final List<LayoutElement> 
body) throws IOException {
+            this(AttributeLayoutUtils.toRanges(tags), body, false);
         }
 
-        public UnionCase(final List<Integer> tags, final List<LayoutElement> 
body) {
-            this.tags = tags;
+        private UnionCase(final List<Range<Integer>> tagRanges, final 
List<LayoutElement> body, final boolean ignored) {
+            this.tagRanges = tagRanges;
             this.body = body;
         }
 
         @Override
         public void addToAttribute(final int index, final NewAttribute 
attribute) {
-            if (body != null) {
-                for (final LayoutElement element : body) {
-                    element.addToAttribute(index, attribute);
-                }
+            for (final LayoutElement element : body) {
+                element.addToAttribute(index, attribute);
             }
         }
 
         public List<LayoutElement> getBody() {
-            return body == null ? Collections.EMPTY_LIST : body;
+            return body;
         }
 
         public boolean hasTag(final int i) {
-            return tags.contains(Integer.valueOf(i));
+            return AttributeLayoutUtils.unionCaseMatches(tagRanges, i);
         }
 
         public boolean hasTag(final long l) {
-            return tags.contains(Integer.valueOf((int) l));
+            return hasTag((int) l);
         }
 
         @Override
         public void readBands(final InputStream in, final int count) throws 
IOException, Pack200Exception {
-            if (body != null) {
-                for (final LayoutElement element : body) {
-                    element.readBands(in, count);
-                }
+            for (final LayoutElement element : body) {
+                element.readBands(in, count);
             }
         }
     }
 
+
     private final AttributeLayout attributeLayout;
 
     private int backwardsCallCount;
 
     protected List<AttributeLayoutElement> attributeLayoutElements;
 
+    private final AttributeLayoutFactory attributeLayoutFactory = new 
AttributeLayoutFactory();
+
     public NewAttributeBands(final Segment segment, final AttributeLayout 
attributeLayout) throws IOException {
         super(segment);
         this.attributeLayout = attributeLayout;
@@ -652,35 +729,6 @@ private Attribute getOneAttribute(final int index, final 
List<AttributeLayoutEle
         return attribute;
     }
 
-    /**
-     * Utility method to get the contents of the given stream, up to the next 
{@code ]}, (ignoring pairs of brackets {@code [} and {@code ]})
-     *
-     * @param stream
-     * @return
-     * @throws IOException If an I/O error occurs.
-     */
-    private StringReader getStreamUpToMatchingBracket(final StringReader 
stream) throws IOException {
-        final StringBuilder sb = new StringBuilder();
-        int foundBracket = -1;
-        while (foundBracket != 0) {
-            final int read = stream.read();
-            if (read == -1) {
-                break;
-            }
-            final char c = (char) read;
-            if (c == ']') {
-                foundBracket++;
-            }
-            if (c == '[') {
-                foundBracket--;
-            }
-            if (!(foundBracket == 0)) {
-                sb.append(c);
-            }
-        }
-        return new StringReader(sb.toString());
-    }
-
     /**
      * Parse the bands relating to this AttributeLayout and return the correct 
class file attributes as a List of {@link Attribute}.
      *
@@ -709,12 +757,7 @@ public List<Attribute> parseAttributes(final InputStream 
in, final int occurrenc
      */
     private void parseLayout() throws IOException {
         if (attributeLayoutElements == null) {
-            attributeLayoutElements = new ArrayList<>();
-            final StringReader stream = new 
StringReader(attributeLayout.getLayout());
-            AttributeLayoutElement e;
-            while ((e = readNextAttributeElement(stream)) != null) {
-                attributeLayoutElements.add(e);
-            }
+            attributeLayoutElements = 
AttributeLayoutUtils.readAttributeLayout(attributeLayout.getLayout(), 
attributeLayoutFactory);
             resolveCalls();
         }
     }
@@ -729,212 +772,6 @@ public void read(final InputStream in) throws 
IOException, Pack200Exception {
         // does nothing - use parseAttributes instead
     }
 
-    /**
-     * Reads a 'body' section of the layout from the given stream
-     *
-     * @param stream
-     * @return List of LayoutElements
-     * @throws IOException If an I/O error occurs.
-     */
-    private List<LayoutElement> readBody(final StringReader stream) throws 
IOException {
-        final List<LayoutElement> layoutElements = new ArrayList<>();
-        LayoutElement e;
-        while ((e = readNextLayoutElement(stream)) != null) {
-            layoutElements.add(e);
-        }
-        return layoutElements;
-    }
-
-    private AttributeLayoutElement readNextAttributeElement(final StringReader 
stream) throws IOException {
-        stream.mark(1);
-        final int next = stream.read();
-        if (next == -1) {
-            return null;
-        }
-        if (next == '[') {
-            return new 
Callable(readBody(getStreamUpToMatchingBracket(stream)));
-        }
-        stream.reset();
-        return readNextLayoutElement(stream);
-    }
-
-    private LayoutElement readNextLayoutElement(final StringReader stream) 
throws IOException {
-        final int nextChar = stream.read();
-        if (nextChar == -1) {
-            return null;
-        }
-        switch (nextChar) {
-        // Integrals
-        case 'B':
-        case 'H':
-        case 'I':
-        case 'V':
-            return new Integral(new String(new char[] { (char) nextChar }));
-        case 'S':
-        case 'F':
-            return new Integral(new String(new char[] { (char) nextChar, 
(char) stream.read() }));
-        case 'P':
-            stream.mark(1);
-            if (stream.read() != 'O') {
-                stream.reset();
-                return new Integral("P" + (char) stream.read());
-            }
-            return new Integral("PO" + (char) stream.read());
-        case 'O':
-            stream.mark(1);
-            if (stream.read() != 'S') {
-                stream.reset();
-                return new Integral("O" + (char) stream.read());
-            }
-            return new Integral("OS" + (char) stream.read());
-
-        // Replication
-        case 'N':
-            final char uintType = (char) stream.read();
-            stream.read(); // '['
-            final String str = readUpToMatchingBracket(stream);
-            return new Replication("" + uintType, str);
-
-        // Union
-        case 'T':
-            String intType = "" + (char) stream.read();
-            if (intType.equals("S")) {
-                intType += (char) stream.read();
-            }
-            final List<UnionCase> unionCases = new ArrayList<>();
-            UnionCase c;
-            while ((c = readNextUnionCase(stream)) != null) {
-                unionCases.add(c);
-            }
-            stream.read(); // '('
-            stream.read(); // ')'
-            stream.read(); // '['
-            List<LayoutElement> body = null;
-            stream.mark(1);
-            final char next = (char) stream.read();
-            if (next != ']') {
-                stream.reset();
-                body = readBody(getStreamUpToMatchingBracket(stream));
-            }
-            return new Union(intType, unionCases, body);
-
-        // Call
-        case '(':
-            final int number = readNumber(stream).intValue();
-            stream.read(); // ')'
-            return new Call(number);
-        // Reference
-        case 'K':
-        case 'R':
-            final StringBuilder string = new StringBuilder("").append((char) 
nextChar).append((char) stream.read());
-            final char nxt = (char) stream.read();
-            string.append(nxt);
-            if (nxt == 'N') {
-                string.append((char) stream.read());
-            }
-            return new Reference(string.toString());
-        }
-        return null;
-    }
-
-    /**
-     * Reads a UnionCase from the stream.
-     *
-     * @param stream source stream.
-     * @return A UnionCase from the stream.
-     * @throws IOException If an I/O error occurs.
-     */
-    private UnionCase readNextUnionCase(final StringReader stream) throws 
IOException {
-        stream.mark(2);
-        stream.read(); // '('
-        final int next = stream.read();
-        char ch = (char) next;
-        if (ch == ')' || next == -1) {
-            stream.reset();
-            return null;
-        }
-        stream.reset();
-        stream.read(); // '('
-        final List<Integer> tags = new ArrayList<>();
-        Integer nextTag;
-        do {
-            nextTag = readNumber(stream);
-            if (nextTag != null) {
-                tags.add(nextTag);
-                stream.read(); // ',' or ')'
-            }
-        } while (nextTag != null);
-        stream.read(); // '['
-        stream.mark(1);
-        ch = (char) stream.read();
-        if (ch == ']') {
-            return new UnionCase(tags);
-        }
-        stream.reset();
-        return new UnionCase(tags, 
readBody(getStreamUpToMatchingBracket(stream)));
-    }
-
-    /**
-     * Reads a number from the stream and return it
-     *
-     * @param stream
-     * @return
-     * @throws IOException If an I/O error occurs.
-     */
-    private Integer readNumber(final StringReader stream) throws IOException {
-        stream.mark(1);
-        final char first = (char) stream.read();
-        final boolean negative = first == '-';
-        if (!negative) {
-            stream.reset();
-        }
-        stream.mark(100);
-        int i;
-        int length = 0;
-        while ((i = stream.read()) != -1 && Character.isDigit((char) i)) {
-            length++;
-        }
-        stream.reset();
-        if (length == 0) {
-            return null;
-        }
-        final char[] digits = new char[length];
-        final int read = stream.read(digits);
-        if (read != digits.length) {
-            throw new Pack200Exception("Error reading from the input stream");
-        }
-        return ParsingUtils.parseIntValue((negative ? "-" : "") + new 
String(digits));
-    }
-
-    /**
-     * Gets the contents of the given stream, up to the next {@code ]}, 
(ignoring pairs of brackets {@code [} and {@code ]})
-     *
-     * @param stream input stream.
-     * @return the contents of the given stream.
-     * @throws IOException If an I/O error occurs.
-     */
-    private String readUpToMatchingBracket(final StringReader stream) throws 
IOException {
-        final StringBuilder sb = new StringBuilder();
-        int foundBracket = -1;
-        while (foundBracket != 0) {
-            final int read = stream.read();
-            if (read == -1) {
-                break;
-            }
-            final char c = (char) read;
-            if (c == ']') {
-                foundBracket++;
-            }
-            if (c == '[') {
-                foundBracket--;
-            }
-            if (!(foundBracket == 0)) {
-                sb.append(c);
-            }
-        }
-        return sb.toString();
-    }
-
     /**
      * Resolve calls in the attribute layout and returns the number of 
backwards calls
      */
diff --git 
a/src/test/java/org/apache/commons/compress/harmony/internal/AttributeLayoutParserTest.java
 
b/src/test/java/org/apache/commons/compress/harmony/internal/AttributeLayoutParserTest.java
new file mode 100644
index 000000000..1b9ff8070
--- /dev/null
+++ 
b/src/test/java/org/apache/commons/compress/harmony/internal/AttributeLayoutParserTest.java
@@ -0,0 +1,149 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.commons.compress.harmony.internal;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+import java.util.stream.Stream;
+
+import org.apache.commons.compress.harmony.pack200.Pack200Exception;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+public class AttributeLayoutParserTest {
+
+    private static class AttributeLayoutElement {
+        private AttributeLayoutElement() {
+        }
+
+        private AttributeLayoutElement(String body) {
+        }
+    }
+
+    private static class AttributeLayoutFactory implements 
AttributeLayoutParser.Factory<AttributeLayoutElement, LayoutElement> {
+
+        @Override
+        public LayoutElement createCall(int callableIndex) {
+            return new LayoutElement(callableIndex);
+        }
+
+        @Override
+        public AttributeLayoutElement createCallable(String body) throws 
Pack200Exception {
+            return new AttributeLayoutElement(body);
+        }
+
+        @Override
+        public LayoutElement createIntegral(String tag) {
+            return new LayoutElement(tag);
+        }
+
+        @Override
+        public LayoutElement createReference(String tag) {
+            return new LayoutElement(tag);
+        }
+
+        @Override
+        public LayoutElement createReplication(String unsignedInt, String 
body) throws Pack200Exception {
+            return new LayoutElement(unsignedInt, body);
+        }
+
+        @Override
+        public LayoutElement createUnion(String anyInt, 
List<AttributeLayoutParser.UnionCaseData> cases, String body) throws 
Pack200Exception {
+            return new LayoutElement(anyInt, cases, body);
+        }
+    }
+
+    private static class LayoutElement extends AttributeLayoutElement {
+        private LayoutElement(int callableIndex) {
+        }
+
+        private LayoutElement(String body) {
+        }
+
+        private LayoutElement(String anyInt, 
List<AttributeLayoutParser.UnionCaseData> cases, String body) {
+        }
+
+        private LayoutElement(String unsignedInt, String body) {
+        }
+    }
+
+    private static final AttributeLayoutFactory FACTORY = new 
AttributeLayoutFactory();
+
+    static Stream<String> validCallableLayouts() {
+        return Stream.of(
+                // Valid callable layouts
+                "[SIK]", "[NI[SIK]O]");
+    }
+
+    static Stream<String> validLayouts() {
+        return Stream.of(
+                // Valid integral layouts
+                "B", "H", "I", "V", "SI", "PI", "OI", "FI", "OSI", "POI",
+                // Valid replication layouts
+                "NI[I]", "NI[SI]", "NI[NI[I]]",
+                // Valid reference layouts
+                "KII", "KINI", "RSI", "RUH", "RQB",
+                // Valid union layouts
+                "TI()[]", "TSI()[]", "TI(1)[]()[]", "TI(1-3)[](4)[]()[]", 
"TI(1-3,5)[](4,6)[]()[]",
+                // Valid call layouts
+                "(1)", "(-2)");
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {
+            // Invalid letters
+            ")", "*", "+", ",", "-", ".", "/", "0", "1", "2", "3", "4", "5", 
"6", "7", "8", "9", ":", ";", "<", "=", ">", "?", "@", "A", "C", "D", "E", "F",
+            "G", "J", "L", "M", "N", "Q", "U",
+            // Invalid integral layouts
+            "S", "SA", "P", "PA", "PO", "POA", "O", "OA", "F", "FA",
+            // Invalid replication layouts
+            "N", "NSI", "NIA", "NI[I", "NI[]",
+            // Invalid reference layouts
+            "K", "KA", "KI", "KIA", "KIN", "KINA",
+            // Invalid callable layouts
+            "[", "[]",
+            // Invalid union layouts
+            "T", "TA", "TI", "TSA", "TI(A)[]()[]",
+            // Invalid call layouts
+            "()", "(A)", "(-A)", "(9999999999999999999999)", "(1234"})
+    void testInvalidLayout(String layout) {
+        final AttributeLayoutParser<AttributeLayoutElement, LayoutElement> 
parser = new AttributeLayoutParser<>(layout, FACTORY);
+        final Pack200Exception ex = assertThrows(Pack200Exception.class, () -> 
parser.readAttributeLayoutElement());
+        assertTrue(ex.getMessage().contains("Corrupted Pack200 archive"), 
"Unexpected exception message: " + ex.getMessage());
+        assertTrue(ex.getMessage().contains(layout), "Unexpected exception 
message: " + ex.getMessage());
+    }
+
+    @ParameterizedTest
+    @MethodSource({"validLayouts", "validCallableLayouts"})
+    void testReadAttributeLayoutElement(String layout) throws Pack200Exception 
{
+        final List<AttributeLayoutElement> result = 
AttributeLayoutUtils.readAttributeLayout(layout, FACTORY);
+        assertFalse(result.isEmpty(), "Expected at least one LayoutElement for 
layout: " + layout);
+    }
+
+    @ParameterizedTest
+    @MethodSource("validLayouts")
+    void testReadBody(String layout) throws Pack200Exception {
+        final List<LayoutElement> result = 
AttributeLayoutUtils.readBody(layout, FACTORY);
+        assertFalse(result.isEmpty(), "Expected at least one LayoutElement for 
layout: " + layout);
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/compress/harmony/internal/AttributeLayoutUtilsTest.java
 
b/src/test/java/org/apache/commons/compress/harmony/internal/AttributeLayoutUtilsTest.java
new file mode 100644
index 000000000..66dcef70e
--- /dev/null
+++ 
b/src/test/java/org/apache/commons/compress/harmony/internal/AttributeLayoutUtilsTest.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.commons.compress.harmony.internal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.commons.lang3.Range;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+class AttributeLayoutUtilsTest {
+
+    @ParameterizedTest
+    @ValueSource(strings = {
+            // Invalid reference_type
+            "AB", "ABI", "CD", "EF",
+            // Missing uint_type
+            "KI", "RC", "RU", "KIN", "RCN", "RUN",
+            // Extra characters
+            "KIBB", "RCBB", "RUNNH"})
+    void testCheckReferenceTag_Invalid(String tag) {
+        assertThrows(IllegalArgumentException.class, () -> {
+            AttributeLayoutUtils.checkReferenceTag(tag);
+        });
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {"KIB", "KJH", "KFI", "KDV", "KSB", "KQV", "KMI", 
"KTI", "KLV", "RCB", "RCH", "RDI", "RFV", "RMI", "RII", "RYB", "RBNI"})
+    void testCheckReferenceTag_Valid(String tag) {
+        assertEquals(tag, AttributeLayoutUtils.checkReferenceTag(tag));
+    }
+
+    @Test
+    void testToRanges() {
+        final List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 
9);
+        final List<Range<Integer>> ranges = 
AttributeLayoutUtils.toRanges(integers);
+        assertEquals(ranges.size(), integers.size());
+        for (int i = 0; i < ranges.size(); i++) {
+            assertEquals(Range.of(integers.get(i), integers.get(i)), 
ranges.get(i), "Range at index " + i + " should be a single value range.");
+        }
+    }
+
+    @Test
+    void testUnionCaseMatches() {
+        final List<Range<Integer>> ranges = Arrays.asList(Range.of(1, 2), 
Range.of(4, 5), Range.of(7, 8));
+        assertTrue(AttributeLayoutUtils.unionCaseMatches(ranges, 1));
+        assertTrue(AttributeLayoutUtils.unionCaseMatches(ranges, 2));
+        assertFalse(AttributeLayoutUtils.unionCaseMatches(ranges, 3));
+        assertTrue(AttributeLayoutUtils.unionCaseMatches(ranges, 4));
+        assertTrue(AttributeLayoutUtils.unionCaseMatches(ranges, 5));
+        assertFalse(AttributeLayoutUtils.unionCaseMatches(ranges, 6));
+        assertTrue(AttributeLayoutUtils.unionCaseMatches(ranges, 7));
+        assertTrue(AttributeLayoutUtils.unionCaseMatches(ranges, 8));
+        assertFalse(AttributeLayoutUtils.unionCaseMatches(ranges, 9));
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/compress/harmony/pack200/Compress626Test.java
 
b/src/test/java/org/apache/commons/compress/harmony/pack200/Compress626Test.java
index 8422b7d77..62c50a6ac 100644
--- 
a/src/test/java/org/apache/commons/compress/harmony/pack200/Compress626Test.java
+++ 
b/src/test/java/org/apache/commons/compress/harmony/pack200/Compress626Test.java
@@ -19,7 +19,7 @@
 
 package org.apache.commons.compress.harmony.pack200;
 
-import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 
 import java.io.IOException;
 import java.io.InputStream;
@@ -37,7 +37,7 @@ class Compress626Test {
     void test() throws Exception {
         final CPUTF8 name = new CPUTF8("");
         final CPUTF8 layout = new CPUTF8("[");
-        assertDoesNotThrow(() -> new NewAttributeBands(1, null, null,
+        assertThrows(Pack200Exception.class, () -> new NewAttributeBands(1, 
null, null,
                 new AttributeDefinitionBands.AttributeDefinition(35, 
AttributeDefinitionBands.CONTEXT_CLASS, name, layout)));
     }
 
@@ -45,7 +45,7 @@ void test() throws Exception {
     void testJar() throws IOException {
         try (InputStream inputStream = 
Files.newInputStream(Paths.get("src/test/resources/org/apache/commons/compress/COMPRESS-626/compress-626-pack200.jar"));
                 JarOutputStream out = new 
JarOutputStream(NullOutputStream.INSTANCE);) {
-            Pack200.newUnpacker().unpack(inputStream, out);
+            assertThrows(Pack200Exception.class, () -> 
Pack200.newUnpacker().unpack(inputStream, out));
         }
     }
 }
diff --git 
a/src/test/java/org/apache/commons/compress/harmony/pack200/Compress628Test.java
 
b/src/test/java/org/apache/commons/compress/harmony/pack200/Compress628Test.java
index d8ed03fed..c777ff418 100644
--- 
a/src/test/java/org/apache/commons/compress/harmony/pack200/Compress628Test.java
+++ 
b/src/test/java/org/apache/commons/compress/harmony/pack200/Compress628Test.java
@@ -19,7 +19,7 @@
 
 package org.apache.commons.compress.harmony.pack200;
 
-import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 
 import org.junit.jupiter.api.Test;
 
@@ -29,8 +29,8 @@ class Compress628Test {
     void test() throws Exception {
         final CPUTF8 name = new CPUTF8("");
         final CPUTF8 layout = new CPUTF8("Re\\T");
-        assertDoesNotThrow(() -> new NewAttributeBands(1, null, null,
-                new AttributeDefinitionBands.AttributeDefinition(35, 
AttributeDefinitionBands.CONTEXT_CLASS, name, layout)));
+        assertThrows(Pack200Exception.class, () -> new NewAttributeBands(1, 
null, null, new AttributeDefinitionBands.AttributeDefinition(35,
+                AttributeDefinitionBands.CONTEXT_CLASS, name, layout)));
     }
 
 }
diff --git 
a/src/test/java/org/apache/commons/compress/harmony/pack200/NewAttributeBandsTest.java
 
b/src/test/java/org/apache/commons/compress/harmony/pack200/NewAttributeBandsTest.java
index 5f2d2c808..5c83d6899 100644
--- 
a/src/test/java/org/apache/commons/compress/harmony/pack200/NewAttributeBandsTest.java
+++ 
b/src/test/java/org/apache/commons/compress/harmony/pack200/NewAttributeBandsTest.java
@@ -183,7 +183,7 @@ void testLayoutWithCalls() throws IOException {
         final CPUTF8 name = new CPUTF8("TestAttribute");
         // @formatter:off
         final CPUTF8 layout = new CPUTF8(
-          "[NH[(1)]][RSH 
NH[RUH(1)]][TB(66,67,73,83,90)[KIH](68)[KDH](70)[KFH](74)[KJH](99)[RSH](101)[RSH
 RUH](115)[RUH](91)[NH[(0)]](64)[RSH[RUH(0)]]()[]]"
+          
"[NH[(1)]][RSHNH[RUH(1)]][TB(66,67,73,83,90)[KIH](68)[KDH](70)[KFH](74)[KJH](99)[RSH](101)[RSHRUH](115)[RUH](91)[NH[(0)]](64)[RSHNH[RUH(0)]]()[]]"
         );
         // @formatter:on
         final MockNewAttributeBands newAttributeBands = new 
MockNewAttributeBands(1, null, null,
diff --git 
a/src/test/java/org/apache/commons/compress/harmony/unpack200/NewAttributeBandsTest.java
 
b/src/test/java/org/apache/commons/compress/harmony/unpack200/NewAttributeBandsTest.java
index 1e9deef33..9d02a7244 100644
--- 
a/src/test/java/org/apache/commons/compress/harmony/unpack200/NewAttributeBandsTest.java
+++ 
b/src/test/java/org/apache/commons/compress/harmony/unpack200/NewAttributeBandsTest.java
@@ -20,6 +20,7 @@
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.io.IOException;
@@ -53,6 +54,11 @@ public List<?> getLayoutElements() {
         }
     }
 
+    private MockNewAttributeBands createNewAttributeBands(final String 
layoutStr) throws IOException, Pack200Exception {
+        return new MockNewAttributeBands(new MockSegment(),
+                new AttributeLayout("test", AttributeLayout.CONTEXT_CLASS, 
layoutStr, 25));
+    }
+
     @Test
     void testEmptyLayout() throws IOException, Pack200Exception {
         final MockNewAttributeBands newAttributeBands = new 
MockNewAttributeBands(new MockSegment(),
@@ -62,7 +68,17 @@ void testEmptyLayout() throws IOException, Pack200Exception {
     }
 
     @ParameterizedTest
-    @ValueSource(strings = { "B", "FB", "SB", "H", "FH", "SH", "I", "FI", 
"SI", "PB", "OB", "OSB", "POB", "PH", "OH", "OSH", "POH", "PI", "OI", "OSI", 
"POI" })
+    @ValueSource(strings = {
+            // unsigned_int
+            "B", "H", "I", "V",
+            // signed_int
+            "SB", "SH", "SI", "SV",
+            // bc_index
+            "PB", "PH", "PI", "PV", "POB", "POH", "POI", "POV",
+            // bc_offset
+            "OB", "OH", "OI", "OV",
+            // flag
+            "FB", "FH", "FI", "FV"})
     void testIntegralLayout(final String layoutStr) throws IOException, 
Pack200Exception {
         final MockNewAttributeBands newAttributeBands = new 
MockNewAttributeBands(new MockSegment(),
                 new AttributeLayout("test", AttributeLayout.CONTEXT_CLASS, 
layoutStr, 25));
@@ -125,7 +141,7 @@ void testLayoutWithBackwardsCall() throws IOException, 
Pack200Exception {
     void testLayoutWithCalls() throws IOException, Pack200Exception {
         // @formatter:off
         final MockNewAttributeBands newAttributeBands = new 
MockNewAttributeBands(new MockSegment(), new AttributeLayout("test", 
AttributeLayout.CONTEXT_FIELD,
-            "[NH[(1)]][RSH 
NH[RUH(1)]][TB(66,67,73,83,90)[KIH](68)[KDH](70)[KFH](74)[KJH](99)[RSH](101)[RSH
 RUH](115)[RUH](91)[NH[(0)]](64)[RSH[RUH(0)]]()[]]",
+            
"[NH[(1)]][RSHNH[RUH(1)]][TB(66,67,73,83,90)[KIH](68)[KDH](70)[KFH](74)[KJH](99)[RSH](101)[RSHRUH](115)[RUH](91)[NH[(0)]](64)[RSHNH[RUH(0)]]()[]]",
             26));
         // @formatter:on
         final List layoutElements = newAttributeBands.getLayoutElements();
@@ -180,6 +196,15 @@ void testReplicationLayout() throws IOException, 
Pack200Exception {
         assertEquals("H", fifthElement.getTag());
     }
 
+    @ParameterizedTest
+    @ValueSource(strings = {"NH[]", "[]"})
+    void testEmptyBodyFails(final String layout) {
+        final Pack200Exception ex = assertThrows(Pack200Exception.class, () -> 
createNewAttributeBands(layout));
+        assertTrue(ex.getMessage().contains(layout), "Unexpected exception 
message: " + ex.getMessage());
+        final Throwable cause = ex.getCause();
+        assertTrue(cause.getMessage().contains("empty"), "Unexpected exception 
message: " + cause.getMessage());
+    }
+
     @Test
     void testUnionLayout() throws IOException, Pack200Exception {
         final MockNewAttributeBands newAttributeBands = new 
MockNewAttributeBands(new MockSegment(),

Reply via email to