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

ggregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-compress.git


The following commit(s) were added to refs/heads/master by this push:
     new e78e02cba Pack200: enforce strict attribute layout parsing (#747)
e78e02cba is described below

commit e78e02cbaf58fd826baa01423045507adc10b0e0
Author: Piotr P. Karwasz <[email protected]>
AuthorDate: Mon Nov 17 12:50:23 2025 +0100

    Pack200: enforce strict attribute layout parsing (#747)
    
    * 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`.
    
    * fix: PMD failure
    
    * fix: restore purpose of `lastPIntegral`
    
    * fix: remove type parameters from Javadoc
    
    This fails on JDK up to 11 and is yet another Javadoc quirk.
    
    * fix: additional bracket in Javadoc
    
    * fix: AttributeLayoutParser Javadoc
    
    * fix: formatting
    
    * Apply suggestions from code review
    
    Co-authored-by: Copilot <[email protected]>
    
    * fix: Javadoc copy/paste
    
    * fix: remove unused import
    
    * fix: additional Javadoc
    
    * fix: `checkAnyIntTag` error message
    
    * fix: remove difference `AttributeLayoutElement`/`LayoutElement`
    
    * feat: add tests with deeply nested layouts
    
    * fix: Add nesting level limit
    
    This change adds a limit to the nesting level of `layout_element`s (64). 
Since all standard attribute layouts have a nesting level lower than 3, the 
limit should be suitable for most purposes.
    
    * fix: AttributeLayoutUtils Javadoc
    
    * fix: invalid test assertions
    
    * fix: checkstyle
    
    * Replace `Range<Integer>` with `IntegerRange`
    
    * Improve internal package Javadoc
    
    ---------
    
    Co-authored-by: Copilot <[email protected]>
---
 pom.xml                                            |  35 +-
 src/changes/changes.xml                            |   1 +
 .../harmony/archive/internal/nls/package-info.java |   9 +-
 .../harmony/internal/AttributeLayoutParser.java    | 444 +++++++++++++++++++++
 .../harmony/internal/AttributeLayoutUtils.java     | 245 ++++++++++++
 .../internal/nls => internal}/package-info.java    |  11 +-
 .../harmony/pack200/NewAttributeBands.java         | 390 ++++++------------
 .../harmony/unpack200/NewAttributeBands.java       | 383 ++++++------------
 .../internal/AttributeLayoutParserTest.java        | 144 +++++++
 .../harmony/internal/AttributeLayoutUtilsTest.java |  79 ++++
 .../compress/harmony/pack200/Compress626Test.java  |   6 +-
 .../compress/harmony/pack200/Compress628Test.java  |   6 +-
 .../harmony/pack200/NewAttributeBandsTest.java     |  19 +-
 .../harmony/unpack200/NewAttributeBandsTest.java   |  48 ++-
 14 files changed, 1259 insertions(+), 561 deletions(-)

diff --git a/pom.xml b/pom.xml
index 9b9618ad5..9e8c1dc37 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 8e4e99cd4..ad0519788 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..23807b8cb 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,13 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-
 /**
- * Internal package.
+ * Internal implementation package, <strong>not for public use</strong>.
+ *
+ * <p>This package is deliberately <strong>not exported</strong> in either the 
OSGi manifest or the JPMS module descriptor, preventing access from external 
code
+ * and other modules.</p>
+ *
+ * <p>All classes and methods in this package are implementation details and 
may change or be removed at any time without notice. External code must not 
depend
+ * on them.</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..52973b778
--- /dev/null
+++ 
b/src/main/java/org/apache/commons/compress/harmony/internal/AttributeLayoutParser.java
@@ -0,0 +1,444 @@
+/*
+ * 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.List;
+
+import org.apache.commons.compress.harmony.pack200.Pack200Exception;
+import org.apache.commons.lang3.IntegerRange;
+
+/**
+ * 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 <T> a common type shared by {@code attribute_layout} and {@code 
callable}.
+ */
+public final class AttributeLayoutParser<T> {
+
+    /**
+     * Factory interface for creating attribute layout elements.
+     */
+    public interface Factory<T> {
+        /**
+         * Creates a {@code call} layout element.
+         *
+         * @param callableIndex Index of the callable to call.
+         * @return A {@code call} layout element.
+         */
+        T createCall(int callableIndex);
+
+        /**
+         * Creates a {@code callable} attribute layout element.
+         *
+         * @param body Body of the callable.
+         * @return A {@code callable} attribute layout element.
+         * @throws Pack200Exception If the callable body is invalid.
+         */
+        T createCallable(List<T> body) throws Pack200Exception;
+
+        /**
+         * Creates an {@code integral} layout element.
+         *
+         * @param tag Integral tag.
+         * @return An {@code integral} layout element.
+         */
+        T createIntegral(String tag);
+
+        /**
+         * Creates a {@code reference} layout element.
+         *
+         * @param tag Reference tag.
+         * @return A {@code reference} layout element.
+         */
+        T createReference(String tag);
+
+        /**
+         * Creates a {@code replication} layout element.
+         *
+         * @param unsignedInt Unsigned int layout definition for the 
replication count.
+         * @param body        Body of the replication.
+         * @return A {@code replication} layout element.
+         * @throws Pack200Exception If the replication body is invalid.
+         */
+        T createReplication(String unsignedInt, List<T> body) throws 
Pack200Exception;
+
+        /**
+         * Creates a {@code union} layout element.
+         *
+         * @param anyInt Any int layout definition for the union tag.
+         * @param cases  List of union cases.
+         * @param body   Body of the union.
+         * @return A {@code union} layout element.
+         * @throws Pack200Exception If the union body is invalid.
+         */
+        T createUnion(String anyInt, List<UnionCaseData<T>> cases, List<T> 
body) throws Pack200Exception;
+    }
+
+    /**
+     * Data class representing a union case in an attribute layout definition.
+     */
+    public static final class UnionCaseData<T> {
+        /**
+         * Body of the union case.
+         */
+        public final List<T> body;
+
+        /**
+         * List of tag ranges for the union case.
+         */
+        public final List<IntegerRange> tagRanges;
+
+        private UnionCaseData(final List<IntegerRange> tagRanges, final 
List<T> body) {
+            this.tagRanges = Collections.unmodifiableList(tagRanges);
+            this.body = body;
+        }
+    }
+
+    private static final int MAX_DEPTH = 64;
+
+    private final CharSequence definition;
+    private final Factory<T> factory;
+    private int p;
+    private int depth;
+
+    public AttributeLayoutParser(final CharSequence definition, final 
Factory<T> factory) {
+        this.definition = definition;
+        this.factory = factory;
+    }
+
+    private void decrementDepth() throws Pack200Exception {
+        depth--;
+        if (depth < 0) {
+            throw new Pack200Exception("Invalid attribute layout definition: 
unmatched closing bracket.");
+        }
+    }
+
+    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 + "'");
+    }
+
+    void incrementDepth() throws Pack200Exception {
+        depth++;
+        if (depth > MAX_DEPTH) {
+            throw new Pack200Exception("Invalid attribute layout definition: 
maximum nesting depth exceeded.");
+        }
+    }
+
+    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 an {@code attribute_layout} definition from the stream.
+     *
+     * @return A {@code attribute_layout} definition.
+     * @throws Pack200Exception If the layout definition is invalid.
+     */
+    List<T> readAttributeLayout() throws Pack200Exception {
+        final List<T> elements = new ArrayList<>();
+        T element;
+        while ((element = readElement()) != null) {
+            elements.add(element);
+        }
+        return elements;
+    }
+
+    private List<T> readBody() throws Pack200Exception {
+        expect('[');
+        incrementDepth();
+        final List<T> bodyElements = readAttributeLayout();
+        decrementDepth();
+        expect(']');
+        return bodyElements;
+    }
+
+    /**
+     * 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.
+     */
+    T readElement() throws Pack200Exception {
+        if (eof()) {
+            return null;
+        }
+        final char first = peek();
+        if (depth == 0 && first == '[') {
+            try {
+                final List<T> 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 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.
+     */
+    T 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 List<T> 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<T>> unionCases = new 
ArrayList<>();
+                    UnionCaseData<T> data;
+                    while ((data = readUnionCase()) != null) {
+                        unionCases.add(data);
+                    }
+                    expect('(');
+                    expect(')');
+                    final List<T> 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());
+                }
+                // End of body
+                case ']':
+                    return null;
+                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 Pack200Exception If the numeral is invalid or out of range.
+     */
+    private int readNumeral() throws Pack200Exception {
+        // 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);
+    }
+
+    /**
+     * Reads a reference tag from the stream.
+     *
+     * @return A reference tag from the stream.
+     * @throws Pack200Exception If the reference tag is invalid.
+     */
+    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 Pack200Exception If the union case is invalid.
+     */
+    private UnionCaseData<T> readUnionCase() throws Pack200Exception {
+        // Check for default case
+        expect('(');
+        char c = peek();
+        if (c == ')') {
+            // Default case
+            p--;
+            return null;
+        }
+        // Read the tag ranges
+        final List<IntegerRange> 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(IntegerRange.of(nextTag, nextTag));
+                }
+            } else { // Second number of a range
+                tagRanges.add(IntegerRange.of((int) startTag, nextTag));
+                startTag = null;
+            }
+        } while (c != ')');
+        // Read the body
+        final List<T> body = readBody();
+        return new UnionCaseData<>(tagRanges, body);
+    }
+
+    /**
+     * Reads an {@code unsigned_int} layout definition from the stream.
+     *
+     * @return an {@code unsigned_int} layout definition from the stream.
+     * @throws Pack200Exception If the definition is invalid.
+     */
+    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..9f65ea4ed
--- /dev/null
+++ 
b/src/main/java/org/apache/commons/compress/harmony/internal/AttributeLayoutUtils.java
@@ -0,0 +1,245 @@
+/*
+ * 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.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.IntegerRange;
+
+/**
+ * Utility methods for {@code attribute_layout} parsing and validation.
+ */
+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 any_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 any_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 <T> a common type shared by {@code attribute_layout} and {@code 
callable}.
+     * @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 <T> List<T> readAttributeLayout(final String definition,
+            final AttributeLayoutParser.Factory<T> factory) throws 
Pack200Exception {
+        final AttributeLayoutParser<T> parser = new 
AttributeLayoutParser<>(definition, factory);
+        return parser.readAttributeLayout();
+    }
+
+    /**
+     * 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 <T> a common type shared by {@code attribute_layout} and {@code 
callable}.
+     * @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 <T> List<T> readBody(final String body, final 
AttributeLayoutParser.Factory<T> factory) throws Pack200Exception {
+        final AttributeLayoutParser<T> parser = new 
AttributeLayoutParser<>(body, factory);
+        // At depth 0, callables are allowed; increment depth to only allow 
layout_elements.
+        parser.incrementDepth();
+        return parser.readAttributeLayout();
+    }
+
+    /**
+     * 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<IntegerRange> toRanges(final List<Integer> tags) {
+        return tags.stream().map(n -> IntegerRange.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<IntegerRange> 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 62%
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..fadc03c9c 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,13 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-
 /**
- * Internal package.
+ * Internal implementation package, <strong>not for public use</strong>.
+ *
+ * <p>This package is deliberately <strong>not exported</strong> in either the 
OSGi manifest or the JPMS module descriptor, preventing access from external 
code
+ * and other modules.</p>
+ *
+ * <p>All classes and methods in this package are implementation details and 
may change or be removed at any time without notice. External code must not 
depend
+ * on them.</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..382396d99 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.IntegerRange;
 import org.objectweb.asm.Label;
 
 /**
@@ -55,6 +57,57 @@ public interface AttributeLayoutElement {
 
     }
 
+    private class AttributeLayoutFactory implements 
AttributeLayoutParser.Factory<LayoutElement> {
+
+        /**
+         * The last P-type integral seen (for use with subsequent O and PO 
types)
+         */
+        private Integral lastPIntegral;
+
+        @Override
+        public LayoutElement createCall(int callableIndex) {
+            return new Call(callableIndex);
+        }
+
+        @Override
+        public LayoutElement createCallable(List<LayoutElement> body) throws 
Pack200Exception {
+            return new Callable(body);
+        }
+
+        @Override
+        public LayoutElement createIntegral(String tag) {
+            final Integral integral;
+            if (tag.startsWith("O") || tag.startsWith("PO")) {
+                integral = new Integral(tag, lastPIntegral);
+            } else {
+                integral = new Integral(tag);
+            }
+            if (tag.startsWith("P")) {
+                lastPIntegral = integral;
+            }
+            return integral;
+        }
+
+        @Override
+        public LayoutElement createReference(String tag) {
+            return new Reference(tag);
+        }
+
+        @Override
+        public LayoutElement createReplication(String unsignedInt, 
List<LayoutElement> body) throws Pack200Exception {
+            return new Replication(unsignedInt, body);
+        }
+
+        @Override
+        public LayoutElement createUnion(String anyInt, 
List<UnionCaseData<LayoutElement>> cases, List<LayoutElement> body) throws 
Pack200Exception {
+            final List<UnionCase> unionCases = new ArrayList<>();
+            for (final UnionCaseData<LayoutElement> unionCaseData : cases) {
+                unionCases.add(new UnionCase(unionCaseData.tagRanges, 
unionCaseData.body, false));
+            }
+            return new Union(anyInt, unionCases, body);
+        }
+    }
+
     public class Call extends LayoutElement {
 
         private final int callableIndex;
@@ -98,7 +151,7 @@ public void setCallable(final Callable callable) {
         }
     }
 
-    public class Callable implements AttributeLayoutElement {
+    public class Callable extends LayoutElement {
 
         private final List<LayoutElement> body;
 
@@ -106,7 +159,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 +228,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 +395,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 +452,25 @@ public class Replication extends LayoutElement {
 
         private final Integral countElement;
 
-        private final List<LayoutElement> layoutElements = new ArrayList<>();
+        private final List<LayoutElement> layoutElements;
+
+        /**
+         * 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(tag, AttributeLayoutUtils.readBody(contents, 
attributeLayoutFactory));
+        }
 
-        public Replication(final String tag, final String contents) throws 
IOException {
+        private Replication(final String tag, final List<LayoutElement> 
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 = contents;
+            if (layoutElements.isEmpty()) {
+                throw new Pack200Exception("Corrupted Pack200 archive: 
Replication body is empty");
             }
         }
 
@@ -426,6 +518,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 +589,19 @@ public void renumberBci(final IntList bciRenumbering, 
final Map<Label, Integer>
      */
     public class UnionCase extends LayoutElement {
 
+        private final List<IntegerRange> 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<IntegerRange> tagRanges, final 
List<LayoutElement> body, final boolean ignored) {
+            this.tagRanges = tagRanges;
             this.body = body;
         }
 
@@ -515,7 +617,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
@@ -533,7 +635,7 @@ public void renumberBci(final IntList bciRenumbering, final 
Map<Label, Integer>
         }
     }
 
-    protected List<AttributeLayoutElement> attributeLayoutElements;
+    protected List<LayoutElement> attributeLayoutElements;
 
     private int[] backwardsCallCounts;
 
@@ -543,8 +645,7 @@ public void renumberBci(final IntList bciRenumbering, final 
Map<Label, Integer>
 
     private boolean usedAtLeastOnce;
 
-    // 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);
@@ -591,35 +692,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 +710,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 +734,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..bf41e72a3 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.IntegerRange;
 
 /**
  * Sets of bands relating to a non-predefined attribute
@@ -76,6 +78,43 @@ private interface AttributeLayoutElement {
 
     }
 
+    private class AttributeLayoutFactory implements 
AttributeLayoutParser.Factory<LayoutElement> {
+
+        @Override
+        public LayoutElement createCall(int callableIndex) {
+            return new Call(callableIndex);
+        }
+
+        @Override
+        public LayoutElement createCallable(List<LayoutElement> body) throws 
Pack200Exception {
+            return new Callable(body);
+        }
+
+        @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, 
List<LayoutElement> body) throws Pack200Exception {
+            return new Replication(unsignedInt, body);
+        }
+
+        @Override
+        public LayoutElement createUnion(String anyInt, 
List<UnionCaseData<LayoutElement>> cases, List<LayoutElement> body) throws 
Pack200Exception {
+            final List<UnionCase> unionCases = new ArrayList<>();
+            for (final UnionCaseData<LayoutElement> unionCaseData : cases) {
+                unionCases.add(new UnionCase(unionCaseData.tagRanges, 
unionCaseData.body, false));
+            }
+            return new Union(anyInt, unionCases, body);
+        }
+    }
+
     public class Call extends LayoutElement {
 
         private final int callableIndex;
@@ -117,7 +156,7 @@ public void setCallable(final Callable callable) {
         }
     }
 
-    public static class Callable implements AttributeLayoutElement {
+    public static class Callable extends LayoutElement {
 
         private final List<LayoutElement> body;
 
@@ -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,25 @@ public class Replication extends LayoutElement {
 
         private final Integral countElement;
 
-        private final List<LayoutElement> layoutElements = new ArrayList<>();
+        private final List<LayoutElement> layoutElements;
+
+        /**
+         * 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(tag, AttributeLayoutUtils.readBody(contents, 
attributeLayoutFactory));
+        }
 
-        public Replication(final String tag, final String contents) throws 
IOException {
+        private Replication(final String tag, final List<LayoutElement> 
layoutElements) 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 = layoutElements;
+            if (layoutElements.isEmpty()) {
+                throw new Pack200Exception("Corrupted Pack200 archive: 
Replication body is empty");
             }
         }
 
@@ -454,6 +525,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,55 +632,57 @@ public void readBands(final InputStream in, final int 
count) throws IOException,
      */
     public class UnionCase extends LayoutElement {
 
-        private List<LayoutElement> body;
-
-        private final List<Integer> tags;
+        private final List<IntegerRange> tagRanges;
+        private final List<LayoutElement> body;
 
         public UnionCase(final List<Integer> tags) {
-            this.tags = tags;
+            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<IntegerRange> 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;
+    protected List<LayoutElement> attributeLayoutElements;
+
+    private final AttributeLayoutFactory attributeLayoutFactory = new 
AttributeLayoutFactory();
 
     public NewAttributeBands(final Segment segment, final AttributeLayout 
attributeLayout) throws IOException {
         super(segment);
@@ -644,7 +725,7 @@ public BHSDCodec getCodec(final String layoutElement) {
      * @param elements TODO
      * @return attribute at the given index.
      */
-    private Attribute getOneAttribute(final int index, final 
List<AttributeLayoutElement> elements) {
+    private Attribute getOneAttribute(final int index, final 
List<LayoutElement> elements) {
         final NewAttribute attribute = new 
NewAttribute(segment.getCpBands().cpUTF8Value(attributeLayout.getName()), 
attributeLayout.getIndex());
         for (final AttributeLayoutElement element : elements) {
             element.addToAttribute(index, attribute);
@@ -652,35 +733,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 +761,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 +776,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..c53b9a976
--- /dev/null
+++ 
b/src/test/java/org/apache/commons/compress/harmony/internal/AttributeLayoutParserTest.java
@@ -0,0 +1,144 @@
+/*
+ * 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 AttributeLayoutFactory implements 
AttributeLayoutParser.Factory<LayoutElement> {
+
+        @Override
+        public LayoutElement createCall(int callableIndex) {
+            return new LayoutElement(callableIndex);
+        }
+
+        @Override
+        public LayoutElement createCallable(List<LayoutElement> body) {
+            return new LayoutElement(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, 
List<LayoutElement> body) {
+            return new LayoutElement(unsignedInt, body);
+        }
+
+        @Override
+        public LayoutElement createUnion(String anyInt, 
List<AttributeLayoutParser.UnionCaseData<LayoutElement>> cases, 
List<LayoutElement> body) {
+            return new LayoutElement(anyInt, cases, body);
+        }
+    }
+
+    private static class LayoutElement {
+        private LayoutElement(int callableIndex) {
+        }
+
+        private LayoutElement(String tag) {
+        }
+
+        private LayoutElement(List<LayoutElement> body) {
+        }
+
+        private LayoutElement(String anyInt, 
List<AttributeLayoutParser.UnionCaseData<LayoutElement>> cases, 
List<LayoutElement> body) {
+        }
+
+        private LayoutElement(String unsignedInt, List<LayoutElement> body) {
+        }
+    }
+
+    private static final AttributeLayoutFactory FACTORY = new 
AttributeLayoutFactory();
+
+    static Stream<String> validCallableLayouts() {
+        return Stream.of(
+                // Valid callable layouts
+                "[SIKLI]", "[NI[SIKLI]OH]");
+    }
+
+    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<LayoutElement> parser = new 
AttributeLayoutParser<>(layout, FACTORY);
+        final Pack200Exception ex = assertThrows(Pack200Exception.class, 
parser::readElement);
+        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 testReadElement(String layout) throws Pack200Exception {
+        final List<LayoutElement> 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..75a0b5b64
--- /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.IntegerRange;
+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<IntegerRange> ranges = 
AttributeLayoutUtils.toRanges(integers);
+        assertEquals(ranges.size(), integers.size());
+        for (int i = 0; i < ranges.size(); i++) {
+            assertEquals(IntegerRange.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<IntegerRange> ranges = Arrays.asList(IntegerRange.of(1, 2), 
IntegerRange.of(4, 5), IntegerRange.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..b32c01821 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
@@ -28,7 +28,6 @@
 import java.util.List;
 
 import 
org.apache.commons.compress.harmony.pack200.AttributeDefinitionBands.AttributeDefinition;
-import 
org.apache.commons.compress.harmony.pack200.NewAttributeBands.AttributeLayoutElement;
 import org.apache.commons.compress.harmony.pack200.NewAttributeBands.Call;
 import org.apache.commons.compress.harmony.pack200.NewAttributeBands.Callable;
 import org.apache.commons.compress.harmony.pack200.NewAttributeBands.Integral;
@@ -52,7 +51,7 @@ private final class MockNewAttributeBands extends 
NewAttributeBands {
             super(effort, cpBands, header, def);
         }
 
-        public List<AttributeLayoutElement> getLayoutElements() {
+        public List<LayoutElement> getLayoutElements() {
             return attributeLayoutElements;
         }
     }
@@ -109,7 +108,7 @@ void testEmptyLayout() throws IOException {
         final CPUTF8 layout = new CPUTF8("");
         final MockNewAttributeBands newAttributeBands = new 
MockNewAttributeBands(1, null, null,
                 new AttributeDefinition(35, 
AttributeDefinitionBands.CONTEXT_CLASS, name, layout));
-        final List<AttributeLayoutElement> layoutElements = 
newAttributeBands.getLayoutElements();
+        final List<LayoutElement> layoutElements = 
newAttributeBands.getLayoutElements();
         assertEquals(0, layoutElements.size());
     }
 
@@ -120,7 +119,7 @@ void testIntegralLayouts(final String layoutStr) throws 
IOException {
         final CPUTF8 layout = new CPUTF8(layoutStr);
         final MockNewAttributeBands newAttributeBands = new 
MockNewAttributeBands(1, null, null,
                 new AttributeDefinition(35, 
AttributeDefinitionBands.CONTEXT_CLASS, name, layout));
-        final List<AttributeLayoutElement> layoutElements = 
newAttributeBands.getLayoutElements();
+        final List<LayoutElement> layoutElements = 
newAttributeBands.getLayoutElements();
         assertEquals(1, layoutElements.size());
         final Integral element = (Integral) layoutElements.get(0);
         assertEquals(layoutStr, element.getTag());
@@ -132,7 +131,7 @@ void testLayoutWithBackwardsCalls() throws Exception {
         CPUTF8 layout = new CPUTF8("[NH[(1)]][KIH][(-1)]");
         MockNewAttributeBands newAttributeBands = new MockNewAttributeBands(1, 
null, null,
                 new AttributeDefinition(35, 
AttributeDefinitionBands.CONTEXT_CLASS, name, layout));
-        List<AttributeLayoutElement> layoutElements = 
newAttributeBands.getLayoutElements();
+        List<LayoutElement> layoutElements = 
newAttributeBands.getLayoutElements();
         assertEquals(3, layoutElements.size());
         Callable firstCallable = (Callable) layoutElements.get(0);
         Callable secondCallable = (Callable) layoutElements.get(1);
@@ -183,12 +182,12 @@ 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,
                 new AttributeDefinition(35, 
AttributeDefinitionBands.CONTEXT_CLASS, name, layout));
-        final List<AttributeLayoutElement> layoutElements = 
newAttributeBands.getLayoutElements();
+        final List<LayoutElement> layoutElements = 
newAttributeBands.getLayoutElements();
         assertEquals(3, layoutElements.size());
         final Callable firstCallable = (Callable) layoutElements.get(0);
         final Callable secondCallable = (Callable) layoutElements.get(1);
@@ -213,7 +212,7 @@ void testReferenceLayouts(final String layoutStr) throws 
IOException {
         final CPUTF8 layout = new CPUTF8(layoutStr);
         final MockNewAttributeBands newAttributeBands = new 
MockNewAttributeBands(1, null, null,
                 new AttributeDefinition(35, 
AttributeDefinitionBands.CONTEXT_CLASS, name, layout));
-        final List<AttributeLayoutElement> layoutElements = 
newAttributeBands.getLayoutElements();
+        final List<LayoutElement> layoutElements = 
newAttributeBands.getLayoutElements();
         assertEquals(1, layoutElements.size());
         final Reference element = (Reference) layoutElements.get(0);
         assertEquals(layoutStr, element.getTag());
@@ -225,7 +224,7 @@ void testReplicationLayouts() throws IOException {
         final CPUTF8 layout = new CPUTF8("NH[PHOHRUHRSHH]");
         final MockNewAttributeBands newAttributeBands = new 
MockNewAttributeBands(1, null, null,
                 new AttributeDefinition(35, 
AttributeDefinitionBands.CONTEXT_CLASS, name, layout));
-        final List<AttributeLayoutElement> layoutElements = 
newAttributeBands.getLayoutElements();
+        final List<LayoutElement> layoutElements = 
newAttributeBands.getLayoutElements();
         assertEquals(1, layoutElements.size());
         final Replication element = (Replication) layoutElements.get(0);
         final Integral countElement = element.getCountElement();
@@ -250,7 +249,7 @@ void testUnionLayout() throws IOException {
         final CPUTF8 layout = new CPUTF8("TB(55)[FH](23)[]()[RSH]");
         final MockNewAttributeBands newAttributeBands = new 
MockNewAttributeBands(1, null, null,
                 new AttributeDefinition(35, 
AttributeDefinitionBands.CONTEXT_CLASS, name, layout));
-        final List<AttributeLayoutElement> layoutElements = 
newAttributeBands.getLayoutElements();
+        final List<LayoutElement> layoutElements = 
newAttributeBands.getLayoutElements();
         assertEquals(1, layoutElements.size());
         final Union element = (Union) layoutElements.get(0);
         final Integral tag = element.getUnionTag();
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..6a904f130 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,32 @@ public List<?> getLayoutElements() {
         }
     }
 
+    private static String createRecursiveLayout(int level, String prefix) {
+        final StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < level; i++) {
+            sb.append(prefix);
+        }
+        sb.append("H");
+        for (int i = 0; i < level; i++) {
+            sb.append("]");
+        }
+        return sb.toString();
+    }
+
+    private MockNewAttributeBands createNewAttributeBands(final String 
layoutStr) throws IOException, Pack200Exception {
+        return new MockNewAttributeBands(new MockSegment(),
+                new AttributeLayout("test", AttributeLayout.CONTEXT_CLASS, 
layoutStr, 25));
+    }
+
+    @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 testEmptyLayout() throws IOException, Pack200Exception {
         final MockNewAttributeBands newAttributeBands = new 
MockNewAttributeBands(new MockSegment(),
@@ -62,7 +89,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 +162,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();
@@ -146,6 +183,13 @@ void testLayoutWithCalls() throws IOException, 
Pack200Exception {
         assertFalse(thirdCallable.isBackwardsCallable());
     }
 
+    @ParameterizedTest
+    @ValueSource(strings = {"NH[", "TH()["})
+    void testRecursiveReplicationLayout(String prefix) {
+        final String layout = createRecursiveLayout(8192, prefix);
+        assertThrows(Pack200Exception.class, () -> 
createNewAttributeBands(layout));
+    }
+
     @ParameterizedTest
     @ValueSource(strings = { "KIB", "KIH", "KII", "KINH", "KJH", "KDH", "KSH", 
"KQH", "RCH", "RSH", "RDH", "RFH", "RMH", "RIH", "RUH", "RQH", "RQNH", "RQNI" })
     void testReferenceLayouts(final String layout) throws IOException, 
Pack200Exception {


Reply via email to