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 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 getBytes ()[B
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.
*
* @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 {
KmClass().apply(this::accept)
@@ -112,7 +112,7 @@ sealed class KotlinClassMetadata(val annotationData: 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 {
KmPackage().apply(this::accept)
@@ -175,7 +175,7 @@ sealed class KotlinClassMetadata(val annotationData: Metadata) {
* Creates a new [KmLambda] instance from this synthetic class metadata.
* 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 {
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.
*
* @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 {
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
* 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,
* 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
fun read(annotationData: Metadata): KotlinClassMetadata? {
if (!JvmMetadataVersion(
annotationData.metadataVersion,
(annotationData.extraInt and (1 shl 3)/* see JvmAnnotationNames.METADATA_STRICT_VERSION_SEMANTICS_FLAG */) != 0
).isCompatibleWithCurrentCompilerVersion()
) return null
fun read(annotationData: Metadata): KotlinClassMetadata {
checkMetadataVersionForRead(annotationData)
// All data is loaded lazily, no exceptions here to handle
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) {
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. " +
@@ -593,7 +616,11 @@ sealed class KotlinClassMetadata(val annotationData: Metadata) {
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
*/
@@ -16,6 +16,7 @@ import kotlinx.metadata.*
import kotlinx.metadata.internal.accept
import kotlinx.metadata.jvm.internal.IgnoreInApiDump
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.wrapWriteIntoIAE
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.
*
* 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
*/
@UnstableMetadataApi
class KotlinModuleMetadata(@Suppress("MemberVisibilityCanBePrivate") val bytes: ByteArray) {
@get:IgnoreInApiDump
internal val data: ModuleMapping = ModuleMapping.loadModuleMapping(
bytes, javaClass.name,
skipMetadataVersionCheck = false,
isJvmPackageNameSupported = true
) {
// TODO: report incorrect versions of modules
}
class KotlinModuleMetadata private constructor(
@Suppress("MemberVisibilityCanBePrivate") val bytes: ByteArray,
@get:IgnoreInApiDump internal val data: ModuleMapping
) {
/**
* 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)
}
/**
* 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
*/
@Deprecated("Writer API is deprecated as excessive and cumbersome. Please use KotlinModuleMetadata.write(kmModule, metadataVersion)")
fun write(metadataVersion: IntArray = COMPATIBLE_METADATA_VERSION): KotlinModuleMetadata =
KotlinModuleMetadata(b.build().serializeToByteArray(JvmMetadataVersion(*metadataVersion), 0))
fun write(metadataVersion: IntArray = COMPATIBLE_METADATA_VERSION): KotlinModuleMetadata {
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.
*
* @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
@UnstableMetadataApi
fun read(bytes: ByteArray): KotlinModuleMetadata? {
fun read(bytes: ByteArray): KotlinModuleMetadata {
return wrapIntoMetadataExceptionWhenNeeded {
val result = KotlinModuleMetadata(bytes)
when (result.data) {
ModuleMapping.EMPTY -> null
ModuleMapping.CORRUPTED ->
val result = dataFromBytes(bytes)
when (result) {
ModuleMapping.EMPTY, ModuleMapping.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]),
* [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
@JvmStatic
@@ -161,6 +163,13 @@ class KotlinModuleMetadata(@Suppress("MemberVisibilityCanBePrivate") val bytes:
fun write(kmModule: KmModule, metadataVersion: IntArray = COMPATIBLE_METADATA_VERSION): KotlinModuleMetadata = wrapWriteIntoIAE {
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.jvm.KotlinClassMetadata
import kotlinx.metadata.jvm.Metadata
import org.jetbrains.kotlin.metadata.jvm.deserialization.JvmMetadataVersion
import org.jetbrains.kotlin.protobuf.InvalidProtocolBufferException
import org.junit.Test
import kotlin.test.assertFailsWith
import kotlin.test.assertIs
import kotlin.test.*
class MetadataExceptionsTest {
@Test
@@ -33,4 +33,35 @@ class MetadataExceptionsTest {
}
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"
)
}
}