From 02f999f8154425f9ca0f693c7b49d1401ecd782f Mon Sep 17 00:00:00 2001 From: Nicolas Baumann Date: Thu, 22 Oct 2026 07:55:41 +0200 Subject: [PATCH 1/2] Add support for the RuntimeVisibleTypeAnnotations and RuntimeInvisibleTypeAnnotations attributes Both attributes (JVMS 4.7.20) were returned as Unknown, so a caller had to parse the raw bytes (target_type/target_info, type_path, annotation) to know about annotations such as JSpecify's @Nullable, which are type annotations. - Const: ATTR_RUNTIME_VISIBLE_TYPE_ANNOTATIONS, ATTR_RUNTIME_INVISIBLE_TYPE_ANNOTATIONS. - TypeAnnotations (base class), RuntimeVisibleTypeAnnotations and RuntimeInvisibleTypeAnnotations, TypeAnnotationEntry (target type and info, type path, AnnotationEntry); read and dump round trip. - Visitor.visitTypeAnnotations (default method), DescendingVisitor, EmptyVisitor, StringRepresentation. Co-Authored-By: Claude Sonnet 5.5 --- src/changes/changes.xml | 1 + src/main/java/org/apache/bcel/Const.java | 18 +- .../org/apache/bcel/classfile/Attribute.java | 4 + .../bcel/classfile/DescendingVisitor.java | 8 + .../apache/bcel/classfile/EmptyVisitor.java | 5 + .../RuntimeInvisibleTypeAnnotations.java | 62 ++++ .../RuntimeVisibleTypeAnnotations.java | 62 ++++ .../bcel/classfile/TypeAnnotationEntry.java | 281 ++++++++++++++++++ .../bcel/classfile/TypeAnnotations.java | 157 ++++++++++ .../org/apache/bcel/classfile/Visitor.java | 10 + .../statics/StringRepresentation.java | 11 + .../bcel/classfile/TypeAnnotationsTest.java | 201 +++++++++++++ .../typeannotations/TypeAnnotated.class | Bin 0 -> 709 bytes .../typeannotations/TypeAnnotated.java | 34 +++ 14 files changed, 852 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/apache/bcel/classfile/RuntimeInvisibleTypeAnnotations.java create mode 100644 src/main/java/org/apache/bcel/classfile/RuntimeVisibleTypeAnnotations.java create mode 100644 src/main/java/org/apache/bcel/classfile/TypeAnnotationEntry.java create mode 100644 src/main/java/org/apache/bcel/classfile/TypeAnnotations.java create mode 100644 src/test/java/org/apache/bcel/classfile/TypeAnnotationsTest.java create mode 100644 src/test/resources/typeannotations/TypeAnnotated.class create mode 100644 src/test/resources/typeannotations/TypeAnnotated.java diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 2639440523..49ba1c0fcb 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). Add and use ClassFormatException.ClassFormatException(String, Object...). + Add support for the RuntimeVisibleTypeAnnotations and RuntimeInvisibleTypeAnnotations attributes. Bump org.apache.commons:commons-parent from 104 to 106. Bump org.apache.commons:commons-lang3 from 3.20.0 to 3.21.0 #531. diff --git a/src/main/java/org/apache/bcel/Const.java b/src/main/java/org/apache/bcel/Const.java index 8eb6f492e0..5b7ad00990 100644 --- a/src/main/java/org/apache/bcel/Const.java +++ b/src/main/java/org/apache/bcel/Const.java @@ -3207,14 +3207,28 @@ public final class Const { */ public static final byte ATTR_PERMITTED_SUBCLASSES = 28; + /** + * Attribute constant for RuntimeVisibleTypeAnnotations. + * + * @since 6.14.0 + */ + public static final byte ATTR_RUNTIME_VISIBLE_TYPE_ANNOTATIONS = 29; + + /** + * Attribute constant for RuntimeInvisibleTypeAnnotations. + * + * @since 6.14.0 + */ + public static final byte ATTR_RUNTIME_INVISIBLE_TYPE_ANNOTATIONS = 30; + /** Count of known attributes. */ - public static final short KNOWN_ATTRIBUTES = 29; // count of attributes + public static final short KNOWN_ATTRIBUTES = 31; // count of attributes private static final String[] ATTRIBUTE_NAMES = { "SourceFile", "ConstantValue", "Code", "Exceptions", "LineNumberTable", "LocalVariableTable", "InnerClasses", "Synthetic", "Deprecated", "PMGClass", "Signature", "StackMap", "RuntimeVisibleAnnotations", "RuntimeInvisibleAnnotations", "RuntimeVisibleParameterAnnotations", "RuntimeInvisibleParameterAnnotations", "AnnotationDefault", "LocalVariableTypeTable", "EnclosingMethod", "StackMapTable", "BootstrapMethods", "MethodParameters", "Module", "ModulePackages", "ModuleMainClass", "NestHost", "NestMembers", "Record", - "PermittedSubclasses" }; + "PermittedSubclasses", "RuntimeVisibleTypeAnnotations", "RuntimeInvisibleTypeAnnotations" }; /** * Constants used in the StackMap attribute. diff --git a/src/main/java/org/apache/bcel/classfile/Attribute.java b/src/main/java/org/apache/bcel/classfile/Attribute.java index 8fb936465b..526a923c57 100644 --- a/src/main/java/org/apache/bcel/classfile/Attribute.java +++ b/src/main/java/org/apache/bcel/classfile/Attribute.java @@ -259,6 +259,10 @@ private static Attribute readAttribute0(final DataInput dataInput, final Constan return new Record(nameIndex, length, dataInput, constantPool); case Const.ATTR_PERMITTED_SUBCLASSES: return new PermittedSubclasses(nameIndex, length, dataInput, constantPool); + case Const.ATTR_RUNTIME_VISIBLE_TYPE_ANNOTATIONS: + return new RuntimeVisibleTypeAnnotations(nameIndex, length, dataInput, constantPool); + case Const.ATTR_RUNTIME_INVISIBLE_TYPE_ANNOTATIONS: + return new RuntimeInvisibleTypeAnnotations(nameIndex, length, dataInput, constantPool); default: // Never reached throw new IllegalStateException("Unrecognized attribute type tag parsed: " + tag); diff --git a/src/main/java/org/apache/bcel/classfile/DescendingVisitor.java b/src/main/java/org/apache/bcel/classfile/DescendingVisitor.java index 816bf091d2..43a48ad4e2 100644 --- a/src/main/java/org/apache/bcel/classfile/DescendingVisitor.java +++ b/src/main/java/org/apache/bcel/classfile/DescendingVisitor.java @@ -537,6 +537,14 @@ public void visitRecordComponent(final RecordComponentInfo recordComponentInfo) stack.pop(); } + /** @since 6.14.0 */ + @Override + public void visitTypeAnnotations(final TypeAnnotations obj) { + stack.push(obj); + obj.accept(visitor); + stack.pop(); + } + @Override public void visitSignature(final Signature attribute) { stack.push(attribute); diff --git a/src/main/java/org/apache/bcel/classfile/EmptyVisitor.java b/src/main/java/org/apache/bcel/classfile/EmptyVisitor.java index d8f315cfaf..a34449e30f 100644 --- a/src/main/java/org/apache/bcel/classfile/EmptyVisitor.java +++ b/src/main/java/org/apache/bcel/classfile/EmptyVisitor.java @@ -303,6 +303,11 @@ public void visitParameterAnnotationEntry(final ParameterAnnotationEntry paramet public void visitPermittedSubclasses(final PermittedSubclasses obj) { } + /** @since 6.14.0 */ + @Override + public void visitTypeAnnotations(final TypeAnnotations obj) { + } + @Override public void visitSignature(final Signature obj) { } diff --git a/src/main/java/org/apache/bcel/classfile/RuntimeInvisibleTypeAnnotations.java b/src/main/java/org/apache/bcel/classfile/RuntimeInvisibleTypeAnnotations.java new file mode 100644 index 0000000000..d3facc7fbe --- /dev/null +++ b/src/main/java/org/apache/bcel/classfile/RuntimeInvisibleTypeAnnotations.java @@ -0,0 +1,62 @@ +/* + * 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 java.io.DataInput; +import java.io.DataOutputStream; +import java.io.IOException; + +import org.apache.bcel.Const; + +/** + * The type annotations of a declaration, recorded in the class file but not visible at runtime. + * + * @since 6.14.0 + */ +public class RuntimeInvisibleTypeAnnotations extends TypeAnnotations { + + /** + * Constructs a new instance. + * + * @param nameIndex Index pointing to the name of the attribute. + * @param length Content length in bytes. + * @param input Input stream. + * @param constantPool Array of constants. + * @throws IOException Thrown when an I/O exception of some sort has occurred. + */ + public RuntimeInvisibleTypeAnnotations(final int nameIndex, final int length, final DataInput input, final ConstantPool constantPool) throws IOException { + super(Const.ATTR_RUNTIME_INVISIBLE_TYPE_ANNOTATIONS, nameIndex, length, input, constantPool, false); + } + + /** + * Creates a deep copy of this attribute. + * + * @return deep copy of this attribute. + */ + @Override + public Attribute copy(final ConstantPool constantPool) { + return (Attribute) clone(); + } + + @Override + public final void dump(final DataOutputStream dos) throws IOException { + super.dump(dos); + writeTypeAnnotations(dos); + } +} diff --git a/src/main/java/org/apache/bcel/classfile/RuntimeVisibleTypeAnnotations.java b/src/main/java/org/apache/bcel/classfile/RuntimeVisibleTypeAnnotations.java new file mode 100644 index 0000000000..ebbcabe6fb --- /dev/null +++ b/src/main/java/org/apache/bcel/classfile/RuntimeVisibleTypeAnnotations.java @@ -0,0 +1,62 @@ +/* + * 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 java.io.DataInput; +import java.io.DataOutputStream; +import java.io.IOException; + +import org.apache.bcel.Const; + +/** + * The type annotations of a declaration, provided to the JVM and visible at runtime. + * + * @since 6.14.0 + */ +public class RuntimeVisibleTypeAnnotations extends TypeAnnotations { + + /** + * Constructs a new instance. + * + * @param nameIndex Index pointing to the name of the attribute. + * @param length Content length in bytes. + * @param input Input stream. + * @param constantPool Array of constants. + * @throws IOException Thrown when an I/O exception of some sort has occurred. + */ + public RuntimeVisibleTypeAnnotations(final int nameIndex, final int length, final DataInput input, final ConstantPool constantPool) throws IOException { + super(Const.ATTR_RUNTIME_VISIBLE_TYPE_ANNOTATIONS, nameIndex, length, input, constantPool, true); + } + + /** + * Creates a deep copy of this attribute. + * + * @return deep copy of this attribute. + */ + @Override + public Attribute copy(final ConstantPool constantPool) { + return (Attribute) clone(); + } + + @Override + public final void dump(final DataOutputStream dos) throws IOException { + super.dump(dos); + writeTypeAnnotations(dos); + } +} diff --git a/src/main/java/org/apache/bcel/classfile/TypeAnnotationEntry.java b/src/main/java/org/apache/bcel/classfile/TypeAnnotationEntry.java new file mode 100644 index 0000000000..cc705c10ad --- /dev/null +++ b/src/main/java/org/apache/bcel/classfile/TypeAnnotationEntry.java @@ -0,0 +1,281 @@ +/* + * 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 java.io.DataInput; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.Arrays; + +import org.apache.bcel.util.Args; + +/** + * Represents one type annotation of a {@link TypeAnnotations} attribute (JVMS 4.7.20): an annotation, the kind of program element it applies to + * (the target type and its target info), and the path to the annotated type inside that element. + * + * @see JVMS 4.7.20 The RuntimeVisibleTypeAnnotations attribute + * @since 6.14.0 + */ +public class TypeAnnotationEntry { + + /** Target type: type parameter declaration of a generic class or interface. */ + public static final int TARGET_CLASS_TYPE_PARAMETER = 0x00; + + /** Target type: type parameter declaration of a generic method or constructor. */ + public static final int TARGET_METHOD_TYPE_PARAMETER = 0x01; + + /** Target type: type in an extends or implements clause of a class declaration. */ + public static final int TARGET_CLASS_EXTENDS = 0x10; + + /** Target type: bound of a type parameter of a generic class or interface. */ + public static final int TARGET_CLASS_TYPE_PARAMETER_BOUND = 0x11; + + /** Target type: bound of a type parameter of a generic method or constructor. */ + public static final int TARGET_METHOD_TYPE_PARAMETER_BOUND = 0x12; + + /** Target type: type in a field or record component declaration. */ + public static final int TARGET_FIELD = 0x13; + + /** Target type: return type of a method, or type of a newly constructed object. */ + public static final int TARGET_METHOD_RETURN = 0x14; + + /** Target type: receiver type of a method or constructor. */ + public static final int TARGET_METHOD_RECEIVER = 0x15; + + /** Target type: type in a formal parameter declaration of a method, constructor, or lambda expression. */ + public static final int TARGET_METHOD_FORMAL_PARAMETER = 0x16; + + /** Target type: type in a throws clause of a method or constructor. */ + public static final int TARGET_THROWS = 0x17; + + /** Target type: type in a local variable declaration. */ + public static final int TARGET_LOCAL_VARIABLE = 0x40; + + /** Target type: type in a resource variable declaration. */ + public static final int TARGET_RESOURCE_VARIABLE = 0x41; + + /** Target type: type in an exception parameter declaration. */ + public static final int TARGET_EXCEPTION_PARAMETER = 0x42; + + /** Target type: type in an instanceof expression. */ + public static final int TARGET_INSTANCEOF = 0x43; + + /** Target type: type in a new expression. */ + public static final int TARGET_NEW = 0x44; + + /** Target type: type in a method reference expression using ::new. */ + public static final int TARGET_CONSTRUCTOR_REFERENCE = 0x45; + + /** Target type: type in a method reference expression using ::Identifier. */ + public static final int TARGET_METHOD_REFERENCE = 0x46; + + /** Target type: type in a cast expression. */ + public static final int TARGET_CAST = 0x47; + + /** Target type: type argument for a generic constructor in a new expression or an explicit constructor invocation. */ + public static final int TARGET_CONSTRUCTOR_INVOCATION_TYPE_ARGUMENT = 0x48; + + /** Target type: type argument for a generic method in a method invocation expression. */ + public static final int TARGET_METHOD_INVOCATION_TYPE_ARGUMENT = 0x49; + + /** Target type: type argument for a generic constructor in a method reference expression using ::new. */ + public static final int TARGET_CONSTRUCTOR_REFERENCE_TYPE_ARGUMENT = 0x4A; + + /** Target type: type argument for a generic method in a method reference expression using ::Identifier. */ + public static final int TARGET_METHOD_REFERENCE_TYPE_ARGUMENT = 0x4B; + + /** + * Empty array of TypeAnnotationEntry objects. + */ + public static final TypeAnnotationEntry[] EMPTY_ARRAY = {}; + + /** Number of bytes of a {@code localvar_target} entry: start_pc, length and index. */ + private static final int LOCAL_VARIABLE_RANGE_SIZE = 6; + + /** + * Gets the number of bytes of the target info that follows a target type. + * + * @param targetType The target type. + * @return the size of the target info, or -1 if it depends on its content (the table of a {@code localvar_target}). + * @throws ClassFormatException if the target type is not defined. + */ + private static int targetInfoSize(final int targetType) { + switch (targetType) { + case TARGET_CLASS_TYPE_PARAMETER: + case TARGET_METHOD_TYPE_PARAMETER: + case TARGET_METHOD_FORMAL_PARAMETER: + return 1; + case TARGET_CLASS_EXTENDS: + case TARGET_CLASS_TYPE_PARAMETER_BOUND: + case TARGET_METHOD_TYPE_PARAMETER_BOUND: + case TARGET_THROWS: + case TARGET_EXCEPTION_PARAMETER: + case TARGET_INSTANCEOF: + case TARGET_NEW: + case TARGET_CONSTRUCTOR_REFERENCE: + case TARGET_METHOD_REFERENCE: + return 2; + case TARGET_FIELD: + case TARGET_METHOD_RETURN: + case TARGET_METHOD_RECEIVER: + return 0; + case TARGET_LOCAL_VARIABLE: + case TARGET_RESOURCE_VARIABLE: + return -1; + case TARGET_CAST: + case TARGET_CONSTRUCTOR_INVOCATION_TYPE_ARGUMENT: + case TARGET_METHOD_INVOCATION_TYPE_ARGUMENT: + case TARGET_CONSTRUCTOR_REFERENCE_TYPE_ARGUMENT: + case TARGET_METHOD_REFERENCE_TYPE_ARGUMENT: + return 3; + default: + throw new ClassFormatException("Invalid type annotation target type: 0x" + Integer.toHexString(targetType)); + } + } + + /** + * Factory method to create a TypeAnnotationEntry from a DataInput. + * + * @param input The input stream. + * @param constantPool The constant pool. + * @param isRuntimeVisible whether the annotation is runtime visible. + * @return The entry. + * @throws IOException Thrown if an I/O error occurs. + */ + public static TypeAnnotationEntry read(final DataInput input, final ConstantPool constantPool, final boolean isRuntimeVisible) throws IOException { + final int targetType = input.readUnsignedByte(); + final int size = targetInfoSize(targetType); + final byte[] targetInfo; + if (size >= 0) { + targetInfo = new byte[size]; + input.readFully(targetInfo); + } else { + final int tableLength = input.readUnsignedShort(); + targetInfo = new byte[2 + tableLength * LOCAL_VARIABLE_RANGE_SIZE]; + targetInfo[0] = (byte) (tableLength >>> 8); + targetInfo[1] = (byte) tableLength; + input.readFully(targetInfo, 2, targetInfo.length - 2); + } + final byte[] typePath = new byte[input.readUnsignedByte() * 2]; + input.readFully(typePath); + return new TypeAnnotationEntry(targetType, targetInfo, typePath, AnnotationEntry.read(input, constantPool, isRuntimeVisible)); + } + + private final int targetType; + + private final byte[] targetInfo; + + private final byte[] typePath; + + private final AnnotationEntry annotationEntry; + + /** + * Constructs a TypeAnnotationEntry. + * + * @param targetType The target type, one of the {@code TARGET_*} constants. + * @param targetInfo The target info, as it is stored in the class file. + * @param typePath The type path, as it is stored in the class file without its length: a pair of bytes (path kind, type argument index) per step. + * @param annotationEntry The annotation. + */ + public TypeAnnotationEntry(final int targetType, final byte[] targetInfo, final byte[] typePath, final AnnotationEntry annotationEntry) { + this.targetType = targetType; + this.targetInfo = targetInfo.clone(); + this.typePath = typePath.clone(); + this.annotationEntry = annotationEntry; + } + + /** + * Writes this entry to a DataOutputStream. + * + * @param dos The data output stream. + * @throws IOException Thrown if an I/O error occurs. + */ + public void dump(final DataOutputStream dos) throws IOException { + dos.writeByte(targetType); + dos.write(targetInfo); + dos.writeByte(Args.requireU1(typePath.length / 2, "typePath.length")); + dos.write(typePath); + annotationEntry.dump(dos); + } + + /** + * Gets the annotation. + * + * @return The annotation. + */ + public AnnotationEntry getAnnotationEntry() { + return annotationEntry; + } + + /** + * Gets the target info, which depends on the target type (JVMS table 4.7.20-A): for instance the index of the type parameter for + * {@link #TARGET_CLASS_TYPE_PARAMETER}, the index of the type parameter then the index of the bound for {@link #TARGET_CLASS_TYPE_PARAMETER_BOUND}, or the + * index of the parameter for {@link #TARGET_METHOD_FORMAL_PARAMETER}. + * + * @return A copy of the target info, as it is stored in the class file. + */ + public byte[] getTargetInfo() { + return targetInfo.clone(); + } + + /** + * Gets the kind of program element the annotation applies to. + * + * @return One of the {@code TARGET_*} constants. + */ + public int getTargetType() { + return targetType; + } + + /** + * Gets the number of steps of the type path, which is empty when the annotation applies to the whole type of the target. + * + * @return The number of steps of the type path. + */ + public int getTypePathLength() { + return typePath.length / 2; + } + + /** + * Gets the kind of a step of the type path: 0 deeper in an array type, 1 deeper in a nested type, 2 on the bound of a wildcard type argument, 3 on a type + * argument of a parameterized type. + * + * @param step Index of the step, from 0 to {@link #getTypePathLength()} excluded. + * @return The kind of the step. + */ + public int getTypePathKind(final int step) { + return typePath[step * 2] & 0xFF; + } + + /** + * Gets the index of the type argument for a step of the type path of kind 3, else 0. + * + * @param step Index of the step, from 0 to {@link #getTypePathLength()} excluded. + * @return The index of the type argument. + */ + public int getTypePathArgumentIndex(final int step) { + return typePath[step * 2 + 1] & 0xFF; + } + + @Override + public String toString() { + return "TypeAnnotationEntry(target=0x" + Integer.toHexString(targetType) + ", info=" + Arrays.toString(targetInfo) + ", path=" + Arrays.toString(typePath) + + ", " + annotationEntry + ")"; + } +} diff --git a/src/main/java/org/apache/bcel/classfile/TypeAnnotations.java b/src/main/java/org/apache/bcel/classfile/TypeAnnotations.java new file mode 100644 index 0000000000..ca2f852125 --- /dev/null +++ b/src/main/java/org/apache/bcel/classfile/TypeAnnotations.java @@ -0,0 +1,157 @@ +/* + * 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 java.io.DataInput; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.Iterator; +import java.util.stream.Stream; + +import org.apache.bcel.Const; +import org.apache.bcel.util.Args; + +/** + * Base class for the type annotation attributes, {@link RuntimeVisibleTypeAnnotations} and {@link RuntimeInvisibleTypeAnnotations}: the annotations written on + * the types used in a declaration, such as {@code List<@Nullable String>} or {@code }, which the attributes of the declaration + * annotations do not hold. + * + * @see JVMS 4.7.20 The RuntimeVisibleTypeAnnotations attribute + * @since 6.14.0 + */ +public abstract class TypeAnnotations extends Attribute implements Iterable { + + private TypeAnnotationEntry[] typeAnnotationTable; + private final boolean isRuntimeVisible; + + /** + * Constructs an instance. + * + * @param annotationType The subclass type of the attribute. + * @param nameIndex Index pointing to the name of the attribute. + * @param length Content length in bytes. + * @param typeAnnotationTable The actual type annotations. + * @param constantPool Array of constants. + * @param isRuntimeVisible whether the annotations are visible at runtime. + */ + public TypeAnnotations(final byte annotationType, final int nameIndex, final int length, final TypeAnnotationEntry[] typeAnnotationTable, + final ConstantPool constantPool, final boolean isRuntimeVisible) { + super(annotationType, nameIndex, length, constantPool); + setTypeAnnotationTable(typeAnnotationTable); + this.isRuntimeVisible = isRuntimeVisible; + } + + /** + * Constructs an instance. + * + * @param annotationType The subclass type of the attribute. + * @param nameIndex Index pointing to the name of the attribute. + * @param length Content length in bytes. + * @param input Input stream. + * @param constantPool Array of constants. + * @param isRuntimeVisible whether the annotations are visible at runtime. + * @throws IOException Thrown if an I/O error occurs. + */ + TypeAnnotations(final byte annotationType, final int nameIndex, final int length, final DataInput input, final ConstantPool constantPool, + final boolean isRuntimeVisible) throws IOException { + this(annotationType, nameIndex, length, (TypeAnnotationEntry[]) null, constantPool, isRuntimeVisible); + final int tableLength = input.readUnsignedShort(); + typeAnnotationTable = new TypeAnnotationEntry[tableLength]; + for (int i = 0; i < tableLength; i++) { + typeAnnotationTable[i] = TypeAnnotationEntry.read(input, constantPool, isRuntimeVisible); + } + } + + @Override + public void accept(final Visitor v) { + v.visitTypeAnnotations(this); + } + + /** + * Gets the number of type annotations. + * + * @return The number of type annotations. + */ + public final int getNumTypeAnnotations() { + return typeAnnotationTable.length; + } + + /** + * Gets the type annotations. + * + * @return The type annotations. + */ + public TypeAnnotationEntry[] getTypeAnnotationEntries() { + return typeAnnotationTable; + } + + /** + * Tests whether the annotations are visible at runtime. + * + * @return true if the annotations are runtime visible. + */ + public boolean isRuntimeVisible() { + return isRuntimeVisible; + } + + @Override + public Iterator iterator() { + return Stream.of(typeAnnotationTable).iterator(); + } + + /** + * Sets the type annotations. + * + * @param typeAnnotationTable The type annotations. + */ + public final void setTypeAnnotationTable(final TypeAnnotationEntry[] typeAnnotationTable) { + this.typeAnnotationTable = typeAnnotationTable != null ? typeAnnotationTable : TypeAnnotationEntry.EMPTY_ARRAY; + } + + /** + * Converts to a String representation. + * + * @return String representation. + */ + @Override + public final String toString() { + final StringBuilder buf = new StringBuilder(Const.getAttributeName(getTag())); + buf.append(":\n"); + for (int i = 0; i < typeAnnotationTable.length; i++) { + buf.append(" ").append(typeAnnotationTable[i]); + if (i < typeAnnotationTable.length - 1) { + buf.append('\n'); + } + } + return buf.toString(); + } + + /** + * Writes the type annotations to a DataOutputStream. + * + * @param dos The data output stream. + * @throws IOException Thrown if an I/O error occurs. + */ + protected void writeTypeAnnotations(final DataOutputStream dos) throws IOException { + dos.writeShort(Args.requireU2(typeAnnotationTable.length, "typeAnnotationTable.length")); + for (final TypeAnnotationEntry element : typeAnnotationTable) { + element.dump(dos); + } + } +} diff --git a/src/main/java/org/apache/bcel/classfile/Visitor.java b/src/main/java/org/apache/bcel/classfile/Visitor.java index 555bbfc3ff..2e64412028 100644 --- a/src/main/java/org/apache/bcel/classfile/Visitor.java +++ b/src/main/java/org/apache/bcel/classfile/Visitor.java @@ -437,6 +437,16 @@ default void visitPermittedSubclasses(final PermittedSubclasses obj) { // empty } + /** + * Visits a RuntimeVisibleTypeAnnotations or RuntimeInvisibleTypeAnnotations attribute. + * + * @param obj The attribute. + * @since 6.14.0 + */ + default void visitTypeAnnotations(final TypeAnnotations obj) { + // empty + } + /** * Visits a {@link Record} object. * diff --git a/src/main/java/org/apache/bcel/verifier/statics/StringRepresentation.java b/src/main/java/org/apache/bcel/verifier/statics/StringRepresentation.java index 0ba7801d66..8819b4f03b 100644 --- a/src/main/java/org/apache/bcel/verifier/statics/StringRepresentation.java +++ b/src/main/java/org/apache/bcel/verifier/statics/StringRepresentation.java @@ -69,6 +69,7 @@ import org.apache.bcel.classfile.StackMap; import org.apache.bcel.classfile.StackMapEntry; import org.apache.bcel.classfile.Synthetic; +import org.apache.bcel.classfile.TypeAnnotations; import org.apache.bcel.classfile.Unknown; import org.apache.bcel.verifier.exc.AssertionViolatedException; @@ -412,6 +413,16 @@ public void visitPermittedSubclasses(final PermittedSubclasses obj) { tostring = toString(obj); } + /** + * Visits RuntimeVisibleTypeAnnotations and RuntimeInvisibleTypeAnnotations attributes. + * + * @since 6.14.0 + */ + @Override + public void visitTypeAnnotations(final TypeAnnotations obj) { + tostring = toString(obj); + } + @Override public void visitRecord(final Record obj) { tostring = toString(obj); diff --git a/src/test/java/org/apache/bcel/classfile/TypeAnnotationsTest.java b/src/test/java/org/apache/bcel/classfile/TypeAnnotationsTest.java new file mode 100644 index 0000000000..9413de238a --- /dev/null +++ b/src/test/java/org/apache/bcel/classfile/TypeAnnotationsTest.java @@ -0,0 +1,201 @@ +/* + * 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.assertNull; +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.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import org.apache.bcel.Const; +import org.apache.bcel.verifier.statics.StringRepresentation; +import org.junit.jupiter.api.Test; + +/** + * Tests the type annotation attributes, read from {@code TypeAnnotated.class} (see {@code TypeAnnotated.java}). + */ +class TypeAnnotationsTest { + + private static final String CLASS_FILE = "src/test/resources/typeannotations/TypeAnnotated.class"; + + private static JavaClass parse() throws IOException { + return new ClassParser(CLASS_FILE).parse(); + } + + private static TypeAnnotations typeAnnotationsOf(final FieldOrMethod member, final byte tag) { + return (TypeAnnotations) Stream.of(member.getAttributes()).filter(a -> a.getTag() == tag).findFirst().orElse(null); + } + + private static Method method(final JavaClass clazz, final String name) { + return Stream.of(clazz.getMethods()).filter(m -> m.getName().equals(name)).findFirst().orElseThrow(AssertionError::new); + } + + private static Field field(final JavaClass clazz, final String name) { + return Stream.of(clazz.getFields()).filter(f -> f.getName().equals(name)).findFirst().orElseThrow(AssertionError::new); + } + + @Test + void readsTheTypeAnnotationOfAField() throws IOException { + final TypeAnnotations annotations = typeAnnotationsOf(field(parse(), "field"), Const.ATTR_RUNTIME_VISIBLE_TYPE_ANNOTATIONS); + assertNotNull(annotations); + assertTrue(annotations instanceof RuntimeVisibleTypeAnnotations); + assertTrue(annotations.isRuntimeVisible()); + assertEquals(1, annotations.getNumTypeAnnotations()); + final TypeAnnotationEntry entry = annotations.getTypeAnnotationEntries()[0]; + assertEquals(TypeAnnotationEntry.TARGET_FIELD, entry.getTargetType()); + assertEquals(0, entry.getTargetInfo().length); + assertEquals(0, entry.getTypePathLength()); + assertEquals("Ltypeannotations/Visible;", entry.getAnnotationEntry().getAnnotationType()); + } + + @Test + void readsTheTypePathOfATypeArgument() throws IOException { + final TypeAnnotations annotations = typeAnnotationsOf(field(parse(), "items"), Const.ATTR_RUNTIME_VISIBLE_TYPE_ANNOTATIONS); + final TypeAnnotationEntry entry = annotations.getTypeAnnotationEntries()[0]; + assertEquals(TypeAnnotationEntry.TARGET_FIELD, entry.getTargetType()); + assertEquals(1, entry.getTypePathLength()); + // a type argument of the parameterized type, the first one + assertEquals(3, entry.getTypePathKind(0)); + assertEquals(0, entry.getTypePathArgumentIndex(0)); + } + + @Test + void readsTheInvisibleAttribute() throws IOException { + final Field hidden = field(parse(), "hidden"); + assertNull(typeAnnotationsOf(hidden, Const.ATTR_RUNTIME_VISIBLE_TYPE_ANNOTATIONS)); + final TypeAnnotations annotations = typeAnnotationsOf(hidden, Const.ATTR_RUNTIME_INVISIBLE_TYPE_ANNOTATIONS); + assertTrue(annotations instanceof RuntimeInvisibleTypeAnnotations); + assertFalse(annotations.isRuntimeVisible()); + assertEquals("Ltypeannotations/Invisible;", annotations.getTypeAnnotationEntries()[0].getAnnotationEntry().getAnnotationType()); + } + + @Test + void readsTheTargetsOfAMethod() throws IOException { + final TypeAnnotations annotations = typeAnnotationsOf(method(parse(), "find"), Const.ATTR_RUNTIME_VISIBLE_TYPE_ANNOTATIONS); + final List targets = new ArrayList<>(); + for (final TypeAnnotationEntry entry : annotations) { + targets.add(entry.getTargetType()); + if (entry.getTargetType() == TypeAnnotationEntry.TARGET_METHOD_FORMAL_PARAMETER) { + // the index of the parameter + assertArrayEquals(new byte[] {0}, entry.getTargetInfo()); + } + } + assertTrue(targets.contains(TypeAnnotationEntry.TARGET_METHOD_RETURN)); + assertTrue(targets.contains(TypeAnnotationEntry.TARGET_METHOD_FORMAL_PARAMETER)); + } + + @Test + void readsTheBoundOfATypeParameterOfTheClass() throws IOException { + final JavaClass clazz = parse(); + final TypeAnnotations annotations = (TypeAnnotations) Stream.of(clazz.getAttributes()) + .filter(a -> a.getTag() == Const.ATTR_RUNTIME_VISIBLE_TYPE_ANNOTATIONS).findFirst().orElse(null); + assertNotNull(annotations); + final TypeAnnotationEntry entry = annotations.getTypeAnnotationEntries()[0]; + assertEquals(TypeAnnotationEntry.TARGET_CLASS_TYPE_PARAMETER_BOUND, entry.getTargetType()); + // the first type parameter, its first bound + assertArrayEquals(new byte[] {0, 0}, entry.getTargetInfo()); + } + + @Test + void dumpsWhatItReads() throws IOException { + final JavaClass clazz = parse(); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (DataOutputStream dos = new DataOutputStream(out)) { + clazz.dump(dos); + } + final JavaClass reread = new ClassParser(new ByteArrayInputStream(out.toByteArray()), CLASS_FILE).parse(); + for (final String name : new String[] {"field", "items", "hidden"}) { + final Field original = field(clazz, name); + final Field copy = field(reread, name); + assertEquals(original.getAttributes().length, copy.getAttributes().length); + for (int i = 0; i < original.getAttributes().length; i++) { + assertEquals(original.getAttributes()[i].toString(), copy.getAttributes()[i].toString()); + } + } + } + + @Test + void copiesTheAttribute() throws IOException { + final JavaClass clazz = parse(); + final TypeAnnotations annotations = typeAnnotationsOf(field(clazz, "field"), Const.ATTR_RUNTIME_VISIBLE_TYPE_ANNOTATIONS); + assertEquals(annotations.toString(), annotations.copy(clazz.getConstantPool()).toString()); + } + + @Test + void isVisitedAndRepresented() throws IOException { + final JavaClass clazz = parse(); + final int[] visited = new int[1]; + new DescendingVisitor(clazz, new EmptyVisitor() { + @Override + public void visitTypeAnnotations(final TypeAnnotations obj) { + visited[0]++; + } + }).visit(); + // the class, three fields and one method have one + assertEquals(5, visited[0]); + final StringRepresentation representation = new StringRepresentation(typeAnnotationsOf(field(clazz, "field"), Const.ATTR_RUNTIME_VISIBLE_TYPE_ANNOTATIONS)); + assertTrue(representation.toString().contains("RuntimeVisibleTypeAnnotations"), representation.toString()); + } + + @Test + void readsTheTableOfALocalVariableTarget() throws IOException { + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (DataOutputStream dos = new DataOutputStream(out)) { + dos.writeByte(TypeAnnotationEntry.TARGET_LOCAL_VARIABLE); + dos.writeShort(2); + for (int i = 0; i < 2; i++) { + dos.writeShort(1 + i); + dos.writeShort(5); + dos.writeShort(i); + } + dos.writeByte(0); + dos.writeShort(1); + dos.writeShort(0); + } + final ConstantPool constantPool = parse().getConstantPool(); + final TypeAnnotationEntry entry = TypeAnnotationEntry.read(new DataInputStream(new ByteArrayInputStream(out.toByteArray())), constantPool, true); + assertEquals(TypeAnnotationEntry.TARGET_LOCAL_VARIABLE, entry.getTargetType()); + assertEquals(2 + 2 * 6, entry.getTargetInfo().length); + final ByteArrayOutputStream dumped = new ByteArrayOutputStream(); + try (DataOutputStream dos = new DataOutputStream(dumped)) { + entry.dump(dos); + } + assertArrayEquals(out.toByteArray(), dumped.toByteArray()); + } + + @Test + void rejectsAnUndefinedTargetType() throws IOException { + final ConstantPool constantPool = parse().getConstantPool(); + final DataInputStream input = new DataInputStream(new ByteArrayInputStream(new byte[] {0x7F, 0, 0, 0})); + assertThrows(ClassFormatException.class, () -> TypeAnnotationEntry.read(input, constantPool, true)); + } +} diff --git a/src/test/resources/typeannotations/TypeAnnotated.class b/src/test/resources/typeannotations/TypeAnnotated.class new file mode 100644 index 0000000000000000000000000000000000000000..f28e8121bc1d6e5f17461ff25631c62a2a13fdda GIT binary patch literal 709 zcmZ`$O-sW-6r4>=tM#L`e%2x?2v!gF;z>(E1VJbYw)CDhYh7u#l5Pb5m7W9-{s4cJ zIGY3w77y9?_RY+DleeE=?;ik8P%XfLo5#!oJmdsQ5B`&1ANqPwzv(^5z7fbZlvbuG z;MNYidCUrIo99RAYdto;QDZ%+cX&Tf?lKVY?vxxdmRrBw+9p(ba7OWMq>UQMu9~Rc zP-djIl|ZHSw|mN@)>B4~$SEa_Bcq1(mYNtsb8R)yzKKF9u>S{cO#5h3lDk(yAhp0w zYUxTp{bQ;+W$K5^Ml%=VfMbg-rR8-r>dCO<^VbO6Da}0`)G`K=wVquaW@fzNbu!YRVJROC?u25fE>?N-nJP9qj&TM@ybMCji-eX6V|aoiKT9_X015lg=dzx e8;>r+A&rCA3ETN@>TDAgD`NxepAMH9EaMwe)}Vp_ literal 0 HcmV?d00001 diff --git a/src/test/resources/typeannotations/TypeAnnotated.java b/src/test/resources/typeannotations/TypeAnnotated.java new file mode 100644 index 0000000000..07c19507f3 --- /dev/null +++ b/src/test/resources/typeannotations/TypeAnnotated.java @@ -0,0 +1,34 @@ +package typeannotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.List; + +// Compiled with: javac --release 8 -d . TypeAnnotated.java (only TypeAnnotated.class is used by the tests) +@Target(ElementType.TYPE_USE) +@Retention(RetentionPolicy.RUNTIME) +@interface Visible { +} + +@Target(ElementType.TYPE_USE) +@Retention(RetentionPolicy.CLASS) +@interface Invisible { +} + +public class TypeAnnotated { + + @Visible + String field; + + List<@Visible String> items; + + @Invisible + String hidden; + + @Visible + String find(@Visible Object key, int count) { + return null; + } +} From 601010faa551bd41147324cd39828ffa8679a6ed Mon Sep 17 00:00:00 2001 From: Nicolas Baumann Date: Thu, 22 Oct 2026 14:13:31 +0200 Subject: [PATCH 2/2] Deep copy of the type annotation attributes, traversal of their annotations, validation of TypeAnnotationEntry, Javadoc - copy(ConstantPool) of RuntimeVisibleTypeAnnotations and RuntimeInvisibleTypeAnnotations copies the entries and binds them and the attribute to the given constant pool. - DescendingVisitor.visitTypeAnnotations visits the AnnotationEntry of each entry. - The TypeAnnotationEntry constructor rejects a type path which is not a sequence of pairs of bytes of at most 255 steps, and a target info whose size does not match its target type. - Javadoc: empty type path, TARGET_CLASS_EXTENDS, scope of the attributes. Co-Authored-By: Claude Sonnet 5.5 --- .../bcel/classfile/DescendingVisitor.java | 1 + .../RuntimeInvisibleTypeAnnotations.java | 10 +- .../RuntimeVisibleTypeAnnotations.java | 10 +- .../bcel/classfile/TypeAnnotationEntry.java | 53 +++++++++- .../bcel/classfile/TypeAnnotations.java | 16 ++- .../bcel/classfile/TypeAnnotationsTest.java | 97 +++++++++++++++++-- 6 files changed, 167 insertions(+), 20 deletions(-) diff --git a/src/main/java/org/apache/bcel/classfile/DescendingVisitor.java b/src/main/java/org/apache/bcel/classfile/DescendingVisitor.java index 43a48ad4e2..b1e49a484b 100644 --- a/src/main/java/org/apache/bcel/classfile/DescendingVisitor.java +++ b/src/main/java/org/apache/bcel/classfile/DescendingVisitor.java @@ -542,6 +542,7 @@ public void visitRecordComponent(final RecordComponentInfo recordComponentInfo) public void visitTypeAnnotations(final TypeAnnotations obj) { stack.push(obj); obj.accept(visitor); + accept(Stream.of(obj.getTypeAnnotationEntries()).map(TypeAnnotationEntry::getAnnotationEntry).toArray(AnnotationEntry[]::new)); stack.pop(); } diff --git a/src/main/java/org/apache/bcel/classfile/RuntimeInvisibleTypeAnnotations.java b/src/main/java/org/apache/bcel/classfile/RuntimeInvisibleTypeAnnotations.java index d3facc7fbe..f772ae41d0 100644 --- a/src/main/java/org/apache/bcel/classfile/RuntimeInvisibleTypeAnnotations.java +++ b/src/main/java/org/apache/bcel/classfile/RuntimeInvisibleTypeAnnotations.java @@ -25,7 +25,7 @@ import org.apache.bcel.Const; /** - * The type annotations of a declaration, recorded in the class file but not visible at runtime. + * The type annotations of a class, field, method, record component or {@link Code} attribute, recorded in the class file but not visible at runtime. * * @since 6.14.0 */ @@ -45,13 +45,17 @@ public RuntimeInvisibleTypeAnnotations(final int nameIndex, final int length, fi } /** - * Creates a deep copy of this attribute. + * Creates a deep copy of this attribute, whose type annotations are independent of this one's and refer to the given constant pool. * + * @param constantPool The constant pool of the copy. * @return deep copy of this attribute. */ @Override public Attribute copy(final ConstantPool constantPool) { - return (Attribute) clone(); + final RuntimeInvisibleTypeAnnotations c = (RuntimeInvisibleTypeAnnotations) clone(); + c.setConstantPool(constantPool); + c.setTypeAnnotationTable(copyTypeAnnotations(constantPool)); + return c; } @Override diff --git a/src/main/java/org/apache/bcel/classfile/RuntimeVisibleTypeAnnotations.java b/src/main/java/org/apache/bcel/classfile/RuntimeVisibleTypeAnnotations.java index ebbcabe6fb..7db33f0127 100644 --- a/src/main/java/org/apache/bcel/classfile/RuntimeVisibleTypeAnnotations.java +++ b/src/main/java/org/apache/bcel/classfile/RuntimeVisibleTypeAnnotations.java @@ -25,7 +25,7 @@ import org.apache.bcel.Const; /** - * The type annotations of a declaration, provided to the JVM and visible at runtime. + * The type annotations of a class, field, method, record component or {@link Code} attribute, provided to the JVM and visible at runtime. * * @since 6.14.0 */ @@ -45,13 +45,17 @@ public RuntimeVisibleTypeAnnotations(final int nameIndex, final int length, fina } /** - * Creates a deep copy of this attribute. + * Creates a deep copy of this attribute, whose type annotations are independent of this one's and refer to the given constant pool. * + * @param constantPool The constant pool of the copy. * @return deep copy of this attribute. */ @Override public Attribute copy(final ConstantPool constantPool) { - return (Attribute) clone(); + final RuntimeVisibleTypeAnnotations c = (RuntimeVisibleTypeAnnotations) clone(); + c.setConstantPool(constantPool); + c.setTypeAnnotationTable(copyTypeAnnotations(constantPool)); + return c; } @Override diff --git a/src/main/java/org/apache/bcel/classfile/TypeAnnotationEntry.java b/src/main/java/org/apache/bcel/classfile/TypeAnnotationEntry.java index cc705c10ad..f4145b3d82 100644 --- a/src/main/java/org/apache/bcel/classfile/TypeAnnotationEntry.java +++ b/src/main/java/org/apache/bcel/classfile/TypeAnnotationEntry.java @@ -18,9 +18,13 @@ */ package org.apache.bcel.classfile; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.DataInput; +import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; +import java.io.UncheckedIOException; import java.util.Arrays; import org.apache.bcel.util.Args; @@ -40,7 +44,7 @@ public class TypeAnnotationEntry { /** Target type: type parameter declaration of a generic method or constructor. */ public static final int TARGET_METHOD_TYPE_PARAMETER = 0x01; - /** Target type: type in an extends or implements clause of a class declaration. */ + /** Target type: type in an extends or implements clause of a class or interface declaration. */ public static final int TARGET_CLASS_EXTENDS = 0x10; /** Target type: bound of a type parameter of a generic class or interface. */ @@ -105,6 +109,9 @@ public class TypeAnnotationEntry { */ public static final TypeAnnotationEntry[] EMPTY_ARRAY = {}; + /** Maximum number of steps of a type path, which its length of one byte limits. */ + private static final int MAX_TYPE_PATH_LENGTH = 255; + /** Number of bytes of a {@code localvar_target} entry: start_pc, length and index. */ private static final int LOCAL_VARIABLE_RANGE_SIZE = 6; @@ -149,6 +156,19 @@ private static int targetInfoSize(final int targetType) { } } + private static void checkTargetInfo(final int targetType, final byte[] targetInfo) { + final int size = targetInfoSize(targetType); + if (size >= 0) { + if (targetInfo.length != size) { + throw new IllegalArgumentException("The target info of the target type 0x" + Integer.toHexString(targetType) + " must have " + size + + " bytes, not " + targetInfo.length); + } + } else if (targetInfo.length < 2 || targetInfo.length != 2 + ((targetInfo[0] & 0xFF) << 8 | targetInfo[1] & 0xFF) * LOCAL_VARIABLE_RANGE_SIZE) { + throw new IllegalArgumentException("The target info of the target type 0x" + Integer.toHexString(targetType) + + " must be a length followed by that many ranges of 6 bytes"); + } + } + /** * Factory method to create a TypeAnnotationEntry from a DataInput. * @@ -192,14 +212,41 @@ public static TypeAnnotationEntry read(final DataInput input, final ConstantPool * @param targetInfo The target info, as it is stored in the class file. * @param typePath The type path, as it is stored in the class file without its length: a pair of bytes (path kind, type argument index) per step. * @param annotationEntry The annotation. + * @throws ClassFormatException if the target type is not defined. + * @throws IllegalArgumentException if the target info does not have the size its target type requires, or if the type path is not a sequence of pairs + * of bytes of at most 255 steps. */ public TypeAnnotationEntry(final int targetType, final byte[] targetInfo, final byte[] typePath, final AnnotationEntry annotationEntry) { + checkTargetInfo(targetType, targetInfo); + if (typePath.length % 2 != 0 || typePath.length > MAX_TYPE_PATH_LENGTH * 2) { + throw new IllegalArgumentException("The type path must be at most " + MAX_TYPE_PATH_LENGTH + " pairs of bytes (kind, argument index), not " + + typePath.length + " bytes"); + } this.targetType = targetType; this.targetInfo = targetInfo.clone(); this.typePath = typePath.clone(); this.annotationEntry = annotationEntry; } + /** + * Creates a deep copy of this entry, whose annotation is independent of this one and refers to the given constant pool. The indexes it holds are kept, as they + * are in {@link Attribute#copy(ConstantPool)}: the constant pool must have the same constants as the one of this entry at these indexes. + * + * @param constantPool The constant pool of the copy. + * @return A deep copy of this entry. + */ + public TypeAnnotationEntry copy(final ConstantPool constantPool) { + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + try { + try (DataOutputStream dos = new DataOutputStream(out)) { + dump(dos); + } + return read(new DataInputStream(new ByteArrayInputStream(out.toByteArray())), constantPool, annotationEntry.isRuntimeVisible()); + } catch (final IOException e) { + throw new UncheckedIOException(e); + } + } + /** * Writes this entry to a DataOutputStream. * @@ -244,7 +291,9 @@ public int getTargetType() { } /** - * Gets the number of steps of the type path, which is empty when the annotation applies to the whole type of the target. + * Gets the number of steps of the type path, which leads from the type of the target to the part of it that is annotated. The path is empty when the + * annotation applies to the outermost part of that type where an annotation is allowed: for a nested type, the outermost class ({@code @A Outer.Inner}), + * while {@code Outer.@A Inner} needs a step. * * @return The number of steps of the type path. */ diff --git a/src/main/java/org/apache/bcel/classfile/TypeAnnotations.java b/src/main/java/org/apache/bcel/classfile/TypeAnnotations.java index ca2f852125..aa13deaff1 100644 --- a/src/main/java/org/apache/bcel/classfile/TypeAnnotations.java +++ b/src/main/java/org/apache/bcel/classfile/TypeAnnotations.java @@ -29,8 +29,10 @@ /** * Base class for the type annotation attributes, {@link RuntimeVisibleTypeAnnotations} and {@link RuntimeInvisibleTypeAnnotations}: the annotations written on - * the types used in a declaration, such as {@code List<@Nullable String>} or {@code }, which the attributes of the declaration - * annotations do not hold. + * the types used in a declaration (such as {@code List<@Nullable String>}, the supertypes of a class, a {@code throws} clause, or the declaration and the + * bounds of a type parameter as in {@code }) or in the code of a method (such as a cast, an {@code instanceof}, a {@code new} + * expression or a local variable), which the attributes of the declaration annotations do not hold. They are attributes of a class, a field, a method, a + * record component or a {@link Code} attribute. * * @see JVMS 4.7.20 The RuntimeVisibleTypeAnnotations attribute * @since 6.14.0 @@ -83,6 +85,16 @@ public void accept(final Visitor v) { v.visitTypeAnnotations(this); } + /** + * Creates deep copies of the type annotations, which refer to the given constant pool. + * + * @param constantPool The constant pool of the copies. + * @return The copies. + */ + protected final TypeAnnotationEntry[] copyTypeAnnotations(final ConstantPool constantPool) { + return Stream.of(typeAnnotationTable).map(e -> e.copy(constantPool)).toArray(TypeAnnotationEntry[]::new); + } + /** * Gets the number of type annotations. * diff --git a/src/test/java/org/apache/bcel/classfile/TypeAnnotationsTest.java b/src/test/java/org/apache/bcel/classfile/TypeAnnotationsTest.java index 9413de238a..53f3031c16 100644 --- a/src/test/java/org/apache/bcel/classfile/TypeAnnotationsTest.java +++ b/src/test/java/org/apache/bcel/classfile/TypeAnnotationsTest.java @@ -23,7 +23,9 @@ 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.assertNotSame; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -144,24 +146,70 @@ void dumpsWhatItReads() throws IOException { } @Test - void copiesTheAttribute() throws IOException { + void copiesTheAttributeWithItsOwnAnnotationsAndTheGivenConstantPool() throws IOException { final JavaClass clazz = parse(); - final TypeAnnotations annotations = typeAnnotationsOf(field(clazz, "field"), Const.ATTR_RUNTIME_VISIBLE_TYPE_ANNOTATIONS); - assertEquals(annotations.toString(), annotations.copy(clazz.getConstantPool()).toString()); + final ConstantPool otherPool = clazz.getConstantPool().copy(); + final TypeAnnotations original = typeAnnotationsOf(field(clazz, "field"), Const.ATTR_RUNTIME_VISIBLE_TYPE_ANNOTATIONS); + final TypeAnnotations copy = (TypeAnnotations) original.copy(otherPool); + assertEquals(original.toString(), copy.toString()); + assertTrue(copy instanceof RuntimeVisibleTypeAnnotations); + assertTrue(copy.isRuntimeVisible()); + // the copy is bound to the given pool, the original to its own + assertSame(otherPool, copy.getConstantPool()); + assertSame(otherPool, copy.getTypeAnnotationEntries()[0].getAnnotationEntry().getConstantPool()); + assertSame(clazz.getConstantPool(), original.getConstantPool()); + assertSame(clazz.getConstantPool(), original.getTypeAnnotationEntries()[0].getAnnotationEntry().getConstantPool()); + // nothing is shared: changing the copy leaves the original unchanged + assertNotSame(original.getTypeAnnotationEntries(), copy.getTypeAnnotationEntries()); + assertNotSame(original.getTypeAnnotationEntries()[0], copy.getTypeAnnotationEntries()[0]); + assertNotSame(original.getTypeAnnotationEntries()[0].getAnnotationEntry(), copy.getTypeAnnotationEntries()[0].getAnnotationEntry()); + copy.getTypeAnnotationEntries()[0].getAnnotationEntry().addElementNameValuePair( + new ElementValuePair(1, new SimpleElementValue(ElementValue.PRIMITIVE_INT, 1, otherPool), otherPool)); + copy.setTypeAnnotationTable(null); + assertEquals(0, original.getTypeAnnotationEntries()[0].getAnnotationEntry().getNumElementValuePairs()); + assertEquals(1, original.getNumTypeAnnotations()); } @Test - void isVisitedAndRepresented() throws IOException { + void copiesTheInvisibleAttribute() throws IOException { final JavaClass clazz = parse(); - final int[] visited = new int[1]; - new DescendingVisitor(clazz, new EmptyVisitor() { + final TypeAnnotations original = typeAnnotationsOf(field(clazz, "hidden"), Const.ATTR_RUNTIME_INVISIBLE_TYPE_ANNOTATIONS); + final TypeAnnotations copy = (TypeAnnotations) original.copy(clazz.getConstantPool().copy()); + assertTrue(copy instanceof RuntimeInvisibleTypeAnnotations); + assertFalse(copy.isRuntimeVisible()); + assertFalse(copy.getTypeAnnotationEntries()[0].getAnnotationEntry().isRuntimeVisible()); + assertEquals(original.toString(), copy.toString()); + } + + @Test + void isVisitedWithItsAnnotations() throws IOException { + final JavaClass clazz = parse(); + final List events = new ArrayList<>(); + final DescendingVisitor[] descending = new DescendingVisitor[1]; + descending[0] = new DescendingVisitor(clazz, new EmptyVisitor() { + @Override + public void visitAnnotationEntry(final AnnotationEntry obj) { + // the annotation is visited within its attribute + events.add("entry " + obj.getAnnotationType() + " in " + descending[0].predecessor().getClass().getSimpleName()); + } + @Override public void visitTypeAnnotations(final TypeAnnotations obj) { - visited[0]++; + events.add("attribute " + obj.getClass().getSimpleName()); } - }).visit(); - // the class, three fields and one method have one - assertEquals(5, visited[0]); + }); + descending[0].visit(); + // the class, three fields and one method have an attribute, which holds six annotations in all + assertEquals(5, events.stream().filter(e -> e.startsWith("attribute")).count()); + assertEquals(6, events.stream().filter(e -> e.startsWith("entry")).count()); + assertEquals(1, events.stream().filter(e -> e.equals("attribute RuntimeInvisibleTypeAnnotations")).count()); + assertEquals(1, events.stream().filter(e -> e.equals("entry Ltypeannotations/Invisible; in RuntimeInvisibleTypeAnnotations")).count()); + assertEquals(5, events.stream().filter(e -> e.startsWith("entry Ltypeannotations/Visible; in RuntimeVisibleTypeAnnotations")).count()); + } + + @Test + void isRepresented() throws IOException { + final JavaClass clazz = parse(); final StringRepresentation representation = new StringRepresentation(typeAnnotationsOf(field(clazz, "field"), Const.ATTR_RUNTIME_VISIBLE_TYPE_ANNOTATIONS)); assertTrue(representation.toString().contains("RuntimeVisibleTypeAnnotations"), representation.toString()); } @@ -198,4 +246,33 @@ void rejectsAnUndefinedTargetType() throws IOException { final DataInputStream input = new DataInputStream(new ByteArrayInputStream(new byte[] {0x7F, 0, 0, 0})); assertThrows(ClassFormatException.class, () -> TypeAnnotationEntry.read(input, constantPool, true)); } + + @Test + void rejectsAMalformedTypePath() throws IOException { + final AnnotationEntry annotation = typeAnnotationsOf(field(parse(), "field"), Const.ATTR_RUNTIME_VISIBLE_TYPE_ANNOTATIONS).getTypeAnnotationEntries()[0] + .getAnnotationEntry(); + // a path is made of pairs of bytes + assertThrows(IllegalArgumentException.class, () -> new TypeAnnotationEntry(TypeAnnotationEntry.TARGET_FIELD, new byte[0], new byte[1], annotation)); + assertThrows(IllegalArgumentException.class, () -> new TypeAnnotationEntry(TypeAnnotationEntry.TARGET_FIELD, new byte[0], new byte[3], annotation)); + // and has at most 255 steps + assertThrows(IllegalArgumentException.class, () -> new TypeAnnotationEntry(TypeAnnotationEntry.TARGET_FIELD, new byte[0], new byte[256 * 2], annotation)); + assertEquals(255, new TypeAnnotationEntry(TypeAnnotationEntry.TARGET_FIELD, new byte[0], new byte[255 * 2], annotation).getTypePathLength()); + } + + @Test + void rejectsATargetInfoOfTheWrongSize() throws IOException { + final AnnotationEntry annotation = typeAnnotationsOf(field(parse(), "field"), Const.ATTR_RUNTIME_VISIBLE_TYPE_ANNOTATIONS).getTypeAnnotationEntries()[0] + .getAnnotationEntry(); + assertThrows(IllegalArgumentException.class, () -> new TypeAnnotationEntry(TypeAnnotationEntry.TARGET_FIELD, new byte[1], new byte[0], annotation)); + assertThrows(IllegalArgumentException.class, + () -> new TypeAnnotationEntry(TypeAnnotationEntry.TARGET_METHOD_FORMAL_PARAMETER, new byte[0], new byte[0], annotation)); + assertThrows(IllegalArgumentException.class, + () -> new TypeAnnotationEntry(TypeAnnotationEntry.TARGET_CLASS_TYPE_PARAMETER_BOUND, new byte[3], new byte[0], annotation)); + assertThrows(ClassFormatException.class, () -> new TypeAnnotationEntry(0x7F, new byte[0], new byte[0], annotation)); + // a table of two ranges of 6 bytes, announced as one + final byte[] twoRangesAnnouncedAsOne = {0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + assertThrows(IllegalArgumentException.class, + () -> new TypeAnnotationEntry(TypeAnnotationEntry.TARGET_LOCAL_VARIABLE, twoRangesAnnouncedAsOne, new byte[0], annotation)); + assertEquals(2, new TypeAnnotationEntry(TypeAnnotationEntry.TARGET_LOCAL_VARIABLE, new byte[2], new byte[0], annotation).getTargetInfo().length); + } }