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 0000000000..b179a41b98 Binary files /dev/null and b/src/test/resources/Java1OakExample.class differ 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!"); + } +}