From c3d363d1b87dc86436bfdb8df724b17b416938f1 Mon Sep 17 00:00:00 2001 From: Leonid Startsev Date: Tue, 4 Apr 2023 19:41:12 +0200 Subject: [PATCH] Throw exception instead of returning null from KotlinClass(Module)Metadata.read() --- .../jvm/api/kotlinx-metadata-jvm.api | 2 +- .../metadata/jvm/KotlinClassMetadata.kt | 53 ++++++++++++++----- .../metadata/jvm/KotlinModuleMetadata.kt | 53 +++++++++++-------- .../metadata/test/MetadataExceptionsTest.kt | 37 +++++++++++-- 4 files changed, 106 insertions(+), 39 deletions(-) diff --git a/libraries/kotlinx-metadata/jvm/api/kotlinx-metadata-jvm.api b/libraries/kotlinx-metadata/jvm/api/kotlinx-metadata-jvm.api index 49673eb18d8..a7c5348df8c 100644 --- a/libraries/kotlinx-metadata/jvm/api/kotlinx-metadata-jvm.api +++ b/libraries/kotlinx-metadata/jvm/api/kotlinx-metadata-jvm.api @@ -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 ([B)V + public synthetic fun ([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; diff --git a/libraries/kotlinx-metadata/jvm/src/kotlinx/metadata/jvm/KotlinClassMetadata.kt b/libraries/kotlinx-metadata/jvm/src/kotlinx/metadata/jvm/KotlinClassMetadata.kt index 7a241581b48..a5403621026 100644 --- a/libraries/kotlinx-metadata/jvm/src/kotlinx/metadata/jvm/KotlinClassMetadata.kt +++ b/libraries/kotlinx-metadata/jvm/src/kotlinx/metadata/jvm/KotlinClassMetadata.kt @@ -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 */ diff --git a/libraries/kotlinx-metadata/jvm/src/kotlinx/metadata/jvm/KotlinModuleMetadata.kt b/libraries/kotlinx-metadata/jvm/src/kotlinx/metadata/jvm/KotlinModuleMetadata.kt index 9d9541e1194..9cd4b3337e4 100644 --- a/libraries/kotlinx-metadata/jvm/src/kotlinx/metadata/jvm/KotlinModuleMetadata.kt +++ b/libraries/kotlinx-metadata/jvm/src/kotlinx/metadata/jvm/KotlinModuleMetadata.kt @@ -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 + ) + } } } diff --git a/libraries/kotlinx-metadata/jvm/test/kotlinx/metadata/test/MetadataExceptionsTest.kt b/libraries/kotlinx-metadata/jvm/test/kotlinx/metadata/test/MetadataExceptionsTest.kt index 5f3cd3bdde4..194d0d2e1e4 100644 --- a/libraries/kotlinx-metadata/jvm/test/kotlinx/metadata/test/MetadataExceptionsTest.kt +++ b/libraries/kotlinx-metadata/jvm/test/kotlinx/metadata/test/MetadataExceptionsTest.kt @@ -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(e.cause) } -} \ No newline at end of file + + private fun doTestVersion(version: IntArray, expectedText: String) { + val md = Metadata(metadataVersion = version) + val iae = assertFailsWith { 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" + ) + } +}