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 {