From 3aae70b4dcbf078f2cb792c6801ceefc1b9a9b8d Mon Sep 17 00:00:00 2001 From: Gary Gregory Date: Sun, 1 Nov 2026 15:43:16 -0500 Subject: [PATCH] [BCEL-341] Support compact Code headers in historical class files 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 + .../org/apache/bcel/classfile/Attribute.java | 45 ++- .../apache/bcel/classfile/ClassParser.java | 51 ++- .../java/org/apache/bcel/classfile/Code.java | 116 ++++++- .../java/org/apache/bcel/classfile/Field.java | 21 +- .../apache/bcel/classfile/FieldOrMethod.java | 61 +++- .../org/apache/bcel/classfile/JavaClass.java | 13 +- .../org/apache/bcel/classfile/Method.java | 24 +- .../org/apache/bcel/classfile/Record.java | 35 +- .../bcel/classfile/RecordComponentInfo.java | 26 +- .../org/apache/bcel/generic/MethodGen.java | 59 +++- .../java/org/apache/bcel/util/BCELifier.java | 4 + .../apache/bcel/Java1OakExampleCreator.java | 85 +++++ .../bcel/classfile/AttributeNestingTest.java | 37 +- .../bcel/classfile/ClassParserTest.java | 10 + .../bcel/classfile/CompactCodeHeaderTest.java | 328 ++++++++++++++++++ .../org/apache/bcel/util/BCELifierTest.java | 32 ++ src/test/resources/Java1OakExample.class | Bin 0 -> 493 bytes src/test/resources/Java1OakExample.java | 27 ++ 19 files changed, 911 insertions(+), 64 deletions(-) create mode 100644 src/test/java/org/apache/bcel/Java1OakExampleCreator.java create mode 100644 src/test/java/org/apache/bcel/classfile/CompactCodeHeaderTest.java create mode 100644 src/test/resources/Java1OakExample.class create mode 100644 src/test/resources/Java1OakExample.java diff --git a/src/changes/changes.xml b/src/changes/changes.xml index dd6ca03101..d3ee4716e2 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -68,6 +68,7 @@ The type attribute can be add,update,fix,remove. Update ECJ versions and generate the SWAP fixture with BCEL (#537). Fix typo 'preceeding' in ConstantPool exception message (#541). Clean up comments for spelling, grammar, and readability. + Support explicit parsing and generation of compact Code headers in class file versions 45.0 through 45.2 (#58). Add and use ClassFormatException.ClassFormatException(String, Object...). Add support for the RuntimeVisibleTypeAnnotations and RuntimeInvisibleTypeAnnotations attributes. diff --git a/src/main/java/org/apache/bcel/classfile/Attribute.java b/src/main/java/org/apache/bcel/classfile/Attribute.java index 2cb0434ff6..e1017cbdb2 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 @@ protected static void println(final String msg) { } /** - * 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. + * + *

+ * 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. + *

+ * + * @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 static Attribute readAttribute(final DataInput dataInput, final ConstantP } 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 static Attribute readAttribute(final DataInput dataInput, final ConstantP } /** - * 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 @@ private static Attribute readAttribute0(final DataInput dataInput, final Constan 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 @@ private static Attribute readAttribute0(final DataInput dataInput, final Constan 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 a5c8eece24..521e3a2d9a 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 @@ static int[] readU2U2Table(final DataInput dataInput) throws IOException { 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 ClassParser(final String zipFile, final String fileName) { 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 ClassFormatException 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). * + *

+ * 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. + *

+ * * @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 @@ private void readAttributes() throws IOException, ClassFormatException { 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 @@ private void readFields() throws IOException, ClassFormatException { 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 @@ private void readMethods() throws IOException { 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 @@ private void readMethods() throws IOException { 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. + * + *

+ * "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. + *

+ * + *

+ * 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()}. + *

+ * + * @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 7b972c11ee..473d297498 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 @@ * } * * + *

+ * 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. + *

+ * + *

+ * 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. + *

+ * * @see Attribute * @see CodeException * @see LineNumberTable @@ -63,6 +78,11 @@ */ 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 Code(final Code code) { * @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 Code(final Code code) { 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 Code(final Code code) { } /** - * Constructs a Code attribute. + * Constructs a Code attribute using a standard eight-byte header. + * + *

+ * Use {@link #setCompactCodeHeader(boolean)} to select the compact four-byte header after construction. + *

* * @param nameIndex Index pointing to the name Code. * @param length Content length in bytes. @@ -134,7 +167,13 @@ public Code(final Code code) { */ 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 Attribute copy(final ConstantPool constantPool) { * * @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 CodeException[] getExceptionTable() { * @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 StackMap getStackMap() { 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 void setCode(final byte[] code) { super.setLength(calculateLength()); // Adjust length } + /** + * Sets the Code header layout and recalculates the attribute length. + * + *

+ * 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)}. + *

+ * + *

+ * 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. + *

+ * + * @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 fca1bd8add..3ebd9c4666 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 static void setComparator(final BCELComparator comparator) { } /** - * 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 77cbaa776f..d47d2b42de 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 @@ protected FieldOrMethod(final FieldOrMethod c) { /** * 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 @@ protected FieldOrMethod copy_(final ConstantPool constantPool) { * @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 94f1e56607..cdeb8c7468 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 JavaClass copy() { /** * Dumps Java class to output stream in binary format. * + *

+ * 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. + *

+ * * @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 void dump(final DataOutputStream file) throws IOException { } 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 235a320409..d78f8dc5b6 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 Method() { } /** - * 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 327dee1bef..0117f850fe 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 @@ private static RecordComponentInfo[] readComponents(final DataInput input, final 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. + * + *

+ * The header selection is forwarded when reading component attributes; it does not change the Record attribute or record component layout. + *

+ * + * @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 e32e1ac70b..e58a1c6d0f 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. + * + *

+ * The header selection is forwarded when reading component attributes; it does not change the record component layout. + *

+ * + * @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 0e0e4aebdd..c411b01bec 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.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 static void setComparator(final BCELComparator compa private InstructionList il; private boolean stripAttributes; + private boolean compactCodeHeader; private LocalVariableTypeTable localVariableTypeTable; private final List variableList = new ArrayList<>(); @@ -327,6 +329,7 @@ public MethodGen(final Method method, final String className, final ConstantPool 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 int getMaxStack() { * Gets method object. Never forget to call setMaxStack() or setMaxStack(max), respectively, before calling this method * (the same applies for max locals). * + *

+ * When initialized from an existing method, the Code header layout is preserved. Use {@link #setCompactCodeHeader(boolean)} to change it. + *

+ * * @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 Method getMethod() { */ 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 Method getMethod() { 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 int hashCode() { 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 makeMutableVersion(final AnnotationEntry[] mutableArray) { return Streams.of(mutableArray).map(ae -> new AnnotationEntryGen(ae, getConstantPool(), false)).collect(Collectors.toList()); } @@ -1182,6 +1211,34 @@ public void setClassName(final String className) { // TODO could be package-prot this.className = className; } + /** + * Sets the header layout of generated Code attributes. + * + *

+ * 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. + *

+ * + *

+ * 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. + *

+ * + *

+ * 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. + *

+ * + * @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 4872a11e9d..34f759e503 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 void visitMethod(final Method method) { } 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 0000000000..9a749c2331 --- /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. + * + *

+ * To regenerate the fixture from the project directory, run: + *

+ *
+ * mvn test-compile exec:java -Dexec.classpathScope=test -Dexec.mainClass=org.apache.bcel.Java1OakExampleCreator
+ * 
+ */ +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 5595e3688d..2799a99b2c 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.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 @@ private static byte[] nestedCodeAttribute(final int depth) throws IOException { 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 f16d0c91f1..a9f21981bd 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 0000000000..6ad5b60492 --- /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 40d1325848..23ed04d557 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.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.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 @@ private void testClassOnPath(final String javaClassFileName) throws Exception { } } + @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 0000000000000000000000000000000000000000..b179a41b98310b339ac13e82385be05b20c5a32c GIT binary patch literal 493 zcmZut%T5A85UgGv%dRM}JbVkp3z{hLjPYVL#*kow;o@n)2@KBel4UUZS$fdKgCF2W z8GF%#Aw**wZnnXJrOC< z*YY+Fj6VUdPOxHwG_^m7o@Eqh(mkm0z-U6lx_!*{qZ+z?hdW;p{XtZ)T5*u~x-c;6y8uPAwyH|#7D98^(b J0?p|btRGivYykiO literal 0 HcmV?d00001 diff --git a/src/test/resources/Java1OakExample.java b/src/test/resources/Java1OakExample.java new file mode 100644 index 0000000000..3c2114ee95 --- /dev/null +++ b/src/test/resources/Java1OakExample.java @@ -0,0 +1,27 @@ +/* + * 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. + */ + +/** + * Source equivalent of the fixture with compact Code headers generated by org.apache.bcel.Java1OakExampleCreator. + */ +public class Java1OakExample { + public static void main(final String[] args) { + System.out.println("Hello Oak!"); + } +}