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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions core/jvmMain/src/kotlinx/serialization/internal/Platform.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
sandwwraith marked this conversation as resolved.
Show resolved Hide resolved
if (this.canonicalName?.let { it.startsWith("java.") || it.startsWith("kotlin.") } != false) return null
// Check it is an object without using kotlin-reflect
val field =
declaredFields.singleOrNull { it.name == "INSTANCE" && it.type == this && Modifier.isStatic(it.modifiers) }
Comment on lines 160 to 161
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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,16 @@
package kotlinx.serialization

import kotlinx.serialization.json.Json
import kotlinx.serialization.test.typeTokenOf
import org.junit.Test
import java.util.HashMap
import java.util.HashSet
import kotlin.collections.LinkedHashMap
import kotlin.collections.Map
import kotlin.collections.hashMapOf
import kotlin.collections.hashSetOf
import kotlin.test.assertEquals
import kotlin.reflect.*
import kotlin.test.*


class JavaCollectionsTest {
Expand All @@ -38,7 +40,7 @@ class JavaCollectionsTest {
)

@Test
fun test() {
fun testJavaCollectionsInsideClass() {
val original = HasHashMap("42", hashMapOf(1 to "1", 2 to "2"), hashSetOf(11), LinkedHashMap(), null)
val serializer = HasHashMap.serializer()
val string = Json.encodeToString(serializer = serializer, value = original)
Expand All @@ -49,4 +51,62 @@ class JavaCollectionsTest {
val restored = Json.decodeFromString(deserializer = serializer, string = string)
assertEquals(expected = original, actual = restored)
}

@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))
Comment on lines +54 to +61
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.

assertNull(serializerOrNull(mapOf<String, String>("a" to "b").javaClass))
assertNull(serializerOrNull(mapOf<String, String>("a" to "b", "b" to "c").javaClass))
// Correct ways of retrieving map serializer:
assertContains(
serializer(typeTokenOf<Map<String, String>>()).descriptor.serialName,
"kotlin.collections.LinkedHashMap"
)
assertContains(
serializer(typeTokenOf<java.util.LinkedHashMap<String, String>>()).descriptor.serialName,
"kotlin.collections.LinkedHashMap"
)
assertContains(
serializer(typeOf<LinkedHashMap<String, String>>()).descriptor.serialName,
"kotlin.collections.LinkedHashMap"
)
}

@Test
fun testTopLevelSetsAndLists() {
// Same reasoning as for maps
assertNull(serializerOrNull(emptyList<String>().javaClass))
assertNull(serializerOrNull(listOf<String>("a").javaClass))
assertNull(serializerOrNull(listOf<String>("a", "b").javaClass))
assertNull(serializerOrNull(emptySet<String>().javaClass))
assertNull(serializerOrNull(setOf<String>("a").javaClass))
assertNull(serializerOrNull(setOf<String>("a", "b").javaClass))
assertContains(
serializer(typeTokenOf<Set<String>>()).descriptor.serialName,
shanshin marked this conversation as resolved.
Show resolved Hide resolved
"kotlin.collections.LinkedHashSet"
)
assertContains(
serializer(typeTokenOf<List<String>>()).descriptor.serialName,
"kotlin.collections.ArrayList"
)
assertContains(
serializer(typeTokenOf<java.util.LinkedHashSet<String>>()).descriptor.serialName,
"kotlin.collections.LinkedHashSet"
)
assertContains(
serializer(typeTokenOf<java.util.ArrayList<String>>()).descriptor.serialName,
"kotlin.collections.ArrayList"
)
}

@Test
fun testAnonymousObject() {
val obj: Any = object {}
assertNull(serializerOrNull(obj.javaClass))
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -209,15 +209,6 @@ class SerializerByTypeTest {
assertEquals(expected, json.encodeToString(serial2 as KSerializer<T>, value))
}

@PublishedApi
internal open class TypeBase<T>

public inline fun <reified T> typeTokenOf(): Type {
val base = object : TypeBase<T>() {}
val superType = base::class.java.genericSuperclass!!
return (superType as ParameterizedType).actualTypeArguments.first()!!
}

class IntBox(val i: Int)

object CustomIntSerializer : KSerializer<IntBox> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright 2017-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.serialization.test

import java.lang.reflect.*


@PublishedApi
internal open class TypeBase<T>

public inline fun <reified T> typeTokenOf(): Type {
val base = object : TypeBase<T>() {}
val superType = base::class.java.genericSuperclass!!
return (superType as ParameterizedType).actualTypeArguments.first()!!
}