Skip to content

Fix reflection on obfuscated record components (#179) - #180

Open
amin3141 wants to merge 1 commit into
yWorks:masterfrom
amin3141:fix-record-components
Open

amin3141 wants to merge 1 commit into
yWorks:masterfrom
amin3141:fix-record-components

Conversation

@amin3141

Copy link
Copy Markdown

Fixes #179.

Obfuscated records could not be used through java.lang.reflect.RecordComponent, which reflective libraries and record serialization rely on:

  • Component signatures were not renamed. ClassFile.remap assumed a record component's Signature "is always a simple name", but it is the component's full generic type, e.g. Ljava/util/List<Lcom/acme/Item;>;. Field and method signatures were renamed; component signatures kept the original class names, so RecordComponent.getGenericType() threw TypeNotPresentException. The component's Signature now goes through remapSignature like the others.
  • Components were renamed like fields, accessors like methods. RecordComponent.getAccessor() finds the accessor by the component's name, and javac guarantees that name equals the accessor's. With the two renamed independently, getAccessor() returned null, and in some records the names crossed over between components. Each component is now renamed with nm.mapMethod(className, name, "()" + descriptor), so it always gets its accessor's new name. That includes accessors with bodies and accessors that implement interface methods. Field names still change independently; the JVM and reflection don't require a component's name to match its field's.

Tests

RecordsTest compiles RecordComponents.txt, obfuscates it with GuardDB, and checks:

  • testObfuscatedRecordsSupportReflection runs code inside the obfuscated JAR that prints, for every component, its resolved generic type and the value its accessor returns. The output must equal the unobfuscated output. The records cover a generic component whose type is renamed, same-typed components (so swapped accessors show up), a type variable, nested generics, explicit accessors with bodies, an accessor that implements an interface method, an alias getter, primitives and arrays, an empty record, and record-typed components.
  • testObfuscatedRecordComponentAttributes reads the Record attribute with ASM. Every component must name an instance method name()descriptor and carry exactly that method's generic signature, and no signature may contain an original class name.
  • testRetainedRecordReferringToObfuscatedClasses keeps a record's class and member names while the class its component refers to is renamed. Component names stay the same, and the signature must be renamed.
  • testUnobfuscatedRecords checks the expected output against the unobfuscated records, so a failure points at obfuscation rather than at the fixture.

Without the fix the three obfuscation tests fail. Each half of the fix is caught on its own: without the signature change the attribute test reports the original class name, and without the naming change it reports a missing accessor.

Like the existing Java 21 SwitchBootstraps tests, these need a Java 16+ test JVM to compile records and print Run test with Java 16 or newer. otherwise. Results:

  • ./gradlew build test on JDK 11: passes, including editorconfigCheck. The record tests print the notice.
  • The full suite on JDK 21 and JDK 24: all tests pass, with the record tests running.

The Signature attribute of a record component was not renamed, so
RecordComponent.getGenericType() threw TypeNotPresentException when a
component's type was obfuscated. Record components were renamed like
their fields while their accessors were renamed like methods, so
RecordComponent.getAccessor() returned null for most components.

Record component signatures are now renamed like the signatures of
fields and methods, and each record component is now renamed like its
accessor method.

RecordsTest compiles a set of records, obfuscates them, runs code that
reflects over them, and checks the Record attribute against the
accessors. It also covers a retained record whose component types are
obfuscated. The tests need Java 16 or newer and print a notice on older
versions, like the existing Java 21 tests.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Obfuscated records break RecordComponent.getGenericType() and getAccessor()

1 participant