diff --git a/src/main/java/org/apache/bcel/Const.java b/src/main/java/org/apache/bcel/Const.java index 24445b2e39..60336ef0e5 100644 --- a/src/main/java/org/apache/bcel/Const.java +++ b/src/main/java/org/apache/bcel/Const.java @@ -3168,13 +3168,21 @@ public final class Const { /** Attribute constant for Record. */ public static final byte ATTR_RECORD = 27; + /** + * Attribute constant for PermittedSubclasses. + * + * @since 6.13.0 + */ + public static final byte ATTR_PERMITTED_SUBCLASSES = 28; + /** Count of known attributes. */ - public static final short KNOWN_ATTRIBUTES = 28; // count of attributes + public static final short KNOWN_ATTRIBUTES = 29; // 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" }; + "StackMapTable", "BootstrapMethods", "MethodParameters", "Module", "ModulePackages", "ModuleMainClass", "NestHost", "NestMembers", "Record", + "PermittedSubclasses" }; /** * 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 d78c5f562b..90246fa422 100644 --- a/src/main/java/org/apache/bcel/classfile/Attribute.java +++ b/src/main/java/org/apache/bcel/classfile/Attribute.java @@ -199,6 +199,8 @@ public static Attribute readAttribute(final DataInput dataInput, final ConstantP return new NestMembers(nameIndex, length, dataInput, constantPool); case Const.ATTR_RECORD: return new Record(nameIndex, length, dataInput, constantPool); + case Const.ATTR_PERMITTED_SUBCLASSES: + return new PermittedSubclasses(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 45180804d9..37f72df782 100644 --- a/src/main/java/org/apache/bcel/classfile/DescendingVisitor.java +++ b/src/main/java/org/apache/bcel/classfile/DescendingVisitor.java @@ -496,6 +496,14 @@ public void visitNestMembers(final NestMembers obj) { stack.pop(); } + /** @since 6.13.0 */ + @Override + public void visitPermittedSubclasses(final PermittedSubclasses obj) { + stack.push(obj); + obj.accept(visitor); + stack.pop(); + } + /** * @since 6.0 */ diff --git a/src/main/java/org/apache/bcel/classfile/EmptyVisitor.java b/src/main/java/org/apache/bcel/classfile/EmptyVisitor.java index 7a16e9ba1e..7a5165bdaf 100644 --- a/src/main/java/org/apache/bcel/classfile/EmptyVisitor.java +++ b/src/main/java/org/apache/bcel/classfile/EmptyVisitor.java @@ -284,6 +284,11 @@ public void visitNestHost(final NestHost obj) { public void visitNestMembers(final NestMembers obj) { } + /** @since 6.13.0 */ + @Override + public void visitPermittedSubclasses(final PermittedSubclasses obj) { + } + /** * @since 6.0 */ diff --git a/src/main/java/org/apache/bcel/classfile/PermittedSubclasses.java b/src/main/java/org/apache/bcel/classfile/PermittedSubclasses.java new file mode 100644 index 0000000000..b5e48f8c65 --- /dev/null +++ b/src/main/java/org/apache/bcel/classfile/PermittedSubclasses.java @@ -0,0 +1,177 @@ +/* + * 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.Const; +import org.apache.bcel.util.Args; +import org.apache.commons.lang3.ArrayUtils; + +/** + * This class is derived from Attribute and records the classes and interfaces that are permitted to extend or + * implement the current class or interface. There may be at most one PermittedSubclasses attribute in a ClassFile + * structure. + * + * @see Attribute + * @since 6.13.0 + */ +public final class PermittedSubclasses extends Attribute { + + private int[] classes; + + /** + * Constructs object from input stream. + * + * @param nameIndex Index in constant pool. + * @param length Content length in bytes. + * @param dataInput Input stream. + * @param constantPool Array of constants. + * @throws IOException if an I/O error occurs. + */ + PermittedSubclasses(final int nameIndex, final int length, final DataInput dataInput, final ConstantPool constantPool) throws IOException { + this(nameIndex, length, (int[]) null, constantPool); + classes = ClassParser.readU2U2Table(dataInput); + } + + /** + * Constructs object from table of class indices in constant pool. + * + * @param nameIndex Index in constant pool. + * @param length Content length in bytes. + * @param classes Table of indices in constant pool. + * @param constantPool Array of constants. + */ + public PermittedSubclasses(final int nameIndex, final int length, final int[] classes, final ConstantPool constantPool) { + super(Const.ATTR_PERMITTED_SUBCLASSES, nameIndex, length, constantPool); + this.classes = ArrayUtils.nullToEmpty(classes); + Args.requireU2(this.classes.length, "classes.length"); + } + + /** + * Initialize from another object. Note that both objects use the same references (shallow copy). Use copy() for a + * physical copy. + * + * @param c Source to copy. + */ + public PermittedSubclasses(final PermittedSubclasses c) { + this(c.getNameIndex(), c.getLength(), c.getClasses(), c.getConstantPool()); + } + + /** + * Called by objects that are traversing the nodes of the tree implicitly defined by the contents of a Java class. + * I.e., the hierarchy of methods, fields, attributes, etc. spawns a tree of objects. + * + * @param v Visitor object. + */ + @Override + public void accept(final Visitor v) { + v.visitPermittedSubclasses(this); + } + + /** + * Creates a deep clone of this object given constant pool. + * + * @return deep copy of this attribute. + */ + @Override + public Attribute copy(final ConstantPool constantPool) { + final PermittedSubclasses c = (PermittedSubclasses) clone(); + if (classes.length > 0) { + c.classes = classes.clone(); + } + c.setConstantPool(constantPool); + return c; + } + + /** + * Dumps PermittedSubclasses attribute to file stream in binary format. + * + * @param file Output file stream. + * @throws IOException if an I/O error occurs. + */ + @Override + public void dump(final DataOutputStream file) throws IOException { + super.dump(file); + file.writeShort(classes.length); + for (final int index : classes) { + file.writeShort(index); + } + } + + /** + * Gets the class indices in constant pool. + * + * @return array of indices into constant pool of class names. + */ + public int[] getClasses() { + return classes; + } + + /** + * Gets permitted class names. + * + * @return string array of class names. + */ + public String[] getClassNames() { + final String[] names = new String[classes.length]; + Arrays.setAll(names, i -> Utility.pathToPackage(super.getConstantPool().getConstantString(classes[i], Const.CONSTANT_Class))); + return names; + } + + /** + * Gets the number of classes. + * + * @return Length of classes table. + */ + public int getNumberClasses() { + return classes.length; + } + + /** + * Sets class indices. + * + * @param classes the list of class indexes Also redefines number_of_classes according to table length. + */ + public void setClasses(final int[] classes) { + this.classes = ArrayUtils.nullToEmpty(classes); + } + + /** + * String representation of PermittedSubclasses (for debugging purposes). + * + * @return String representation, that is, a list of permitted subclasses. + */ + @Override + public String toString() { + final StringBuilder buf = new StringBuilder(); + buf.append("PermittedSubclasses("); + buf.append(classes.length); + buf.append("):\n"); + for (final int index : classes) { + final String className = super.getConstantPool().getConstantString(index, Const.CONSTANT_Class); + buf.append(" ").append(Utility.compactClassName(className, false)).append("\n"); + } + return buf.substring(0, buf.length() - 1); // remove the last newline + } +} diff --git a/src/main/java/org/apache/bcel/classfile/Visitor.java b/src/main/java/org/apache/bcel/classfile/Visitor.java index 86fa8b572e..5df8e24332 100644 --- a/src/main/java/org/apache/bcel/classfile/Visitor.java +++ b/src/main/java/org/apache/bcel/classfile/Visitor.java @@ -411,6 +411,16 @@ default void visitNestMembers(final NestMembers obj) { // empty } + /** + * Visits a PermittedSubclasses attribute. + * + * @param obj the attribute. + * @since 6.13.0 + */ + default void visitPermittedSubclasses(final PermittedSubclasses obj) { + // empty + } + /** * Visits a ParameterAnnotations attribute. * 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 88b0d53cda..e9329f909c 100644 --- a/src/main/java/org/apache/bcel/verifier/statics/StringRepresentation.java +++ b/src/main/java/org/apache/bcel/verifier/statics/StringRepresentation.java @@ -61,6 +61,7 @@ import org.apache.bcel.classfile.Node; import org.apache.bcel.classfile.ParameterAnnotationEntry; import org.apache.bcel.classfile.ParameterAnnotations; +import org.apache.bcel.classfile.PermittedSubclasses; import org.apache.bcel.classfile.Record; import org.apache.bcel.classfile.RecordComponentInfo; import org.apache.bcel.classfile.Signature; @@ -385,6 +386,16 @@ public void visitNestMembers(final NestMembers obj) { tostring = toString(obj); } + /** + * Visits PermittedSubclasses attribute. + * + * @since 6.13.0 + */ + @Override + public void visitPermittedSubclasses(final PermittedSubclasses obj) { + tostring = toString(obj); + } + /** * @since 6.0 */ diff --git a/src/test/java/org/apache/bcel/classfile/PermittedSubclassesTest.java b/src/test/java/org/apache/bcel/classfile/PermittedSubclassesTest.java new file mode 100644 index 0000000000..503349caf1 --- /dev/null +++ b/src/test/java/org/apache/bcel/classfile/PermittedSubclassesTest.java @@ -0,0 +1,72 @@ +/* + * 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.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +import org.apache.bcel.AbstractTest; +import org.apache.bcel.verifier.statics.StringRepresentation; +import org.junit.jupiter.api.Test; + +class PermittedSubclassesTest extends AbstractTest { + + private static final String SEALED_JAR_PATH = "src/test/resources/sealed/sealed-demo-jdk21.0.8.jar"; + + private static final String SHAPE_CLASS_ENTRY = "org/jd/core/v1/SealedDemo$Shape.class"; + + private JavaClass parseJarClass(final String entryName) throws IOException { + try (JarFile jar = new JarFile(SEALED_JAR_PATH)) { + final JarEntry entry = jar.getJarEntry(entryName); + assertNotNull(entry, "Missing jar entry: " + entryName); + try (InputStream inputStream = jar.getInputStream(entry)) { + return new ClassParser(inputStream, entryName).parse(); + } + } + } + + @Test + void readsPermittedSubclassesAttribute() throws IOException { + final JavaClass clazz = parseJarClass(SHAPE_CLASS_ENTRY); + final Attribute[] attributes = findAttribute("PermittedSubclasses", clazz); + assertEquals(1, attributes.length, "Expected one PermittedSubclasses attribute"); + final PermittedSubclasses permittedSubclasses = (PermittedSubclasses) attributes[0]; + final List classNames = Arrays.asList(permittedSubclasses.getClassNames()); + assertEquals(2, classNames.size(), "Expected two permitted subclasses"); + assertTrue(classNames.contains("org.jd.core.v1.SealedDemo$Circle"), "Missing permitted subclass Circle"); + assertTrue(classNames.contains("org.jd.core.v1.SealedDemo$Rectangle"), "Missing permitted subclass Rectangle"); + } + + @Test + void stringRepresentationHandlesPermittedSubclasses() throws IOException { + final JavaClass clazz = parseJarClass(SHAPE_CLASS_ENTRY); + final Attribute[] attributes = findAttribute("PermittedSubclasses", clazz); + final PermittedSubclasses permittedSubclasses = (PermittedSubclasses) attributes[0]; + assertEquals(permittedSubclasses.toString(), new StringRepresentation(permittedSubclasses).toString()); + } +} diff --git a/src/test/resources/sealed/SealedDemo.java b/src/test/resources/sealed/SealedDemo.java new file mode 100644 index 0000000000..cda1f71b9f --- /dev/null +++ b/src/test/resources/sealed/SealedDemo.java @@ -0,0 +1,43 @@ +package org.jd.core.v1; + +public class SealedDemo { + + sealed interface Shape permits Circle, Rectangle { + double area(); + } + + static final class Circle implements Shape { + private final double radius; + + Circle(double radius) { + this.radius = radius; + } + + @Override + public double area() { + return Math.PI * radius * radius; + } + } + + static non-sealed class Rectangle implements Shape { + private final double width; + private final double height; + + Rectangle(double width, double height) { + this.width = width; + this.height = height; + } + + @Override + public double area() { + return width * height; + } + } + + double sealedSwitch(Shape shape) { + return switch (shape) { + case Circle circle -> circle.area(); + case Rectangle rectangle -> rectangle.area(); + }; + } +} diff --git a/src/test/resources/sealed/sealed-demo-jdk21.0.8.jar b/src/test/resources/sealed/sealed-demo-jdk21.0.8.jar new file mode 100644 index 0000000000..46d49ef985 Binary files /dev/null and b/src/test/resources/sealed/sealed-demo-jdk21.0.8.jar differ