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