Throw exception instead of returning null from KotlinClass(Module)Metadata.read()

This commit is contained in:
Leonid Startsev
2023-04-04 19:41:12 +02:00
committed by Space Team
parent 1c629e748f
commit c3d363d1b8
4 changed files with 106 additions and 39 deletions
@@ -1398,7 +1398,7 @@ public final class kotlinx/metadata/jvm/KotlinClassMetadata$Unknown : kotlinx/me
public final class kotlinx/metadata/jvm/KotlinModuleMetadata { public final class kotlinx/metadata/jvm/KotlinModuleMetadata {
public static final field Companion Lkotlinx/metadata/jvm/KotlinModuleMetadata$Companion; public static final field Companion Lkotlinx/metadata/jvm/KotlinModuleMetadata$Companion;
public fun <init> ([B)V public synthetic fun <init> ([BLkotlinx/metadata/internal/metadata/jvm/deserialization/ModuleMapping;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun accept (Lkotlinx/metadata/jvm/KmModuleVisitor;)V public final fun accept (Lkotlinx/metadata/jvm/KmModuleVisitor;)V
public final fun getBytes ()[B public final fun getBytes ()[B
public static final fun read ([B)Lkotlinx/metadata/jvm/KotlinModuleMetadata; public static final fun read ([B)Lkotlinx/metadata/jvm/KotlinModuleMetadata;
@@ -48,7 +48,7 @@ sealed class KotlinClassMetadata(val annotationData: Metadata) {
/** /**
* Returns a new [KmClass] instance created from this class metadata. * Returns a new [KmClass] instance created from this class metadata.
* *
* @throws IllegalArgumentException if metadata is malformed and can't be transformed into [KmClass]. * @throws IllegalArgumentException if metadata is malformed or inconsistent and can't be transformed into [KmClass].
*/ */
fun toKmClass(): KmClass = wrapIntoMetadataExceptionWhenNeeded { fun toKmClass(): KmClass = wrapIntoMetadataExceptionWhenNeeded {
KmClass().apply(this::accept) KmClass().apply(this::accept)
@@ -112,7 +112,7 @@ sealed class KotlinClassMetadata(val annotationData: Metadata) {
/** /**
* Creates a new [KmPackage] instance from this file facade metadata. * Creates a new [KmPackage] instance from this file facade metadata.
* *
* @throws IllegalArgumentException if metadata is malformed and can't be transformed into [KmPackage]. * @throws IllegalArgumentException if metadata is malformed or inconsistent and can't be transformed into [KmPackage].
*/ */
fun toKmPackage(): KmPackage = wrapIntoMetadataExceptionWhenNeeded { fun toKmPackage(): KmPackage = wrapIntoMetadataExceptionWhenNeeded {
KmPackage().apply(this::accept) KmPackage().apply(this::accept)
@@ -175,7 +175,7 @@ sealed class KotlinClassMetadata(val annotationData: Metadata) {
* Creates a new [KmLambda] instance from this synthetic class metadata. * Creates a new [KmLambda] instance from this synthetic class metadata.
* Returns `null` if this synthetic class does not represent a lambda. * Returns `null` if this synthetic class does not represent a lambda.
* *
* @throws IllegalArgumentException if metadata is malformed and can't be transformed into [KmLambda]. * @throws IllegalArgumentException if metadata is malformed or inconsistent and can't be transformed into [KmLambda].
*/ */
fun toKmLambda(): KmLambda? = wrapIntoMetadataExceptionWhenNeeded { fun toKmLambda(): KmLambda? = wrapIntoMetadataExceptionWhenNeeded {
if (isLambda) KmLambda().apply(this::accept) else null if (isLambda) KmLambda().apply(this::accept) else null
@@ -340,7 +340,7 @@ sealed class KotlinClassMetadata(val annotationData: Metadata) {
/** /**
* Creates a new [KmPackage] instance from this multi-file class part metadata. * Creates a new [KmPackage] instance from this multi-file class part metadata.
* *
* @throws IllegalArgumentException if metadata is malformed and can't be transformed into [KmPackage]. * @throws IllegalArgumentException if metadata is malformed or inconsistent and can't be transformed into [KmPackage].
*/ */
fun toKmPackage(): KmPackage = wrapIntoMetadataExceptionWhenNeeded { fun toKmPackage(): KmPackage = wrapIntoMetadataExceptionWhenNeeded {
KmPackage().apply(this::accept) KmPackage().apply(this::accept)
@@ -518,20 +518,23 @@ sealed class KotlinClassMetadata(val annotationData: Metadata) {
/** /**
* Reads and parses the given annotation data of a Kotlin JVM class file and returns the correct type of [KotlinClassMetadata] encoded by * Reads and parses the given annotation data of a Kotlin JVM class file and returns the correct type of [KotlinClassMetadata] encoded by
* this annotation, or `null` if this annotation data has an unsupported metadata version. * this annotation, if metadata version is supported.
* *
* [annotationData] may be obtained reflectively, constructed manually or with helper [kotlinx.metadata.jvm.Metadata] function, * [annotationData] may be obtained reflectively, constructed manually or with helper [kotlinx.metadata.jvm.Metadata] function,
* or equivalent [KotlinClassHeader] can be used. * or equivalent [KotlinClassHeader] can be used.
* *
* @throws IllegalArgumentException if the metadata cannot be parsed from binary format or is inconsistent with itself * Metadata version is supported if it is greater or equal than 1.1, and less or equal than [COMPATIBLE_METADATA_VERSION] + 1 minor version.
* Note that metadata version is 1.1 for Kotlin < 1.4, and is equal to the language version starting from Kotlin 1.4.
* For example, if the latest Kotlin version is 1.7.0, the latest kotlinx-metadata-jvm can read binaries produced by Kotlin
* compilers from 1.0 to 1.8.* inclusively.
*
* @throws IllegalArgumentException if the metadata version is unsupported
*
* @see COMPATIBLE_METADATA_VERSION
*/ */
@JvmStatic @JvmStatic
fun read(annotationData: Metadata): KotlinClassMetadata? { fun read(annotationData: Metadata): KotlinClassMetadata {
if (!JvmMetadataVersion( checkMetadataVersionForRead(annotationData)
annotationData.metadataVersion,
(annotationData.extraInt and (1 shl 3)/* see JvmAnnotationNames.METADATA_STRICT_VERSION_SEMANTICS_FLAG */) != 0
).isCompatibleWithCurrentCompilerVersion()
) return null
// All data is loaded lazily, no exceptions here to handle // All data is loaded lazily, no exceptions here to handle
return when (annotationData.kind) { return when (annotationData.kind) {
@@ -544,6 +547,26 @@ sealed class KotlinClassMetadata(val annotationData: Metadata) {
} }
} }
private fun checkMetadataVersionForRead(annotationData: Metadata) {
if (annotationData.metadataVersion.isEmpty())
throw IllegalArgumentException("Provided Metadata instance does not have metadataVersion in it and therefore is malformed and cannot be read.")
val jvmMetadataVersion = JvmMetadataVersion(
annotationData.metadataVersion,
(annotationData.extraInt and (1 shl 3)/* see JvmAnnotationNames.METADATA_STRICT_VERSION_SEMANTICS_FLAG */) != 0
)
throwIfNotCompatible(jvmMetadataVersion)
}
internal fun throwIfNotCompatible(jvmMetadataVersion: JvmMetadataVersion) {
if (!jvmMetadataVersion.isCompatibleWithCurrentCompilerVersion()) {
// Kotlin 1.0 produces classfiles with metadataVersion = 1.1.0, while 1.0.0 represents unsupported pre-1.0 Kotlin (see JvmMetadataVersion.kt:39)
val postfix =
if (!jvmMetadataVersion.isAtLeast(1, 1, 0)) "while minimum supported version is 1.1.0 (Kotlin 1.0)."
else "while maximum supported version is ${JvmMetadataVersion.INSTANCE_NEXT}. To support newer versions, update the kotlinx-metadata-jvm library."
throw IllegalArgumentException("Provided Metadata instance has version $jvmMetadataVersion, $postfix")
}
}
private fun checkMetadataVersion(version: IntArray) { private fun checkMetadataVersion(version: IntArray) {
require(version.size >= 2 && version[0] >= 1 && (version[0] > 1 || version[1] >= 4)) { require(version.size >= 2 && version[0] >= 1 && (version[0] > 1 || version[1] >= 4)) {
"This version of kotlinx-metadata-jvm doesn't support writing Kotlin metadata of version earlier than 1.4. " + "This version of kotlinx-metadata-jvm doesn't support writing Kotlin metadata of version earlier than 1.4. " +
@@ -593,7 +616,11 @@ sealed class KotlinClassMetadata(val annotationData: Metadata) {
const val MULTI_FILE_CLASS_PART_KIND = 5 const val MULTI_FILE_CLASS_PART_KIND = 5
/** /**
* The latest metadata version supported by this version of the library. * The latest stable metadata version supported by this version of the library.
* The library can read Kotlin metadata produced by Kotlin compilers from 1.0 up to and including this version + 1 minor.
*
* For example, if the latest supported stable Kotlin version is 1.7.0, kotlinx-metadata-jvm can read binaries produced by Kotlin compilers from 1.0
* to 1.8.* inclusively. In this case, this property will have the value `[1, 7, 0]`.
* *
* @see Metadata.metadataVersion * @see Metadata.metadataVersion
*/ */
@@ -16,6 +16,7 @@ import kotlinx.metadata.*
import kotlinx.metadata.internal.accept import kotlinx.metadata.internal.accept
import kotlinx.metadata.jvm.internal.IgnoreInApiDump import kotlinx.metadata.jvm.internal.IgnoreInApiDump
import kotlinx.metadata.jvm.KotlinClassMetadata.Companion.COMPATIBLE_METADATA_VERSION import kotlinx.metadata.jvm.KotlinClassMetadata.Companion.COMPATIBLE_METADATA_VERSION
import kotlinx.metadata.jvm.KotlinClassMetadata.Companion.throwIfNotCompatible
import kotlinx.metadata.jvm.internal.wrapIntoMetadataExceptionWhenNeeded import kotlinx.metadata.jvm.internal.wrapIntoMetadataExceptionWhenNeeded
import kotlinx.metadata.jvm.internal.wrapWriteIntoIAE import kotlinx.metadata.jvm.internal.wrapWriteIntoIAE
import org.jetbrains.kotlin.metadata.jvm.JvmModuleProtoBuf import org.jetbrains.kotlin.metadata.jvm.JvmModuleProtoBuf
@@ -28,26 +29,26 @@ import org.jetbrains.kotlin.metadata.jvm.deserialization.serializeToByteArray
* Represents the parsed metadata of a Kotlin JVM module file. * Represents the parsed metadata of a Kotlin JVM module file.
* *
* To create an instance of [KotlinModuleMetadata], load the contents of the `.kotlin_module` file into a byte array * To create an instance of [KotlinModuleMetadata], load the contents of the `.kotlin_module` file into a byte array
* and call [KotlinModuleMetadata.read]. * and call [KotlinModuleMetadata.read]. Then it is possible to transform the result into [KmModule] with [KotlinModuleMetadata.toKmModule].
*
* `.kotlin_module` file is produced per Kotlin compilation, and contains auxiliary information, such as a map of all single- and multi-file facades ([KmModule.packageParts]),
* `@OptionalExpectation` declarations ([KmModule.optionalAnnotationClasses]), and module annotations ([KmModule.annotations).
* *
* @property bytes the byte array representing the contents of a `.kotlin_module` file * @property bytes the byte array representing the contents of a `.kotlin_module` file
*/ */
@UnstableMetadataApi @UnstableMetadataApi
class KotlinModuleMetadata(@Suppress("MemberVisibilityCanBePrivate") val bytes: ByteArray) { class KotlinModuleMetadata private constructor(
@get:IgnoreInApiDump @Suppress("MemberVisibilityCanBePrivate") val bytes: ByteArray,
internal val data: ModuleMapping = ModuleMapping.loadModuleMapping( @get:IgnoreInApiDump internal val data: ModuleMapping
bytes, javaClass.name, ) {
skipMetadataVersionCheck = false,
isJvmPackageNameSupported = true
) {
// TODO: report incorrect versions of modules
}
/** /**
* Visits metadata of this module with a new [KmModule] instance and returns that instance. * Visits metadata of this module with a new [KmModule] instance and returns that instance.
*
* @throws IllegalArgumentException if parsed metadata is inconsistent and can't be transformed into [KmModule].
*/ */
fun toKmModule(): KmModule = fun toKmModule(): KmModule = wrapIntoMetadataExceptionWhenNeeded {
KmModule().apply(this::accept) KmModule().apply(this::accept)
}
/** /**
* A [KmModuleVisitor] that generates the metadata of a Kotlin JVM module file. * A [KmModuleVisitor] that generates the metadata of a Kotlin JVM module file.
@@ -96,8 +97,10 @@ class KotlinModuleMetadata(@Suppress("MemberVisibilityCanBePrivate") val bytes:
* [KotlinClassMetadata.COMPATIBLE_METADATA_VERSION] by default * [KotlinClassMetadata.COMPATIBLE_METADATA_VERSION] by default
*/ */
@Deprecated("Writer API is deprecated as excessive and cumbersome. Please use KotlinModuleMetadata.write(kmModule, metadataVersion)") @Deprecated("Writer API is deprecated as excessive and cumbersome. Please use KotlinModuleMetadata.write(kmModule, metadataVersion)")
fun write(metadataVersion: IntArray = COMPATIBLE_METADATA_VERSION): KotlinModuleMetadata = fun write(metadataVersion: IntArray = COMPATIBLE_METADATA_VERSION): KotlinModuleMetadata {
KotlinModuleMetadata(b.build().serializeToByteArray(JvmMetadataVersion(*metadataVersion), 0)) val bytes = b.build().serializeToByteArray(JvmMetadataVersion(*metadataVersion), 0)
return KotlinModuleMetadata(bytes, dataFromBytes(bytes))
}
} }
/** /**
@@ -131,19 +134,18 @@ class KotlinModuleMetadata(@Suppress("MemberVisibilityCanBePrivate") val bytes:
* or `null` if this byte array encodes a module with an unsupported metadata version. * or `null` if this byte array encodes a module with an unsupported metadata version.
* *
* @throws IllegalArgumentException if an error happened while parsing the given byte array, * @throws IllegalArgumentException if an error happened while parsing the given byte array,
* which means that it's either not the content of a .kotlin_module file, or it has been corrupted. * which means that it's either not the content of a `.kotlin_module` file, or it has been corrupted.
*/ */
@JvmStatic @JvmStatic
@UnstableMetadataApi @UnstableMetadataApi
fun read(bytes: ByteArray): KotlinModuleMetadata? { fun read(bytes: ByteArray): KotlinModuleMetadata {
return wrapIntoMetadataExceptionWhenNeeded { return wrapIntoMetadataExceptionWhenNeeded {
val result = KotlinModuleMetadata(bytes) val result = dataFromBytes(bytes)
when (result.data) { when (result) {
ModuleMapping.EMPTY -> null ModuleMapping.EMPTY, ModuleMapping.CORRUPTED ->
ModuleMapping.CORRUPTED ->
throw InconsistentKotlinMetadataException("Data is not the content of a .kotlin_module file, or it has been corrupted.") throw InconsistentKotlinMetadataException("Data is not the content of a .kotlin_module file, or it has been corrupted.")
else -> result
} }
KotlinModuleMetadata(bytes, result)
} }
} }
@@ -153,7 +155,7 @@ class KotlinModuleMetadata(@Suppress("MemberVisibilityCanBePrivate") val bytes:
* @param metadataVersion metadata version to be written to the metadata (see [Metadata.metadataVersion]), * @param metadataVersion metadata version to be written to the metadata (see [Metadata.metadataVersion]),
* [KotlinClassMetadata.COMPATIBLE_METADATA_VERSION] by default * [KotlinClassMetadata.COMPATIBLE_METADATA_VERSION] by default
* *
* @throws IllegalArgumentException if [kmModule] is not correct and cannot be written or if [metadataVersion] is not supported for writing. * @throws IllegalArgumentException if [kmModule] is not correct and can't be written or if [metadataVersion] is not supported for writing.
*/ */
@UnstableMetadataApi @UnstableMetadataApi
@JvmStatic @JvmStatic
@@ -161,6 +163,13 @@ class KotlinModuleMetadata(@Suppress("MemberVisibilityCanBePrivate") val bytes:
fun write(kmModule: KmModule, metadataVersion: IntArray = COMPATIBLE_METADATA_VERSION): KotlinModuleMetadata = wrapWriteIntoIAE { fun write(kmModule: KmModule, metadataVersion: IntArray = COMPATIBLE_METADATA_VERSION): KotlinModuleMetadata = wrapWriteIntoIAE {
Writer().also { kmModule.accept(it) }.write(metadataVersion) Writer().also { kmModule.accept(it) }.write(metadataVersion)
} }
private fun dataFromBytes(bytes: ByteArray): ModuleMapping {
return ModuleMapping.loadModuleMapping(
bytes, "KotlinModuleMetadata", skipMetadataVersionCheck = false,
isJvmPackageNameSupported = true, reportIncompatibleVersionError = ::throwIfNotCompatible
)
}
} }
} }
@@ -8,10 +8,10 @@ package kotlinx.metadata.test
import kotlinx.metadata.KmClass import kotlinx.metadata.KmClass
import kotlinx.metadata.jvm.KotlinClassMetadata import kotlinx.metadata.jvm.KotlinClassMetadata
import kotlinx.metadata.jvm.Metadata import kotlinx.metadata.jvm.Metadata
import org.jetbrains.kotlin.metadata.jvm.deserialization.JvmMetadataVersion
import org.jetbrains.kotlin.protobuf.InvalidProtocolBufferException import org.jetbrains.kotlin.protobuf.InvalidProtocolBufferException
import org.junit.Test import org.junit.Test
import kotlin.test.assertFailsWith import kotlin.test.*
import kotlin.test.assertIs
class MetadataExceptionsTest { class MetadataExceptionsTest {
@Test @Test
@@ -33,4 +33,35 @@ class MetadataExceptionsTest {
} }
assertIs<UninitializedPropertyAccessException>(e.cause) assertIs<UninitializedPropertyAccessException>(e.cause)
} }
}
private fun doTestVersion(version: IntArray, expectedText: String) {
val md = Metadata(metadataVersion = version)
val iae = assertFailsWith<IllegalArgumentException> { KotlinClassMetadata.read(md) }
assertContains(iae.message.orEmpty(), expectedText)
}
@Test
fun testReadObsoleteVersion() {
doTestVersion(intArrayOf(0, 1, 0), "version 0.1.0, while minimum supported version is 1.1.0")
doTestVersion(intArrayOf(1, 0, 0), "version 1.0.0, while minimum supported version is 1.1.0")
doTestVersion(intArrayOf(1, 0, 255), "version 1.0.255, while minimum supported version is 1.1.0")
}
@Test
fun testReadNewerVersion() {
val versionPlus2 = JvmMetadataVersion.INSTANCE.next().next()
doTestVersion(
versionPlus2.toArray(),
"version $versionPlus2, while maximum supported version is ${JvmMetadataVersion.INSTANCE_NEXT}"
)
}
@Test
fun testInvalidVersion() {
doTestVersion(intArrayOf(), "instance does not have metadataVersion in it and therefore is malformed and cannot be read")
doTestVersion(
JvmMetadataVersion.INVALID_VERSION.toArray(),
"instance does not have metadataVersion in it and therefore is malformed and cannot be read"
)
}
}