From 0204a615f8a015b46611075628ef3b6f6c76d5d3 Mon Sep 17 00:00:00 2001 From: EL MOUSSAOUI Ayman Date: Wed, 28 Jan 2026 18:09:39 +0100 Subject: [PATCH 1/3] accept Java 25 class file version --- .../yworks/yguard/obf/classfile/ClassConstants.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/yworks/yguard/obf/classfile/ClassConstants.java b/src/main/java/com/yworks/yguard/obf/classfile/ClassConstants.java index 86473eb..76bdc81 100644 --- a/src/main/java/com/yworks/yguard/obf/classfile/ClassConstants.java +++ b/src/main/java/com/yworks/yguard/obf/classfile/ClassConstants.java @@ -27,7 +27,8 @@ public interface ClassConstants /** * The constant MAJOR_VERSION. */ - public static final int MAJOR_VERSION = 0x41; + // Java 25 class file major version. + public static final int MAJOR_VERSION = 0x45; /** * The constant ACC_PUBLIC. @@ -53,6 +54,11 @@ public interface ClassConstants * The constant ACC_SUPER. */ public static final int ACC_SUPER = 0x0020; + + /** + * The constant ACC_IDENTITY. + */ + public static final int ACC_IDENTITY = 0x0020; /** * The constant ACC_SYNCHRONIZED. */ @@ -326,6 +332,9 @@ public interface ClassConstants // new in java 17 public static final String ATTR_PermittedSubclasses = "PermittedSubclasses"; + // new in java 25 (preview) + public static final String ATTR_LoadableDescriptors = "LoadableDescriptors"; + /** * The constant REF_getField. */ @@ -396,6 +405,7 @@ public interface ClassConstants ATTR_ModuleMainClass, ATTR_NestHost, ATTR_NestMembers, + ATTR_LoadableDescriptors, }; /** @@ -425,5 +435,6 @@ public interface ClassConstants ATTR_NestMembers, ATTR_PermittedSubclasses, ATTR_Record, + ATTR_LoadableDescriptors, }; } From b28654f679ed9711582daeb8183483fb8f741cb4 Mon Sep 17 00:00:00 2001 From: EL MOUSSAOUI Ayman Date: Wed, 28 Jan 2026 18:09:42 +0100 Subject: [PATCH 2/3] preserve and remap LoadableDescriptors attribute --- .../yworks/yguard/obf/classfile/AttrInfo.java | 21 +++-- .../yguard/obf/classfile/ClassFile.java | 24 +++++ .../LoadableDescriptorsAttrInfo.java | 88 +++++++++++++++++++ 3 files changed, 124 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/yworks/yguard/obf/classfile/LoadableDescriptorsAttrInfo.java diff --git a/src/main/java/com/yworks/yguard/obf/classfile/AttrInfo.java b/src/main/java/com/yworks/yguard/obf/classfile/AttrInfo.java index 0785d16..97ecfe0 100644 --- a/src/main/java/com/yworks/yguard/obf/classfile/AttrInfo.java +++ b/src/main/java/com/yworks/yguard/obf/classfile/AttrInfo.java @@ -36,7 +36,7 @@ public class AttrInfo implements ClassConstants * The length of the attribute in bytes. */ protected int u4attrLength; - private byte info[]; + protected byte[] info; /** * The Owner. @@ -173,14 +173,17 @@ else if (ATTR_SourceDebugExtension.equals(attrName)) { else if (ATTR_Record.equals(attrName)) { ai = new RecordAttrInfo(cf, attrNameIndex, attrLength); } - else if (ATTR_PermittedSubclasses.equals(attrName)) { - ai = new PermittedSubclassesAttrInfo(cf, attrNameIndex, attrLength); - } - else { - if ( attrLength > 0 ) { - Logger.getInstance().warning( "Unrecognized attribute '" + attrName + "' in " + Conversion.toJavaClass( cf.getName() ) ); - } - ai = new AttrInfo( cf, attrNameIndex, attrLength ); + else if (ATTR_PermittedSubclasses.equals(attrName)) { + ai = new PermittedSubclassesAttrInfo(cf, attrNameIndex, attrLength); + } + else if (ATTR_LoadableDescriptors.equals(attrName)) { + ai = new LoadableDescriptorsAttrInfo(cf, attrNameIndex, attrLength); + } + else { + if ( attrLength > 0 ) { + Logger.getInstance().warning( "Unrecognized attribute '" + attrName + "' in " + Conversion.toJavaClass( cf.getName() ) ); + } + ai = new AttrInfo( cf, attrNameIndex, attrLength ); } } else diff --git a/src/main/java/com/yworks/yguard/obf/classfile/ClassFile.java b/src/main/java/com/yworks/yguard/obf/classfile/ClassFile.java index 4be2fd4..2457680 100644 --- a/src/main/java/com/yworks/yguard/obf/classfile/ClassFile.java +++ b/src/main/java/com/yworks/yguard/obf/classfile/ClassFile.java @@ -942,6 +942,8 @@ public void remap(NameMapper nm, boolean replaceClassNameStrings, PrintWriter lo } } else if (attrInfo instanceof SignatureAttrInfo){ remapSignature(nm, (SignatureAttrInfo) attrInfo); + } else if (attrInfo instanceof LoadableDescriptorsAttrInfo) { + remapLoadableDescriptors(nm, (LoadableDescriptorsAttrInfo) attrInfo); } else if (attrInfo instanceof SourceFileAttrInfo) { SourceFileAttrInfo source = (SourceFileAttrInfo) attrInfo; CpInfo cpInfo = getCpEntry(source.getSourceFileIndex()); @@ -1731,6 +1733,28 @@ private void remapSignature(NameMapper nm, SignatureAttrInfo signature){ } } } + + private void remapLoadableDescriptors(NameMapper nm, LoadableDescriptorsAttrInfo attr) { + if (!attr.isParsed()) { + return; + } + + final int[] indices = attr.getDescriptorIndices(); + for (int i = 0; i < indices.length; i++) { + final CpInfo cpInfo = getCpEntry(indices[i]); + if (!(cpInfo instanceof Utf8CpInfo)) { + continue; + } + + final Utf8CpInfo utf = (Utf8CpInfo) cpInfo; + final String orig = utf.getString(); + final String remap = nm.mapDescriptor(orig); + if (!orig.equals(remap)) { + final int remapIndex = constantPool.remapUtf8To(remap, indices[i]); + attr.setDescriptorIndex(i, remapIndex); + } + } + } private int remapNT(Utf8CpInfo refUtf, String remapRef, Utf8CpInfo descUtf, String remapDesc, NameAndTypeCpInfo nameTypeInfo, int nameAndTypeIndex){ // If a remap is required, make a new N&T (increment ref count on 'name' and diff --git a/src/main/java/com/yworks/yguard/obf/classfile/LoadableDescriptorsAttrInfo.java b/src/main/java/com/yworks/yguard/obf/classfile/LoadableDescriptorsAttrInfo.java new file mode 100644 index 0000000..22b95da --- /dev/null +++ b/src/main/java/com/yworks/yguard/obf/classfile/LoadableDescriptorsAttrInfo.java @@ -0,0 +1,88 @@ +package com.yworks.yguard.obf.classfile; + +import java.io.ByteArrayInputStream; +import java.io.DataInput; +import java.io.DataInputStream; +import java.io.DataOutput; +import java.io.IOException; + +/** + * Representation of the (preview) LoadableDescriptors attribute. + * + * The attribute is treated as a list of constant pool indices to descriptor + * strings. If the attribute payload does not match the expected structure, + * the bytes are preserved unchanged. + */ +public class LoadableDescriptorsAttrInfo extends AttrInfo { + private int[] u2descriptorIndices = new int[0]; + private boolean parsed; + + LoadableDescriptorsAttrInfo(final ClassFile cf, final int attrNameIndex, final int attrLength) { + super(cf, attrNameIndex, attrLength); + } + + protected String getAttrName() { + return ATTR_LoadableDescriptors; + } + + int[] getDescriptorIndices() { + return u2descriptorIndices; + } + + void setDescriptorIndex(final int i, final int cpIndex) { + u2descriptorIndices[i] = cpIndex; + } + + boolean isParsed() { + return parsed; + } + + protected void markUtf8RefsInInfo(final ConstantPool pool) { + if (!parsed) { + return; + } + for (int i = 0; i < u2descriptorIndices.length; i++) { + pool.incRefCount(u2descriptorIndices[i]); + } + } + + protected void readInfo(final DataInput din) throws IOException { + // Always preserve original bytes. + info = new byte[u4attrLength]; + din.readFully(info); + + // Best-effort parse; if it doesn't match exactly, keep bytes only. + parsed = false; + u2descriptorIndices = new int[0]; + try { + final DataInputStream dis = new DataInputStream(new ByteArrayInputStream(info)); + final int count = dis.readUnsignedShort(); + if (u4attrLength != 2 + 2 * count) { + return; + } + final int[] indices = new int[count]; + for (int i = 0; i < count; i++) { + indices[i] = dis.readUnsignedShort(); + } + if (dis.available() != 0) { + return; + } + u2descriptorIndices = indices; + parsed = true; + } catch (RuntimeException ignored) { + // Keep raw bytes. + } + } + + public void writeInfo(final DataOutput dout) throws IOException { + if (!parsed) { + super.writeInfo(dout); + return; + } + + dout.writeShort(u2descriptorIndices.length); + for (int i = 0; i < u2descriptorIndices.length; i++) { + dout.writeShort(u2descriptorIndices[i]); + } + } +} From 93ac7a138ccf8edcb3c5f2abb2d366fd83351ed1 Mon Sep 17 00:00:00 2001 From: EL MOUSSAOUI Ayman Date: Wed, 28 Jan 2026 18:09:45 +0100 Subject: [PATCH 3/3] add tests for Java 25 class files --- .../obf/classfile/ClassFileVersionTest.java | 169 ++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 src/test/java/com/yworks/yguard/obf/classfile/ClassFileVersionTest.java diff --git a/src/test/java/com/yworks/yguard/obf/classfile/ClassFileVersionTest.java b/src/test/java/com/yworks/yguard/obf/classfile/ClassFileVersionTest.java new file mode 100644 index 0000000..428bdd3 --- /dev/null +++ b/src/test/java/com/yworks/yguard/obf/classfile/ClassFileVersionTest.java @@ -0,0 +1,169 @@ +package com.yworks.yguard.obf.classfile; + +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.nio.file.Files; + +import static org.junit.Assert.*; + +public class ClassFileVersionTest { + @Test + public void acceptsMajorVersion69() throws Exception { + final ClassFile cf = ClassFile.create(new DataInputStream(new ByteArrayInputStream(minimalClassWithLoadableDescriptors(69)))); + assertEquals("pkg/Test", cf.getName()); + } + + @Test + public void remapsLoadableDescriptors() throws Exception { + final ClassFile cf = ClassFile.create(new DataInputStream(new ByteArrayInputStream(minimalClassWithLoadableDescriptors(69)))); + + final StringWriter sw = new StringWriter(); + cf.remap(new TestNameMapper(), false, new PrintWriter(sw)); + + LoadableDescriptorsAttrInfo attr = null; + for (AttrInfo ai : cf.getAttributes()) { + if (ai instanceof LoadableDescriptorsAttrInfo) { + attr = (LoadableDescriptorsAttrInfo) ai; + break; + } + } + assertNotNull(attr); + assertTrue(attr.isParsed()); + + final int idx = attr.getDescriptorIndices()[0]; + assertTrue(cf.getCpEntry(idx) instanceof Utf8CpInfo); + assertEquals("Lpkg/New;", ((Utf8CpInfo) cf.getCpEntry(idx)).getString()); + } + + private static byte[] minimalClassWithLoadableDescriptors(final int major) throws Exception { + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + final DataOutputStream dout = new DataOutputStream(baos); + + dout.writeInt(0xCAFEBABE); + dout.writeShort(0); + dout.writeShort(major); + + // constant_pool_count + dout.writeShort(7); + + // #1 Utf8 pkg/Test + writeUtf8(dout, "pkg/Test"); + + // #2 Class #1 + dout.writeByte(7); + dout.writeShort(1); + + // #3 Utf8 java/lang/Object + writeUtf8(dout, "java/lang/Object"); + + // #4 Class #3 + dout.writeByte(7); + dout.writeShort(3); + + // #5 Utf8 LoadableDescriptors + writeUtf8(dout, "LoadableDescriptors"); + + // #6 Utf8 Lpkg/Old; + writeUtf8(dout, "Lpkg/Old;"); + + // access_flags (public + super) + dout.writeShort(0x0021); + // this_class, super_class + dout.writeShort(2); + dout.writeShort(4); + + // interfaces + dout.writeShort(0); + + // fields + dout.writeShort(0); + + // methods + dout.writeShort(0); + + // attributes_count + dout.writeShort(1); + + // attribute_info: LoadableDescriptors + dout.writeShort(5); + dout.writeInt(4); + dout.writeShort(1); + dout.writeShort(6); + + dout.flush(); + return baos.toByteArray(); + } + + private static void writeUtf8(final DataOutputStream dout, final String s) throws Exception { + dout.writeByte(1); + dout.writeUTF(s); + } + + private static final class TestNameMapper implements NameMapper, ClassConstants { + public String[] getAttrsToKeep(final String className) { + return REQUIRED_ATTRS; + } + + public String mapClass(final String className) { + return className; + } + + public String mapMethod(final String className, final String methodName, final String descriptor) { + return methodName; + } + + public String mapAnnotationField(final String className, final String annotationFieldName) { + return annotationFieldName; + } + + public String mapField(final String className, final String fieldName) { + return fieldName; + } + + public String mapDescriptor(final String descriptor) { + if ("Lpkg/Old;".equals(descriptor)) { + return "Lpkg/New;"; + } + return descriptor; + } + + public String mapSignature(final String signature) { + return signature; + } + + public String mapSourceFile(final String className, final String sourceFile) { + return sourceFile; + } + + public boolean mapLineNumberTable( + final String className, + final String methodName, + final String methodSignature, + final LineNumberTableAttrInfo info + ) { + return true; + } + + public String mapLocalVariable( + final String thisClassName, + final String methodName, + final String descriptor, + final String string + ) { + return string; + } + + public String mapPackage(final String packageName) { + return packageName; + } + } +}