This is an automated email from the ASF dual-hosted git repository.
garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-bcel.git
The following commit(s) were added to refs/heads/master by this push:
new bb86e554 [BCEL-341] Support compact Code headers in historical class
files (#542)
bb86e554 is described below
commit bb86e5547a5371485fb266972c66b2b0e6d29b03
Author: Gary Gregory <[email protected]>
AuthorDate: Sun Nov 1 15:49:05 2026 -0500
[BCEL-341] Support compact Code headers in historical class files (#542)
Add explicit support for compact Code headers in versions 45.0 through
45.2 while retaining standard headers by default.
Preserve header settings during copying and code generation, and select
a consistent layout when dumping classes. Document field widths,
validation, and version handling, and add regression tests.
---
src/changes/changes.xml | 1 +
.../java/org/apache/bcel/classfile/Attribute.java | 45 ++-
.../org/apache/bcel/classfile/ClassParser.java | 51 +++-
src/main/java/org/apache/bcel/classfile/Code.java | 116 +++++++-
src/main/java/org/apache/bcel/classfile/Field.java | 21 +-
.../org/apache/bcel/classfile/FieldOrMethod.java | 61 +++-
.../java/org/apache/bcel/classfile/JavaClass.java | 13 +-
.../java/org/apache/bcel/classfile/Method.java | 24 +-
.../java/org/apache/bcel/classfile/Record.java | 35 ++-
.../apache/bcel/classfile/RecordComponentInfo.java | 26 +-
.../java/org/apache/bcel/generic/MethodGen.java | 59 +++-
src/main/java/org/apache/bcel/util/BCELifier.java | 4 +
.../org/apache/bcel/Java1OakExampleCreator.java | 85 ++++++
.../bcel/classfile/AttributeNestingTest.java | 37 ++-
.../org/apache/bcel/classfile/ClassParserTest.java | 10 +
.../bcel/classfile/CompactCodeHeaderTest.java | 328 +++++++++++++++++++++
.../java/org/apache/bcel/util/BCELifierTest.java | 32 ++
src/test/resources/Java1OakExample.class | Bin 0 -> 493 bytes
.../Java1OakExample.java} | 16 +-
19 files changed, 888 insertions(+), 76 deletions(-)
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index dd6ca031..d3ee4716 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -68,6 +68,7 @@ The <action> type attribute can be add,update,fix,remove.
<action type="fix" dev="ggregory" due-to="Gary
Gregory">Update ECJ versions and generate the SWAP fixture with BCEL
(#537).</action>
<action type="fix" dev="ggregory" due-to="Mohd
Irshad">Fix typo 'preceeding' in ConstantPool exception message (#541).</action>
<action type="fix" dev="ggregory" due-to="Gary
Gregory">Clean up comments for spelling, grammar, and readability.</action>
+ <action issue="BCEL-341" type="fix" dev="ggregory" due-to="Gary Gregory,
Hippo">Support explicit parsing and generation of compact Code headers in class
file versions 45.0 through 45.2 (#58).</action>
<!-- ADD -->
<action type="add" dev="ggregory" due-to="Gary
Gregory">Add and use ClassFormatException.ClassFormatException(String,
Object...).</action>
<action type="add" dev="ggregory" due-to="Nicolas
Baumann, Gary Gregory">Add support for the RuntimeVisibleTypeAnnotations and
RuntimeInvisibleTypeAnnotations attributes.</action>
diff --git a/src/main/java/org/apache/bcel/classfile/Attribute.java
b/src/main/java/org/apache/bcel/classfile/Attribute.java
index 2cb0434f..e1017cbd 100644
--- a/src/main/java/org/apache/bcel/classfile/Attribute.java
+++ b/src/main/java/org/apache/bcel/classfile/Attribute.java
@@ -121,18 +121,39 @@ public abstract class Attribute implements Cloneable,
Node {
}
/**
- * Class method reads one attribute from the input data stream. This
method must not be accessible from the outside. It
- * is called by the Field and Method constructor methods.
+ * Reads one attribute using standard eight-byte Code headers.
*
- * @see Field
- * @see Method
* @param dataInput Input stream.
* @param constantPool Array of constants.
* @return Attribute.
* @throws IOException Thrown if an I/O error occurs.
* @since 6.0
+ * @see #readAttribute(DataInput, ConstantPool, boolean)
*/
public static Attribute readAttribute(final DataInput dataInput, final
ConstantPool constantPool) throws IOException {
+ return readAttribute(dataInput, constantPool, false);
+ }
+
+ /**
+ * Reads one attribute using the selected Code header layout.
+ *
+ * <p>
+ * The compact layout uses {@code u1}, {@code u1}, and {@code u2} for
{@code max_stack}, {@code max_locals}, and {@code code_length}, respectively.
These
+ * fields occupy 4 bytes instead of the standard 8 bytes; the rest of the
Code attribute layout is unchanged. This layout is intended for historical class
+ * files with version 45.0 through 45.2. This method has no enclosing
class version to check, so the caller must select the layout. The selection also
+ * applies to Code attributes encountered while reading nested attributes.
+ * </p>
+ *
+ * @param dataInput Input stream.
+ * @param constantPool Constant pool.
+ * @param compactCodeHeader Whether to read compact four-byte Code headers
({@code true}) or standard eight-byte headers ({@code false}).
+ * @return The attribute.
+ * @throws IOException Thrown if an I/O error occurs.
+ * @throws ClassFormatException Thrown if the attribute is malformed or
exceeds the nesting limit.
+ * @since 6.14.0
+ * @see Code#isCompactCodeHeader()
+ */
+ public static Attribute readAttribute(final DataInput dataInput, final
ConstantPool constantPool, final boolean compactCodeHeader) throws IOException {
// Track the nesting depth to guard against malicious class files that
nest attributes (for example, a Code attribute inside a Code attribute, or
// mutually recursive Record component attributes) deeply enough to
overflow the parser's stack (CWE-674).
final int depth = NESTING_DEPTH.get().intValue() + 1;
@@ -143,7 +164,7 @@ public abstract class Attribute implements Cloneable, Node {
}
NESTING_DEPTH.set(Integer.valueOf(depth));
try {
- return readAttribute0(dataInput, constantPool);
+ return readAttribute0(dataInput, constantPool, compactCodeHeader);
} finally {
if (depth == 1) {
NESTING_DEPTH.remove();
@@ -154,24 +175,22 @@ public abstract class Attribute implements Cloneable,
Node {
}
/**
- * Class method reads one attribute from the input data stream. This
method must not be accessible from the outside. It
- * is called by the Field and Method constructor methods.
+ * Reads one attribute using standard eight-byte Code headers.
*
- * @see Field
- * @see Method
* @param dataInputStream Input stream.
* @param constantPool Array of constants.
* @return Attribute.
* @throws IOException Thrown if an I/O error occurs.
+ * @see #readAttribute(DataInput, ConstantPool, boolean)
*/
public static Attribute readAttribute(final DataInputStream
dataInputStream, final ConstantPool constantPool) throws IOException {
return readAttribute((DataInput) dataInputStream, constantPool);
}
/**
- * Reads one attribute without tracking the nesting depth; only to be
called by {@link #readAttribute(DataInput, ConstantPool)}.
+ * Reads one attribute without tracking the nesting depth; only to be
called by {@link #readAttribute(DataInput, ConstantPool, boolean)}.
*/
- private static Attribute readAttribute0(final DataInput dataInput, final
ConstantPool constantPool) throws IOException {
+ private static Attribute readAttribute0(final DataInput dataInput, final
ConstantPool constantPool, final boolean compactCodeHeader) throws IOException {
byte tag = Const.ATTR_UNKNOWN; // Unknown attribute
// Get class name from constant pool via 'name_index' indirection
final int nameIndex = dataInput.readUnsignedShort();
@@ -201,7 +220,7 @@ public abstract class Attribute implements Cloneable, Node {
case Const.ATTR_SOURCE_FILE:
return new SourceFile(nameIndex, length, dataInput, constantPool);
case Const.ATTR_CODE:
- return new Code(nameIndex, length, dataInput, constantPool);
+ return new Code(nameIndex, length, dataInput, constantPool,
compactCodeHeader);
case Const.ATTR_EXCEPTIONS:
return new ExceptionTable(nameIndex, length, dataInput,
constantPool);
case Const.ATTR_LINE_NUMBER_TABLE:
@@ -256,7 +275,7 @@ public abstract class Attribute implements Cloneable, Node {
case Const.ATTR_NEST_MEMBERS:
return new NestMembers(nameIndex, length, dataInput, constantPool);
case Const.ATTR_RECORD:
- return new Record(nameIndex, length, dataInput, constantPool);
+ return new Record(nameIndex, length, dataInput, constantPool,
compactCodeHeader);
case Const.ATTR_PERMITTED_SUBCLASSES:
return new PermittedSubclasses(nameIndex, length, dataInput,
constantPool);
case Const.ATTR_RUNTIME_VISIBLE_TYPE_ANNOTATIONS:
diff --git a/src/main/java/org/apache/bcel/classfile/ClassParser.java
b/src/main/java/org/apache/bcel/classfile/ClassParser.java
index a5c8eece..521e3a2d 100644
--- a/src/main/java/org/apache/bcel/classfile/ClassParser.java
+++ b/src/main/java/org/apache/bcel/classfile/ClassParser.java
@@ -60,6 +60,10 @@ public final class ClassParser {
private int superclassNameIndex;
private int major; // Compiler version
private int minor; // Compiler version
+ // Actual Code layout selected after checking both the explicit option and
the class file version.
+ private boolean compactCodeHeader;
+ // Versions 45.0 through 45.2 can also use standard Code headers, so the
narrower layout requires an explicit option.
+ private boolean compactCodeHeaderEnabled;
private int accessFlags; // Access rights of parsed class
private int[] interfaces; // Names of implemented interfaces
private ConstantPool constantPool; // collection of constants
@@ -111,11 +115,27 @@ public final class ClassParser {
this.fileName = fileName;
}
+ /**
+ * Gets whether compact Code header parsing is enabled for class file
versions 45.0 through 45.2.
+ *
+ * @return Whether compact Code header parsing is enabled.
+ * @since 6.14.0
+ * @see #setCompactCodeHeaderEnabled(boolean)
+ */
+ public boolean isCompactCodeHeaderEnabled() {
+ return compactCodeHeaderEnabled;
+ }
+
/**
* Parses the given Java class file and return an object that represents
the contained data, that is, constants, methods,
* fields and commands. A <em>ClassFormatException</em> is raised, if the
file is not a valid .class file. (This does
* not include verification of the byte code as it is performed by the
Java interpreter).
*
+ * <p>
+ * Code attributes use the standard eight-byte header by default,
including in class files with version 45.0 through 45.2.
+ * Use {@link #setCompactCodeHeaderEnabled(boolean)} to read the compact
four-byte header used by historical files with those versions.
+ * </p>
+ *
* @return Class object representing the parsed class file.
* @throws IOException Thrown if an I/O error occurs.
* @throws ClassFormatException Thrown if a class is malformed or cannot
be interpreted as a class file.
@@ -192,7 +212,7 @@ public final class ClassParser {
final int attributesCount = dataInputStream.readUnsignedShort();
attributes = new Attribute[attributesCount];
for (int i = 0; i < attributesCount; i++) {
- attributes[i] = Attribute.readAttribute(dataInputStream,
constantPool);
+ attributes[i] = Attribute.readAttribute(dataInputStream,
constantPool, compactCodeHeader);
}
}
@@ -237,7 +257,7 @@ public final class ClassParser {
final int fieldsCount = dataInputStream.readUnsignedShort();
fields = new Field[fieldsCount];
for (int i = 0; i < fieldsCount; i++) {
- fields[i] = new Field(dataInputStream, constantPool);
+ fields[i] = new Field(dataInputStream, constantPool,
compactCodeHeader);
}
}
@@ -273,7 +293,7 @@ public final class ClassParser {
final int methodsCount = dataInputStream.readUnsignedShort();
methods = new Method[methodsCount];
for (int i = 0; i < methodsCount; i++) {
- methods[i] = new Method(dataInputStream, constantPool);
+ methods[i] = new Method(dataInputStream, constantPool,
compactCodeHeader);
}
}
@@ -286,5 +306,30 @@ public final class ClassParser {
private void readVersion() throws IOException, ClassFormatException {
minor = dataInputStream.readUnsignedShort();
major = dataInputStream.readUnsignedShort();
+ // The option resolves the ambiguous layout for 45.0 through 45.2;
versions 45.3 and later always use standard headers.
+ compactCodeHeader = compactCodeHeaderEnabled && major ==
Const.MAJOR_1_1 && minor < Const.MINOR_1_1;
+ }
+
+ /**
+ * Sets whether to read compact Code headers in class file versions 45.0
through 45.2.
+ *
+ * <p>
+ * "Compact" refers to the widths of {@code max_stack}, {@code
max_locals}, and {@code code_length}: unsigned one-, one-, and two-byte fields,
+ * respectively, totaling 4 bytes. Standard headers use unsigned two-,
two-, and four-byte fields, totaling 8 bytes. These sizes exclude the common
+ * six-byte attribute prefix. The layout of that prefix and the remaining
Code fields is unchanged.
+ * </p>
+ *
+ * <p>
+ * This option is disabled by default because modern JVMs also accept
versions 45.0 through 45.2 with standard headers. The version therefore
+ * cannot determine the layout by itself; enabling this option selects the
historical compact layout. Other class file versions always use standard
+ * headers, regardless of this option. Set this option before calling
{@link #parse()}.
+ * </p>
+ *
+ * @param enabled Whether to enable compact Code header parsing.
+ * @since 6.14.0
+ * @see Code#isCompactCodeHeader()
+ */
+ public void setCompactCodeHeaderEnabled(final boolean enabled) {
+ compactCodeHeaderEnabled = enabled;
}
}
diff --git a/src/main/java/org/apache/bcel/classfile/Code.java
b/src/main/java/org/apache/bcel/classfile/Code.java
index 7b972c11..473d2974 100644
--- a/src/main/java/org/apache/bcel/classfile/Code.java
+++ b/src/main/java/org/apache/bcel/classfile/Code.java
@@ -56,6 +56,21 @@ import org.apache.commons.lang3.ArrayUtils;
* }
* </pre>
*
+ * <p>
+ * The layout above uses the standard Code header. This class also supports
compact Code headers found in historical class file versions 45.0 through 45.2.
+ * Here, "compact" describes the narrower encoding of three fields: {@code
max_stack} and {@code max_locals} each use {@code u1}, and {@code code_length}
+ * uses {@code u2}, instead of {@code u2}, {@code u2}, and {@code u4},
respectively. The notation {@code u1}, {@code u2}, and {@code u4} denotes
unsigned
+ * one-, two-, and four-byte values. These three fields therefore occupy 4
bytes instead of 8, excluding the common six-byte attribute prefix.
+ * The layout of that prefix and the remaining Code fields is unchanged; the
bytecode itself is not compressed.
+ * {@link #isCompactCodeHeader()} identifies this narrower layout.
+ * </p>
+ *
+ * <p>
+ * Class file versions 45.0 through 45.2 also occur with standard Code headers
accepted by modern JVMs, so the version alone does not identify the layout.
+ * {@link ClassParser} uses standard headers by default; {@link
ClassParser#setCompactCodeHeaderEnabled(boolean)} explicitly selects compact
headers
+ * for those versions.
+ * </p>
+ *
* @see Attribute
* @see CodeException
* @see LineNumberTable
@@ -63,6 +78,11 @@ import org.apache.commons.lang3.ArrayUtils;
*/
public final class Code extends Attribute {
+ /**
+ * Whether {@code max_stack}, {@code max_locals}, and {@code code_length}
use {@code u1/u1/u2} (4 bytes) instead of {@code u2/u2/u4} (8 bytes).
+ * Compact describes these field widths, not compression of the bytecode
or the rest of the attribute.
+ */
+ private boolean compactCodeHeader;
private int maxStack; // Maximum size of stack used by this method // TODO
this could be made final (setter is not used)
private int maxLocals; // Number of local variables // TODO this could be
made final (setter is not used)
private byte[] code; // Actual byte code
@@ -77,7 +97,7 @@ public final class Code extends Attribute {
*/
public Code(final Code code) {
this(code.getNameIndex(), code.getLength(), code.getMaxStack(),
code.getMaxLocals(), code.getCode(), code.getExceptionTable(),
code.getAttributes(),
- code.getConstantPool());
+ code.getConstantPool(), code.compactCodeHeader);
}
/**
@@ -90,10 +110,19 @@ public final class Code extends Attribute {
* @throws ClassFormatException Thrown if the code array read from {@code
file} is greater than {@link Const#MAX_CODE_SIZE}.
*/
Code(final int nameIndex, final int length, final DataInput dataInput,
final ConstantPool constantPool) throws IOException {
- // Initialize with some default values which will be overwritten later
- this(nameIndex, length, dataInput.readUnsignedShort(),
dataInput.readUnsignedShort(), (byte[]) null, (CodeException[]) null,
(Attribute[]) null,
- constantPool);
- final int codeLength = Args.requireU4(dataInput.readInt(), 0,
Const.MAX_CODE_SIZE, "Code length attribute");
+ this(nameIndex, length, dataInput, constantPool, false);
+ }
+
+ /**
+ * Reads a Code attribute using the selected header layout.
+ */
+ Code(final int nameIndex, final int length, final DataInput dataInput,
final ConstantPool constantPool, final boolean compactCodeHeader) throws
IOException {
+ // Compact headers encode max_stack/max_locals/code_length as
u1/u1/u2; standard headers use u2/u2/u4.
+ // Initialize the remaining fields with default values, then read them
below.
+ this(nameIndex, length, compactCodeHeader ?
dataInput.readUnsignedByte() : dataInput.readUnsignedShort(),
+ compactCodeHeader ? dataInput.readUnsignedByte() :
dataInput.readUnsignedShort(), null, null, null, constantPool,
compactCodeHeader);
+ final int codeLength = Args.requireU4(compactCodeHeader ?
dataInput.readUnsignedShort() : dataInput.readInt(), 0, Const.MAX_CODE_SIZE,
+ "Code length attribute");
code = new byte[codeLength]; // Read byte code
dataInput.readFully(code);
/*
@@ -110,7 +139,7 @@ public final class Code extends Attribute {
final int attributesCount = dataInput.readUnsignedShort();
attributes = new Attribute[attributesCount];
for (int i = 0; i < attributesCount; i++) {
- attributes[i] = readAttribute(dataInput, constantPool);
+ attributes[i] = readAttribute(dataInput, constantPool,
compactCodeHeader);
}
/*
* Adjust length, because of setAttributes in this(), s.b. length is
incorrect, because it didn't take the internal attributes into account yet! Very
@@ -120,7 +149,11 @@ public final class Code extends Attribute {
}
/**
- * Constructs a Code attribute.
+ * Constructs a Code attribute using a standard eight-byte header.
+ *
+ * <p>
+ * Use {@link #setCompactCodeHeader(boolean)} to select the compact
four-byte header after construction.
+ * </p>
*
* @param nameIndex Index pointing to the name <em>Code</em>.
* @param length Content length in bytes.
@@ -134,7 +167,13 @@ public final class Code extends Attribute {
*/
public Code(final int nameIndex, final int length, final int maxStack,
final int maxLocals, final byte[] code, final CodeException[] exceptionTable,
final Attribute[] attributes, final ConstantPool constantPool) {
+ this(nameIndex, length, maxStack, maxLocals, code, exceptionTable,
attributes, constantPool, false);
+ }
+
+ private Code(final int nameIndex, final int length, final int maxStack,
final int maxLocals, final byte[] code, final CodeException[] exceptionTable,
+ final Attribute[] attributes, final ConstantPool constantPool, final
boolean compactCodeHeader) {
super(Const.ATTR_CODE, nameIndex, length, constantPool);
+ this.compactCodeHeader = compactCodeHeader;
this.maxStack = Args.requireU2(maxStack, "maxStack");
this.maxLocals = Args.requireU2(maxLocals, "maxLocals");
this.code = ArrayUtils.nullToEmpty(code);
@@ -194,13 +233,26 @@ public final class Code extends Attribute {
*
* @param file Output file stream.
* @throws IOException Thrown if an I/O error occurs.
+ * @throws ClassFormatException Thrown if a compact header's {@code
max_stack} or {@code max_locals} is outside 0 through 255,
+ * or its code length exceeds 65535 bytes.
*/
@Override
public void dump(final DataOutputStream file) throws IOException {
+ if (compactCodeHeader) {
+ Args.requireU1(maxStack, "maxStack");
+ Args.requireU1(maxLocals, "maxLocals");
+ Args.requireU2(code.length, "Code length attribute");
+ }
super.dump(file);
- file.writeShort(maxStack);
- file.writeShort(maxLocals);
- file.writeInt(code.length);
+ if (compactCodeHeader) {
+ file.writeByte(maxStack);
+ file.writeByte(maxLocals);
+ file.writeShort(code.length);
+ } else {
+ file.writeShort(maxStack);
+ file.writeShort(maxLocals);
+ file.writeInt(code.length);
+ }
file.write(code, 0, code.length);
file.writeShort(Args.requireU2(exceptionTable.length,
"exceptionTable.length"));
for (final CodeException exception : exceptionTable) {
@@ -247,7 +299,8 @@ public final class Code extends Attribute {
* @return The internal length of this code attribute (minus the first 6
bytes) and excluding all its attributes.
*/
private int getInternalLength() {
- return 2 /* maxStack */ + 2 /* maxLocals */ + 4 /* code length */
+ // Only max_stack, max_locals, and code_length differ: 1 + 1 + 2
compact bytes versus 2 + 2 + 4 standard bytes.
+ return (compactCodeHeader ? 4 : 8)
+ code.length /* byte-code */
+ 2 /* exception-table length */
+ 8 * (exceptionTable == null ? 0 : exceptionTable.length) /*
exception table */
@@ -330,6 +383,18 @@ public final class Code extends Attribute {
return null;
}
+ /**
+ * Gets whether this attribute uses the compact four-byte Code header.
+ *
+ * @return Whether {@code max_stack}, {@code max_locals}, and {@code
code_length} use {@code u1}, {@code u1}, and {@code u2}, respectively,
+ * instead of the standard {@code u2}, {@code u2}, and {@code u4}.
+ * @since 6.14.0
+ * @see #setCompactCodeHeader(boolean)
+ */
+ public boolean isCompactCodeHeader() {
+ return compactCodeHeader;
+ }
+
/**
* Sets the attributes for this Code.
*
@@ -352,6 +417,35 @@ public final class Code extends Attribute {
super.setLength(calculateLength()); // Adjust length
}
+ /**
+ * Sets the Code header layout and recalculates the attribute length.
+ *
+ * <p>
+ * Compact headers encode {@code max_stack}, {@code max_locals}, and
{@code code_length} as {@code u1}, {@code u1}, and {@code u2}, respectively,
+ * totaling 4 bytes instead of the standard 8. This limits {@code
max_stack} and {@code max_locals} to 0 through 255 and the code length to 65535
bytes.
+ * The remaining attribute layout is unchanged. Compact headers are
intended for historical class file versions 45.0 through 45.2.
+ * Enabling compact headers validates these values before changing the
layout. Subsequent changes to them are checked by {@link
#dump(DataOutputStream)}.
+ * </p>
+ *
+ * <p>
+ * This setting controls this attribute's own dump. When dumping an entire
class, {@link JavaClass#dump(DataOutputStream)} selects one layout for
+ * all method Code attributes based on the class version and the methods'
header settings, without changing this attribute's setting.
+ * </p>
+ *
+ * @param compactCodeHeader Whether to use a compact four-byte header
({@code true}) or a standard eight-byte header ({@code false}).
+ * @throws ClassFormatException Thrown if {@code compactCodeHeader} is
{@code true} and the current values do not fit in a compact header.
+ * @since 6.14.0
+ */
+ public void setCompactCodeHeader(final boolean compactCodeHeader) {
+ if (compactCodeHeader) {
+ Args.requireU1(maxStack, "maxStack");
+ Args.requireU1(maxLocals, "maxLocals");
+ Args.requireU2(code.length, "Code length attribute");
+ }
+ this.compactCodeHeader = compactCodeHeader;
+ super.setLength(calculateLength());
+ }
+
/**
* Sets the exception table.
*
diff --git a/src/main/java/org/apache/bcel/classfile/Field.java
b/src/main/java/org/apache/bcel/classfile/Field.java
index fca1bd8a..3ebd9c46 100644
--- a/src/main/java/org/apache/bcel/classfile/Field.java
+++ b/src/main/java/org/apache/bcel/classfile/Field.java
@@ -71,13 +71,30 @@ public final class Field extends FieldOrMethod {
}
/**
- * Constructs object from file stream.
+ * Constructs a Field from a data input using standard Code headers.
*
* @param file Input stream.
* @param constantPool The constant pool.
+ * @throws IOException Thrown if an I/O error occurs.
+ * @throws ClassFormatException Thrown if the field or one of its
attributes is malformed.
*/
Field(final DataInput file, final ConstantPool constantPool) throws
IOException, ClassFormatException {
- super(file, constantPool);
+ this(file, constantPool, false);
+ }
+
+ /**
+ * Constructs a Field from a data input using the selected Code header
layout.
+ *
+ * @param file Input stream.
+ * @param constantPool The constant pool.
+ * @param compactCodeHeader Whether to read any Code attributes
encountered with compact four-byte headers ({@code true})
+ * or standard eight-byte headers ({@code false}).
+ * @throws IOException Thrown if an I/O error occurs.
+ * @throws ClassFormatException Thrown if the field or one of its
attributes is malformed.
+ * @see Attribute#readAttribute(DataInput, ConstantPool, boolean)
+ */
+ Field(final DataInput file, final ConstantPool constantPool, final boolean
compactCodeHeader) throws IOException {
+ super(file, constantPool, compactCodeHeader);
}
/**
diff --git a/src/main/java/org/apache/bcel/classfile/FieldOrMethod.java
b/src/main/java/org/apache/bcel/classfile/FieldOrMethod.java
index 77cbaa77..d47d2b42 100644
--- a/src/main/java/org/apache/bcel/classfile/FieldOrMethod.java
+++ b/src/main/java/org/apache/bcel/classfile/FieldOrMethod.java
@@ -67,32 +67,52 @@ public abstract class FieldOrMethod extends AccessFlags
implements Cloneable, No
private String signatureAttributeString;
private boolean searchedForSignatureAttribute;
+ /**
+ * Constructs an empty field or method whose properties must be set before
use.
+ */
FieldOrMethod() {
}
/**
- * Constructs object from file stream.
+ * Constructs a field or method from a data input using standard Code
headers.
*
* @param file Input stream.
* @param constantPool The constant pool.
* @throws IOException Thrown if an I/O error occurs.
+ * @throws ClassFormatException Thrown if the field or method, or one of
its attributes, is malformed.
*/
protected FieldOrMethod(final DataInput file, final ConstantPool
constantPool) throws IOException {
+ this(file, constantPool, false);
+ }
+
+ /**
+ * Constructs a field or method from a data input using the selected Code
header layout.
+ *
+ * @param file Input stream.
+ * @param constantPool The constant pool.
+ * @param compactCodeHeader Whether to read any Code attributes
encountered with compact four-byte headers ({@code true})
+ * or standard eight-byte headers ({@code false}).
+ * @throws IOException Thrown if an I/O error occurs.
+ * @throws ClassFormatException Thrown if the field or method, or one of
its attributes, is malformed.
+ * @see Attribute#readAttribute(DataInput, ConstantPool, boolean)
+ */
+ FieldOrMethod(final DataInput file, final ConstantPool constantPool, final
boolean compactCodeHeader) throws IOException {
this(file.readUnsignedShort(), file.readUnsignedShort(),
file.readUnsignedShort(), null, constantPool);
final int attributesCount = file.readUnsignedShort();
attributes = new Attribute[attributesCount];
for (int i = 0; i < attributesCount; i++) {
- attributes[i] = Attribute.readAttribute(file, constantPool);
+ attributes[i] = Attribute.readAttribute(file, constantPool,
compactCodeHeader);
}
this.attributes_count = attributesCount; // init deprecated field
}
/**
- * Constructs object from file stream.
+ * Constructs a field or method from a data input stream using standard
Code headers.
*
* @param file Input stream.
* @param constantPool The constant pool.
* @throws IOException Thrown if an I/O error occurs.
+ * @throws ClassFormatException Thrown if the field or method, or one of
its attributes, is malformed.
* @deprecated (6.0) Use {@link #FieldOrMethod(java.io.DataInput,
ConstantPool)} instead.
*/
@java.lang.Deprecated
@@ -113,8 +133,8 @@ public abstract class FieldOrMethod extends AccessFlags
implements Cloneable, No
/**
* Constructs a FieldOrMethod.
*
- * @param accessFlags Access rights of method.
- * @param nameIndex Points to field name in constant pool.
+ * @param accessFlags Access rights of the field or method.
+ * @param nameIndex Points to the field or method name in the constant
pool.
* @param signatureIndex Points to encoded signature.
* @param attributes Collection of attributes.
* @param constantPool Array of constants.
@@ -154,15 +174,44 @@ public abstract class FieldOrMethod extends AccessFlags
implements Cloneable, No
* @throws IOException Thrown if an I/O error occurs.
*/
public final void dump(final DataOutputStream file) throws IOException {
+ dump(file, attributes);
+ }
+
+ private void dump(final DataOutputStream file, final Attribute[]
outputAttributes) throws IOException {
file.writeShort(super.getAccessFlags());
file.writeShort(name_index);
file.writeShort(signature_index);
file.writeShort(Args.requireU2(attributes_count, "attributes_count"));
- for (final Attribute attribute : attributes) {
+ for (final Attribute attribute : outputAttributes) {
attribute.dump(file);
}
}
+ /**
+ * Dumps a member with the Code encoding selected by its enclosing class,
without modifying its attributes.
+ *
+ * @param file Output stream.
+ * @param compactCodeHeader Whether to write Code attributes with compact
four-byte headers ({@code true})
+ * or standard eight-byte headers ({@code false}).
+ * @throws IOException Thrown if an I/O error occurs.
+ * @throws ClassFormatException Thrown if an attribute fails validation.
+ */
+ final void dump(final DataOutputStream file, final boolean
compactCodeHeader) throws IOException {
+ final Attribute[] sourceAttributes = getAttributes();
+ Attribute[] outputAttributes = sourceAttributes;
+ for (int i = 0; i < sourceAttributes.length; i++) {
+ if (sourceAttributes[i] instanceof Code && ((Code)
sourceAttributes[i]).isCompactCodeHeader() != compactCodeHeader) {
+ if (outputAttributes == sourceAttributes) {
+ outputAttributes = sourceAttributes.clone();
+ }
+ final Code code = new Code((Code) sourceAttributes[i]);
+ code.setCompactCodeHeader(compactCodeHeader);
+ outputAttributes[i] = code;
+ }
+ }
+ dump(file, outputAttributes);
+ }
+
/**
* Gets annotations on the field or method.
*
diff --git a/src/main/java/org/apache/bcel/classfile/JavaClass.java
b/src/main/java/org/apache/bcel/classfile/JavaClass.java
index 94f1e566..cdeb8c74 100644
--- a/src/main/java/org/apache/bcel/classfile/JavaClass.java
+++ b/src/main/java/org/apache/bcel/classfile/JavaClass.java
@@ -338,10 +338,21 @@ public class JavaClass extends AccessFlags implements
Cloneable, Node, Comparabl
/**
* Dumps Java class to output stream in binary format.
*
+ * <p>
+ * For versions 45.0 through 45.2, all method Code attributes use compact
four-byte headers if any method's Code attribute returns {@code true} from
+ * {@link Code#isCompactCodeHeader()}. Otherwise, all method Code
attributes use standard eight-byte headers. Versions 45.3 and later always use
standard
+ * headers, including after a version change. The header size refers only
to {@code max_stack}, {@code max_locals}, and {@code code_length}.
+ * These encoding adjustments do not modify the methods or their
attributes.
+ * </p>
+ *
* @param file Output stream.
* @throws IOException Thrown if an I/O error occurs.
+ * @throws ClassFormatException Thrown if a method's values do not fit the
selected Code header layout.
*/
public void dump(final DataOutputStream file) throws IOException {
+ // Use one layout for all method Code headers. A version change can
require widening headers without modifying shared attributes.
+ final boolean compactCodeHeader = major == Const.MAJOR_1_1 && minor <
Const.MINOR_1_1 && Arrays.stream(methods)
+
.map(Method::getCode).filter(Objects::nonNull).anyMatch(Code::isCompactCodeHeader);
file.writeInt(Const.JVM_CLASSFILE_MAGIC);
file.writeShort(minor);
file.writeShort(major);
@@ -359,7 +370,7 @@ public class JavaClass extends AccessFlags implements
Cloneable, Node, Comparabl
}
file.writeShort(Args.requireU2(methods.length, "methods.length"));
for (final Method method : methods) {
- method.dump(file);
+ method.dump(file, compactCodeHeader);
}
if (attributes != null) {
file.writeShort(Args.requireU2(attributes.length,
"attributes.length"));
diff --git a/src/main/java/org/apache/bcel/classfile/Method.java
b/src/main/java/org/apache/bcel/classfile/Method.java
index 235a3204..d78f8dc5 100644
--- a/src/main/java/org/apache/bcel/classfile/Method.java
+++ b/src/main/java/org/apache/bcel/classfile/Method.java
@@ -79,21 +79,37 @@ public final class Method extends FieldOrMethod {
}
/**
- * Constructs object from file stream.
+ * Constructs a Method from a data input using standard Code headers.
*
* @param file Input stream.
+ * @param constantPool The constant pool.
* @throws IOException Thrown if an I/O error occurs.
- * @throws ClassFormatException Thrown if a class is malformed or cannot
be interpreted as a class file.
+ * @throws ClassFormatException Thrown if the method or one of its
attributes is malformed.
*/
Method(final DataInput file, final ConstantPool constantPool) throws
IOException, ClassFormatException {
- super(file, constantPool);
+ this(file, constantPool, false);
+ }
+
+ /**
+ * Constructs a Method from a data input using the selected Code header
layout.
+ *
+ * @param file Input stream.
+ * @param constantPool The constant pool.
+ * @param compactCodeHeader Whether to read any Code attributes
encountered with compact four-byte headers ({@code true})
+ * or standard eight-byte headers ({@code false}).
+ * @throws IOException Thrown if an I/O error occurs.
+ * @throws ClassFormatException Thrown if the method or one of its
attributes is malformed.
+ * @see Attribute#readAttribute(DataInput, ConstantPool, boolean)
+ */
+ Method(final DataInput file, final ConstantPool constantPool, final
boolean compactCodeHeader) throws IOException {
+ super(file, constantPool, compactCodeHeader);
}
/**
* Constructs a Method.
*
* @param accessFlags Access rights of method.
- * @param nameIndex Points to field name in constant pool.
+ * @param nameIndex Points to method name in constant pool.
* @param signatureIndex Points to encoded signature.
* @param attributes Collection of attributes.
* @param constantPool Array of constants.
diff --git a/src/main/java/org/apache/bcel/classfile/Record.java
b/src/main/java/org/apache/bcel/classfile/Record.java
index 327dee1b..0117f850 100644
--- a/src/main/java/org/apache/bcel/classfile/Record.java
+++ b/src/main/java/org/apache/bcel/classfile/Record.java
@@ -39,12 +39,12 @@ public final class Record extends Attribute {
private static final RecordComponentInfo[] EMPTY_RCI_ARRAY = {};
- private static RecordComponentInfo[] readComponents(final DataInput input,
final ConstantPool constantPool)
+ private static RecordComponentInfo[] readComponents(final DataInput input,
final ConstantPool constantPool, final boolean compactCodeHeader)
throws IOException {
final int classCount = input.readUnsignedShort();
final RecordComponentInfo[] components = new
RecordComponentInfo[classCount];
for (int i = 0; i < classCount; i++) {
- components[i] = new RecordComponentInfo(input, constantPool);
+ components[i] = new RecordComponentInfo(input, constantPool,
compactCodeHeader);
}
return components;
}
@@ -52,17 +52,40 @@ public final class Record extends Attribute {
private RecordComponentInfo[] components;
/**
- * Constructs object from input stream.
+ * Constructs a Record attribute from a data input using standard Code
headers for nested attributes.
*
- * @param nameIndex Index in constant pool.
+ * @param nameIndex Constant pool index of the attribute name.
* @param length Content length in bytes.
* @param input Input stream.
- * @param constantPool Array of constants.
+ * @param constantPool The constant pool.
* @throws IOException Thrown if an I/O error occurs.
+ * @throws ClassFormatException Thrown if the attribute or one of its
components is malformed.
*/
Record(final int nameIndex, final int length, final DataInput input, final
ConstantPool constantPool)
throws IOException {
- this(nameIndex, length, readComponents(input, constantPool),
constantPool);
+ this(nameIndex, length, input, constantPool, false);
+ }
+
+ /**
+ * Constructs a Record attribute from a data input using the selected Code
header layout for nested attributes.
+ *
+ * <p>
+ * The header selection is forwarded when reading component attributes; it
does not change the Record attribute or record component layout.
+ * </p>
+ *
+ * @param nameIndex Constant pool index of the attribute name.
+ * @param length Content length in bytes.
+ * @param input Input stream.
+ * @param constantPool The constant pool.
+ * @param compactCodeHeader Whether to read any Code attributes
encountered with compact four-byte headers ({@code true})
+ * or standard eight-byte headers ({@code false}).
+ * @throws IOException Thrown if an I/O error occurs.
+ * @throws ClassFormatException Thrown if the attribute or one of its
components is malformed.
+ * @see Attribute#readAttribute(DataInput, ConstantPool, boolean)
+ */
+ Record(final int nameIndex, final int length, final DataInput input, final
ConstantPool constantPool, final boolean compactCodeHeader)
+ throws IOException {
+ this(nameIndex, length, readComponents(input, constantPool,
compactCodeHeader), constantPool);
}
/**
diff --git a/src/main/java/org/apache/bcel/classfile/RecordComponentInfo.java
b/src/main/java/org/apache/bcel/classfile/RecordComponentInfo.java
index e32e1ac7..e58a1c6d 100644
--- a/src/main/java/org/apache/bcel/classfile/RecordComponentInfo.java
+++ b/src/main/java/org/apache/bcel/classfile/RecordComponentInfo.java
@@ -41,19 +41,39 @@ public class RecordComponentInfo implements Node {
private final ConstantPool constantPool;
/**
- * Constructs a new instance from an input stream.
+ * Constructs record component information from a data input using
standard Code headers for nested attributes.
*
* @param input Input stream.
- * @param constantPool Array of constants.
+ * @param constantPool The constant pool.
* @throws IOException Thrown if an I/O error occurs.
+ * @throws ClassFormatException Thrown if a component attribute is
malformed.
*/
public RecordComponentInfo(final DataInput input, final ConstantPool
constantPool) throws IOException {
+ this(input, constantPool, false);
+ }
+
+ /**
+ * Constructs record component information from a data input using the
selected Code header layout for nested attributes.
+ *
+ * <p>
+ * The header selection is forwarded when reading component attributes; it
does not change the record component layout.
+ * </p>
+ *
+ * @param input Input stream.
+ * @param constantPool The constant pool.
+ * @param compactCodeHeader Whether to read any Code attributes
encountered with compact four-byte headers ({@code true})
+ * or standard eight-byte headers ({@code false}).
+ * @throws IOException Thrown if an I/O error occurs.
+ * @throws ClassFormatException Thrown if a component attribute is
malformed.
+ * @see Attribute#readAttribute(DataInput, ConstantPool, boolean)
+ */
+ RecordComponentInfo(final DataInput input, final ConstantPool
constantPool, final boolean compactCodeHeader) throws IOException {
this.index = input.readUnsignedShort();
this.descriptorIndex = input.readUnsignedShort();
final int attributesCount = input.readUnsignedShort();
this.attributes = new Attribute[attributesCount];
for (int j = 0; j < attributesCount; j++) {
- attributes[j] = Attribute.readAttribute(input, constantPool);
+ attributes[j] = Attribute.readAttribute(input, constantPool,
compactCodeHeader);
}
this.constantPool = constantPool;
}
diff --git a/src/main/java/org/apache/bcel/generic/MethodGen.java
b/src/main/java/org/apache/bcel/generic/MethodGen.java
index 0e0e4aeb..c411b01b 100644
--- a/src/main/java/org/apache/bcel/generic/MethodGen.java
+++ b/src/main/java/org/apache/bcel/generic/MethodGen.java
@@ -45,6 +45,7 @@ import org.apache.bcel.classfile.ParameterAnnotationEntry;
import org.apache.bcel.classfile.ParameterAnnotations;
import org.apache.bcel.classfile.RuntimeVisibleParameterAnnotations;
import org.apache.bcel.classfile.Utility;
+import org.apache.bcel.util.Args;
import org.apache.bcel.util.BCELComparator;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.stream.Streams;
@@ -225,6 +226,7 @@ public class MethodGen extends FieldGenOrMethodGen {
private InstructionList il;
private boolean stripAttributes;
+ private boolean compactCodeHeader;
private LocalVariableTypeTable localVariableTypeTable;
private final List<LocalVariableGen> variableList = new ArrayList<>();
@@ -327,6 +329,7 @@ public class MethodGen extends FieldGenOrMethodGen {
Attribute a = attribute;
if (a instanceof Code) {
final Code c = (Code) a;
+ compactCodeHeader = c.isCompactCodeHeader();
setMaxStack(c.getMaxStack());
setMaxLocals(c.getMaxLocals());
final CodeException[] ces = c.getExceptionTable();
@@ -881,7 +884,12 @@ public class MethodGen extends FieldGenOrMethodGen {
* Gets method object. Never forget to call setMaxStack() or
setMaxStack(max), respectively, before calling this method
* (the same applies for max locals).
*
+ * <p>
+ * When initialized from an existing method, the Code header layout is
preserved. Use {@link #setCompactCodeHeader(boolean)} to change it.
+ * </p>
+ *
* @return method object.
+ * @throws org.apache.bcel.classfile.ClassFormatException Thrown if Code
values do not fit the selected header layout.
*/
public Method getMethod() {
final String signature = getSignature();
@@ -899,7 +907,16 @@ public class MethodGen extends FieldGenOrMethodGen {
*/
if (!variableList.isEmpty() && !stripAttributes) {
updateLocalVariableTable(getLocalVariableTable(cp));
- addCodeAttribute(lvt = getLocalVariableTable(cp));
+ lvt = getLocalVariableTable(cp);
+ }
+ // Updating the local variable table can increase maxLocals. Validate
before adding temporary attributes.
+ if (compactCodeHeader && byteCode != null && !isAbstract() &&
!isNative()) {
+ Args.requireU1(maxStack, "maxStack");
+ Args.requireU1(maxLocals, "maxLocals");
+ Args.requireU2(byteCode.length, "Code length attribute");
+ }
+ if (lvt != null) {
+ addCodeAttribute(lvt);
}
if (localVariableTypeTable != null) {
// LocalVariable length in LocalVariableTypeTable is not updated
automatically. It's a difference with
@@ -935,6 +952,7 @@ public class MethodGen extends FieldGenOrMethodGen {
2 + excLen + // exceptions
2 + attrsLen, // attributes
maxStack, maxLocals, byteCode, cExc, codeAttrs,
cp.getConstantPool());
+ code.setCompactCodeHeader(compactCodeHeader);
addAttribute(code);
}
final Attribute[] annotations = addRuntimeAnnotationsAsAttribute(cp);
@@ -991,6 +1009,17 @@ public class MethodGen extends FieldGenOrMethodGen {
return bcelComparator.hashCode(this);
}
+ /**
+ * Gets whether generated Code attributes use compact four-byte headers.
+ *
+ * @return Whether generated Code attributes use compact headers.
+ * @since 6.14.0
+ * @see Code#isCompactCodeHeader()
+ */
+ public boolean isCompactCodeHeader() {
+ return compactCodeHeader;
+ }
+
private List<AnnotationEntryGen> makeMutableVersion(final
AnnotationEntry[] mutableArray) {
return Streams.of(mutableArray).map(ae -> new AnnotationEntryGen(ae,
getConstantPool(), false)).collect(Collectors.toList());
}
@@ -1182,6 +1211,34 @@ public class MethodGen extends FieldGenOrMethodGen {
this.className = className;
}
+ /**
+ * Sets the header layout of generated Code attributes.
+ *
+ * <p>
+ * Compact headers encode {@code max_stack}, {@code max_locals}, and
{@code code_length} as unsigned one-, one-, and two-byte fields, respectively,
+ * totaling 4 bytes instead of the standard 8. This changes only these
field widths, not the bytecode or the remaining attribute layout.
+ * Compact headers are intended for historical class file versions 45.0
through 45.2.
+ * </p>
+ *
+ * <p>
+ * Newly created methods use standard headers by default. Methods
initialized from an existing method preserve its header layout.
+ * {@link #getMethod()} checks that compact headers have {@code max_stack}
and {@code max_locals} from 0 through 255 and a code length
+ * of at most 65535 bytes.
+ * </p>
+ *
+ * <p>
+ * When dumping the enclosing class, {@link
org.apache.bcel.classfile.JavaClass#dump(java.io.DataOutputStream)} may select
a different layout
+ * for consistency with the class version and its other methods. This does
not change the generated Code attribute's header setting.
+ * </p>
+ *
+ * @param compactCodeHeader Whether to generate compact four-byte Code
headers ({@code true}) or standard eight-byte headers ({@code false}).
+ * @since 6.14.0
+ * @see Code#setCompactCodeHeader(boolean)
+ */
+ public void setCompactCodeHeader(final boolean compactCodeHeader) {
+ this.compactCodeHeader = compactCodeHeader;
+ }
+
/**
* Sets the instruction list.
*
diff --git a/src/main/java/org/apache/bcel/util/BCELifier.java
b/src/main/java/org/apache/bcel/util/BCELifier.java
index 4872a11e..34f759e5 100644
--- a/src/main/java/org/apache/bcel/util/BCELifier.java
+++ b/src/main/java/org/apache/bcel/util/BCELifier.java
@@ -374,6 +374,10 @@ public class BCELifier extends
org.apache.bcel.classfile.EmptyVisitor {
}
final Code code = method.getCode();
if (code != null) {
+ // Preserve this method's compact header setting only for versions
that support it; JavaClass.dump selects the class-wide layout.
+ if (code.isCompactCodeHeader() && clazz.getMajor() ==
Const.MAJOR_1_1 && clazz.getMinor() < Const.MINOR_1_1) {
+ printWriter.println(" method.setCompactCodeHeader(true);");
+ }
final StackMap stackMap = code.getStackMap();
if (stackMap != null) {
stackMap.accept(this);
diff --git a/src/test/java/org/apache/bcel/Java1OakExampleCreator.java
b/src/test/java/org/apache/bcel/Java1OakExampleCreator.java
new file mode 100644
index 00000000..9a749c23
--- /dev/null
+++ b/src/test/java/org/apache/bcel/Java1OakExampleCreator.java
@@ -0,0 +1,85 @@
+/*
+ * 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.bcel;
+
+import java.io.IOException;
+
+import org.apache.bcel.classfile.JavaClass;
+import org.apache.bcel.classfile.Method;
+import org.apache.bcel.generic.ArrayType;
+import org.apache.bcel.generic.ClassGen;
+import org.apache.bcel.generic.InstructionConst;
+import org.apache.bcel.generic.InstructionFactory;
+import org.apache.bcel.generic.InstructionList;
+import org.apache.bcel.generic.MethodGen;
+import org.apache.bcel.generic.Type;
+
+/**
+ * Creates the version 45.2 fixture corresponding to {@code
src/test/resources/Java1OakExample.java}, using compact Code headers.
+ * Each header stores {@code max_stack}, {@code max_locals}, and {@code
code_length} in one, one, and two bytes, respectively.
+ *
+ * <p>
+ * To regenerate the fixture from the project directory, run:
+ * </p>
+ * <pre>
+ * mvn test-compile exec:java -Dexec.classpathScope=test
-Dexec.mainClass=org.apache.bcel.Java1OakExampleCreator
+ * </pre>
+ */
+public final class Java1OakExampleCreator {
+
+ /**
+ * Creates the example class with compact Code headers.
+ *
+ * @return The example class.
+ */
+ public static JavaClass create() {
+ final ClassGen cg = new ClassGen("Java1OakExample",
"java.lang.Object", "Java1OakExample.java",
+ Const.ACC_PUBLIC | Const.ACC_SUPER, new String[0]);
+ cg.setMinor(2);
+ cg.addEmptyConstructor(Const.ACC_PUBLIC);
+ final InstructionList il = new
InstructionFactory(cg).createPrintln("Hello Oak!");
+ il.append(InstructionConst.RETURN);
+ final MethodGen main = new MethodGen(Const.ACC_PUBLIC |
Const.ACC_STATIC, Type.VOID, new Type[] {new ArrayType(Type.STRING, 1)},
+ new String[] {"args"}, "main", cg.getClassName(), il,
cg.getConstantPool());
+ main.setMaxStack();
+ main.setMaxLocals();
+ cg.addMethod(main.getMethod());
+ il.dispose();
+ final JavaClass result = cg.getJavaClass();
+ // Select the historical four-byte Code header explicitly; version
45.2 can also use the standard eight-byte layout.
+ for (final Method method : result.getMethods()) {
+ method.getCode().setCompactCodeHeader(true);
+ }
+ return result;
+ }
+
+ /**
+ * Writes the fixture to its resource path or the given file.
+ *
+ * @param args Optional output file name.
+ * @throws IOException Thrown if the output cannot be written.
+ */
+ public static void main(final String[] args) throws IOException {
+ create().dump(args.length == 0 ?
"src/test/resources/Java1OakExample.class" : args[0]);
+ }
+
+ private Java1OakExampleCreator() {
+ }
+}
diff --git a/src/test/java/org/apache/bcel/classfile/AttributeNestingTest.java
b/src/test/java/org/apache/bcel/classfile/AttributeNestingTest.java
index 5595e368..2799a99b 100644
--- a/src/test/java/org/apache/bcel/classfile/AttributeNestingTest.java
+++ b/src/test/java/org/apache/bcel/classfile/AttributeNestingTest.java
@@ -29,7 +29,8 @@ import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
-import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
/**
* Tests that {@link Attribute#readAttribute(DataInput, ConstantPool)} bounds
the attribute nesting depth instead of recursing until a
@@ -40,17 +41,23 @@ class AttributeNestingTest {
/**
* Builds a Code attribute nested {@code depth} times inside itself: each
level declares one sub-attribute, which is again a Code attribute.
*/
- private static byte[] nestedCodeAttribute(final int depth) throws
IOException {
+ private static byte[] nestedCodeAttribute(final int depth, final boolean
compactCodeHeader) throws IOException {
byte[] inner = {};
int attributeCount = 0;
for (int i = 0; i < depth; i++) {
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (DataOutputStream dos = new DataOutputStream(baos)) {
dos.writeShort(1); // attribute_name_index -> "Code"
- dos.writeInt(12 + inner.length); // attribute_length
- dos.writeShort(0); // max_stack
- dos.writeShort(0); // max_locals
- dos.writeInt(0); // code_length
+ dos.writeInt((compactCodeHeader ? 8 : 12) + inner.length); //
attribute_length
+ if (compactCodeHeader) {
+ dos.writeByte(0); // max_stack
+ dos.writeByte(0); // max_locals
+ dos.writeShort(0); // code_length
+ } else {
+ dos.writeShort(0); // max_stack
+ dos.writeShort(0); // max_locals
+ dos.writeInt(0); // code_length
+ }
dos.writeShort(0); // exception_table_length
dos.writeShort(attributeCount); // attributes_count
dos.write(inner);
@@ -61,21 +68,23 @@ class AttributeNestingTest {
return inner;
}
- @Test
- void testDeeplyNestedCodeAttributeRejected() throws IOException {
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void testDeeplyNestedCodeAttributeRejected(final boolean
compactCodeHeader) throws IOException {
final ConstantPool cp = new ConstantPool(new ConstantUtf8("unused
index 0"), new ConstantUtf8("Code"));
- final byte[] bytes = nestedCodeAttribute(1_000);
+ final byte[] bytes = nestedCodeAttribute(1_000, compactCodeHeader);
try (DataInputStream in = new DataInputStream(new
ByteArrayInputStream(bytes))) {
- assertThrows(ClassFormatException.class, () ->
Attribute.readAttribute((DataInput) in, cp));
+ assertThrows(ClassFormatException.class, () ->
Attribute.readAttribute(in, cp, compactCodeHeader));
}
}
- @Test
- void testModeratelyNestedCodeAttributeAccepted() throws IOException {
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void testModeratelyNestedCodeAttributeAccepted(final boolean
compactCodeHeader) throws IOException {
final ConstantPool cp = new ConstantPool(new ConstantUtf8("unused
index 0"), new ConstantUtf8("Code"));
- final byte[] bytes = nestedCodeAttribute(3);
+ final byte[] bytes = nestedCodeAttribute(3, compactCodeHeader);
try (DataInputStream in = new DataInputStream(new
ByteArrayInputStream(bytes))) {
- assertTrue(Attribute.readAttribute((DataInput) in, cp) instanceof
Code);
+ assertTrue(Attribute.readAttribute(in, cp, compactCodeHeader)
instanceof Code);
}
}
}
diff --git a/src/test/java/org/apache/bcel/classfile/ClassParserTest.java
b/src/test/java/org/apache/bcel/classfile/ClassParserTest.java
index f16d0c91..a9f21981 100644
--- a/src/test/java/org/apache/bcel/classfile/ClassParserTest.java
+++ b/src/test/java/org/apache/bcel/classfile/ClassParserTest.java
@@ -19,8 +19,11 @@
package org.apache.bcel.classfile;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import java.io.IOException;
+
import org.junit.jupiter.api.Test;
/**
@@ -32,4 +35,11 @@ class ClassParserTest {
void testParseInvalidClass() {
assertThrows(ClassFormatException.class, () -> new
ClassParser("src/test/resources/org/apache/commons/bcel/classfile/CodeCtor.class").parse());
}
+
+ @Test
+ void testParseOakClassFile() throws IOException {
+ final ClassParser parser = new
ClassParser("src/test/resources/Java1OakExample.class");
+ parser.setCompactCodeHeaderEnabled(true);
+ assertNotNull(parser.parse());
+ }
}
diff --git a/src/test/java/org/apache/bcel/classfile/CompactCodeHeaderTest.java
b/src/test/java/org/apache/bcel/classfile/CompactCodeHeaderTest.java
new file mode 100644
index 00000000..6ad5b604
--- /dev/null
+++ b/src/test/java/org/apache/bcel/classfile/CompactCodeHeaderTest.java
@@ -0,0 +1,328 @@
+/*
+ * 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.bcel.classfile;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.EOFException;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.Arrays;
+
+import org.apache.bcel.Const;
+import org.apache.bcel.Java1OakExampleCreator;
+import org.apache.bcel.generic.ClassGen;
+import org.apache.bcel.generic.ConstantPoolGen;
+import org.apache.bcel.generic.FieldGen;
+import org.apache.bcel.generic.MethodGen;
+import org.apache.bcel.generic.Type;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledOnJre;
+import org.junit.jupiter.api.condition.JRE;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+/**
+ * Tests compact four-byte and standard eight-byte Code headers and their
preservation during bytecode manipulation.
+ * The sizes cover max_stack, max_locals, and code_length only; the rest of
the Code attribute layout is unchanged.
+ */
+class CompactCodeHeaderTest {
+
+ private static final class TestLoader extends ClassLoader {
+ Class<?> define(final byte[] bytes) {
+ return defineClass(null, bytes, 0, bytes.length);
+ }
+ }
+
+ private static JavaClass compactHeaderClass() throws IOException {
+ return parse(fixture(), true);
+ }
+
+ private static byte[] dump(final Code code) throws IOException {
+ final ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ try (DataOutputStream out = new DataOutputStream(bytes)) {
+ code.dump(out);
+ }
+ assertEquals(code.getLength() + 6, bytes.size());
+ return bytes.toByteArray();
+ }
+
+ private static byte[] fixture() throws IOException {
+ return
Files.readAllBytes(Paths.get("src/test/resources/Java1OakExample.class"));
+ }
+
+ private static Code mainCode(final JavaClass clazz) {
+ return Arrays.stream(clazz.getMethods()).filter(method ->
"main".equals(method.getName())).findFirst().get().getCode();
+ }
+
+ private static JavaClass parse(final byte[] bytes, final boolean
compactCodeHeader) throws IOException {
+ final ClassParser parser = new ClassParser(new
ByteArrayInputStream(bytes), "Java1OakExample.class");
+ assertFalse(parser.isCompactCodeHeaderEnabled());
+ parser.setCompactCodeHeaderEnabled(compactCodeHeader);
+ assertEquals(compactCodeHeader, parser.isCompactCodeHeaderEnabled());
+ return parser.parse();
+ }
+
+ private static Code readCode(final byte[] bytes, final ConstantPool cp,
final boolean compactCodeHeader) throws IOException {
+ try (DataInputStream in = new DataInputStream(new
ByteArrayInputStream(bytes))) {
+ final Code code = (Code) Attribute.readAttribute(in, cp,
compactCodeHeader);
+ assertEquals(0, in.available());
+ return code;
+ }
+ }
+
+ private static JavaClass rebuild(final JavaClass clazz) {
+ final ClassGen cg = new ClassGen(clazz);
+ for (final Method method : cg.getMethods()) {
+ final MethodGen mg = new MethodGen(method, cg.getClassName(),
cg.getConstantPool());
+ assertTrue(mg.isCompactCodeHeader());
+ cg.replaceMethod(method, mg.getMethod());
+ }
+ return cg.getJavaClass();
+ }
+
+ @Test
+ void testAddedMethodUsesEnclosingCompactCodeHeader() throws IOException {
+ final ClassGen cg = new ClassGen(compactHeaderClass());
+ final Method main = Arrays.stream(cg.getMethods()).filter(method ->
"main".equals(method.getName())).findFirst().get();
+ final MethodGen mg = new MethodGen(main, cg.getClassName(),
cg.getConstantPool());
+ mg.setName("anotherMethod");
+ mg.setCompactCodeHeader(false);
+ final Method added = mg.getMethod();
+ cg.addMethod(added);
+ final JavaClass parsed = parse(cg.getJavaClass().getBytes(), true);
+ assertEquals(3, parsed.getMethods().length);
+ for (final Method method : parsed.getMethods()) {
+ assertTrue(method.getCode().isCompactCodeHeader());
+ }
+ assertFalse(added.getCode().isCompactCodeHeader(), "Dump must not
modify shared method attributes");
+ added.getCode().setMaxLocals(256);
+ assertThrows(ClassFormatException.class, () ->
cg.getJavaClass().getBytes());
+ }
+
+ @Test
+ void testAttributeLayoutSelection() throws IOException {
+ final Code code = mainCode(compactHeaderClass());
+ assertArrayEquals(dump(code), dump(readCode(dump(code),
code.getConstantPool(), true)));
+ code.setCompactCodeHeader(false);
+ try (DataInputStream in = new DataInputStream(new
ByteArrayInputStream(dump(code)))) {
+ final Code parsed = (Code) Attribute.readAttribute(in,
code.getConstantPool());
+ assertFalse(parsed.isCompactCodeHeader());
+ assertEquals(0, in.available());
+ assertArrayEquals(code.getCode(), parsed.getCode());
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = {0, 65535})
+ void testCodeLengthLimits(final int length) throws IOException {
+ final Code code = mainCode(compactHeaderClass());
+ code.setCode(new byte[length]);
+ assertEquals(length, readCode(dump(code), code.getConstantPool(),
true).getCode().length);
+ code.setCode(new byte[65536]);
+ assertThrows(ClassFormatException.class, () -> dump(code));
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = {-1, 256})
+ void testCompactCodeHeaderBounds(final int invalid) throws IOException {
+ final Code code = mainCode(compactHeaderClass());
+ code.setMaxStack(255);
+ code.setMaxLocals(255);
+ final Code parsed = readCode(dump(code), code.getConstantPool(), true);
+ assertEquals(255, parsed.getMaxStack());
+ assertEquals(255, parsed.getMaxLocals());
+ code.setMaxStack(invalid);
+ assertThrows(ClassFormatException.class, () -> dump(code));
+ code.setMaxStack(255);
+ code.setMaxLocals(invalid);
+ assertThrows(ClassFormatException.class, () -> dump(code));
+ }
+
+ @Test
+ void testCompactCodeHeaderValidatesBeforeChangingMode() throws IOException
{
+ final Code code = mainCode(compactHeaderClass());
+ code.setCompactCodeHeader(false);
+ code.setMaxStack(256);
+ code.setMaxLocals(256);
+ assertEquals(256, readCode(dump(code), code.getConstantPool(),
false).getMaxStack());
+ final int length = code.getLength();
+ assertThrows(ClassFormatException.class, () ->
code.setCompactCodeHeader(true));
+ assertFalse(code.isCompactCodeHeader());
+ assertEquals(length, code.getLength());
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = {0, 1, 2})
+ void testCompactCodeHeaderVersionsRoundTrip(final int minor) throws
IOException {
+ final byte[] bytes = fixture();
+ bytes[5] = (byte) minor;
+ final JavaClass clazz = parse(bytes, true);
+ assertEquals(minor, clazz.getMinor());
+ assertArrayEquals(bytes, clazz.getBytes());
+ assertTrue(mainCode(clazz).isCompactCodeHeader());
+ }
+
+ @Test
+ void testCopiesPreserveLayout() throws IOException {
+ final JavaClass original = compactHeaderClass();
+ assertArrayEquals(fixture(), original.copy().getBytes());
+ final Code code = mainCode(original);
+ final Code shallow = new Code(code);
+ final Code deep = (Code) code.copy(code.getConstantPool());
+ assertTrue(shallow.isCompactCodeHeader());
+ assertTrue(deep.isCompactCodeHeader());
+ assertArrayEquals(dump(code), dump(shallow));
+ assertArrayEquals(dump(code), dump(deep));
+ shallow.setCompactCodeHeader(false);
+ assertTrue(code.isCompactCodeHeader());
+ }
+
+ @Test
+ void testFixtureValuesAndReproducibility() throws IOException {
+ assertArrayEquals(Java1OakExampleCreator.create().getBytes(),
fixture());
+ final JavaClass clazz = compactHeaderClass();
+ assertEquals("Java1OakExample", clazz.getClassName());
+ assertEquals(45, clazz.getMajor());
+ assertEquals(2, clazz.getMinor());
+ assertEquals(2, clazz.getMethods().length);
+ final Code main = mainCode(clazz);
+ assertEquals(2, main.getMaxStack());
+ assertEquals(1, main.getMaxLocals());
+ assertEquals(9, main.getCode().length);
+ assertNotNull(main.getLocalVariableTable());
+ assertEquals(0, main.getExceptionTable().length);
+ }
+
+ @Test
+ void testLengthRecalculation() throws IOException {
+ final Code code = mainCode(compactHeaderClass());
+ code.setAttributes(Attribute.EMPTY_ARRAY);
+ code.setExceptionTable(new CodeException[] {new CodeException(0, 1, 0,
0)});
+ code.setCode(new byte[] {(byte) Const.RETURN});
+ assertEquals(17, code.getLength());
+ final Code parsed = readCode(dump(code), code.getConstantPool(), true);
+ assertEquals(1, parsed.getExceptionTable().length);
+ assertEquals(1, parsed.getCode().length);
+ code.setCompactCodeHeader(false);
+ assertEquals(21, code.getLength());
+ assertEquals(1, readCode(dump(code), code.getConstantPool(),
false).getExceptionTable().length);
+ code.setCompactCodeHeader(true);
+ assertEquals(17, code.getLength());
+ }
+
+ @Test
+ void testLocalVariableGrowthFailureDoesNotLeaveAttributes() throws
IOException {
+ final JavaClass clazz = compactHeaderClass();
+ final Method main = Arrays.stream(clazz.getMethods()).filter(method ->
"main".equals(method.getName())).findFirst().get();
+ final MethodGen mg = new MethodGen(main, clazz.getClassName(), new
ConstantPoolGen(clazz.getConstantPool()));
+ mg.addLocalVariable("extra", Type.INT, 255,
mg.getInstructionList().getStart(), mg.getInstructionList().getEnd());
+ // Rebuilding the local variable table restores the required count of
256 during getMethod().
+ mg.setMaxLocals(1);
+ assertThrows(ClassFormatException.class, mg::getMethod);
+ assertEquals(0, mg.getCodeAttributes().length, "Failed generation must
not leave temporary attributes");
+ mg.setCompactCodeHeader(false);
+ final Code code = mg.getMethod().getCode();
+ assertEquals(256, code.getMaxLocals());
+ assertEquals(1,
Arrays.stream(code.getAttributes()).filter(LocalVariableTable.class::isInstance).count());
+ }
+
+ @Test
+ void testMethodGenPreservesLayout() throws IOException {
+ final JavaClass clazz = compactHeaderClass();
+ final JavaClass rebuilt = parse(rebuild(clazz).getBytes(), true);
+ assertTrue(mainCode(rebuilt).isCompactCodeHeader());
+ assertArrayEquals(mainCode(clazz).getCode(),
mainCode(rebuilt).getCode());
+ final MethodGen mg = new MethodGen(clazz.getMethods()[0],
clazz.getClassName(), new ConstantPoolGen(clazz.getConstantPool()));
+ assertTrue(mg.copy(clazz.getClassName(), new
ConstantPoolGen(clazz.getConstantPool())).getMethod().getCode().isCompactCodeHeader());
+ mg.setMaxStack(256);
+ assertThrows(ClassFormatException.class, mg::getMethod);
+ mg.setCompactCodeHeader(false);
+ assertFalse(mg.getMethod().getCode().isCompactCodeHeader());
+ }
+
+ @Test
+ void testNonCodeAttributesWithCompactCodeHeaders() throws IOException {
+ final ClassGen cg = new ClassGen(compactHeaderClass());
+ final FieldGen fg = new FieldGen(Const.ACC_PUBLIC | Const.ACC_STATIC |
Const.ACC_FINAL, Type.INT, "VALUE", cg.getConstantPool());
+ fg.setInitValue(42);
+ cg.addField(fg.getField());
+ final JavaClass parsed = parse(cg.getJavaClass().getBytes(), true);
+ assertEquals("42",
parsed.getFields()[0].getConstantValue().toString());
+ assertEquals("Java1OakExample.java", parsed.getSourceFileName());
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = {0, 1, 2, 3})
+ void testStandardCodeHeaderRemainsDefault(final int minor) throws
IOException {
+ final JavaClass clazz = compactHeaderClass();
+ for (final Method method : clazz.getMethods()) {
+ method.getCode().setCompactCodeHeader(false);
+ }
+ clazz.setMinor(minor);
+ final byte[] bytes = clazz.getBytes();
+ final JavaClass parsed = parse(bytes, false);
+ assertFalse(mainCode(parsed).isCompactCodeHeader());
+ assertEquals(1, mainCode(parsed).getMaxLocals());
+ assertArrayEquals(bytes, parsed.getBytes());
+ }
+
+ @Test
+ void testTruncatedCompactCodeAttribute() throws IOException {
+ final Code code = mainCode(compactHeaderClass());
+ final byte[] bytes = dump(code);
+ assertThrows(EOFException.class, () -> readCode(Arrays.copyOf(bytes,
bytes.length - 1), code.getConstantPool(), true));
+ }
+
+ @Test
+ @EnabledOnJre(JRE.JAVA_8)
+ void testValidCompactCodeHeaderFixtureAndRegeneratedMethodsLoadInJava8()
throws Exception {
+ assertNotNull(new
TestLoader().define(fixture()).getDeclaredMethod("main", String[].class));
+ assertNotNull(new
TestLoader().define(rebuild(compactHeaderClass()).getBytes()).getDeclaredMethod("main",
String[].class));
+ }
+
+ @ParameterizedTest
+ @CsvSource({"45, 3", "46, 0"})
+ void testVersionChangesSelectStandardCodeHeaders(final int major, final
int minor) throws Exception {
+ final JavaClass clazz = compactHeaderClass();
+ clazz.setMajor(major);
+ clazz.setMinor(minor);
+ final byte[] bytes = clazz.getBytes();
+ assertFalse(mainCode(parse(bytes, false)).isCompactCodeHeader());
+ assertFalse(mainCode(parse(bytes, true)).isCompactCodeHeader(),
"Compact Code header parsing must only apply to versions 45.0 through 45.2");
+ assertTrue(mainCode(clazz).isCompactCodeHeader(), "Dump must not
mutate the original attributes");
+ assertNotNull(new TestLoader().define(bytes).getDeclaredMethod("main",
String[].class));
+ final ClassGen cg = new ClassGen(compactHeaderClass());
+ cg.setMajor(major);
+ cg.setMinor(minor);
+ assertFalse(mainCode(parse(cg.getJavaClass().getBytes(),
false)).isCompactCodeHeader());
+ }
+}
diff --git a/src/test/java/org/apache/bcel/util/BCELifierTest.java
b/src/test/java/org/apache/bcel/util/BCELifierTest.java
index 40d13258..23ed04d5 100644
--- a/src/test/java/org/apache/bcel/util/BCELifierTest.java
+++ b/src/test/java/org/apache/bcel/util/BCELifierTest.java
@@ -42,6 +42,7 @@ import org.apache.bcel.classfile.ClassParser;
import org.apache.bcel.classfile.ConstantValue;
import org.apache.bcel.classfile.Field;
import org.apache.bcel.classfile.JavaClass;
+import org.apache.bcel.classfile.Method;
import org.apache.bcel.classfile.Utility;
import org.apache.bcel.generic.BinaryOpCreator;
import org.apache.bcel.generic.ClassGen;
@@ -57,6 +58,7 @@ import org.apache.commons.lang3.SystemUtils;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledForJreRange;
import org.junit.jupiter.api.condition.JRE;
+import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
@@ -308,6 +310,36 @@ class BCELifierTest extends AbstractTest {
}
}
+ @ParameterizedTest
+ @CsvSource({"true, 45, 2", "false, 45, 2", "true, 45, 3", "true, 46, 0"})
+ void testCompactCodeHeaderRoundTrip(final boolean compactCodeHeader, final
int major, final int minor, @TempDir final Path directory) throws Exception {
+ final ClassParser parser = new
ClassParser("src/test/resources/Java1OakExample.class");
+ parser.setCompactCodeHeaderEnabled(true);
+ final JavaClass original = parser.parse();
+ original.setMajor(major);
+ original.setMinor(minor);
+ for (final Method method : original.getMethods()) {
+ method.getCode().setCompactCodeHeader(compactCodeHeader);
+ }
+ final ByteArrayOutputStream source = new ByteArrayOutputStream();
+ new BCELifier(original, source).start();
+ Files.write(directory.resolve("Java1OakExampleCreator.java"),
source.toByteArray());
+ final File workDir = directory.toFile();
+ exec(workDir, getAppJavaC(), "-source", "8", "-target", "8", "-cp",
CLASSPATH, "Java1OakExampleCreator.java");
+ exec(workDir, getAppJava(), "-cp", CLASSPATH,
"Java1OakExampleCreator");
+ final boolean expectedCompactCodeHeader = compactCodeHeader && major
== 45 && minor < 3;
+ final ClassParser generatedParser = new
ClassParser(directory.resolve("Java1OakExample.class").toString());
+ generatedParser.setCompactCodeHeaderEnabled(expectedCompactCodeHeader);
+ final JavaClass generated = generatedParser.parse();
+ assertEquals(major, generated.getMajor());
+ assertEquals(minor, generated.getMinor());
+ assertEquals(original.getMethods().length,
generated.getMethods().length);
+ for (final Method method : generated.getMethods()) {
+ assertEquals(expectedCompactCodeHeader,
method.getCode().isCompactCodeHeader());
+ assertEquals(1, method.getCode().getMaxLocals());
+ }
+ }
+
@Test
void testCreateInvokeEscapesConstantPoolName() throws Exception {
// A hostile constant pool can hold any UTF-8 as a referenced method
name.
diff --git a/src/test/resources/Java1OakExample.class
b/src/test/resources/Java1OakExample.class
new file mode 100644
index 00000000..b179a41b
Binary files /dev/null and b/src/test/resources/Java1OakExample.class differ
diff --git a/src/test/java/org/apache/bcel/classfile/ClassParserTest.java
b/src/test/resources/Java1OakExample.java
similarity index 68%
copy from src/test/java/org/apache/bcel/classfile/ClassParserTest.java
copy to src/test/resources/Java1OakExample.java
index f16d0c91..3c2114ee 100644
--- a/src/test/java/org/apache/bcel/classfile/ClassParserTest.java
+++ b/src/test/resources/Java1OakExample.java
@@ -17,19 +17,11 @@
* under the License.
*/
-package org.apache.bcel.classfile;
-
-import static org.junit.jupiter.api.Assertions.assertThrows;
-
-import org.junit.jupiter.api.Test;
-
/**
- * Tests {@link ClassParser}.
+ * Source equivalent of the fixture with compact Code headers generated by
org.apache.bcel.Java1OakExampleCreator.
*/
-class ClassParserTest {
-
- @Test
- void testParseInvalidClass() {
- assertThrows(ClassFormatException.class, () -> new
ClassParser("src/test/resources/org/apache/commons/bcel/classfile/CodeCtor.class").parse());
+public class Java1OakExample {
+ public static void main(final String[] args) {
+ System.out.println("Hello Oak!");
}
}