Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix IllegalAccessException being thrown on an attempt to retrieve ser… #2469

Merged
merged 3 commits into from
Oct 26, 2023

Conversation

sandwwraith
Copy link
Member

…ializer for some private implementation classes from stdlib.

Fixes #2449

…ializer for some private implementation classes from stdlib.

Fixes #2449
Copy link
Contributor

@pdvrieze pdvrieze left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've gone in and added some tests. Using modifiers fixes the original problem, but also addresses some other object cases (tests added).

Comment on lines +54 to +61

@Test
fun testTopLevelMaps() {
// Returning null here is a deliberate choice: map constructor functions may return different specialized
// implementations (e.g., kotlin.collections.EmptyMap or java.util.Collections.SingletonMap)
// that may or may not be generic. Since we generally cannot return a generic serializer using Java class only,
// all attempts to get map serializer using only .javaClass should return null.
assertNull(serializerOrNull(emptyMap<String, String>().javaClass))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two additional tests that should be added here. This adds two additional test cases (both fail with the suggested implementation):

  • Getting a serializer from an anonymous object (canonicalName returns null) throws an NPE
  • Getting a serializer from a local named object throws because it is not built in
Suggested change
@Test
fun testTopLevelMaps() {
// Returning null here is a deliberate choice: map constructor functions may return different specialized
// implementations (e.g., kotlin.collections.EmptyMap or java.util.Collections.SingletonMap)
// that may or may not be generic. Since we generally cannot return a generic serializer using Java class only,
// all attempts to get map serializer using only .javaClass should return null.
assertNull(serializerOrNull(emptyMap<String, String>().javaClass))
private object namedObject
@Test
fun testTopLevelMaps() {
// Returning null here is a deliberate choice: map constructor functions may return different specialized
// implementations (e.g., kotlin.collections.EmptyMap or java.util.Collections.SingletonMap)
// that may or may not be generic. Since we generally cannot return a generic serializer using Java class only,
// all attempts to get map serializer using only .javaClass should return null.
assertNull(serializerOrNull(emptyMap<String, String>().javaClass))
assertNull(serializerOrNull(object {}.javaClass))
assertNull(serializerOrNull(namedObject.javaClass))

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing out the problem with the anonymous class! Adding a safe call to canonicalName is enough to fix it, though.

@@ -153,6 +153,9 @@ private fun <T : Any> Class<T>.createEnumSerializer(): KSerializer<T> {
}

private fun <T : Any> Class<T>.findObjectSerializer(): KSerializer<T>? {
// Special case to avoid IllegalAccessException on Java11+ (#2449)
// There are no serializable objects in the stdlib anyway.
if (this.canonicalName.let { it.startsWith("java.") || it.startsWith("kotlin.") }) return null
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is that it is not accessible (IllegalAccessException), if it is not accessible then it is also not an object. See the suggested additions to the test suite (JavaCollectionsTest) that are not fixed by the solution.

Suggested change
if (this.canonicalName.let { it.startsWith("java.") || it.startsWith("kotlin.") }) return null
if (!Modifier.isPublic(this.modifiers)) return null

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check will not work correctly for protected situation like this one:

open class A {
  @Serializable protected object B {}
}

Regarding @Serializable private object X — you're right; this code will throw an IllegalAccessError now. However, it will indicate problems with users' code instead of masking it behind the null. Also, we support @Serializable private class X even if it is in a different package. So, probably, it should be supported instead of returning null. Seems like it is out of the scope of this PR anyways.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe the "best" way would be to just catch the IllegalAccessException as it is otherwise really hard to know what is accessible.

Copy link
Member Author

@sandwwraith sandwwraith Oct 25, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's an adequate approach (although we lose information that is contained in exception message). However, I should first check if we can support @Serializable private object X properly.

Comment on lines 160 to 161
val field =
declaredFields.singleOrNull { it.name == "INSTANCE" && it.type == this && Modifier.isStatic(it.modifiers) }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

INSTANCE could also not be public, and therefore the type would not be an object (small chance, but add a check for it being public).

Suggested change
val field =
declaredFields.singleOrNull { it.name == "INSTANCE" && it.type == this && Modifier.isStatic(it.modifiers) }
val field = declaredFields.singleOrNull {
it.name == "INSTANCE" &&
it.type == this &&
Modifier.isStatic(it.modifiers) &&
Modifier.isPublic(it.modifiers)
} ?: return null

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you provide an example? IIRC Kotlin doesn't produce INSTANCE outside of Kotlin objects and it's hard to produce such code by yourself

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically if someone were to have a Java based singleton such as:

class BreakThings {
    private static BreakThings INSTANCE = BreakThings();

    public static BreakThings getInstance() { return INSTANCE; }
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's ok to fail or have unspecified behavior when asked for serializer for some java class

@qwwdfsad qwwdfsad removed their request for review October 25, 2023 14:31
@sandwwraith sandwwraith merged commit b44f03f into dev Oct 26, 2023
3 checks passed
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.

3 participants